Adding ability to add attachment points when editing augmented part

This commit is contained in:
Ian Roddis committed 2026-07-02 14:13:09 -03:00
1 parent ceae27a12e
commit d67b4ceb65
7 files changed
+355 -1

No files matched your search

+11
View File
@@ -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]:
+34
View File
@@ -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;
@@ -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 %}
<div id="augmented-part-ap-{{ apoint.id }}" class="ap-item">
<span class="ap-item-name">{{ apoint.attachment_point_name.name }}</span>
<span class="ap-item-type">{{ conn_type }}</span>
<code class="ap-item-conn">{{ conn_label }}</code>
<div class="row-actions">
<button class="btn btn-ghost"
hx-get="/admin/augmented-parts/{{ apoint.augmented_part_id }}/attachment-points/{{ apoint.id }}/edit"
hx-target="#augmented-part-ap-{{ apoint.id }}"
hx-swap="outerHTML">Edit</button>
<button class="btn btn-danger"
hx-delete="/admin/augmented-parts/{{ apoint.augmented_part_id }}/attachment-points/{{ apoint.id }}"
hx-target="#ap-{{ apoint.augmented_part_id }}-attachment-points"
hx-swap="outerHTML"
hx-confirm="Delete this attachment point?">Delete</button>
</div>
</div>
@@ -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 %}
<div id="augmented-part-ap-{{ apoint.id }}">
<form hx-post="/admin/augmented-parts/{{ apoint.augmented_part_id }}/attachment-points/{{ apoint.id }}"
hx-target="#augmented-part-ap-{{ apoint.id }}"
hx-swap="outerHTML"
class="inline-edit" style="padding:0.3rem 0">
<label>Name
<select name="attachment_point_name_id" required>
{% for apn in attachment_point_names %}
<option value="{{ apn.id }}" {{ "selected" if apn.id == apoint.attachment_point_name_id }}>{{ apn.name }}</option>
{% endfor %}
</select>
</label>
<label>Type
<select name="connection_type" required
hx-get="/admin/attachment-points/connection-options"
hx-target="#edit-ap-{{ apoint.id }}-connection-options"
hx-trigger="change"
hx-include="[name='connection_type']">
<option value="">— type —</option>
<option value="iso_thread" {{ "selected" if current_type == "iso_thread" }}>ISO Thread</option>
<option value="unc_thread" {{ "selected" if current_type == "unc_thread" }}>UNC Thread</option>
<option value="clamp_connection" {{ "selected" if current_type == "clamp_connection" }}>Clamp Connection</option>
<option value="static_connection" {{ "selected" if current_type == "static_connection" }}>Static Connection</option>
</select>
</label>
<div id="edit-ap-{{ apoint.id }}-connection-options">
{% include "admin/_attachment_point_connection_select.html" %}
</div>
<div class="row-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-ghost"
hx-get="/admin/augmented-parts/{{ apoint.augmented_part_id }}/attachment-points/{{ apoint.id }}/row"
hx-target="#augmented-part-ap-{{ apoint.id }}"
hx-swap="outerHTML">Cancel</button>
</div>
</form>
</div>
@@ -0,0 +1,38 @@
<div id="ap-{{ augmented_part_id }}-attachment-points" class="ap-inline-section">
<h3>Attachment Points</h3>
{% for apoint in attachment_points_list %}
{% include "admin/_augmented_part_ap_row.html" %}
{% else %}
<p class="muted" style="font-size:0.85rem;margin:0 0 0.5rem">No attachment points yet.</p>
{% endfor %}
<form hx-post="/admin/augmented-parts/{{ augmented_part_id }}/attachment-points"
hx-target="#ap-{{ augmented_part_id }}-attachment-points"
hx-swap="outerHTML"
class="inline-edit" style="margin-top:0.5rem">
<label>Name
<select name="attachment_point_name_id" required>
<option value="">— select —</option>
{% for apn in attachment_point_names %}
<option value="{{ apn.id }}">{{ apn.name }}</option>
{% endfor %}
</select>
</label>
<label>Type
<select name="connection_type" required
hx-get="/admin/attachment-points/connection-options"
hx-target="#ap-{{ augmented_part_id }}-add-connection-options"
hx-trigger="change"
hx-include="[name='connection_type']">
<option value="">— type —</option>
<option value="iso_thread">ISO Thread</option>
<option value="unc_thread">UNC Thread</option>
<option value="clamp_connection">Clamp Connection</option>
<option value="static_connection">Static Connection</option>
</select>
</label>
<div id="ap-{{ augmented_part_id }}-add-connection-options"></div>
<button type="submit" class="btn btn-primary">Add</button>
</form>
</div>
@@ -1,6 +1,7 @@
<tr id="augmented-part-{{ ap.id }}">
<td colspan="9">
<form hx-post="/admin/augmented-parts/{{ ap.id }}"
<form id="augmented-part-edit-form-{{ ap.id }}"
hx-post="/admin/augmented-parts/{{ ap.id }}"
hx-target="#augmented-part-{{ ap.id }}"
hx-swap="outerHTML"
class="inline-edit">
@@ -52,5 +53,10 @@
hx-swap="outerHTML">Cancel</button>
</div>
</form>
<div hx-get="/admin/augmented-parts/{{ ap.id }}/attachment-points"
hx-trigger="load"
hx-swap="outerHTML">
</div>
</td>
</tr>
+184
View File
@@ -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 ------------------------------------------------------------