Add implementation plan for product selection landing page (Feature 002)
Completed planning phases 0 and 1 for simple landing page feature. ## Plan Overview: **Approach**: Minimal addition to existing Flask app - reuse Product model, add one route, one template. No new dependencies or complexity. **Constitution Check**: ✅ All 5 principles satisfied - Specification-first development (spec.md complete) - Test-first discipline (TDD workflow defined) - Independent user stories (3 stories, all independently testable) - Simplicity (reuses existing Flask/Jinja2/Product architecture) - Documentation as code (all artifacts in specs/002-product-list/) ## Artifacts Created: ### Phase 0: Research (research.md) - Reuses infrastructure from feature 001 (Flask, Jinja2, file storage) - Single new decision: Product.load_active() method for filtering/sorting - Performance analysis: <100ms for 100 products (well under 1s target) ### Phase 1: Design & Contracts - **data-model.md**: Documents Product model extension (load_active method) - **contracts/landing-page.yaml**: OpenAPI contract for GET / route - **quickstart.md**: Developer implementation guide with: - Step-by-step implementation checklist - Code snippets for route, template, tests - TDD workflow (write tests → verify fail → implement → pass) - Manual verification checklist ### Agent Context - Updated CLAUDE.md with feature technologies (no new tech added) ## Implementation Summary: **New Files** (to be created): - app/routes/landing.py - Landing page route handler - app/templates/landing/index.html - Product list template - tests/contract/test_landing_routes.py - Contract tests (6 scenarios) - tests/integration/test_landing_flow.py - User journey test **Modified Files**: - app/models/product.py - Add load_active() class method - app/__init__.py - Register landing blueprint ## Key Technical Decisions: 1. **Filtering**: status=='active' AND submission_url_slug exists 2. **Sorting**: Alphabetical by name (case-insensitive), then product_id 3. **Empty State**: "No products are currently accepting feedback" message 4. **XSS Prevention**: Jinja2 auto-escaping (no manual escaping needed) 5. **Performance**: File I/O sufficient (<1s for 100 products, no caching) ## Next Steps: 1. Run /speckit.tasks to generate tasks.md 2. Run /speckit.implement to execute TDD workflow 3. Verify all tests pass 4. Manual verification checklist 5. Create pull request 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
# Quickstart: Product Selection Landing Page
|
||||
|
||||
**Feature**: 002-product-list | **For**: Developers implementing this feature
|
||||
|
||||
## Overview
|
||||
|
||||
Add a landing page at `/` that lists all active products for feedback submission. This is a simple addition to the existing Flask app: one route, one template, and corresponding tests.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### 1. Extend Product Model
|
||||
|
||||
**File**: `app/models/product.py`
|
||||
|
||||
**Add this class method**:
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def load_active(cls):
|
||||
"""Load all active products, sorted alphabetically by name then product_id.
|
||||
|
||||
Returns:
|
||||
list[Product]: Active products with valid submission_url_slug, sorted.
|
||||
"""
|
||||
all_products = cls.load_all()
|
||||
active = [p for p in all_products
|
||||
if p.status == 'active' and p.submission_url_slug]
|
||||
return sorted(active, key=lambda p: (p.name.lower(), p.product_id))
|
||||
```
|
||||
|
||||
**Why**: Centralizes filtering and sorting logic. Reusable if other features need active product lists.
|
||||
|
||||
---
|
||||
|
||||
### 2. Create Landing Route
|
||||
|
||||
**File**: `app/routes/landing.py` (new file)
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```python
|
||||
from flask import Blueprint, render_template, current_app
|
||||
from app.models.product import Product
|
||||
|
||||
landing_bp = Blueprint('landing', __name__)
|
||||
|
||||
@landing_bp.route('/')
|
||||
def index():
|
||||
"""Landing page showing all active products."""
|
||||
try:
|
||||
products = Product.load_active()
|
||||
current_app.logger.info(
|
||||
f'Landing page accessed: {len(products)} active products'
|
||||
)
|
||||
return render_template('landing/index.html', products=products)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Error loading landing page: {e}', exc_info=True)
|
||||
return render_template('landing/index.html', products=[])
|
||||
```
|
||||
|
||||
**Register blueprint** in `app/__init__.py`:
|
||||
|
||||
```python
|
||||
from app.routes.landing import landing_bp
|
||||
app.register_blueprint(landing_bp)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Create Landing Template
|
||||
|
||||
**File**: `app/templates/landing/index.html` (new file)
|
||||
|
||||
**Template structure**:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Select a Product - Reklamator</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
|
||||
h1 { color: #333; }
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 4px; }
|
||||
a { font-size: 1.2em; color: #0066cc; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
p { margin: 5px 0 0 0; color: #666; }
|
||||
.empty-state { color: #666; padding: 20px; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Select a Product for Feedback</h1>
|
||||
|
||||
{% if products %}
|
||||
<ul>
|
||||
{% for product in products %}
|
||||
<li>
|
||||
<a href="/submit/{{ product.submission_url_slug }}">
|
||||
{{ product.name }}
|
||||
</a>
|
||||
{% if product.description %}
|
||||
<p>{{ product.description }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="empty-state">
|
||||
No products are currently accepting feedback. Please check back later.
|
||||
</p>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Key points**:
|
||||
- Jinja2 auto-escaping prevents XSS (product.name, product.description)
|
||||
- No JavaScript (pure server-side rendering)
|
||||
- Minimal inline CSS (no frameworks)
|
||||
- Conditional rendering for empty state
|
||||
- Only shows description if present (no placeholder text)
|
||||
|
||||
---
|
||||
|
||||
### 4. Write Contract Tests
|
||||
|
||||
**File**: `tests/contract/test_landing_routes.py` (new file)
|
||||
|
||||
**Test scenarios to implement**:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import os
|
||||
import yaml
|
||||
from app.models.product import Product
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_get_landing_page_with_products(client, temp_data_dir):
|
||||
"""T301: GET / returns 200 with product list"""
|
||||
# Setup: Create 2 active products
|
||||
# Assert: 200 OK, both products in HTML
|
||||
pass
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_get_landing_page_no_products(client, temp_data_dir):
|
||||
"""T302: GET / with no active products shows empty state"""
|
||||
# Assert: 200 OK, contains "No products are currently accepting feedback"
|
||||
pass
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_get_landing_page_filters_archived(client, temp_data_dir):
|
||||
"""T303: GET / excludes archived products"""
|
||||
# Setup: 1 active, 1 archived
|
||||
# Assert: Only active product shown
|
||||
pass
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_get_landing_page_sorting(client, temp_data_dir):
|
||||
"""T304: GET / sorts products alphabetically (name, then product_id)"""
|
||||
# Setup: Products with names "Zebra", "Apple", "apple" (different product_ids)
|
||||
# Assert: Correct alphabetical order
|
||||
pass
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_get_landing_page_xss_prevention(client, temp_data_dir):
|
||||
"""T305: GET / escapes HTML in product names"""
|
||||
# Setup: Product with name "<script>alert('xss')</script>"
|
||||
# Assert: HTML is escaped, script not executed
|
||||
pass
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_get_landing_page_missing_slug(client, temp_data_dir):
|
||||
"""T306: GET / excludes products with missing submission_url_slug"""
|
||||
# Setup: Product with submission_url_slug = None
|
||||
# Assert: Product not shown in list
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Write Integration Tests
|
||||
|
||||
**File**: `tests/integration/test_landing_flow.py` (new file)
|
||||
|
||||
**User journey test**:
|
||||
|
||||
```python
|
||||
@pytest.mark.integration
|
||||
def test_landing_to_submission_flow(client, temp_data_dir):
|
||||
"""T307: Complete flow - landing page → product selection → submission form"""
|
||||
# Step 1: Visit landing page, see products
|
||||
# Step 2: Click product link
|
||||
# Step 3: Verify redirected to /submit/{slug}
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Implementation Notes
|
||||
|
||||
### XSS Prevention
|
||||
- ✅ Jinja2 auto-escaping handles product names and descriptions
|
||||
- ✅ No manual HTML escaping needed
|
||||
- ✅ Test with `<script>` tags in product names to verify
|
||||
|
||||
### Empty State Handling
|
||||
- ✅ Check `{% if products %}` in template
|
||||
- ✅ Display message: "No products are currently accepting feedback. Please check back later."
|
||||
- ✅ No blank page or error
|
||||
|
||||
### Logging
|
||||
- ✅ Log landing page access with product count
|
||||
- ✅ Log errors if product loading fails
|
||||
- ✅ Use `current_app.logger.info()` for access logs
|
||||
|
||||
### Backwards Compatibility
|
||||
- ✅ Existing `/submit/{slug}` routes unchanged
|
||||
- ✅ Direct product URLs still work
|
||||
- ✅ Landing page is additive only
|
||||
|
||||
### Performance
|
||||
- ✅ Target: <1 second for up to 100 products
|
||||
- ✅ File I/O ~10-50ms for 100 YAML files
|
||||
- ✅ No caching needed for MVP
|
||||
|
||||
---
|
||||
|
||||
## Testing Workflow (TDD)
|
||||
|
||||
**Follow this order** (Constitution Principle II):
|
||||
|
||||
1. **Write contract tests** (test_landing_routes.py) - all should FAIL
|
||||
2. **Verify tests fail** - proves they test something meaningful
|
||||
3. **Implement Product.load_active()** method
|
||||
4. **Implement landing route** (landing.py)
|
||||
5. **Create landing template** (index.html)
|
||||
6. **Run tests** - contract tests should PASS
|
||||
7. **Write integration tests** (test_landing_flow.py) - should FAIL
|
||||
8. **Fix any issues** - integration tests should PASS
|
||||
9. **Refactor** while keeping tests green
|
||||
|
||||
---
|
||||
|
||||
## Manual Verification Checklist
|
||||
|
||||
After all tests pass, manually verify:
|
||||
|
||||
- [ ] Visit `/` - see product list or empty state
|
||||
- [ ] Click product link - redirected to `/submit/{slug}`
|
||||
- [ ] Check with 0 active products - see empty message
|
||||
- [ ] Check with 1 active product - see single product
|
||||
- [ ] Check with 10+ active products - alphabetical order
|
||||
- [ ] Check product with no description - no placeholder text
|
||||
- [ ] Check product with long name - proper wrapping
|
||||
- [ ] Check as anonymous user - page accessible
|
||||
- [ ] Check as authenticated user - same page shown
|
||||
- [ ] Check page source - no JavaScript present
|
||||
|
||||
---
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
✅ All contract tests passing
|
||||
✅ All integration tests passing
|
||||
✅ Product.load_active() method implemented
|
||||
✅ Landing route registered and functional
|
||||
✅ Landing template created with proper escaping
|
||||
✅ Manual verification completed
|
||||
✅ Code follows existing Flask/Jinja2 patterns
|
||||
✅ No new dependencies added
|
||||
✅ Documentation updated (this file)
|
||||
|
||||
**Next**: Commit to branch `002-product-list` and create pull request
|
||||
Reference in New Issue
Block a user