Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Implementation Plan - North Pole Wishlist

## Phase 0: Git Setup
- [x] Check if the current directory is an initialized git repository.
- [x] If it is, create and checkout a new feature branch named `north-pole-wishlist`.

## Phase 1: Project Initialization & Structure
- [x] Create a virtual environment and activate it.
- [x] Create a `requirements.txt` file with dependencies: `Flask`, `Flask-SQLAlchemy`, `Flask-WTF`, `email_validator`.
- [x] Install dependencies.
- [x] Create the project directory structure:
```
north_pole_wishlist/
├── static/
│ ├── css/
│ └── images/
├── templates/
├── models.py
├── forms.py
├── routes.py
├── config.py
└── app.py
```
- [x] Create `config.py` with basic Flask configuration and SQLite database URI.
- [x] Create a basic `app.py` to verify the setup and serve a "Hello World" route.

## Phase 2: Database Models (SQLAlchemy 2.0)
- [x] Implement the `Gift` model in `models.py` using `so.Mapped` and `so.mapped_column`.
- [x] Implement the `Vote` model in `models.py` with a relationship to `Gift`.
- [x] Implement the `Comment` model in `models.py` with a relationship to `Gift`.
- [x] Configure the database extension in `app.py` and import models.
- [x] Create a script or use `flask shell` to initialize the SQLite database tables.

## Phase 3: Forms & Backend Logic
- [x] Create `forms.py` using WTForms.
- [x] `GiftForm`: title (max 100), description (max 500), category (SelectField).
- [x] `VoteForm`: score (integer 1-5).
- [x] `CommentForm`: author_name (optional), content (max 500).
- [x] Create `routes.py` and register a blueprint or attach to app.
- [x] Implement the `GET /gift/new` and `POST /gift/new` logic to add gifts to the database.
- [x] Implement `POST /gift/<int:gift_id>/vote` logic.
- [x] Implement `POST /gift/<int:gift_id>/comment` logic.

## Phase 4: Views & Templates (Basic)
- [x] Create `templates/base.html` with Bootstrap 5 CDN links and a navigation bar.
- [x] Create `templates/index.html` to display a simple list of gifts (fetching all gifts for now).
- [x] Create `templates/create_gift.html` to render the `GiftForm`.
- [x] Create `templates/gift_detail.html` to show gift details, comments, and the voting form.
- [x] Update `routes.py` to render these templates instead of returning strings.

## Phase 5: Theming & Visuals
- [x] Create `static/css/style.css` and define the Christmas color palette:
- Red: `#D42426`, Green: `#165B33`, White: `#F8F9FA`, Gold: `#FFD700`.
- [x] Update `base.html` to include `style.css` and Google Fonts (*Mountains of Christmas*, *Merryweather*).
- [x] Generate the "Santa Claus flying on his sleigh" hero image using Nano Banana and save to `static/images/hero.png`.
- [x] Update `index.html` to include the Hero image section.
- [x] Style the "Snowflake" rating system in `gift_detail.html` (using icons).
- [x] Create a custom 404 page `templates/404.html` with the "Lost in the Snow" theme.

## Phase 6: Advanced Logic (Sorting & Filtering)
- [x] Update `routes.py` for the Home route (`/`) to accept `category` and `sort_by` query parameters.
- [x] Implement SQLAlchemy 2.0 queries for filtering by category.
- [x] Implement sorting logic:
- [x] Recency (default): `gift.created_at` desc.
- [x] Top Rated: Average vote score.
- [x] Most Popular: Total vote count.
- [x] Update `index.html` to include UI controls for filtering (dropdown/links) and sorting.

## Phase 7: Completion & Version Control
- [ ] Verify application functionality (Create gift, Vote, Comment, Filter, Sort).
- [ ] Run a final lint/formatting check.
- [ ] Create a `README.md` file explaining the architecture, how to run the app, and the features.
- [ ] Add all changes to the repository (`git add .`).
- [ ] Commit the changes (`git commit -m "Complete implementation of North Pole Wishlist"`).
- [ ] Push the feature branch to the remote repository.
- [ ] Open a pull request for the feature branch.
69 changes: 69 additions & 0 deletions north_pole_wishlist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# North Pole Wishlist

North Pole Wishlist is a community-driven web application designed to help users discover, share, and curate the best holiday gift ideas.

## Features

- **Share Gift Ideas**: Users can submit their own gift suggestions with a title, description, and category.
- **Voting System**: "Naughty or Nice" voting allows users to rate gifts from 1 to 5 snowflakes.
- **Comments**: Community members can leave comments and reviews on gift ideas.
- **Filtering & Sorting**: Discover gifts by category (e.g., For Kids, Tech, Decorations) and sort by Newest, Top Rated, or Most Popular.
- **Festive Theme**: A fully custom Christmas-themed UI built with Bootstrap 5 and custom CSS.

## Architecture

The application follows the MVC pattern and is built with:
- **Backend**: Python, Flask, SQLAlchemy 2.0 (ORM)
- **Frontend**: Jinja2 Templates, Bootstrap 5, Custom CSS
- **Database**: SQLite (default) or PostgreSQL

## Project Structure

```
north_pole_wishlist/
├── app.py # Application entry point
├── config.py # Configuration settings
├── forms.py # WTForms definitions
├── models.py # SQLAlchemy database models
├── routes.py # Route definitions and views
├── static/ # CSS, Images, JS
└── templates/ # HTML Templates
```

## How to Run Locally

1. **Clone the repository**:
```bash
git clone <repository-url>
cd north-pole-wishlist
```

2. **Create and activate a virtual environment**:
```bash
python3 -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
```

3. **Install dependencies**:
```bash
pip install -r requirements.txt
```

4. **Initialize the Database**:
```bash
python3 -c "from app import app, db; app.app_context().push(); db.create_all()"
```

5. **Run the application**:
```bash
python3 app.py
```

6. **Open in Browser**:
Visit `http://127.0.0.1:5000`

## Credits

- Hero Image generated by Nano Banana.
- Fonts from Google Fonts (Mountains of Christmas, Merryweather).
- Icons by Bootstrap Icons.
Binary file added north_pole_wishlist/__pycache__/app.cpython-313.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
17 changes: 17 additions & 0 deletions north_pole_wishlist/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from flask import Flask, render_template
from config import Config
from models import db
from routes import routes

app = Flask(__name__)
app.config.from_object(Config)

db.init_app(app)
app.register_blueprint(routes)

@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404

if __name__ == '__main__':
app.run(debug=True)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM Running a Flask application with debug mode enabled in a production environment can lead to significant security risks. The Werkzeug debugger, which is enabled in debug mode, allows for arbitrary code execution in the context of the application.
Suggested change
app.run(debug=True)
if __name__ == '__main__':
app.run(debug=app.config.get('DEBUG', False))

6 changes: 6 additions & 0 deletions north_pole_wishlist/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import os

class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess-santa-secret'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH The application uses a hardcoded and predictable secret key if the `SECRET_KEY` environment variable is not set. This can allow an attacker to forge session cookies and other signed data.
Suggested change
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess-santa-secret'
SECRET_KEY = os.environ.get('SECRET_KEY')
if not SECRET_KEY:
raise ValueError("No SECRET_KEY set for Flask application")

SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
25 changes: 25 additions & 0 deletions north_pole_wishlist/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField, IntegerField, SubmitField
from wtforms.validators import DataRequired, Length, NumberRange, Optional

class GiftForm(FlaskForm):
title = StringField('Gift Name', validators=[DataRequired(), Length(max=100)])
description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)])
category = SelectField('Category', choices=[
('For Kids', 'For Kids'),
('For Parents', 'For Parents'),
('Stocking Stuffers', 'Stocking Stuffers'),
('DIY / Homemade', 'DIY / Homemade'),
('Tech & Gadgets', 'Tech & Gadgets'),
('Decorations', 'Decorations')
], validators=[DataRequired()])
submit = SubmitField('Submit Gift')

class VoteForm(FlaskForm):
score = IntegerField('Score (1-5)', validators=[DataRequired(), NumberRange(min=1, max=5)])
submit = SubmitField('Vote')

class CommentForm(FlaskForm):
author_name = StringField('Your Name (Optional)', validators=[Optional(), Length(max=100)])
content = TextAreaField('Comment', validators=[DataRequired(), Length(max=500)])
submit = SubmitField('Post Comment')
Binary file added north_pole_wishlist/instance/app.db
Binary file not shown.
33 changes: 33 additions & 0 deletions north_pole_wishlist/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from datetime import datetime
from typing import List
import sqlalchemy as sa
import sqlalchemy.orm as so
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Gift(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
title: so.Mapped[str] = so.mapped_column(sa.String(100), nullable=False)
description: so.Mapped[str] = so.mapped_column(sa.String(500), nullable=False)
category: so.Mapped[str] = so.mapped_column(sa.String(50), nullable=False)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 It's recommended to use timezone-aware datetimes, preferably datetime.now(timezone.utc), instead of datetime.utcnow() to avoid potential issues with timezones and daylight saving. This would require importing timezone from the datetime module.

created_at: so.Mapped[datetime] = so.mapped_column(default=datetime.utcnow)

votes: so.Mapped[List["Vote"]] = so.relationship(back_populates="gift", cascade="all, delete-orphan")
comments: so.Mapped[List["Comment"]] = so.relationship(back_populates="gift", cascade="all, delete-orphan")

class Vote(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
gift_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey("gift.id"), nullable=False)
score: so.Mapped[int] = so.mapped_column(nullable=False) # 1-5

gift: so.Mapped["Gift"] = so.relationship(back_populates="votes")

class Comment(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
gift_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey("gift.id"), nullable=False)
author_name: so.Mapped[str] = so.mapped_column(sa.String(100), nullable=True, default="Secret Santa")
content: so.Mapped[str] = so.mapped_column(sa.String(500), nullable=False)
created_at: so.Mapped[datetime] = so.mapped_column(default=datetime.utcnow)

gift: so.Mapped["Gift"] = so.relationship(back_populates="comments")
4 changes: 4 additions & 0 deletions north_pole_wishlist/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Flask
Flask-SQLAlchemy
Flask-WTF
email_validator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 The email_validator package is listed in requirements.txt but does not appear to be used in the application. Consider removing it if it's not needed to keep dependencies minimal.

88 changes: 88 additions & 0 deletions north_pole_wishlist/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash
import sqlalchemy as sa
from models import db, Gift, Vote, Comment
from forms import GiftForm, VoteForm, CommentForm

routes = Blueprint('routes', __name__)

@routes.route('/', methods=['GET'])
def index():
category = request.args.get('category')
sort_by = request.args.get('sort_by', 'recency')

stmt = sa.select(Gift)

if category:
stmt = stmt.where(Gift.category == category)

if sort_by == 'top_rated':
# Subquery to calculate average score
subquery = sa.select(
Vote.gift_id,
sa.func.avg(Vote.score).label('avg_score')
).group_by(Vote.gift_id).subquery()

# Join with subquery and order by avg_score
stmt = stmt.outerjoin(subquery, Gift.id == subquery.c.gift_id).order_by(subquery.c.avg_score.desc().nullslast())

elif sort_by == 'most_popular':
# Subquery to count votes
subquery = sa.select(
Vote.gift_id,
sa.func.count(Vote.id).label('vote_count')
).group_by(Vote.gift_id).subquery()

# Join with subquery and order by vote_count
stmt = stmt.outerjoin(subquery, Gift.id == subquery.c.gift_id).order_by(subquery.c.vote_count.desc().nullslast())
else:
# Default: Recency
stmt = stmt.order_by(Gift.created_at.desc())

gifts = db.session.scalars(stmt).all()
return render_template('index.html', gifts=gifts, current_category=category, current_sort=sort_by)

@routes.route('/gift/new', methods=['GET', 'POST'])
def new_gift():
form = GiftForm()
if form.validate_on_submit():
gift = Gift(
title=form.title.data,
description=form.description.data,
category=form.category.data
)
db.session.add(gift)
db.session.commit()
return redirect(url_for('routes.index'))
return render_template('create_gift.html', form=form)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Instead of returning a plain string for a 404 error, it's more consistent with the application's theming to render the custom 404.html template.```suggestion
return render_template('404.html'), 404


@routes.route('/gift/<int:gift_id>', methods=['GET'])
def gift_detail(gift_id):
gift = db.session.get(Gift, gift_id)
if not gift:
return "Gift not found", 404
vote_form = VoteForm()
comment_form = CommentForm()
return render_template('gift_detail.html', gift=gift, vote_form=vote_form, comment_form=comment_form)

@routes.route('/gift/<int:gift_id>/vote', methods=['POST'])
def vote_gift(gift_id):
form = VoteForm()
if form.validate_on_submit():
vote = Vote(gift_id=gift_id, score=form.score.data)
db.session.add(vote)
db.session.commit()
return redirect(url_for('routes.gift_detail', gift_id=gift_id))

@routes.route('/gift/<int:gift_id>/comment', methods=['POST'])
def comment_gift(gift_id):
form = CommentForm()
if form.validate_on_submit():
author = form.author_name.data if form.author_name.data else "Secret Santa"
comment = Comment(
gift_id=gift_id,
author_name=author,
content=form.content.data
)
db.session.add(comment)
db.session.commit()
return redirect(url_for('routes.gift_detail', gift_id=gift_id))
47 changes: 47 additions & 0 deletions north_pole_wishlist/static/css/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
:root {
--christmas-red: #D42426;
--christmas-green: #165B33;
--christmas-white: #F8F9FA;
--christmas-gold: #FFD700;
}

body {
background-color: var(--christmas-white);
font-family: 'Merryweather', serif;
}

h1, h2, h3, h4, h5, h6, .navbar-brand {
font-family: 'Mountains of Christmas', cursive;
color: var(--christmas-red);
}

.navbar {
background-color: var(--christmas-green) !important;
}

.navbar-brand, .nav-link {
color: var(--christmas-white) !important;
}

.btn-primary {
background-color: var(--christmas-red);
border-color: var(--christmas-red);
}

.btn-primary:hover {
background-color: #a81c1e;
border-color: #a81c1e;
}

.card {
border: 1px solid var(--christmas-gold);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}

.card-title {
color: var(--christmas-green);
}

.snowflake-icon {
color: #a0d3e8; /* Light blue for snowflakes */
}
Binary file added north_pole_wishlist/static/images/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions north_pole_wishlist/templates/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends 'base.html' %}

{% block content %}
<div class="row justify-content-center text-center mt-5">
<div class="col-md-8">
<h1 class="display-1 text-primary">404</h1>
<h2>Lost in the Snow?</h2>
<p class="lead">It seems you've wandered off the path to Santa's Workshop.</p>
<p>The page you are looking for might have been buried under a snowdrift.</p>
<a href="{{ url_for('routes.index') }}" class="btn btn-primary btn-lg mt-3">Return Home</a>
</div>
</div>
{% endblock %}
Loading