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,147 @@
|
||||
# Data Model: Product Selection Landing Page
|
||||
|
||||
**Feature**: 002-product-list | **Date**: 2025-10-17
|
||||
|
||||
## Overview
|
||||
|
||||
This feature reuses the existing **Product** entity from feature 001-build-an-application with a minor extension. No new entities are created.
|
||||
|
||||
## Existing Entity: Product
|
||||
|
||||
**Source**: `app/models/product.py` (from feature 001)
|
||||
|
||||
**Attributes** (relevant to this feature):
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `product_id` | str | Yes | Unique identifier (e.g., "001-acme-app") |
|
||||
| `name` | str | Yes | Display name for the product |
|
||||
| `submission_url_slug` | str | Yes | URL segment for submission form |
|
||||
| `status` | str | Yes | "active" or "archived" |
|
||||
| `description` | str | No | Brief product description (optional) |
|
||||
|
||||
**Storage**: File-based YAML at `data/products/{product-id}/config.yaml`
|
||||
|
||||
**Example**:
|
||||
```yaml
|
||||
product_id: "001-acme-app"
|
||||
name: "Acme Application"
|
||||
submission_url_slug: "acme-app"
|
||||
status: "active"
|
||||
description: "Enterprise resource planning system"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model Extension
|
||||
|
||||
### New Class Method: `load_active()`
|
||||
|
||||
**Purpose**: Load and return only active products with valid submission URLs, sorted for display.
|
||||
|
||||
**Signature**:
|
||||
```python
|
||||
@classmethod
|
||||
def load_active(cls) -> list[Product]:
|
||||
"""Load all active products, sorted alphabetically by name then product_id."""
|
||||
```
|
||||
|
||||
**Returns**: List of Product instances where:
|
||||
- `status == 'active'`
|
||||
- `submission_url_slug` is present and valid
|
||||
- Sorted by: `(name.lower(), product_id)`
|
||||
|
||||
**Implementation Location**: `app/models/product.py`
|
||||
|
||||
**Usage Example**:
|
||||
```python
|
||||
from app.models.product import Product
|
||||
|
||||
# In route handler
|
||||
products = Product.load_active()
|
||||
# Returns sorted list of active products ready for display
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Landing Page Load Sequence
|
||||
|
||||
1. **Request**: User navigates to `/`
|
||||
2. **Route Handler**: `app/routes/landing.py`
|
||||
- Calls `Product.load_active()`
|
||||
3. **Data Loading**: Product model
|
||||
- Reads all `data/products/*/config.yaml` files
|
||||
- Filters: `status == 'active'` AND `submission_url_slug` exists
|
||||
- Sorts: By `(name.lower(), product_id)`
|
||||
4. **Response**: Render template with product list
|
||||
- Pass `products` to `templates/landing/index.html`
|
||||
- Template loops over products, displays name/description/link
|
||||
|
||||
### Diagram
|
||||
|
||||
```
|
||||
User → GET / → landing.py → Product.load_active() → [Product, Product, ...]
|
||||
↓
|
||||
Templates (Jinja2) → HTML Response → User
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Product Visibility (for landing page)
|
||||
|
||||
A product is **visible** on the landing page if and only if:
|
||||
1. `status == 'active'` (FR-003)
|
||||
2. `submission_url_slug` is not None/empty (FR-015)
|
||||
|
||||
Products failing either condition are **excluded** from the list.
|
||||
|
||||
### Sorting Rules (FR-009)
|
||||
|
||||
Products are sorted by:
|
||||
1. **Primary**: `name` (case-insensitive alphabetical)
|
||||
2. **Secondary**: `product_id` (alphabetical, for ties)
|
||||
|
||||
**Examples**:
|
||||
- Input: `[{"name": "Zebra", "product_id": "001"}, {"name": "Apple", "product_id": "002"}]`
|
||||
- Output: `[{"name": "Apple", ...}, {"name": "Zebra", ...}]`
|
||||
|
||||
- Input: `[{"name": "App", "product_id": "002"}, {"name": "App", "product_id": "001"}]`
|
||||
- Output: `[{"name": "App", "product_id": "001"}, {"name": "App", "product_id": "002"}]`
|
||||
|
||||
---
|
||||
|
||||
## No New Entities
|
||||
|
||||
This feature introduces **no new entities**. All data structures are reused from feature 001:
|
||||
- Product entity (extended with `load_active()` method only)
|
||||
- File-based YAML storage (unchanged)
|
||||
- No database tables, no new data files
|
||||
|
||||
---
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
### Test Data Requirements
|
||||
|
||||
For comprehensive testing, create products with:
|
||||
- Various statuses: "active", "archived"
|
||||
- With/without descriptions
|
||||
- With/without valid `submission_url_slug`
|
||||
- Duplicate names (to test secondary sort)
|
||||
- Various name cases ("Apple", "apple", "APPLE")
|
||||
|
||||
### Expected Behaviors
|
||||
|
||||
| Scenario | Expected Result |
|
||||
|----------|----------------|
|
||||
| Product with `status: active` | Included in list |
|
||||
| Product with `status: archived` | Excluded from list |
|
||||
| Product with missing `submission_url_slug` | Excluded from list |
|
||||
| Products with same name | Sorted by product_id |
|
||||
| Empty product directory | Returns empty list |
|
||||
|
||||
**Reference Tests**: See `tests/contract/test_landing_routes.py` for data model validation tests.
|
||||
Reference in New Issue
Block a user