Files
skeleton-api/app/web/scopes.py
T

144 lines
4.7 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, HTTPException, Request, Response, status
from fastapi.responses import HTMLResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.crud import audit as audit_crud
from app.crud import augmented_part as augmented_part_crud
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
def _can_write_scopes(user: User) -> bool:
return user.is_superuser or "augmented_parts:write" in user.permissions
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,
"can_write_scopes": _can_write_scopes(user),
**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: 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, "can_write_scopes": _can_write_scopes(user)},
)
@router.delete("/scopes/{augmented_part_id}")
async def delete_scope(
augmented_part_id: int,
db: AsyncSession = Depends(get_db),
actor: User = Depends(web_require("augmented_parts:write")),
):
ap = await augmented_part_crud.get(db, augmented_part_id)
if ap is None:
raise HTTPException(status_code=404, detail="Scope not found")
label = ap.sku or ap.description
await augmented_part_crud.delete(db, ap)
await audit_crud.log(
db,
actor=actor,
action="delete",
resource_type="AugmentedPart",
resource_id=augmented_part_id,
description=audit_crud.describe_delete(
"AugmentedPart", augmented_part_id, label
),
)
return Response(status_code=status.HTTP_200_OK)