-
Notifications
You must be signed in to change notification settings - Fork 4
North Pole Wishlist Implementation #5
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,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. |
| 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. |
| 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) | ||
| 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' | ||||||||||
|
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.
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
|
||||||||||
| SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db' | ||||||||||
| SQLALCHEMY_TRACK_MODIFICATIONS = False | ||||||||||
| 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') |
| 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) | ||
|
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. 🟡 It's recommended to use timezone-aware datetimes, preferably |
||
| 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") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Flask | ||
| Flask-SQLAlchemy | ||
| Flask-WTF | ||
| email_validator | ||
|
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 |
||
| 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) | ||
|
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. 🟡 Instead of returning a plain string for a 404 error, it's more consistent with the application's theming to render the custom |
||
|
|
||
| @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)) | ||
| 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 */ | ||
| } |
| 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 %} |
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.