-
Notifications
You must be signed in to change notification settings - Fork 3
North Pole Wishlist Implementation #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| app.db | ||
| server.log | ||
| __pycache__/ | ||
| *.pyc |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # 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 Initialization | ||
| - [x] Create a `requirements.txt` file with dependencies: `Flask`, `SQLAlchemy`, `Flask-SQLAlchemy`, `Flask-Migrate`, `python-dotenv`. | ||
| - [x] Create a `config.py` file to handle environment configurations (Secret key, Database URL). | ||
| - [x] Create the application package structure: `app/` folder and `app/__init__.py`. | ||
| - [x] Implement the Flask "Application Factory" pattern in `app/__init__.py`. | ||
| - [x] Create a `run.py` entry point file in the root directory. | ||
| - [x] Verify the server starts (`python run.py`) and returns a "Hello World" on the root route. | ||
|
|
||
| ## Phase 2: Database Layer | ||
| - [x] Create `app/models.py`. | ||
| - [x] Define the `GiftIdea` model using SQLAlchemy 2.0 syntax (`Mapped`, `mapped_column`). | ||
| - [x] Define the `Vote` model with a Foreign Key to `GiftIdea`. | ||
| - [x] Define the `Comment` model with a Foreign Key to `GiftIdea`. | ||
| - [x] Initialize Flask-Migrate in the app factory. | ||
| - [x] Run `flask db init`, `flask db migrate -m "Initial migration"`, and `flask db upgrade` to create the SQLite database. | ||
|
|
||
| ## Phase 3: Core API & Logic (Backend) | ||
| - [x] Create `app/routes.py` and define a Blueprint for the main application. | ||
| - [x] Register the Blueprint in `app/__init__.py`. | ||
| - [x] Implement `POST /api/gifts` endpoint to accept JSON data and save new gifts. | ||
| - [x] Implement `GET /api/gifts` endpoint with query parameters for filtering (`category`) and sorting (`nice_list`, `popularity`, `newest`). | ||
| - [x] Implement `POST /api/gifts/<id>/vote` endpoint to save scores. | ||
| - [x] Implement `POST /api/gifts/<id>/comments` and `GET /api/gifts/<id>/comments` endpoints. | ||
|
|
||
| ## Phase 4: Frontend Base & Assets | ||
| - [x] Create `app/templates/base.html`. | ||
| - [x] Add Tailwind CSS via CDN to `base.html` head. | ||
| - [x] Configure the Tailwind theme in `base.html` (script tag) to include custom colors: `santa-red` (red-600), `pine-green` (green-700), `snow-white` (slate-50). | ||
| - [x] Build the layout in `base.html`: Festive Header (Navigation), Main Content Block, Footer. | ||
| - [x] Add a "Hero" section to the header with a placeholder Santa/Sleigh image. | ||
|
|
||
| ## Phase 5: Frontend Feature Implementation | ||
| - [x] Create `app/templates/index.html` extending `base.html`. | ||
| - [x] Implement the "Submit Gift" form in a Modal or separate section on `index.html`. | ||
| - [x] Create a Jinja2 macro or component for the "Gift Card" to display Gift Name, Description, Category Badge, and Score. | ||
| - [x] Render the list of `GiftIdea` items on `index.html` (server-side initial render). | ||
|
|
||
| ## Phase 6: Interactivity (JavaScript) | ||
| - [x] Create `app/static/js/main.js` and link it in `base.html`. | ||
| - [x] Implement AJAX fetch logic to handle the "Submit Gift" form without reloading. | ||
| - [x] Add event listeners to the "Vote" buttons (Snowflakes) to call `POST /api/gifts/<id>/vote` and update the UI score dynamically. | ||
| - [x] Add "Show Comments" toggle on Gift Cards to fetch and display comments asynchronously. | ||
| - [x] Implement the "Post Comment" logic via AJAX. | ||
|
|
||
| ## Phase 7: Completion & Version Control | ||
| - [ ] Verify all functionalities: Submission, Voting, Commenting, Filtering/Sorting. | ||
| - [ ] Create a `README.md` file explaining the application functions, architecture, and how to run 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, using the Gemini CLI github MCP server. | ||
| - [ ] Open a pull request for the feature branch using the Gemini CLI github MCP server, leave it open for review, don't merge it. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # North Pole Wishlist 🎅 | ||
|
|
||
| Welcome to the North Pole Wishlist, a festive community platform where users can share, discover, and vote on the best holiday gift ideas! | ||
|
|
||
| ## Features | ||
|
|
||
| - **Gift Submission**: Share your unique gift ideas with the community. | ||
| - **Categorization**: Organize gifts by categories like Kids, Parents, Tech, DIY, etc. | ||
| - **Voting ("Naughty or Nice")**: Upvote your favorite gifts to help them reach the top of the "Nice List". | ||
| - **Comments**: Discuss gift ideas, ask for purchase links, or share reviews. | ||
| - **Festive UI**: A fully themed "Santa's Workshop" interface using Tailwind CSS. | ||
|
|
||
| ## Tech Stack | ||
|
|
||
| - **Backend**: Python, Flask, SQLAlchemy 2.0 | ||
| - **Database**: SQLite (Development), with Flask-Migrate (Alembic) | ||
| - **Frontend**: HTML5 (Jinja2 Templates), Tailwind CSS, Vanilla JavaScript | ||
|
|
||
| ## Setup & Running Locally | ||
|
|
||
| 1. **Clone the repository**: | ||
| ```bash | ||
| git clone <repository-url> | ||
| cd north-pole-wishlist | ||
| ``` | ||
|
|
||
| 2. **Install Dependencies**: | ||
| ```bash | ||
| pip install -r requirements.txt | ||
| ``` | ||
|
|
||
| 3. **Initialize the Database**: | ||
| ```bash | ||
| export FLASK_APP=run.py | ||
| flask db upgrade | ||
| ``` | ||
|
|
||
| 4. **Run the Application**: | ||
| ```bash | ||
| python run.py | ||
| ``` | ||
|
|
||
| 5. **Visit**: Open [http://127.0.0.1:5000](http://127.0.0.1:5000) in your browser. | ||
|
|
||
| ## API Endpoints | ||
|
|
||
| - `GET /api/gifts`: List all gifts (supports `category` and `sort_by` params). | ||
| - `POST /api/gifts`: Submit a new gift. | ||
| - `POST /api/gifts/<id>/vote`: Vote for a gift. | ||
| - `POST /api/gifts/<id>/comments`: Add a comment. | ||
| - `GET /api/gifts/<id>/comments`: Get comments for a gift. | ||
|
|
||
| ## Project Structure | ||
|
|
||
| - `app/`: Application source code. | ||
| - `models.py`: Database models. | ||
| - `routes.py`: API endpoints and views. | ||
| - `templates/`: HTML templates. | ||
| - `static/`: CSS/JS assets. | ||
| - `migrations/`: Database migration scripts. | ||
| - `config.py`: Configuration settings. | ||
| - `run.py`: Entry point. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| from flask import Flask | ||
| from flask_sqlalchemy import SQLAlchemy | ||
| from flask_migrate import Migrate | ||
| from config import Config | ||
|
|
||
| db = SQLAlchemy() | ||
| migrate = Migrate() | ||
|
|
||
| def create_app(config_class=Config): | ||
| app = Flask(__name__) | ||
| app.config.from_object(config_class) | ||
|
|
||
| db.init_app(app) | ||
| migrate.init_app(app, db) | ||
|
|
||
| from app import models # Register models | ||
|
|
||
| from app import routes | ||
| app.register_blueprint(routes.bp) | ||
|
|
||
| return app |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| from datetime import datetime | ||
| from typing import List, Optional | ||
| import sqlalchemy as sa | ||
| import sqlalchemy.orm as so | ||
| from app import db | ||
|
|
||
| class GiftIdea(db.Model): | ||
| id: so.Mapped[int] = so.mapped_column(primary_key=True) | ||
| name: so.Mapped[str] = so.mapped_column(sa.String(100)) | ||
| description: so.Mapped[str] = so.mapped_column(sa.String(500)) | ||
| category: so.Mapped[str] = so.mapped_column(sa.String(50)) | ||
| created_at: so.Mapped[datetime] = so.mapped_column(default=datetime.utcnow) | ||
|
|
||
| votes: so.Mapped[List['Vote']] = so.relationship(back_populates='gift') | ||
| comments: so.Mapped[List['Comment']] = so.relationship(back_populates='gift') | ||
|
|
||
| def to_dict(self): | ||
| return { | ||
| 'id': self.id, | ||
| 'name': self.name, | ||
| 'description': self.description, | ||
| 'category': self.category, | ||
| 'created_at': self.created_at.isoformat(), | ||
| 'average_score': self.average_score, | ||
| 'vote_count': len(self.votes), | ||
| 'comment_count': len(self.comments) | ||
| } | ||
|
|
||
| @property | ||
| def average_score(self): | ||
| if not self.votes: | ||
| return 0 | ||
| return sum(v.score for v in self.votes) / len(self.votes) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🟡 For data integrity, it's good practice to explicitly set `nullable=False` for foreign key columns that are required.
|
||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🟡 For data integrity, it's good practice to explicitly set `nullable=False` for columns that are required.
|
||
| 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_idea.id')) | ||
| score: so.Mapped[int] = so.mapped_column(sa.Integer) | ||
| created_at: so.Mapped[datetime] = so.mapped_column(default=datetime.utcnow) | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🟡 For data integrity, it's good practice to explicitly set `nullable=False` for foreign key columns that are required.
|
||
| gift: so.Mapped['GiftIdea'] = so.relationship(back_populates='votes') | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🟡 For data integrity, it's good practice to explicitly set `nullable=False` for columns that are required.
|
||
| 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_idea.id')) | ||
| author_name: so.Mapped[str] = so.mapped_column(sa.String(50), default="Secret Santa") | ||
| content: so.Mapped[str] = so.mapped_column(sa.String(500)) | ||
| created_at: so.Mapped[datetime] = so.mapped_column(default=datetime.utcnow) | ||
|
|
||
| gift: so.Mapped['GiftIdea'] = so.relationship(back_populates='comments') | ||
|
|
||
| def to_dict(self): | ||
| return { | ||
| 'id': self.id, | ||
| 'gift_id': self.gift_id, | ||
| 'author_name': self.author_name, | ||
| 'content': self.content, | ||
| 'created_at': self.created_at.isoformat() | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| from flask import Blueprint, jsonify, request, render_template | ||
| from app import db | ||
| from app.models import GiftIdea, Vote, Comment | ||
| import sqlalchemy as sa | ||
| from sqlalchemy import func | ||
|
|
||
| bp = Blueprint('main', __name__) | ||
|
|
||
| def _get_filtered_gifts(category=None, sort_by='newest'): | ||
| query = sa.select(GiftIdea) | ||
|
|
||
| if category: | ||
| query = query.where(GiftIdea.category == category) | ||
|
|
||
| # Sorting | ||
| if sort_by == 'nice_list': | ||
| # AVG score desc, Count desc | ||
| subq = ( | ||
| sa.select( | ||
| Vote.gift_id, | ||
| func.avg(Vote.score).label('avg_score'), | ||
| func.count(Vote.id).label('vote_count') | ||
| ) | ||
| .group_by(Vote.gift_id) | ||
| .subquery() | ||
| ) | ||
| query = query.outerjoin(subq, GiftIdea.id == subq.c.gift_id) | ||
| query = query.order_by(subq.c.avg_score.desc().nulls_last(), subq.c.vote_count.desc()) | ||
|
|
||
| elif sort_by == 'popularity': | ||
| # Vote count desc | ||
| subq = ( | ||
| sa.select( | ||
| Vote.gift_id, | ||
| func.count(Vote.id).label('vote_count') | ||
| ) | ||
| .group_by(Vote.gift_id) | ||
| .subquery() | ||
| ) | ||
| query = query.outerjoin(subq, GiftIdea.id == subq.c.gift_id) | ||
| query = query.order_by(subq.c.vote_count.desc().nulls_last()) | ||
|
|
||
| else: # newest | ||
| query = query.order_by(GiftIdea.created_at.desc()) | ||
|
|
||
| return db.session.scalars(query).all() | ||
|
|
||
| @bp.route('/') | ||
| def index(): | ||
| category = request.args.get('category') | ||
| sort_by = request.args.get('sort_by', 'newest') | ||
| gifts = _get_filtered_gifts(category, sort_by) | ||
| return render_template('index.html', gifts=gifts) | ||
|
|
||
| @bp.route('/api/gifts', methods=['GET']) | ||
| def get_gifts(): | ||
| category = request.args.get('category') | ||
| sort_by = request.args.get('sort_by', 'newest') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🟠 The API endpoints lack rate limiting, leaving them vulnerable to denial-of-service (DoS) attacks or abuse. Implementing a rate-limiting solution (e.g., using `Flask-Limiter`) is recommended to protect the application from excessive requests.
|
||
| gifts = _get_filtered_gifts(category, sort_by) | ||
| return jsonify([g.to_dict() for g in gifts]) | ||
|
|
||
| @bp.route('/api/gifts', methods=['POST']) | ||
| def create_gift(): | ||
| data = request.get_json() | ||
| if not data or not data.get('name') or not data.get('description') or not data.get('category'): | ||
| return jsonify({'error': 'Missing required fields'}), 400 | ||
|
|
||
| # Validation logic could go here (length checks) | ||
| if len(data['name']) > 100: | ||
| return jsonify({'error': 'Name too long'}), 400 | ||
|
|
||
| gift = GiftIdea( | ||
| name=data['name'], | ||
| description=data['description'], | ||
| category=data['category'] | ||
| ) | ||
| db.session.add(gift) | ||
| db.session.commit() | ||
| return jsonify(gift.to_dict()), 201 | ||
|
|
||
| @bp.route('/api/gifts/<int:id>/vote', methods=['POST']) | ||
| def vote_gift(id): | ||
| data = request.get_json() | ||
| score = data.get('score') | ||
| if not score or not isinstance(score, int) or not (1 <= score <= 5): | ||
| return jsonify({'error': 'Invalid score (1-5)'}), 400 | ||
|
|
||
| gift = db.session.get(GiftIdea, id) | ||
| if not gift: | ||
| return jsonify({'error': 'Gift not found'}), 404 | ||
|
|
||
| vote = Vote(gift_id=id, score=score) | ||
| db.session.add(vote) | ||
| db.session.commit() | ||
|
|
||
| return jsonify({ | ||
| 'message': 'Vote recorded', | ||
| 'new_average': gift.average_score, | ||
| 'total_votes': len(gift.votes) | ||
| }) | ||
|
|
||
| @bp.route('/api/gifts/<int:id>/comments', methods=['GET']) | ||
| def get_comments(id): | ||
| gift = db.session.get(GiftIdea, id) | ||
| if not gift: | ||
| return jsonify({'error': 'Gift not found'}), 404 | ||
|
|
||
| return jsonify([c.to_dict() for c in gift.comments]) | ||
|
|
||
| @bp.route('/api/gifts/<int:id>/comments', methods=['POST']) | ||
| def create_comment(id): | ||
| data = request.get_json() | ||
| content = data.get('content') | ||
| author_name = data.get('author_name', 'Secret Santa') | ||
|
|
||
| if not content or len(content) < 10: | ||
| return jsonify({'error': 'Content must be at least 10 chars'}), 400 | ||
|
|
||
| gift = db.session.get(GiftIdea, id) | ||
| if not gift: | ||
| return jsonify({'error': 'Gift not found'}), 404 | ||
|
|
||
| comment = Comment(gift_id=id, content=content, author_name=author_name) | ||
| db.session.add(comment) | ||
| db.session.commit() | ||
|
|
||
| return jsonify(comment.to_dict()), 201 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.