148 lines
4.3 KiB
Markdown
148 lines
4.3 KiB
Markdown
# 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.
|