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:
@@ -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())
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user