From aef60d77bec96a5e5dc2f8b470240dff49c3bc7c Mon Sep 17 00:00:00 2001
From: Ian Roddis <31021769+iroddis@users.noreply.github.com>
Date: Tue, 28 Jul 2026 22:04:40 -0300
Subject: [PATCH] Adding ability to edit attachment points on augmented item
details
---
.../admin/_augmented_part_ap_row.html | 2 +
.../_augmented_part_attachment_points.html | 2 +
.../admin/augmented_part_detail.html | 32 +++--------
app/web/admin.py | 54 +++++++++++++++----
tests/test_augmented_part_detail.py | 30 +++++++++++
5 files changed, 83 insertions(+), 37 deletions(-)
diff --git a/app/templates/admin/_augmented_part_ap_row.html b/app/templates/admin/_augmented_part_ap_row.html
index c622b4d..733dffd 100644
--- a/app/templates/admin/_augmented_part_ap_row.html
+++ b/app/templates/admin/_augmented_part_ap_row.html
@@ -22,6 +22,7 @@
{{ apoint.attachment_point_name.name }}
{{ conn_type }}
{{ conn_label }}
+ {% if can_write_attachment_points %}
No attachment points yet.
{% endfor %}
+ {% if can_write_attachment_points %}
← Augmented Parts
@@ -62,20 +48,14 @@
{% endif %}
-
-
-
Attachment points
- {% for point in attachment_points %}
-
- {{ point.attachment_point_name.name }}
- {{ conn_desc(point) }}
-
- {% else %}
-
No attachment points recorded.
- {% endfor %}
-
+ {% if can_read_attachment_points %}
+
+ {% include "admin/_augmented_part_attachment_points.html" %}
+
+ {% endif %}
+
{% if can_read_fits %}
{% include "admin/_augmented_part_fits.html" %}
diff --git a/app/web/admin.py b/app/web/admin.py
index cec78ce..5dc057c 100644
--- a/app/web/admin.py
+++ b/app/web/admin.py
@@ -1792,17 +1792,26 @@ async def augmented_part_detail(
if ap is None:
raise HTTPException(status_code=404, detail="Augmented part not found")
can_read_fits = user.is_superuser or "fits:read" in user.permissions
+ can_read_attachment_points = (
+ user.is_superuser or "attachment_points:read" in user.permissions
+ )
ctx = {
"user": user,
"ap": ap,
"scope_info": await scope_info_crud.get_by_augmented_part(
db, augmented_part_id
),
- "attachment_points": await attachment_point_crud.list_for_augmented_part(
- db, augmented_part_id
- ),
"can_read_fits": can_read_fits,
+ "can_read_attachment_points": can_read_attachment_points,
}
+ if can_read_attachment_points:
+ ctx.update(
+ await _augmented_part_ap_ctx(
+ db,
+ augmented_part_id,
+ can_write=_can_write_attachment_points(user),
+ )
+ )
if can_read_fits:
ctx.update(
await _augmented_part_fits_ctx(
@@ -1909,7 +1918,13 @@ 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:
+def _can_write_attachment_points(user: User) -> bool:
+ return user.is_superuser or "attachment_points:write" in user.permissions
+
+
+async def _augmented_part_ap_ctx(
+ db: AsyncSession, augmented_part_id: int, *, can_write: bool = True
+) -> dict:
return {
"augmented_part_id": augmented_part_id,
"attachment_points_list": await attachment_point_crud.list_for_augmented_part(
@@ -1920,6 +1935,7 @@ async def _augmented_part_ap_ctx(db: AsyncSession, augmented_part_id: int) -> di
"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),
+ "can_write_attachment_points": can_write,
}
@@ -1931,9 +1947,11 @@ async def augmented_part_attachment_points(
request: Request,
augmented_part_id: int,
db: AsyncSession = Depends(get_db),
- _: User = Depends(web_require("attachment_points:read")),
+ user: User = Depends(web_require("attachment_points:read")),
):
- ctx = await _augmented_part_ap_ctx(db, augmented_part_id)
+ ctx = await _augmented_part_ap_ctx(
+ db, augmented_part_id, can_write=_can_write_attachment_points(user)
+ )
return templates.TemplateResponse(
request, "admin/_augmented_part_attachment_points.html", ctx
)
@@ -1976,7 +1994,9 @@ async def create_augmented_part_attachment_point(
"AttachmentPoint", apoint.id, f"part {augmented_part_id}"
),
)
- ctx = await _augmented_part_ap_ctx(db, augmented_part_id)
+ ctx = await _augmented_part_ap_ctx(
+ db, augmented_part_id, can_write=_can_write_attachment_points(actor)
+ )
return templates.TemplateResponse(
request, "admin/_augmented_part_attachment_points.html", ctx
)
@@ -2022,13 +2042,18 @@ async def augmented_part_attachment_point_row(
augmented_part_id: int,
attachment_point_id: int,
db: AsyncSession = Depends(get_db),
- _: User = Depends(web_require("attachment_points:read")),
+ user: 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}
+ request,
+ "admin/_augmented_part_ap_row.html",
+ {
+ "apoint": apoint,
+ "can_write_attachment_points": _can_write_attachment_points(user),
+ },
)
@@ -2075,7 +2100,12 @@ async def update_augmented_part_attachment_point(
),
)
return templates.TemplateResponse(
- request, "admin/_augmented_part_ap_row.html", {"apoint": apoint}
+ request,
+ "admin/_augmented_part_ap_row.html",
+ {
+ "apoint": apoint,
+ "can_write_attachment_points": _can_write_attachment_points(actor),
+ },
)
@@ -2103,7 +2133,9 @@ async def delete_augmented_part_attachment_point(
"AttachmentPoint", attachment_point_id, f"part {augmented_part_id}"
),
)
- ctx = await _augmented_part_ap_ctx(db, augmented_part_id)
+ ctx = await _augmented_part_ap_ctx(
+ db, augmented_part_id, can_write=_can_write_attachment_points(actor)
+ )
return templates.TemplateResponse(
request, "admin/_augmented_part_attachment_points.html", ctx
)
diff --git a/tests/test_augmented_part_detail.py b/tests/test_augmented_part_detail.py
index d6def94..773b263 100644
--- a/tests/test_augmented_part_detail.py
+++ b/tests/test_augmented_part_detail.py
@@ -91,6 +91,36 @@ async def test_detail_shows_attributes_and_fits_readonly(
assert "Add fit" not in body
assert "Mark Exact" not in body
assert "Recompute possible fits" not in body
+ # ...and attachment-point add/edit/delete controls are hidden too
+ assert 'name="attachment_point_name_id"' not in body
+ assert "Delete this attachment point?" not in body
+
+
+@pytest.mark.asyncio
+async def test_detail_write_controls_for_attachment_point_editor(
+ client, seeded, make_user, db_sessionmaker
+):
+ async with db_sessionmaker() as db:
+ ids = await _seed_part_with_fit(db)
+ perms = seeded["perms"]
+ editor = await role_crud.create(
+ db,
+ name="ap-editor",
+ permission_ids=[
+ perms["augmented_parts:read"].id,
+ perms["attachment_points:read"].id,
+ perms["attachment_points:write"].id,
+ ],
+ )
+ editor_id = editor.id
+ await make_user("apeditor@example.com", role_ids=[editor_id])
+ await _web_login(client, "apeditor@example.com")
+
+ resp = await client.get(f"/admin/augmented-parts/{ids['scope_id']}")
+ assert resp.status_code == 200
+ body = resp.text
+ assert 'name="attachment_point_name_id"' in body
+ assert "Delete this attachment point?" in body
@pytest.mark.asyncio