From fe6f9cca051cc66dd67e5354afc3f91ea4f75dc6 Mon Sep 17 00:00:00 2001 From: Ian Roddis <31021769+iroddis@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:56:46 -0300 Subject: [PATCH] Adding Part Types --- .../e23aa8dd2536_add_part_types_table.py | 34 +++++++ app/api/part_types.py | 66 +++++++++++++ app/api/router.py | 3 +- app/crud/part_type.py | 43 +++++++++ app/models/__init__.py | 2 + app/models/part_type.py | 10 ++ app/schemas/part_type.py | 19 ++++ app/templates/_nav.html | 3 + app/templates/admin/_part_type_row.html | 15 +++ app/templates/admin/_part_type_row_edit.html | 18 ++++ app/templates/admin/_part_types_table.html | 16 ++++ app/templates/admin/part_types.html | 18 ++++ app/web/admin.py | 92 +++++++++++++++++++ scripts/seed.py | 13 ++- 14 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 alembic/versions/e23aa8dd2536_add_part_types_table.py create mode 100644 app/api/part_types.py create mode 100644 app/crud/part_type.py create mode 100644 app/models/part_type.py create mode 100644 app/schemas/part_type.py create mode 100644 app/templates/admin/_part_type_row.html create mode 100644 app/templates/admin/_part_type_row_edit.html create mode 100644 app/templates/admin/_part_types_table.html create mode 100644 app/templates/admin/part_types.html diff --git a/alembic/versions/e23aa8dd2536_add_part_types_table.py b/alembic/versions/e23aa8dd2536_add_part_types_table.py new file mode 100644 index 0000000..3dfe743 --- /dev/null +++ b/alembic/versions/e23aa8dd2536_add_part_types_table.py @@ -0,0 +1,34 @@ +"""add_part_types_table + +Revision ID: e23aa8dd2536 +Revises: a52e7ca7a925 +Create Date: 2026-06-25 10:49:56.463466 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e23aa8dd2536' +down_revision: Union[str, None] = 'a52e7ca7a925' +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! ### + op.create_table('part_types', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('part_types') + # ### end Alembic commands ### diff --git a/app/api/part_types.py b/app/api/part_types.py new file mode 100644 index 0000000..b1024b8 --- /dev/null +++ b/app/api/part_types.py @@ -0,0 +1,66 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.crud import part_type as part_type_crud +from app.deps import require_permission +from app.models.user import User +from app.schemas.part_type import PartTypeCreate, PartTypeRead, PartTypeUpdate + +router = APIRouter() + + +@router.get("", response_model=list[PartTypeRead]) +async def list_part_types( + skip: int = 0, + limit: int = 100, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("part_types:read")), +): + return await part_type_crud.list_all(db, skip=skip, limit=limit) + + +@router.post("", response_model=PartTypeRead, status_code=status.HTTP_201_CREATED) +async def create_part_type( + payload: PartTypeCreate, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("part_types:write")), +): + return await part_type_crud.create(db, name=payload.name) + + +@router.get("/{part_type_id}", response_model=PartTypeRead) +async def get_part_type( + part_type_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("part_types:read")), +): + pt = await part_type_crud.get(db, part_type_id) + if pt is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Part type not found") + return pt + + +@router.patch("/{part_type_id}", response_model=PartTypeRead) +async def update_part_type( + part_type_id: int, + payload: PartTypeUpdate, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("part_types:write")), +): + pt = await part_type_crud.get(db, part_type_id) + if pt is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Part type not found") + return await part_type_crud.update(db, pt, payload.model_dump(exclude_unset=True)) + + +@router.delete("/{part_type_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_part_type( + part_type_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("part_types:delete")), +): + pt = await part_type_crud.get(db, part_type_id) + if pt is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Part type not found") + await part_type_crud.delete(db, pt) diff --git a/app/api/router.py b/app/api/router.py index 8fdd972..c6c7a02 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api import auth, iso_threads, manufacturers, roles, scope_models, unc_threads, users +from app.api import auth, iso_threads, manufacturers, part_types, roles, scope_models, unc_threads, users api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) @@ -10,3 +10,4 @@ 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_types.router, prefix="/part-types", tags=["part-types"]) diff --git a/app/crud/part_type.py b/app/crud/part_type.py new file mode 100644 index 0000000..05c9cd3 --- /dev/null +++ b/app/crud/part_type.py @@ -0,0 +1,43 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.part_type import PartType + + +async def get(db: AsyncSession, part_type_id: int) -> PartType | None: + return await db.get(PartType, part_type_id) + + +async def get_by_name(db: AsyncSession, name: str) -> PartType | None: + result = await db.execute(select(PartType).where(PartType.name == name)) + return result.scalar_one_or_none() + + +async def list_all( + db: AsyncSession, skip: int = 0, limit: int = 100 +) -> list[PartType]: + result = await db.execute( + select(PartType).order_by(PartType.name).offset(skip).limit(limit) + ) + return list(result.scalars().all()) + + +async def create(db: AsyncSession, *, name: str) -> PartType: + pt = PartType(name=name) + db.add(pt) + await db.commit() + await db.refresh(pt) + return pt + + +async def update(db: AsyncSession, part_type: PartType, data: dict) -> PartType: + for field, value in data.items(): + setattr(part_type, field, value) + await db.commit() + await db.refresh(part_type) + return part_type + + +async def delete(db: AsyncSession, part_type: PartType) -> None: + await db.delete(part_type) + await db.commit() diff --git a/app/models/__init__.py b/app/models/__init__.py index 3559cee..377190b 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -3,6 +3,7 @@ from app.models.associations import role_permissions, user_roles from app.models.iso_thread import IsoThread from app.models.manufacturer import Manufacturer +from app.models.part_type import PartType from app.models.permission import Permission from app.models.scope_model import ScopeModel from app.models.refresh_token import RefreshToken @@ -13,6 +14,7 @@ from app.models.user import User __all__ = [ "IsoThread", "Manufacturer", + "PartType", "Permission", "ScopeModel", "RefreshToken", diff --git a/app/models/part_type.py b/app/models/part_type.py new file mode 100644 index 0000000..294b3cc --- /dev/null +++ b/app/models/part_type.py @@ -0,0 +1,10 @@ +from sqlalchemy.orm import Mapped, mapped_column + +from app.core.database import Base + + +class PartType(Base): + __tablename__ = "part_types" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(nullable=False, unique=True) diff --git a/app/schemas/part_type.py b/app/schemas/part_type.py new file mode 100644 index 0000000..0090755 --- /dev/null +++ b/app/schemas/part_type.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel, ConfigDict + + +class PartTypeBase(BaseModel): + name: str + + +class PartTypeCreate(PartTypeBase): + pass + + +class PartTypeUpdate(BaseModel): + name: str | None = None + + +class PartTypeRead(PartTypeBase): + model_config = ConfigDict(from_attributes=True) + + id: int diff --git a/app/templates/_nav.html b/app/templates/_nav.html index eccc55d..6ece4e8 100644 --- a/app/templates/_nav.html +++ b/app/templates/_nav.html @@ -20,6 +20,9 @@ {% if user.is_superuser or "scope_models:read" in user.permissions %} Scope Models {% endif %} + {% if user.is_superuser or "part_types:read" in user.permissions %} + Part Types + {% endif %} API docs