From 2767fb1c8e64a3bf9554e236b4d7b760216526f1 Mon Sep 17 00:00:00 2001
From: Ian Roddis <31021769+iroddis@users.noreply.github.com>
Date: Thu, 2 Jul 2026 13:57:03 -0300
Subject: [PATCH] Adding AttachmentPointNames
---
...70e48bda8829_add_attachment_point_names.py | 29 +++++
app/api/attachment_point_names.py | 92 +++++++++++++++
app/api/router.py | 3 +-
app/crud/attachment_point_name.py | 50 ++++++++
app/models/__init__.py | 2 +
app/models/attachment_point_name.py | 12 ++
app/schemas/attachment_point_name.py | 18 +++
app/templates/_nav.html | 9 +-
.../admin/_attachment_point_name_row.html | 15 +++
.../_attachment_point_name_row_edit.html | 18 +++
.../admin/_attachment_point_names_table.html | 18 +++
.../admin/attachment_point_names.html | 21 ++++
app/web/admin.py | 109 ++++++++++++++++++
scripts/seed.py | 13 ++-
14 files changed, 406 insertions(+), 3 deletions(-)
create mode 100644 alembic/versions/70e48bda8829_add_attachment_point_names.py
create mode 100644 app/api/attachment_point_names.py
create mode 100644 app/crud/attachment_point_name.py
create mode 100644 app/models/attachment_point_name.py
create mode 100644 app/schemas/attachment_point_name.py
create mode 100644 app/templates/admin/_attachment_point_name_row.html
create mode 100644 app/templates/admin/_attachment_point_name_row_edit.html
create mode 100644 app/templates/admin/_attachment_point_names_table.html
create mode 100644 app/templates/admin/attachment_point_names.html
diff --git a/alembic/versions/70e48bda8829_add_attachment_point_names.py b/alembic/versions/70e48bda8829_add_attachment_point_names.py
new file mode 100644
index 0000000..d96ab49
--- /dev/null
+++ b/alembic/versions/70e48bda8829_add_attachment_point_names.py
@@ -0,0 +1,29 @@
+"""add attachment_point_names
+
+Revision ID: 70e48bda8829
+Revises: c7882cedc9ae
+Create Date: 2026-07-02 13:54:58.335394
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '70e48bda8829'
+down_revision: Union[str, None] = 'c7882cedc9ae'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
diff --git a/app/api/attachment_point_names.py b/app/api/attachment_point_names.py
new file mode 100644
index 0000000..26fc5bd
--- /dev/null
+++ b/app/api/attachment_point_names.py
@@ -0,0 +1,92 @@
+from fastapi import APIRouter, Depends, HTTPException, status
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.core.database import get_db
+from app.crud import attachment_point_name as apn_crud
+from app.crud import audit as audit_crud
+from app.deps import require_permission
+from app.models.user import User
+from app.schemas.attachment_point_name import (
+ AttachmentPointNameCreate,
+ AttachmentPointNameRead,
+ AttachmentPointNameUpdate,
+)
+
+router = APIRouter()
+
+
+@router.get("", response_model=list[AttachmentPointNameRead])
+async def list_attachment_point_names(
+ skip: int = 0,
+ limit: int = 100,
+ db: AsyncSession = Depends(get_db),
+ _: User = Depends(require_permission("attachment_point_names:read")),
+):
+ return await apn_crud.list_all(db, skip=skip, limit=limit)
+
+
+@router.post(
+ "", response_model=AttachmentPointNameRead, status_code=status.HTTP_201_CREATED
+)
+async def create_attachment_point_name(
+ payload: AttachmentPointNameCreate,
+ db: AsyncSession = Depends(get_db),
+ actor: User = Depends(require_permission("attachment_point_names:write")),
+):
+ apn = await apn_crud.create(db, name=payload.name)
+ await audit_crud.log(
+ db, actor=actor, action="create", resource_type="AttachmentPointName",
+ resource_id=apn.id,
+ description=audit_crud.describe_create("AttachmentPointName", apn.id, apn.name),
+ )
+ return apn
+
+
+@router.get("/{apn_id}", response_model=AttachmentPointNameRead)
+async def get_attachment_point_name(
+ apn_id: int,
+ db: AsyncSession = Depends(get_db),
+ _: User = Depends(require_permission("attachment_point_names:read")),
+):
+ apn = await apn_crud.get(db, apn_id)
+ if apn is None:
+ raise HTTPException(status_code=404, detail="Attachment point name not found")
+ return apn
+
+
+@router.patch("/{apn_id}", response_model=AttachmentPointNameRead)
+async def update_attachment_point_name(
+ apn_id: int,
+ payload: AttachmentPointNameUpdate,
+ db: AsyncSession = Depends(get_db),
+ actor: User = Depends(require_permission("attachment_point_names:write")),
+):
+ apn = await apn_crud.get(db, apn_id)
+ if apn is None:
+ raise HTTPException(status_code=404, detail="Attachment point name not found")
+ changes = payload.model_dump(exclude_unset=True)
+ apn = await apn_crud.update(db, apn, changes)
+ await audit_crud.log(
+ db, actor=actor, action="update", resource_type="AttachmentPointName",
+ resource_id=apn.id,
+ description=audit_crud.describe_update("AttachmentPointName", apn.id, changes),
+ )
+ return apn
+
+
+@router.delete("/{apn_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_attachment_point_name(
+ apn_id: int,
+ db: AsyncSession = Depends(get_db),
+ actor: User = Depends(require_permission("attachment_point_names:delete")),
+):
+ apn = await apn_crud.get(db, apn_id)
+ if apn is None:
+ raise HTTPException(status_code=404, detail="Attachment point name not found")
+ name = apn.name
+ await apn_crud.delete(db, apn)
+ await audit_crud.log(
+ db, actor=actor, action="delete", resource_type="AttachmentPointName",
+ resource_id=apn_id,
+ description=audit_crud.describe_delete("AttachmentPointName", apn_id, name),
+ )
diff --git a/app/api/router.py b/app/api/router.py
index 8e40377..1e7569f 100644
--- a/app/api/router.py
+++ b/app/api/router.py
@@ -1,6 +1,6 @@
from fastapi import APIRouter
-from app.api import audit_log, augmented_parts, auth, clamp_connections, imported_parts, iso_threads, manufacturers, part_colours, part_types, roles, scope_models, static_connections, sub_part_types, unc_threads, users
+from app.api import attachment_point_names, audit_log, augmented_parts, auth, clamp_connections, imported_parts, iso_threads, manufacturers, part_colours, part_types, roles, scope_models, static_connections, sub_part_types, unc_threads, users
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
@@ -15,6 +15,7 @@ api_router.include_router(part_types.router, prefix="/part-types", tags=["part-t
api_router.include_router(sub_part_types.router, prefix="/sub-part-types", tags=["sub-part-types"])
api_router.include_router(static_connections.router, prefix="/static-connections", tags=["static-connections"])
api_router.include_router(clamp_connections.router, prefix="/clamp-connections", tags=["clamp-connections"])
+api_router.include_router(attachment_point_names.router, prefix="/attachment-point-names", tags=["attachment-point-names"])
api_router.include_router(imported_parts.router, prefix="/imported-parts", tags=["imported-parts"])
api_router.include_router(augmented_parts.router, prefix="/augmented-parts", tags=["augmented-parts"])
api_router.include_router(audit_log.router, prefix="/audit-log", tags=["audit-log"])
diff --git a/app/crud/attachment_point_name.py b/app/crud/attachment_point_name.py
new file mode 100644
index 0000000..e2195ba
--- /dev/null
+++ b/app/crud/attachment_point_name.py
@@ -0,0 +1,50 @@
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.models.attachment_point_name import AttachmentPointName
+
+
+async def get(db: AsyncSession, apn_id: int) -> AttachmentPointName | None:
+ return await db.get(AttachmentPointName, apn_id)
+
+
+async def get_by_name(db: AsyncSession, name: str) -> AttachmentPointName | None:
+ result = await db.execute(
+ select(AttachmentPointName).where(AttachmentPointName.name == name)
+ )
+ return result.scalar_one_or_none()
+
+
+async def list_all(
+ db: AsyncSession, skip: int = 0, limit: int = 100
+) -> list[AttachmentPointName]:
+ result = await db.execute(
+ select(AttachmentPointName)
+ .order_by(AttachmentPointName.name)
+ .offset(skip)
+ .limit(limit)
+ )
+ return list(result.scalars().all())
+
+
+async def create(db: AsyncSession, *, name: str) -> AttachmentPointName:
+ apn = AttachmentPointName(name=name)
+ db.add(apn)
+ await db.commit()
+ await db.refresh(apn)
+ return apn
+
+
+async def update(
+ db: AsyncSession, apn: AttachmentPointName, data: dict
+) -> AttachmentPointName:
+ for field, value in data.items():
+ setattr(apn, field, value)
+ await db.commit()
+ await db.refresh(apn)
+ return apn
+
+
+async def delete(db: AsyncSession, apn: AttachmentPointName) -> None:
+ await db.delete(apn)
+ await db.commit()
diff --git a/app/models/__init__.py b/app/models/__init__.py
index a4e1f8f..22e285e 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -1,6 +1,7 @@
"""Import all models so they register with ``Base.metadata``."""
from app.models.associations import augmented_part_imported_parts, role_permissions, user_roles
+from app.models.attachment_point_name import AttachmentPointName
from app.models.audit_log import AuditLog
from app.models.augmented_part import AugmentedPart
from app.models.clamp_connection import ClampConnection
@@ -20,6 +21,7 @@ from app.models.unc_thread import UncThread
from app.models.user import User
__all__ = [
+ "AttachmentPointName",
"AuditLog",
"AugmentedPart",
"ClampConnection",
diff --git a/app/models/attachment_point_name.py b/app/models/attachment_point_name.py
new file mode 100644
index 0000000..a76a136
--- /dev/null
+++ b/app/models/attachment_point_name.py
@@ -0,0 +1,12 @@
+from sqlalchemy import UniqueConstraint
+from sqlalchemy.orm import Mapped, mapped_column
+
+from app.core.database import Base
+
+
+class AttachmentPointName(Base):
+ __tablename__ = "attachment_point_names"
+ __table_args__ = (UniqueConstraint("name"),)
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: Mapped[str] = mapped_column(nullable=False)
diff --git a/app/schemas/attachment_point_name.py b/app/schemas/attachment_point_name.py
new file mode 100644
index 0000000..fa799b9
--- /dev/null
+++ b/app/schemas/attachment_point_name.py
@@ -0,0 +1,18 @@
+from pydantic import BaseModel, ConfigDict
+
+
+class AttachmentPointNameBase(BaseModel):
+ name: str
+
+
+class AttachmentPointNameCreate(AttachmentPointNameBase):
+ pass
+
+
+class AttachmentPointNameUpdate(BaseModel):
+ name: str | None = None
+
+
+class AttachmentPointNameRead(AttachmentPointNameBase):
+ model_config = ConfigDict(from_attributes=True)
+ id: int
diff --git a/app/templates/_nav.html b/app/templates/_nav.html
index 51cc99c..9c03ad5 100644
--- a/app/templates/_nav.html
+++ b/app/templates/_nav.html
@@ -2,7 +2,7 @@
{% set p = user.permissions %}
{% set can_admin = su or "users:read" in p or "roles:read" in p or "audit_log:read" in p %}
{% set can_connections = su or "clamp_connections:read" in p or "iso_threads:read" in p or "static_connections:read" in p or "unc_threads:read" in p %}
-{% set can_labels = su or "manufacturers:read" in p or "part_colours:read" in p or "part_types:read" in p or "scope_models:read" in p or "sub_part_types:read" in p %}
+{% set can_labels = su or "attachment_point_names:read" in p or "manufacturers:read" in p or "part_colours:read" in p or "part_types:read" in p or "scope_models:read" in p or "sub_part_types:read" in p %}
{% set can_database = su or "imported_parts:read" in p or "augmented_parts:read" in p %}
{% set can_parts = can_connections or can_labels or can_database %}
@@ -103,6 +103,10 @@