Files
fitguide/app/web/scopes.py
T

104 lines
3.5 KiB
Python

"""Top-level, read-only scope fit guide at ``/scopes``.
The site's primary view: a table of scopes with their specs, plus one column per
non-Scope part type listing the accessories that fit each scope. Supports HTMX
search/filter over scope attributes and required accessory fits.
"""
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.crud import manufacturer as manufacturer_crud
from app.crud import part_type as part_type_crud
from app.crud import scope_model as scope_model_crud
from app.crud import sub_part_type as sub_part_type_crud
from app.models.user import User
from app.services import scope_guide as scope_guide_service
from app.web.admin import web_require
from app.web.templates import templates
router = APIRouter(include_in_schema=False)
def _to_int(value: str) -> int | None:
return int(value) if value.strip() else None
async def _build(db: AsyncSession, filters: dict) -> dict:
"""Run the guide query for the given raw (string) filter values."""
return await scope_guide_service.build(
db,
description=filters["description"],
manufacturer_id=_to_int(filters["manufacturer_id"]),
scope_model_id=_to_int(filters["scope_model_id"]),
sub_part_type_id=_to_int(filters["sub_part_type_id"]),
fits_part_type_id=_to_int(filters["fits_part_type_id"]),
)
async def _filter_options(db: AsyncSession) -> dict:
"""Dropdown option lists for the filter bar."""
scope_type = await part_type_crud.get_by_name(
db, scope_guide_service.SCOPE_TYPE_NAME
)
sub_part_types = [
spt
for spt in await sub_part_type_crud.list_all(db, limit=1000)
if scope_type is not None and spt.part_type_id == scope_type.id
]
return {
"manufacturers": await manufacturer_crud.list_all(db, limit=1000),
"scope_models": await scope_model_crud.list_all(db, limit=1000),
"sub_part_types": sub_part_types,
}
@router.get("/scopes", response_class=HTMLResponse)
async def scopes_page(
request: Request,
description: str = "",
manufacturer_id: str = "",
scope_model_id: str = "",
sub_part_type_id: str = "",
fits_part_type_id: str = "",
db: AsyncSession = Depends(get_db),
user: User = Depends(web_require("augmented_parts:read", "fits:read")),
):
filters = {
"description": description,
"manufacturer_id": manufacturer_id,
"scope_model_id": scope_model_id,
"sub_part_type_id": sub_part_type_id,
"fits_part_type_id": fits_part_type_id,
}
guide = await _build(db, filters)
return templates.TemplateResponse(
request,
"scopes.html",
{"user": user, "filters": filters, **guide, **(await _filter_options(db))},
)
@router.get("/scopes/filter", response_class=HTMLResponse)
async def scopes_filter(
request: Request,
description: str = "",
manufacturer_id: str = "",
scope_model_id: str = "",
sub_part_type_id: str = "",
fits_part_type_id: str = "",
db: AsyncSession = Depends(get_db),
_: User = Depends(web_require("augmented_parts:read", "fits:read")),
):
filters = {
"description": description,
"manufacturer_id": manufacturer_id,
"scope_model_id": scope_model_id,
"sub_part_type_id": sub_part_type_id,
"fits_part_type_id": fits_part_type_id,
}
guide = await _build(db, filters)
return templates.TemplateResponse(request, "_scopes_table_card.html", guide)