Task 4.7: Update template helpers to use key identifier

- Add series context helpers: prepare_series_context, get_series_by_key, filter_series_by_missing_episodes
- Update module docstring with identifier convention documentation
- Add unit tests for new series context helper functions
- Update infrastructure.md with template helpers documentation
- Mark Phase 4 (API Layer) as complete
This commit is contained in:
2025-11-28 16:00:15 +01:00
parent 014e22390e
commit 5934c7666c
4 changed files with 329 additions and 55 deletions

View File

@@ -3,13 +3,25 @@ Template integration utilities for FastAPI application.
This module provides utilities for template rendering with common context
and helper functions.
Series Identifier Convention:
- `key`: Primary identifier for all series operations
(URL-safe, e.g., "attack-on-titan")
- `folder`: Metadata only for filesystem operations and display
(e.g., "Attack on Titan (2013)")
All template helpers that handle series data use `key` for identification and
provide `folder` as display metadata only.
"""
import logging
from pathlib import Path
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
from fastapi import Request
from fastapi.templating import Jinja2Templates
logger = logging.getLogger(__name__)
# Configure templates directory
TEMPLATES_DIR = Path(__file__).parent.parent / "web" / "templates"
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
@@ -82,15 +94,124 @@ def validate_template_exists(template_name: str) -> bool:
def list_available_templates() -> list[str]:
"""
Get list of all available template files.
Returns:
List of template file names
"""
if not TEMPLATES_DIR.exists():
return []
return [
f.name
for f in TEMPLATES_DIR.glob("*.html")
if f.is_file()
]
def prepare_series_context(
series_data: List[Dict[str, Any]],
sort_by: str = "name"
) -> List[Dict[str, Any]]:
"""
Prepare series data for template rendering.
This function ensures series data follows the identifier convention:
- `key` is used as the primary identifier for all operations
- `folder` is included as metadata for display purposes
Args:
series_data: List of series dictionaries from the API
sort_by: Field to sort by ("name", "key", or "folder")
Returns:
List of series dictionaries prepared for template use
Raises:
ValueError: If series_data contains items without required 'key' field
Example:
>>> series = [
... {"key": "attack-on-titan", "name": "Attack on Titan",
... "folder": "Attack on Titan (2013)"},
... {"key": "one-piece", "name": "One Piece",
... "folder": "One Piece (1999)"}
... ]
>>> prepared = prepare_series_context(series, sort_by="name")
"""
if not series_data:
return []
prepared = []
for series in series_data:
if "key" not in series:
logger.warning(
"Series data missing 'key' field: %s",
series.get("name", "unknown")
)
continue
prepared_item = {
"key": series["key"],
"name": series.get("name", series["key"]),
"folder": series.get("folder", ""),
**{k: v for k, v in series.items()
if k not in ("key", "name", "folder")}
}
prepared.append(prepared_item)
# Sort by specified field
if sort_by in ("name", "key", "folder"):
prepared.sort(key=lambda x: x.get(sort_by, "").lower())
return prepared
def get_series_by_key(
series_data: List[Dict[str, Any]],
key: str
) -> Optional[Dict[str, Any]]:
"""
Find a series in the data by its key.
Uses `key` as the identifier (not `folder`) following the project
identifier convention.
Args:
series_data: List of series dictionaries
key: The unique series key to search for
Returns:
The series dictionary if found, None otherwise
Example:
>>> series = [{"key": "attack-on-titan", "name": "Attack on Titan"}]
>>> result = get_series_by_key(series, "attack-on-titan")
>>> result["name"]
'Attack on Titan'
"""
for series in series_data:
if series.get("key") == key:
return series
return None
def filter_series_by_missing_episodes(
series_data: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
Filter series to only include those with missing episodes.
Args:
series_data: List of series dictionaries with 'missing_episodes' field
Returns:
Filtered list containing only series with missing episodes
Note:
Identification uses `key`, not `folder`.
"""
return [
series for series in series_data
if series.get("missing_episodes")
and any(episodes for episodes in series["missing_episodes"].values())
]