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
80 changes: 80 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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: Environment & Project Structure
- [x] Set up a Python virtual environment and activate it.
- [x] Install required packages: `Flask`, `Flask-SQLAlchemy`, `Flask-WTF`, `email_validator` (if needed for WTForms).
- [x] Create `requirements.txt` with frozen dependencies.
- [x] Create project structure:
```
/
├── app.py
├── config.py
├── models.py
├── forms.py
├── static/
│ ├── css/
│ │ └── style.css
│ └── images/
└── templates/
├── base.html
├── index.html
├── submit.html
└── detail.html
```
- [x] Create `config.py` with secret key and database URI configuration.
- [x] Create a basic `app.py` to verify Flask runs.

## Phase 2: Database & Models
- [x] Configure `Flask-SQLAlchemy` in `app.py` using modern SQLAlchemy 2.0 style.
- [x] In `models.py`, implement the `Gift` model with fields: `id`, `title`, `description`, `category`, `created_at`.
- [x] In `models.py`, implement the `Vote` model with fields: `id`, `gift_id`, `score`.
- [x] In `models.py`, implement the `Comment` model with fields: `id`, `gift_id`, `author_name`, `content`, `created_at`.
- [x] Create a database initialization script or function to create tables (`db.create_all()`).

## Phase 3: Forms & Backend Logic
- [x] In `forms.py`, create `GiftForm` using Flask-WTF (Title, Description, Category select).
- [x] In `forms.py`, create `CommentForm` (Author Name, Content).
- [x] In `forms.py`, create `VoteForm` (Score integer/radio).
- [x] In `app.py`, implement the `POST /gift/new` route to handle gift submission and validation.
- [x] In `app.py`, implement the `POST /gift/<int:id>/vote` route to save votes.
- [x] In `app.py`, implement the `POST /gift/<int:id>/comment` route to save comments.
- [x] In `app.py`, implement the `GET /` route with query parameters for sorting (Ranking/Recency) and filtering (Category). Use `sqlalchemy.select` with `func.avg` for ranking logic.

## Phase 4: Frontend - Base & Home
- [x] Create `templates/base.html` including Bootstrap 5 (via CDN) and a custom stylesheet link.
- [x] In `static/css/style.css`, define the "Christmas" theme variables:
- Primary: `#D42426` (Santa Red)
- Success: `#165B33` (Pine Green)
- Background: `#F8F8FF` (Snow White)
- [x] Generate a "Santa flying on a sleigh" hero image using Nano Banana (or use a placeholder) and save to `static/images/hero.png`.
- [x] Implement `templates/index.html`:
- Display the Hero image.
- Add Filter and Sort controls (dropdowns/links).
- Iterate through gifts and display them as Bootstrap Cards.
- Show average rating and comment count on cards.

## Phase 5: Frontend - Details & Interactions
- [x] Implement `templates/submit.html`: Render `GiftForm` with festive styling.
- [x] Implement `templates/detail.html`:
- Display full gift details.
- Show list of comments.
- Render `CommentForm` for new comments.
- Render `VoteForm` or a custom interactive star/snowflake rating widget.
- [x] Integrate "Snowflake" icons (Bootstrap Icons `bi-snow2`) for ratings in both the list and detail views.

## Phase 6: Final Polish
- [x] Add Flash message support in `base.html` for feedback (e.g., "Gift submitted!", "Vote cast!").
- [x] Ensure all forms have validation error display.
- [x] Verify responsive layout on mobile/desktop.

## Phase 7: Completion & Version Control
- [ ] Verify application functionality (Submit gift, Vote, Comment, Sort/Filter).
- [ ] Create a `README.md` file explaining the application functions, how to interact with them, the architecture, file breakdown and how to run and test it locally.
- [ ] 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, creating a branch with the same name in the remote repository.
- [ ] Open a pull request for the feature branch using the Gemini CLI github MCP server, leave it open for review, don't merge it.
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# North Pole Wishlist

North Pole Wishlist is a festive community platform where users can discover, share, and rate the best holiday gift ideas. It's designed with a "Santa's Workshop" theme to bring holiday cheer to your gift planning.

## Features

* **Share Gift Ideas**: Submit new gift suggestions with titles, descriptions, and categories.
* **Discover Gifts**: Browse a feed of community-submitted gifts.
* **Vote & Rate**: Rate gifts using a 1-5 "Snowflake" scale.
* **Discuss**: Comment on gift ideas to share reviews or ask questions.
* **Sort & Filter**: Find the perfect gift by filtering categories (e.g., "For Kids", "Tech") or sorting by popularity/recency.

## Tech Stack

* **Backend**: Python 3, Flask
* **Database**: SQLite with SQLAlchemy 2.0 ORM
* **Frontend**: Bootstrap 5, Jinja2 Templates, Custom CSS
* **Forms**: Flask-WTF

## Installation & Setup

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
python init_db.py
```

5. **Run the application**:
```bash
python app.py
```
The app will be available at `http://127.0.0.1:5000`.

## Project Structure

* `app.py`: Main application entry point and route definitions.
* `models.py`: Database models (Gift, Vote, Comment).
* `forms.py`: WTForms definitions for validation.
* `config.py`: Configuration settings.
* `templates/`: HTML templates (Jinja2).
* `static/`: CSS, Images, and JavaScript.

## License

Merry Christmas! 🎄
Binary file added __pycache__/app.cpython-313.pyc
Copy link

Choose a reason for hiding this comment

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

🟡 The `__pycache__` directory and other generated files like logs should not be committed to the repository. Please add `__pycache__/`, `*.log`, and `instance/` to a `.gitignore` file to prevent these files from being tracked.

Binary file not shown.
Binary file added __pycache__/config.cpython-313.pyc
Binary file not shown.
Binary file added __pycache__/forms.cpython-313.pyc
Binary file not shown.
Binary file added __pycache__/models.cpython-313.pyc
Binary file not shown.
144 changes: 144 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from flask import Flask, render_template, redirect, url_for, flash, request
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy import desc, func
import sqlalchemy as sa
from config import Config
from forms import GiftForm, CommentForm, VoteForm

class Base(DeclarativeBase):
pass

Comment on lines +8 to +11
Copy link

Choose a reason for hiding this comment

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

🟡 For better code organization and to avoid potential circular dependencies, it's a good practice to move the `db` object initialization to a separate file (e.g., `extensions.py`). This also resolves the circular import between `app.py` and `models.py`.

In extensions.py:

from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

db = SQLAlchemy(model_class=Base)

In app.py:

# from flask_sqlalchemy import SQLAlchemy
# from sqlalchemy.orm import DeclarativeBase
from extensions import db
# ...
# class Base(DeclarativeBase):
#     pass
#
# db = SQLAlchemy(model_class=Base)

In models.py:

# from app import db
from extensions import db

db = SQLAlchemy(model_class=Base)

def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)

db.init_app(app)
Copy link

Choose a reason for hiding this comment

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

🟡 Importing within functions is generally discouraged as it can lead to hidden dependencies and make the code harder to read and maintain. All imports should be at the top of the file.
Suggested change
db.init_app(app)
from flask import Flask, render_template, redirect, url_for, flash, request
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy import desc, func
import sqlalchemy as sa
from config import Config
from forms import GiftForm, CommentForm, VoteForm
import models
from models import Gift, Vote, Comment


with app.app_context():
Comment on lines +17 to +20
Copy link

Choose a reason for hiding this comment

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

🟠 Calling `db.create_all()` within the application context like this is convenient for development but unsuitable for production. It can lead to issues with database migrations as the schema evolves. Consider using a database migration tool like Flask-Migrate (which uses Alembic) to manage schema changes.
Suggested change
db.init_app(app)
with app.app_context():
db.init_app(app)
# with app.app_context():
# import models
# from models import Gift, Vote, Comment
# db.create_all()

import models
from models import Gift, Vote, Comment
db.create_all()

@app.route('/')
def index():
from models import Gift, Vote

category_filter = request.args.get('category')
sort_by = request.args.get('sort', 'recency')

# Base query: Join Gift with Vote to calculate average score
# Using left outer join to include gifts with no votes
stmt = (
sa.select(
Gift,
func.avg(Vote.score).label('avg_score'),
func.count(Vote.id).label('vote_count')
)
.outerjoin(Vote)
.group_by(Gift.id)
)

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

if sort_by == 'ranking':
# Sort by average score descending, then by number of votes
stmt = stmt.order_by(desc('avg_score'), desc('vote_count'))
else:
# Default: Recency (Newest first)
stmt = stmt.order_by(desc(Gift.created_at))

results = db.session.execute(stmt).all()

# Determine available categories for filter dropdown
categories = ['For Kids', 'For Parents', 'Stocking Stuffers', 'DIY', 'Tech', 'Decorations']
Copy link

Choose a reason for hiding this comment

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

🟢 The list of categories is hardcoded in the route. This is fine for a small application, but if the categories are expected to change, it would be better to define them in `config.py` or even move them to a separate table in the database. This would make them easier to manage.


return render_template('index.html', gifts=results, categories=categories, current_filter=category_filter, current_sort=sort_by)

@app.route('/submit', methods=['GET', 'POST'])
def submit_gift():
from models import 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()
flash('Gift idea submitted successfully!', 'success')
return redirect(url_for('index'))
return render_template('submit.html', form=form)

@app.route('/gift/<int:id>', methods=['GET'])
def gift_detail(id):
from models import Gift, Comment, Vote

gift = db.session.get(Gift, id)
if not gift:
flash('Gift not found.', 'danger')
return redirect(url_for('index'))

# Calculate score manually for detail view
avg_score = db.session.scalar(
sa.select(func.avg(Vote.score)).where(Vote.gift_id == id)
)
vote_count = db.session.scalar(
sa.select(func.count(Vote.id)).where(Vote.gift_id == id)
)

comments = db.session.scalars(
sa.select(Comment).where(Comment.gift_id == id).order_by(desc(Comment.created_at))
).all()

vote_form = VoteForm()
comment_form = CommentForm()

return render_template('detail.html',
gift=gift,
avg_score=avg_score,
vote_count=vote_count,
comments=comments,
vote_form=vote_form,
comment_form=comment_form)

@app.route('/gift/<int:id>/vote', methods=['POST'])
def vote_gift(id):
from models import Vote
form = VoteForm()
if form.validate_on_submit():
vote = Vote(gift_id=id, score=int(form.score.data))
db.session.add(vote)
db.session.commit()
flash('Vote cast successfully!', 'success')
else:
flash('Invalid vote.', 'danger')
return redirect(url_for('gift_detail', id=id))

@app.route('/gift/<int:id>/comment', methods=['POST'])
def comment_gift(id):
from models import Comment
form = CommentForm()
if form.validate_on_submit():
author = form.author_name.data if form.author_name.data else "Secret Santa"
comment = Comment(
gift_id=id,
author_name=author,
content=form.content.data
)
db.session.add(comment)
db.session.commit()
flash('Comment added!', 'success')
else:
flash('Comment too short or invalid.', 'danger')
return redirect(url_for('gift_detail', id=id))

return app

if __name__ == '__main__':
app = create_app()
app.run(debug=True)
6 changes: 6 additions & 0 deletions 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.

🟠 Using a hardcoded default secret key is a security risk. If the `SECRET_KEY` environment variable is not set, the application will fall back to a predictable key, which could be exploited. It's better to make the `SECRET_KEY` a required environment variable and have the application fail to start if it's not set.
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")

Copy link

Choose a reason for hiding this comment

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

HIGH A hardcoded secret key is used in config.py. Secret keys should not be hardcoded in the source code. They should be loaded from environment variables or a secrets management system.

Suggested change
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess-santa-secret'
SECRET_KEY = os.environ.get('SECRET_KEY')

SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///north_pole.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
9 changes: 9 additions & 0 deletions flask.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 103-519-779
127.0.0.1 - - [09/Dec/2025 10:11:00] "GET / HTTP/1.1" 200 -
8 changes: 8 additions & 0 deletions flask_db_init.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 103-519-779
78 changes: 78 additions & 0 deletions flask_test.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 103-519-779
Traceback (most recent call last):
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context
self.dialect.do_execute(
^
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/default.py", line 951, in do_execute
cursor.execute(statement, parameters)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
sqlite3.OperationalError: no such table: gift

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 1536, in __call__
return self.wsgi_app(environ, start_response)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 1514, in wsgi_app
response = self.handle_exception(e)
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
File "/Users/galloro/cli-spec-driven-dev/app.py", line 54, in index
results = db.session.execute(stmt).all()
~~~~~~~~~~~~~~~~~~^^^^^^
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/orm/scoping.py", line 765, in execute
return self._proxied.execute(

File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py", line 2351, in execute
return self._execute_internal(

File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py", line 2249, in _execute_internal
result: Result[Any] = compile_state_cls.orm_execute_statement(

File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/orm/context.py", line 306, in orm_execute_statement
result = conn.execute(

File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1419, in execute
return meth(

File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/sql/elements.py", line 526, in _execute_on_connection
return connection._execute_clauseelement(

File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1641, in _execute_clauseelement
ret = self._execute_context(

File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1846, in _execute_context
return self._exec_single_context(

File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1986, in _exec_single_context
self._handle_dbapi_exception(
^
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 2355, in _handle_dbapi_exception
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context
self.dialect.do_execute(
^
File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/default.py", line 951, in do_execute
cursor.execute(statement, parameters)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: gift
[SQL: SELECT gift.id, gift.title, gift.description, gift.category, gift.created_at, avg(vote.score) AS avg_score, count(vote.id) AS vote_count
FROM gift LEFT OUTER JOIN vote ON gift.id = vote.gift_id GROUP BY gift.id ORDER BY gift.created_at DESC]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
127.0.0.1 - - [09/Dec/2025 10:24:44] "HEAD / HTTP/1.1" 500 -
127.0.0.1 - - [09/Dec/2025 10:24:44] "HEAD /submit HTTP/1.1" 200 -
Loading