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 %}
+
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 @@
|
-
+
+
+
|
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 ------------------------------------------------------------