Adding fits

This commit is contained in:
Ian Roddis committed 2026-07-14 15:24:05 -03:00
1 parent 8185f1f8b3
commit ab39191af5
19 files changed
+1253 -1

No files matched your search

+256
View File
@@ -21,14 +21,18 @@ from app.crud import scope_model as scope_model_crud
from app.crud import attachment_point as attachment_point_crud
from app.crud import attachment_point_name as apn_crud
from app.crud import clamp_connection as clamp_connection_crud
from app.crud import fit as fit_crud
from app.crud import fit_note as fit_note_crud
from app.crud import static_connection as static_connection_crud
from app.crud import sub_part_type as sub_part_type_crud
from app.crud import unc_thread as unc_thread_crud
from app.crud import user as user_crud
from app.deps import get_current_web_user
from app.models.fit import FitLabel
from app.models.unc_thread import UncSeries
from app.models.user import User
from app.services import augmented_part_sync as augmented_part_sync_service
from app.services import fit_matching as fit_matching_service
from app.services import odata as odata_service
from app.web.templates import templates
@@ -1978,6 +1982,258 @@ async def delete_augmented_part_attachment_point(
)
# --- Fits ----------------------------------------------------------------
async def _fit_view(db: AsyncSession, fit, part_id: int) -> dict:
if fit.augmented_part_a_id == part_id:
own_ap, other_ap, other_part_id = (
fit.attachment_point_a,
fit.attachment_point_b,
fit.augmented_part_b_id,
)
else:
own_ap, other_ap, other_part_id = (
fit.attachment_point_b,
fit.attachment_point_a,
fit.augmented_part_a_id,
)
other_part = await augmented_part_crud.get(db, other_part_id)
return {"fit": fit, "own_ap": own_ap, "other_ap": other_ap, "other_part": other_part}
async def _augmented_part_fits_ctx(
db: AsyncSession,
augmented_part_id: int,
*,
message: str | None = None,
error: str | None = None,
) -> dict:
fits = await fit_crud.list_for_part(db, augmented_part_id)
other_parts = [
p
for p in await augmented_part_crud.list_all(db, limit=500)
if p.id != augmented_part_id
]
return {
"augmented_part_id": augmented_part_id,
"fit_views": [await _fit_view(db, f, augmented_part_id) for f in fits],
"own_points": await attachment_point_crud.list_for_augmented_part(
db, augmented_part_id
),
"other_parts": other_parts,
"message": message,
"error": error,
}
@router.get("/augmented-parts/{augmented_part_id}/fits", response_class=HTMLResponse)
async def augmented_part_fits(
request: Request,
augmented_part_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(web_require("fits:read")),
):
ctx = await _augmented_part_fits_ctx(db, augmented_part_id)
return templates.TemplateResponse(request, "admin/_augmented_part_fits.html", ctx)
@router.post(
"/augmented-parts/{augmented_part_id}/fits/recompute", response_class=HTMLResponse
)
async def recompute_augmented_part_fits(
request: Request,
augmented_part_id: int,
db: AsyncSession = Depends(get_db),
actor: User = Depends(web_require("fits:write")),
):
result = await fit_matching_service.recompute_for_part(db, augmented_part_id)
await audit_crud.log(
db,
actor=actor,
action="update",
resource_type="Fit",
resource_id=augmented_part_id,
description=(
f"Recomputed fits for AugmentedPart #{augmented_part_id}: "
f"{result.created} created, {result.removed} removed"
),
)
ctx = await _augmented_part_fits_ctx(
db,
augmented_part_id,
message=f"{result.created} created, {result.removed} removed.",
)
return templates.TemplateResponse(request, "admin/_augmented_part_fits.html", ctx)
@router.get(
"/augmented-parts/{augmented_part_id}/fits/other-points",
response_class=HTMLResponse,
)
async def fit_other_points(
request: Request,
augmented_part_id: int,
other_augmented_part_id: str = "",
db: AsyncSession = Depends(get_db),
_: User = Depends(web_require("fits:write")),
):
points = (
await attachment_point_crud.list_for_augmented_part(
db, int(other_augmented_part_id)
)
if other_augmented_part_id
else []
)
return templates.TemplateResponse(
request, "admin/_fit_other_points_select.html", {"points": points}
)
@router.post("/augmented-parts/{augmented_part_id}/fits", response_class=HTMLResponse)
async def create_augmented_part_fit(
request: Request,
augmented_part_id: int,
own_attachment_point_id: int = Form(...),
other_attachment_point_id: str = Form(""),
db: AsyncSession = Depends(get_db),
actor: User = Depends(web_require("fits:write")),
):
error = None
if not other_attachment_point_id:
error = "Select the other part's attachment point."
else:
ap_a = await attachment_point_crud.get(db, own_attachment_point_id)
ap_b = await attachment_point_crud.get(db, int(other_attachment_point_id))
if ap_a is None or ap_b is None:
error = "Attachment point not found."
elif ap_a.augmented_part_id == ap_b.augmented_part_id:
error = "A part cannot fit itself."
elif await fit_crud.get_by_attachment_points(db, ap_a, ap_b) is not None:
error = "That fit already exists."
else:
fit = await fit_crud.create(db, ap_a=ap_a, ap_b=ap_b, label=FitLabel.EXACT)
await audit_crud.log(
db,
actor=actor,
action="create",
resource_type="Fit",
resource_id=fit.id,
description=audit_crud.describe_create(
"Fit", fit.id, f"attachment points {ap_a.id}↔{ap_b.id}"
),
)
ctx = await _augmented_part_fits_ctx(db, augmented_part_id, error=error)
return templates.TemplateResponse(request, "admin/_augmented_part_fits.html", ctx)
@router.post(
"/augmented-parts/{augmented_part_id}/fits/{fit_id}/label",
response_class=HTMLResponse,
)
async def update_augmented_part_fit_label(
request: Request,
augmented_part_id: int,
fit_id: int,
label: FitLabel = Form(...),
db: AsyncSession = Depends(get_db),
actor: User = Depends(web_require("fits:write")),
):
fit = await fit_crud.get(db, fit_id)
if fit is None:
raise HTTPException(status_code=404, detail="Fit not found")
fit = await fit_crud.set_label(db, fit, label)
await audit_crud.log(
db,
actor=actor,
action="update",
resource_type="Fit",
resource_id=fit.id,
description=audit_crud.describe_update("Fit", fit.id, {"label": label.value}),
)
view = await _fit_view(db, fit, augmented_part_id)
return templates.TemplateResponse(
request,
"admin/_augmented_part_fit_row.html",
{"augmented_part_id": augmented_part_id, **view},
)
@router.post(
"/augmented-parts/{augmented_part_id}/fits/{fit_id}/notes",
response_class=HTMLResponse,
)
async def create_augmented_part_fit_note(
request: Request,
augmented_part_id: int,
fit_id: int,
body: str = Form(...),
db: AsyncSession = Depends(get_db),
actor: User = Depends(web_require("fits:write")),
):
fit = await fit_crud.get(db, fit_id)
if fit is None:
raise HTTPException(status_code=404, detail="Fit not found")
if body.strip():
await fit_note_crud.create(db, fit=fit, author=actor, body=body.strip())
await db.refresh(fit, attribute_names=["notes"])
view = await _fit_view(db, fit, augmented_part_id)
return templates.TemplateResponse(
request,
"admin/_augmented_part_fit_row.html",
{"augmented_part_id": augmented_part_id, **view},
)
@router.delete(
"/augmented-parts/{augmented_part_id}/fits/{fit_id}", response_class=HTMLResponse
)
async def delete_augmented_part_fit(
request: Request,
augmented_part_id: int,
fit_id: int,
db: AsyncSession = Depends(get_db),
actor: User = Depends(web_require("fits:delete")),
):
fit = await fit_crud.get(db, fit_id)
if fit is not None:
label = f"attachment points {fit.attachment_point_a_id}↔{fit.attachment_point_b_id}"
await fit_crud.delete(db, fit)
await audit_crud.log(
db,
actor=actor,
action="delete",
resource_type="Fit",
resource_id=fit_id,
description=audit_crud.describe_delete("Fit", fit_id, label),
)
ctx = await _augmented_part_fits_ctx(db, augmented_part_id)
return templates.TemplateResponse(request, "admin/_augmented_part_fits.html", ctx)
@router.post("/augmented-parts/fits/recompute", response_class=HTMLResponse)
async def recompute_all_fits(
request: Request,
db: AsyncSession = Depends(get_db),
actor: User = Depends(web_require("fits:write")),
):
result = await fit_matching_service.recompute_all(db)
await audit_crud.log(
db,
actor=actor,
action="update",
resource_type="Fit",
resource_id=None,
description=(
f"Recomputed fits for all parts: {result.created} created, "
f"{result.removed} removed"
),
)
return HTMLResponse(
f"Recompute complete: {result.created} created, {result.removed} removed."
)
# --- Part Colours ------------------------------------------------------------