From 2e7002b73bd82e9b33a38edd9576c1293c0f1d2f Mon Sep 17 00:00:00 2001 From: Ian Roddis <31021769+iroddis@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:34:33 -0300 Subject: [PATCH] adding part colour --- .../versions/0c3d1a5cff1f_add_part_colours.py | 29 +++++ app/api/part_colours.py | 86 ++++++++++++++ app/api/router.py | 3 +- app/crud/part_colour.py | 45 ++++++++ app/models/__init__.py | 2 + app/models/part_colour.py | 12 ++ app/schemas/part_colour.py | 18 +++ app/templates/_nav.html | 3 + app/templates/admin/_part_colour_row.html | 15 +++ .../admin/_part_colour_row_edit.html | 18 +++ app/templates/admin/_part_colours_table.html | 18 +++ app/templates/admin/part_colours.html | 18 +++ app/web/admin.py | 109 ++++++++++++++++++ scripts/seed.py | 5 +- 14 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 alembic/versions/0c3d1a5cff1f_add_part_colours.py create mode 100644 app/api/part_colours.py create mode 100644 app/crud/part_colour.py create mode 100644 app/models/part_colour.py create mode 100644 app/schemas/part_colour.py create mode 100644 app/templates/admin/_part_colour_row.html create mode 100644 app/templates/admin/_part_colour_row_edit.html create mode 100644 app/templates/admin/_part_colours_table.html create mode 100644 app/templates/admin/part_colours.html diff --git a/alembic/versions/0c3d1a5cff1f_add_part_colours.py b/alembic/versions/0c3d1a5cff1f_add_part_colours.py new file mode 100644 index 0000000..1a6896c --- /dev/null +++ b/alembic/versions/0c3d1a5cff1f_add_part_colours.py @@ -0,0 +1,29 @@ +"""add part_colours + +Revision ID: 0c3d1a5cff1f +Revises: 5b6a7817b408 +Create Date: 2026-07-02 12:22:24.106795 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0c3d1a5cff1f' +down_revision: Union[str, None] = '5b6a7817b408' +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/part_colours.py b/app/api/part_colours.py new file mode 100644 index 0000000..569d347 --- /dev/null +++ b/app/api/part_colours.py @@ -0,0 +1,86 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.crud import audit as audit_crud +from app.crud import part_colour as part_colour_crud +from app.deps import require_permission +from app.models.user import User +from app.schemas.part_colour import PartColourCreate, PartColourRead, PartColourUpdate + +router = APIRouter() + + +@router.get("", response_model=list[PartColourRead]) +async def list_part_colours( + skip: int = 0, + limit: int = 100, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("part_colours:read")), +): + return await part_colour_crud.list_all(db, skip=skip, limit=limit) + + +@router.post("", response_model=PartColourRead, status_code=status.HTTP_201_CREATED) +async def create_part_colour( + payload: PartColourCreate, + db: AsyncSession = Depends(get_db), + actor: User = Depends(require_permission("part_colours:write")), +): + pc = await part_colour_crud.create(db, name=payload.name) + await audit_crud.log( + db, actor=actor, action="create", resource_type="PartColour", + resource_id=pc.id, + description=audit_crud.describe_create("PartColour", pc.id, pc.name), + ) + return pc + + +@router.get("/{part_colour_id}", response_model=PartColourRead) +async def get_part_colour( + part_colour_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("part_colours:read")), +): + pc = await part_colour_crud.get(db, part_colour_id) + if pc is None: + raise HTTPException(status_code=404, detail="Part colour not found") + return pc + + +@router.patch("/{part_colour_id}", response_model=PartColourRead) +async def update_part_colour( + part_colour_id: int, + payload: PartColourUpdate, + db: AsyncSession = Depends(get_db), + actor: User = Depends(require_permission("part_colours:write")), +): + pc = await part_colour_crud.get(db, part_colour_id) + if pc is None: + raise HTTPException(status_code=404, detail="Part colour not found") + changes = payload.model_dump(exclude_unset=True) + pc = await part_colour_crud.update(db, pc, changes) + await audit_crud.log( + db, actor=actor, action="update", resource_type="PartColour", + resource_id=pc.id, + description=audit_crud.describe_update("PartColour", pc.id, changes), + ) + return pc + + +@router.delete("/{part_colour_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_part_colour( + part_colour_id: int, + db: AsyncSession = Depends(get_db), + actor: User = Depends(require_permission("part_colours:delete")), +): + pc = await part_colour_crud.get(db, part_colour_id) + if pc is None: + raise HTTPException(status_code=404, detail="Part colour not found") + name = pc.name + await part_colour_crud.delete(db, pc) + await audit_crud.log( + db, actor=actor, action="delete", resource_type="PartColour", + resource_id=part_colour_id, + description=audit_crud.describe_delete("PartColour", part_colour_id, name), + ) diff --git a/app/api/router.py b/app/api/router.py index 725a2a6..1867d9e 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, auth, imported_parts, iso_threads, manufacturers, part_types, roles, scope_models, static_connections, sub_part_types, unc_threads, users +from app.api import audit_log, auth, 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"]) @@ -10,6 +10,7 @@ api_router.include_router(iso_threads.router, prefix="/iso-threads", tags=["iso- api_router.include_router(unc_threads.router, prefix="/unc-threads", tags=["unc-threads"]) api_router.include_router(manufacturers.router, prefix="/manufacturers", tags=["manufacturers"]) api_router.include_router(scope_models.router, prefix="/scope-models", tags=["scope-models"]) +api_router.include_router(part_colours.router, prefix="/part-colours", tags=["part-colours"]) api_router.include_router(part_types.router, prefix="/part-types", tags=["part-types"]) 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"]) diff --git a/app/crud/part_colour.py b/app/crud/part_colour.py new file mode 100644 index 0000000..af0bd93 --- /dev/null +++ b/app/crud/part_colour.py @@ -0,0 +1,45 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.part_colour import PartColour + + +async def get(db: AsyncSession, part_colour_id: int) -> PartColour | None: + return await db.get(PartColour, part_colour_id) + + +async def get_by_name(db: AsyncSession, name: str) -> PartColour | None: + result = await db.execute( + select(PartColour).where(PartColour.name == name) + ) + return result.scalar_one_or_none() + + +async def list_all( + db: AsyncSession, skip: int = 0, limit: int = 100 +) -> list[PartColour]: + result = await db.execute( + select(PartColour).order_by(PartColour.name).offset(skip).limit(limit) + ) + return list(result.scalars().all()) + + +async def create(db: AsyncSession, *, name: str) -> PartColour: + pc = PartColour(name=name) + db.add(pc) + await db.commit() + await db.refresh(pc) + return pc + + +async def update(db: AsyncSession, pc: PartColour, data: dict) -> PartColour: + for field, value in data.items(): + setattr(pc, field, value) + await db.commit() + await db.refresh(pc) + return pc + + +async def delete(db: AsyncSession, pc: PartColour) -> None: + await db.delete(pc) + await db.commit() diff --git a/app/models/__init__.py b/app/models/__init__.py index 536fc53..ab6a99b 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -6,6 +6,7 @@ from app.models.imported_part import ImportedPart from app.models.imported_part_config import ImportedPartConfig from app.models.iso_thread import IsoThread from app.models.manufacturer import Manufacturer +from app.models.part_colour import PartColour from app.models.part_type import PartType from app.models.permission import Permission from app.models.scope_model import ScopeModel @@ -22,6 +23,7 @@ __all__ = [ "ImportedPartConfig", "IsoThread", "Manufacturer", + "PartColour", "PartType", "Permission", "ScopeModel", diff --git a/app/models/part_colour.py b/app/models/part_colour.py new file mode 100644 index 0000000..66a45c4 --- /dev/null +++ b/app/models/part_colour.py @@ -0,0 +1,12 @@ +from sqlalchemy import UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column + +from app.core.database import Base + + +class PartColour(Base): + __tablename__ = "part_colours" + __table_args__ = (UniqueConstraint("name"),) + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(nullable=False) diff --git a/app/schemas/part_colour.py b/app/schemas/part_colour.py new file mode 100644 index 0000000..d999e09 --- /dev/null +++ b/app/schemas/part_colour.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel, ConfigDict + + +class PartColourBase(BaseModel): + name: str + + +class PartColourCreate(PartColourBase): + pass + + +class PartColourUpdate(BaseModel): + name: str | None = None + + +class PartColourRead(PartColourBase): + model_config = ConfigDict(from_attributes=True) + id: int diff --git a/app/templates/_nav.html b/app/templates/_nav.html index 99a8a7b..7293219 100644 --- a/app/templates/_nav.html +++ b/app/templates/_nav.html @@ -29,6 +29,9 @@ {% if user.is_superuser or "imported_parts:read" in user.permissions %} Imported Parts {% endif %} + {% if user.is_superuser or "part_colours:read" in user.permissions %} + Part Colours + {% endif %} {% if user.is_superuser or "static_connections:read" in user.permissions %} Static Connections {% endif %} diff --git a/app/templates/admin/_part_colour_row.html b/app/templates/admin/_part_colour_row.html new file mode 100644 index 0000000..c63231d --- /dev/null +++ b/app/templates/admin/_part_colour_row.html @@ -0,0 +1,15 @@ +
| ID | +Name | ++ |
|---|---|---|
| No part colours recorded. | ||