From d67b4ceb65b21f5bdf44874d681a9256baa81918 Mon Sep 17 00:00:00 2001 From: Ian Roddis <31021769+iroddis@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:13:09 -0300 Subject: [PATCH] Adding ability to add attachment points when editing augmented part --- app/crud/attachment_point.py | 11 ++ app/static/css/style.css | 34 ++++ .../admin/_augmented_part_ap_row.html | 36 ++++ .../admin/_augmented_part_ap_row_edit.html | 45 +++++ .../_augmented_part_attachment_points.html | 38 ++++ .../admin/_augmented_part_row_edit.html | 8 +- app/web/admin.py | 184 ++++++++++++++++++ 7 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 app/templates/admin/_augmented_part_ap_row.html create mode 100644 app/templates/admin/_augmented_part_ap_row_edit.html create mode 100644 app/templates/admin/_augmented_part_attachment_points.html diff --git a/app/crud/attachment_point.py b/app/crud/attachment_point.py index 17c146e..948ba84 100644 --- a/app/crud/attachment_point.py +++ b/app/crud/attachment_point.py @@ -16,6 +16,17 @@ async def get(db: AsyncSession, attachment_point_id: int) -> AttachmentPoint | N return await db.get(AttachmentPoint, attachment_point_id) +async def list_for_augmented_part( + db: AsyncSession, augmented_part_id: int +) -> list[AttachmentPoint]: + result = await db.execute( + select(AttachmentPoint) + .where(AttachmentPoint.augmented_part_id == augmented_part_id) + .order_by(AttachmentPoint.id) + ) + return list(result.scalars().all()) + + async def list_all( db: AsyncSession, skip: int = 0, limit: int = 100 ) -> list[AttachmentPoint]: diff --git a/app/static/css/style.css b/app/static/css/style.css index 4ef253a..1e4cc20 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -366,6 +366,40 @@ input[type=password] { align-items: end; } +/* Inline attachment-points sub-section inside an edit row */ +.ap-inline-section { + border-top: 1px solid var(--border); + margin-top: 1rem; + padding-top: 0.75rem; +} + +.ap-inline-section h3 { + margin: 0 0 0.5rem; + font-size: 0.8rem; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.ap-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.3rem 0; + border-bottom: 1px solid var(--border); + font-size: 0.9rem; +} + +.ap-item:last-of-type { + border-bottom: none; + margin-bottom: 0.5rem; +} + +.ap-item-name { font-weight: 500; flex: 0 0 auto; } +.ap-item-type { color: var(--muted); flex: 0 0 auto; font-size: 0.8rem; } +.ap-item-conn { flex: 1; } +.ap-item .row-actions { margin-left: auto; } + /* Buttons */ .btn { font: inherit; diff --git a/app/templates/admin/_augmented_part_ap_row.html b/app/templates/admin/_augmented_part_ap_row.html new file mode 100644 index 0000000..c622b4d --- /dev/null +++ b/app/templates/admin/_augmented_part_ap_row.html @@ -0,0 +1,36 @@ +{% if apoint.iso_thread %} + {% set conn_type = "ISO" %} + {% set conn_label = "M" ~ apoint.iso_thread.diameter_mm ~ "×" ~ apoint.iso_thread.pitch_mm %} +{% elif apoint.unc_thread %} + {% set conn_type = "UNC" %} + {% set conn_label = apoint.unc_thread.diameter_fraction ~ "-" ~ apoint.unc_thread.tpi ~ " " ~ apoint.unc_thread.series.value %} +{% elif apoint.clamp_connection %} + {% set conn_type = "Clamp" %} + {% if apoint.clamp_connection.min_mm is not none %} + {% set conn_label = apoint.clamp_connection.min_mm ~ "–" ~ apoint.clamp_connection.max_mm ~ " mm" %} + {% else %} + {% set conn_label = apoint.clamp_connection.min_in ~ "–" ~ apoint.clamp_connection.max_in ~ " in" %} + {% endif %} +{% elif apoint.static_connection %} + {% set conn_type = "Static" %} + {% set conn_label = apoint.static_connection.od_mm ~ " mm OD" %} +{% else %} + {% set conn_type = "—" %} + {% set conn_label = "—" %} +{% endif %} +
+ {{ apoint.attachment_point_name.name }} + {{ conn_type }} + {{ conn_label }} +
+ + +
+
diff --git a/app/templates/admin/_augmented_part_ap_row_edit.html b/app/templates/admin/_augmented_part_ap_row_edit.html new file mode 100644 index 0000000..526b2ca --- /dev/null +++ b/app/templates/admin/_augmented_part_ap_row_edit.html @@ -0,0 +1,45 @@ +{% if apoint.iso_thread_id %}{% set current_type = "iso_thread" %} +{% elif apoint.unc_thread_id %}{% set current_type = "unc_thread" %} +{% elif apoint.clamp_connection_id %}{% set current_type = "clamp_connection" %} +{% elif apoint.static_connection_id %}{% set current_type = "static_connection" %} +{% else %}{% set current_type = "" %}{% endif %} +{# _attachment_point_connection_select.html expects `ap` for pre-selection #} +{% set ap = apoint %} +{% set connection_type = current_type %} +
+
+ + +
+ {% include "admin/_attachment_point_connection_select.html" %} +
+
+ + +
+
+
diff --git a/app/templates/admin/_augmented_part_attachment_points.html b/app/templates/admin/_augmented_part_attachment_points.html new file mode 100644 index 0000000..5d16ab3 --- /dev/null +++ b/app/templates/admin/_augmented_part_attachment_points.html @@ -0,0 +1,38 @@ +
+

Attachment Points

+ + {% for apoint in attachment_points_list %} + {% include "admin/_augmented_part_ap_row.html" %} + {% else %} +

No attachment points yet.

+ {% endfor %} + +
+ + +
+ +
+
diff --git a/app/templates/admin/_augmented_part_row_edit.html b/app/templates/admin/_augmented_part_row_edit.html index 575be13..773900b 100644 --- a/app/templates/admin/_augmented_part_row_edit.html +++ b/app/templates/admin/_augmented_part_row_edit.html @@ -1,6 +1,7 @@ -
@@ -52,5 +53,10 @@ hx-swap="outerHTML">Cancel
+ +
+
diff --git a/app/web/admin.py b/app/web/admin.py index f1d7181..0c6a411 100644 --- a/app/web/admin.py +++ b/app/web/admin.py @@ -1385,6 +1385,190 @@ async def delete_augmented_part( return Response(status_code=status.HTTP_200_OK) +async def _augmented_part_ap_ctx(db: AsyncSession, augmented_part_id: int) -> dict: + return { + "augmented_part_id": augmented_part_id, + "attachment_points_list": await attachment_point_crud.list_for_augmented_part( + db, augmented_part_id + ), + "attachment_point_names": await apn_crud.list_all(db), + "iso_threads": await iso_thread_crud.list_all(db), + "unc_threads": await unc_thread_crud.list_all(db), + "clamp_connections": await clamp_connection_crud.list_all(db), + "static_connections": await static_connection_crud.list_all(db), + } + + +@router.get( + "/augmented-parts/{augmented_part_id}/attachment-points", response_class=HTMLResponse +) +async def augmented_part_attachment_points( + request: Request, + augmented_part_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("attachment_points:read")), +): + ctx = await _augmented_part_ap_ctx(db, augmented_part_id) + return templates.TemplateResponse( + request, "admin/_augmented_part_attachment_points.html", ctx + ) + + +@router.post( + "/augmented-parts/{augmented_part_id}/attachment-points", response_class=HTMLResponse +) +async def create_augmented_part_attachment_point( + request: Request, + augmented_part_id: int, + attachment_point_name_id: int = Form(...), + connection_type: str = Form(""), + iso_thread_id: str = Form(""), + unc_thread_id: str = Form(""), + clamp_connection_id: str = Form(""), + static_connection_id: str = Form(""), + db: AsyncSession = Depends(get_db), + actor: User = Depends(web_require("attachment_points:write")), +): + apoint = await attachment_point_crud.create( + db, + augmented_part_id=augmented_part_id, + attachment_point_name_id=attachment_point_name_id, + iso_thread_id=int(iso_thread_id) if iso_thread_id else None, + unc_thread_id=int(unc_thread_id) if unc_thread_id else None, + clamp_connection_id=int(clamp_connection_id) if clamp_connection_id else None, + static_connection_id=int(static_connection_id) if static_connection_id else None, + ) + await audit_crud.log( + db, actor=actor, action="create", resource_type="AttachmentPoint", + resource_id=apoint.id, + description=audit_crud.describe_create( + "AttachmentPoint", apoint.id, + f"part {augmented_part_id}" + ), + ) + ctx = await _augmented_part_ap_ctx(db, augmented_part_id) + return templates.TemplateResponse( + request, "admin/_augmented_part_attachment_points.html", ctx + ) + + +@router.get( + "/augmented-parts/{augmented_part_id}/attachment-points/{attachment_point_id}/edit", + response_class=HTMLResponse, +) +async def edit_augmented_part_attachment_point( + request: Request, + augmented_part_id: int, + attachment_point_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("attachment_points:write")), +): + apoint = await attachment_point_crud.get(db, attachment_point_id) + if apoint is None: + raise HTTPException(status_code=404, detail="Attachment point not found") + ctx = await _augmented_part_ap_ctx(db, augmented_part_id) + ctx["apoint"] = apoint + if apoint.iso_thread_id: + ctx["connection_type"] = "iso_thread" + elif apoint.unc_thread_id: + ctx["connection_type"] = "unc_thread" + elif apoint.clamp_connection_id: + ctx["connection_type"] = "clamp_connection" + elif apoint.static_connection_id: + ctx["connection_type"] = "static_connection" + else: + ctx["connection_type"] = "" + return templates.TemplateResponse( + request, "admin/_augmented_part_ap_row_edit.html", ctx + ) + + +@router.get( + "/augmented-parts/{augmented_part_id}/attachment-points/{attachment_point_id}/row", + response_class=HTMLResponse, +) +async def augmented_part_attachment_point_row( + request: Request, + augmented_part_id: int, + attachment_point_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("attachment_points:read")), +): + apoint = await attachment_point_crud.get(db, attachment_point_id) + if apoint is None: + raise HTTPException(status_code=404, detail="Attachment point not found") + return templates.TemplateResponse( + request, "admin/_augmented_part_ap_row.html", {"apoint": apoint} + ) + + +@router.post( + "/augmented-parts/{augmented_part_id}/attachment-points/{attachment_point_id}", + response_class=HTMLResponse, +) +async def update_augmented_part_attachment_point( + request: Request, + augmented_part_id: int, + attachment_point_id: int, + attachment_point_name_id: int = Form(...), + connection_type: str = Form(""), + iso_thread_id: str = Form(""), + unc_thread_id: str = Form(""), + clamp_connection_id: str = Form(""), + static_connection_id: str = Form(""), + db: AsyncSession = Depends(get_db), + actor: User = Depends(web_require("attachment_points:write")), +): + apoint = await attachment_point_crud.get(db, attachment_point_id) + if apoint is None: + raise HTTPException(status_code=404, detail="Attachment point not found") + changes = { + "attachment_point_name_id": attachment_point_name_id, + "iso_thread_id": int(iso_thread_id) if iso_thread_id else None, + "unc_thread_id": int(unc_thread_id) if unc_thread_id else None, + "clamp_connection_id": int(clamp_connection_id) if clamp_connection_id else None, + "static_connection_id": int(static_connection_id) if static_connection_id else None, + } + apoint = await attachment_point_crud.update(db, apoint, changes) + await audit_crud.log( + db, actor=actor, action="update", resource_type="AttachmentPoint", + resource_id=attachment_point_id, + description=audit_crud.describe_update( + "AttachmentPoint", attachment_point_id, changes + ), + ) + return templates.TemplateResponse( + request, "admin/_augmented_part_ap_row.html", {"apoint": apoint} + ) + + +@router.delete( + "/augmented-parts/{augmented_part_id}/attachment-points/{attachment_point_id}", + response_class=HTMLResponse, +) +async def delete_augmented_part_attachment_point( + request: Request, + augmented_part_id: int, + attachment_point_id: int, + db: AsyncSession = Depends(get_db), + actor: User = Depends(web_require("attachment_points:delete")), +): + apoint = await attachment_point_crud.get(db, attachment_point_id) + if apoint is not None: + await attachment_point_crud.delete(db, apoint) + await audit_crud.log( + db, actor=actor, action="delete", resource_type="AttachmentPoint", + resource_id=attachment_point_id, + description=audit_crud.describe_delete( + "AttachmentPoint", attachment_point_id, f"part {augmented_part_id}" + ), + ) + ctx = await _augmented_part_ap_ctx(db, augmented_part_id) + return templates.TemplateResponse( + request, "admin/_augmented_part_attachment_points.html", ctx + ) + + # --- Part Colours ------------------------------------------------------------