-
Notifications
You must be signed in to change notification settings - Fork 3
Complete implementation of North Pole Wishlist #2
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,78 @@ | ||
| # 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 | ||
| - [x] Set up a Python virtual environment (`python3 -m venv venv`) and activate it. | ||
| - [x] Create a `requirements.txt` file including `Flask`, `Flask-SQLAlchemy`, and any other necessary libraries. | ||
| - [x] Install the dependencies (`pip install -r requirements.txt`). | ||
| - [x] Create the basic project structure: | ||
| - `app.py` (Main application file) | ||
| - `models.py` (Database models) | ||
| - `templates/` (HTML templates) | ||
| - `static/css/` (Stylesheets) | ||
| - `static/img/` (Images) | ||
| - [x] Create a minimal `app.py` to verify the Flask setup works (Hello World). | ||
|
|
||
| ## Phase 2: Database & Models | ||
| - [x] Configure SQLite database connection in `app.py`. | ||
| - [x] In `models.py`, define the `Gift` model using SQLAlchemy 2.0 style (Mapped, mapped_column). | ||
| - Fields: `id`, `title`, `description`, `category`, `created_at`. | ||
| - [x] In `models.py`, define the `Vote` model. | ||
| - Fields: `id`, `gift_id`, `score`, `created_at`. | ||
| - [x] In `models.py`, define the `Comment` model. | ||
| - 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: Core Backend Logic & Routes | ||
| - [x] Implement the Home route (`GET /`): | ||
| - Query all gifts from the database. | ||
| - Implement filtering by category (via query parameter). | ||
| - Implement sorting (Ranking: avg score, Popularity: vote count, Recency: created_at). | ||
| - [x] Implement the Submit Gift route (`GET /gifts/new` & `POST /gifts/new`): | ||
| - GET: Render the submission form. | ||
| - POST: Validate input (lengths, required fields). Create new `Gift` record. | ||
| - [x] Implement the Gift Detail route (`GET /gifts/<int:gift_id>`): | ||
| - Query specific gift by ID. | ||
| - Calculate average rating and fetch related comments. | ||
| - [x] Implement the Voting route (`POST /gifts/<int:gift_id>/vote`): | ||
| - Validate score (1-5). Create `Vote` record. Redirect or return updated score. | ||
| - [x] Implement the Commenting route (`POST /gifts/<int:gift_id>/comment`): | ||
| - Validate content length. Default author name if empty. Create `Comment` record. | ||
|
|
||
| ## Phase 4: Frontend Implementation | ||
| - [x] Create `templates/base.html`: | ||
| - Include Bootstrap 5 via CDN. | ||
| - Link custom CSS (`static/css/style.css`). | ||
| - Define the layout (Navigation, Content Block, Footer). | ||
| - [x] Create `static/css/style.css`: | ||
| - Implement the "Christmas Aesthetic" (Deep Red #8B0000, Forest Green #228B22). | ||
| - Style fonts and general layout. | ||
| - [x] Create `templates/index.html` (Home): | ||
| - Extend `base.html`. | ||
| - Hero section with Santa image placeholder. | ||
| - Grid of Gift Cards showing Title, Category, Rating. | ||
| - [x] Create `templates/submit_gift.html`: | ||
| - Form with fields: Title, Category (Select), Description. | ||
| - [x] Create `templates/gift_detail.html`: | ||
| - Detailed view of the gift. | ||
| - Section for Voting (Snowflake icons). | ||
| - Section for Comments list and "Add Comment" form. | ||
|
|
||
| ## Phase 5: Refinement & Assets | ||
| - [x] Add the "Santa flying on sleigh" hero image to `static/img/` (or use a placeholder/generated one). | ||
| - [x] Add the "Snowflake" icon for ratings (SVG or image). | ||
| - [x] Ensure all forms have proper HTML5 validation attributes (`required`, `maxlength`). | ||
| - [x] Test all routes and flows manually to ensure stability. | ||
|
|
||
| ## Phase 6: Completion & Version Control | ||
| - [ ] Verify application functionality (Submit a gift, vote on it, comment on it, check filters). | ||
| - [ ] Create a `README.md` file: | ||
| - Explain features (Gift submission, Voting, Comments). | ||
| - Instructions to run locally (`python app.py`). | ||
| - Tech stack details. | ||
| - [ ] 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # North Pole Wishlist | ||
|
|
||
| The **North Pole Wishlist** is a community-driven platform designed to help users discover, share, and curate the best gift ideas for the holiday season. It features a festive Christmas theme and allows elves (users) to submit ideas, vote on them ("Naughty or Nice"), and discuss the best presents. | ||
|
|
||
| ## Features | ||
|
|
||
| - **Browse Gifts**: View a curated list of gift ideas, filterable by category (Kids, Parents, Tech, etc.) and sortable by popularity or rating. | ||
| - **Submit Ideas**: Share your own gift suggestions with the community. | ||
| - **Naughty or Nice Voting**: Rate gifts on a scale of 1 to 5 Snowflakes. | ||
| - **Elf Discussion**: Leave comments on gift ideas to share reviews or ask questions. | ||
| - **Festive UI**: A fully themed interface with Santa, snowflakes, and holiday colors. | ||
|
|
||
| ## Tech Stack | ||
|
|
||
| - **Backend**: Python 3, Flask | ||
| - **Database**: SQLite (Development), SQLAlchemy 2.0 ORM | ||
| - **Frontend**: HTML5, Jinja2 Templates, Bootstrap 5, Custom CSS | ||
| - **Icons**: FontAwesome | ||
|
|
||
| ## Local Development 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. **Run the application**: | ||
| ```bash | ||
| python app.py | ||
| ``` | ||
|
|
||
| 5. **Open in Browser**: | ||
| Visit `http://127.0.0.1:5000` to start exploring the wishlist! | ||
|
|
||
| ## Project Structure | ||
|
|
||
| - `app.py`: Main Flask application entry point and route definitions. | ||
| - `models.py`: SQLAlchemy database models (Gift, Vote, Comment). | ||
| - `templates/`: Jinja2 HTML templates. | ||
| - `static/`: CSS styles and images. | ||
| - `instance/`: Contains the SQLite database (`northpole.db`). | ||
|
|
||
| ## License | ||
|
|
||
| Copyright 2025 North Pole Workshop. Made with ❤️ and ❄️. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,129 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from flask import Flask, render_template, request, redirect, url_for, abort | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from flask_sqlalchemy import SQLAlchemy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sqlalchemy.orm import DeclarativeBase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sqlalchemy import select, func, desc | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class Base(DeclarativeBase): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db = SQLAlchemy(model_class=Base) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def create_app(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app = Flask(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///northpole.db') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.init_app(app) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with app.app_context(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import models | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.create_all() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from models import Gift, Vote, Comment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.route('/') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def index(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| category = request.args.get('category') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sort_by = request.args.get('sort', 'newest') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stmt = select(Gift) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if category: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stmt = stmt.where(Gift.category == category) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if sort_by == 'popular': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Sort by vote count | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stmt = stmt.outerjoin(Gift.votes).group_by(Gift.id).order_by(func.count(Vote.id).desc()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif sort_by == 'ranking': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Sort by average score | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stmt = stmt.outerjoin(Gift.votes).group_by(Gift.id).order_by(func.avg(Vote.score).desc()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: # newest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stmt = stmt.order_by(Gift.created_at.desc()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gifts = db.session.scalars(stmt).all() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Calculate average scores for display (could be optimized) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gift_stats = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for gift in gifts: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avg_score = db.session.scalar(select(func.avg(Vote.score)).where(Vote.gift_id == gift.id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vote_count = db.session.scalar(select(func.count(Vote.id)).where(Vote.gift_id == gift.id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gift_stats[gift.id] = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'avg_score': round(avg_score, 1) if avg_score else 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'vote_count': vote_count | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+55
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 current implementation for fetching gift statistics causes an N+1 query problem. It executes two additional database queries for every gift in the list. This can be optimized by computing the statistics for all gifts in a single, more efficient query.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return render_template('index.html', gifts=gifts, stats=gift_stats, current_category=category, current_sort=sort_by) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.route('/gifts/new', methods=['GET', 'POST']) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def submit_gift(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if request.method == 'POST': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title = request.form.get('title') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description = request.form.get('description') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| category = request.form.get('category') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not title or len(title) > 100: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Basic validation, ideally flash error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "Invalid Title", 400 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not description or len(description) > 500: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "Invalid Description", 400 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not category: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "Category required", 400 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new_gift = Gift(title=title, description=description, category=category) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.session.add(new_gift) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.session.commit() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return redirect(url_for('index')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return render_template('submit_gift.html') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.route('/gifts/<int:gift_id>') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def gift_detail(gift_id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gift = db.get_or_404(Gift, gift_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avg_score = db.session.scalar(select(func.avg(Vote.score)).where(Vote.gift_id == gift_id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vote_count = db.session.scalar(select(func.count(Vote.id)).where(Vote.gift_id == gift_id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+86
to
+87
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 queries for
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Get comments ordered by newest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| comments = db.session.scalars(select(Comment).where(Comment.gift_id == gift_id).order_by(Comment.created_at.desc())).all() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stats = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'avg_score': round(avg_score, 1) if avg_score else 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'vote_count': vote_count | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return render_template('gift_detail.html', gift=gift, stats=stats, comments=comments) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.route('/gifts/<int:gift_id>/vote', methods=['POST']) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def vote_gift(gift_id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score = request.form.get('score', type=int) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not score or score < 1 or score > 5: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "Invalid Score", 400 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vote = Vote(gift_id=gift_id, score=score) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.session.add(vote) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.session.commit() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return redirect(url_for('gift_detail', gift_id=gift_id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.route('/gifts/<int:gift_id>/comment', methods=['POST']) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def comment_gift(gift_id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| author = request.form.get('author_name') or "Secret Santa" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content = request.form.get('content') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not content or len(content) < 10 or len(content) > 500: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "Content length must be between 10 and 500 characters", 400 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| comment = Comment(gift_id=gift_id, author_name=author, content=content) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.session.add(comment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.session.commit() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return redirect(url_for('gift_detail', gift_id=gift_id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return app | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app = create_app() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if __name__ == '__main__': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.run(debug=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| from datetime import datetime | ||
| from sqlalchemy import String, Integer, ForeignKey, DateTime | ||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | ||
| from app import db | ||
|
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 A more scalable approach is to initialize the Example ( from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
db = SQLAlchemy(model_class=Base)You would then import |
||
|
|
||
| class Gift(db.Model): | ||
| __tablename__ = 'gift_idea' | ||
|
|
||
| id: Mapped[int] = mapped_column(primary_key=True) | ||
| title: Mapped[str] = mapped_column(String(100), nullable=False) | ||
| description: Mapped[str] = mapped_column(String(500), nullable=False) | ||
| category: Mapped[str] = mapped_column(String(50), nullable=False) | ||
| created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) | ||
|
|
||
| votes: Mapped[list["Vote"]] = relationship(back_populates="gift") | ||
| comments: Mapped[list["Comment"]] = relationship(back_populates="gift") | ||
|
|
||
| class Vote(db.Model): | ||
| __tablename__ = 'vote' | ||
|
|
||
| id: Mapped[int] = mapped_column(primary_key=True) | ||
| gift_id: Mapped[int] = mapped_column(ForeignKey('gift_idea.id'), nullable=False) | ||
| score: Mapped[int] = mapped_column(Integer, nullable=False) | ||
| created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) | ||
|
|
||
| gift: Mapped["Gift"] = relationship(back_populates="votes") | ||
|
|
||
| class Comment(db.Model): | ||
| __tablename__ = 'comment' | ||
|
|
||
| id: Mapped[int] = mapped_column(primary_key=True) | ||
| gift_id: Mapped[int] = mapped_column(ForeignKey('gift_idea.id'), nullable=False) | ||
| author_name: Mapped[str] = mapped_column(String(50), nullable=False, default="Secret Santa") | ||
| content: Mapped[str] = mapped_column(String(500), nullable=False) | ||
| created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) | ||
|
|
||
| gift: Mapped["Gift"] = relationship(back_populates="comments") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Flask | ||
| Flask-SQLAlchemy | ||
| SQLAlchemy |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| :root { | ||
| --christmas-red: #8B0000; | ||
| --christmas-green: #228B22; | ||
| --snow-white: #F8F9FA; | ||
| --gold: #FFD700; | ||
| } | ||
|
|
||
| body { | ||
| background-color: var(--snow-white); | ||
| display: flex; | ||
| flex-direction: column; | ||
| min-height: 100vh; | ||
| } | ||
|
|
||
| .bg-christmas-red { | ||
| background-color: var(--christmas-red) !important; | ||
| } | ||
|
|
||
| .bg-christmas-green { | ||
| background-color: var(--christmas-green) !important; | ||
| } | ||
|
|
||
| .text-christmas-red { | ||
| color: var(--christmas-red) !important; | ||
| } | ||
|
|
||
| .text-christmas-green { | ||
| color: var(--christmas-green) !important; | ||
| } | ||
|
|
||
| .btn-christmas-red { | ||
| background-color: var(--christmas-red); | ||
| border-color: var(--christmas-red); | ||
| } | ||
|
|
||
| .btn-christmas-red:hover { | ||
| background-color: #660000; | ||
| border-color: #660000; | ||
| color: white; | ||
| } | ||
|
|
||
| .font-christmas { | ||
| font-family: 'Mountains of Christmas', cursive; | ||
| font-weight: 700; | ||
| } | ||
|
|
||
| .card { | ||
| border: none; | ||
| box-shadow: 0 4px 8px rgba(0,0,0,0.1); | ||
| transition: transform 0.2s; | ||
| } | ||
|
|
||
| .card:hover { | ||
| transform: translateY(-5px); | ||
| } | ||
|
|
||
| .snowflake-rating { | ||
| color: var(--gold); | ||
| } | ||
|
|
||
| .hero-section { | ||
| background-color: #e9ecef; | ||
| padding: 2rem; | ||
| border-radius: 0.5rem; | ||
| margin-bottom: 2rem; | ||
| background-size: cover; | ||
| background-position: center; | ||
| position: relative; | ||
| color: white; | ||
| text-shadow: 2px 2px 4px rgba(0,0,0,0.7); | ||
| } | ||
|
|
||
| /* Fallback if image not loaded, though we'll try to use one */ | ||
| .hero-overlay { | ||
| background-color: rgba(0, 0, 0, 0.4); | ||
| position: absolute; | ||
| top: 0; | ||
| left: 0; | ||
| width: 100%; | ||
| height: 100%; | ||
| border-radius: 0.5rem; | ||
| z-index: 1; | ||
| } | ||
|
|
||
| .hero-content { | ||
| position: relative; | ||
| z-index: 2; | ||
| } |
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.
🟡 Using a hardcoded default for the
SECRET_KEYis a security risk, as it could be used in a production environment if theSECRET_KEYenvironment variable is not set. For production, the key should always be set as an environment variable and the application should fail to start if it's missing. For development, you can generate a random key.