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 %}
+ {% endif %} diff --git a/app/templates/admin/_augmented_part_attachment_points.html b/app/templates/admin/_augmented_part_attachment_points.html index 5d16ab3..44f0976 100644 --- a/app/templates/admin/_augmented_part_attachment_points.html +++ b/app/templates/admin/_augmented_part_attachment_points.html @@ -7,6 +7,7 @@

No attachment points yet.

{% endfor %} + {% if can_write_attachment_points %}
+ {% endif %} diff --git a/app/templates/admin/augmented_part_detail.html b/app/templates/admin/augmented_part_detail.html index 61cda6f..2d9caec 100644 --- a/app/templates/admin/augmented_part_detail.html +++ b/app/templates/admin/augmented_part_detail.html @@ -3,20 +3,6 @@ {% block body %} {% include "_nav.html" %} -{%- macro conn_desc(point) -%} - {%- if point.iso_thread -%} - ISO M{{ point.iso_thread.diameter_mm }}×{{ point.iso_thread.pitch_mm }} - {%- elif point.unc_thread -%} - UNC {{ point.unc_thread.diameter_fraction }}-{{ point.unc_thread.tpi }} {{ point.unc_thread.series.value }} - {%- elif point.clamp_connection -%} - Clamp {{ point.clamp_connection.min_mm if point.clamp_connection.min_mm is not none else point.clamp_connection.min_in }}–{{ point.clamp_connection.max_mm if point.clamp_connection.max_mm is not none else point.clamp_connection.max_in }} - {%- elif point.static_connection -%} - Static {{ point.static_connection.od_mm }} mm OD - {%- else -%} - — - {%- endif -%} -{%- endmacro -%} -

← 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