Files
Reklamator/specs/002-product-list/quickstart.md
T

277 lines
8.1 KiB
Markdown
Raw Normal View History

# 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