From b7ad51717495c9e61807dca193c623a8fddacb32 Mon Sep 17 00:00:00 2001 From: Ian Roddis <31021769+iroddis@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:46:39 -0300 Subject: [PATCH] Adding Manufacturers and ScopeModels --- .claude/settings.local.json | 7 + .../a192abbf4091_add_manufacturers_table.py | 47 ++++ .../a52e7ca7a925_add_scope_models_table.py | 36 +++ app/api/manufacturers.py | 76 ++++++ app/api/router.py | 4 +- app/api/scope_models.py | 74 ++++++ app/crud/manufacturer.py | 46 ++++ app/crud/scope_model.py | 44 ++++ app/models/__init__.py | 4 + app/models/manufacturer.py | 12 + app/models/scope_model.py | 18 ++ app/schemas/manufacturer.py | 23 ++ app/schemas/scope_model.py | 26 ++ app/templates/_nav.html | 6 + app/templates/admin/_manufacturer_row.html | 23 ++ .../admin/_manufacturer_row_edit.html | 20 ++ app/templates/admin/_manufacturers_table.html | 16 ++ app/templates/admin/_scope_model_row.html | 23 ++ .../admin/_scope_model_row_edit.html | 26 ++ app/templates/admin/_scope_models_table.html | 16 ++ app/templates/admin/manufacturers.html | 20 ++ app/templates/admin/scope_models.html | 27 +++ app/web/admin.py | 222 ++++++++++++++++++ scripts/seed.py | 8 +- 24 files changed, 822 insertions(+), 2 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 alembic/versions/a192abbf4091_add_manufacturers_table.py create mode 100644 alembic/versions/a52e7ca7a925_add_scope_models_table.py create mode 100644 app/api/manufacturers.py create mode 100644 app/api/scope_models.py create mode 100644 app/crud/manufacturer.py create mode 100644 app/crud/scope_model.py create mode 100644 app/models/manufacturer.py create mode 100644 app/models/scope_model.py create mode 100644 app/schemas/manufacturer.py create mode 100644 app/schemas/scope_model.py create mode 100644 app/templates/admin/_manufacturer_row.html create mode 100644 app/templates/admin/_manufacturer_row_edit.html create mode 100644 app/templates/admin/_manufacturers_table.html create mode 100644 app/templates/admin/_scope_model_row.html create mode 100644 app/templates/admin/_scope_model_row_edit.html create mode 100644 app/templates/admin/_scope_models_table.html create mode 100644 app/templates/admin/manufacturers.html create mode 100644 app/templates/admin/scope_models.html diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..6d6b5ee --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(uv run *)" + ] + } +} diff --git a/alembic/versions/a192abbf4091_add_manufacturers_table.py b/alembic/versions/a192abbf4091_add_manufacturers_table.py new file mode 100644 index 0000000..1529003 --- /dev/null +++ b/alembic/versions/a192abbf4091_add_manufacturers_table.py @@ -0,0 +1,47 @@ +"""add_manufacturers_table + +Revision ID: a192abbf4091 +Revises: 627d4460a94f +Create Date: 2026-06-25 10:32:11.398529 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a192abbf4091' +down_revision: Union[str, None] = '627d4460a94f' +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('manufacturers', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('url', sa.String(), nullable=False), + sa.Column('logo_url', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('unc_threads', schema=None) as batch_op: + batch_op.alter_column('series', + existing_type=sa.VARCHAR(length=8), + type_=sa.Enum('UNC', 'UNF', 'UNEF', 'UN', 'UNS', name='uncseries'), + existing_nullable=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('unc_threads', schema=None) as batch_op: + batch_op.alter_column('series', + existing_type=sa.Enum('UNC', 'UNF', 'UNEF', 'UN', 'UNS', name='uncseries'), + type_=sa.VARCHAR(length=8), + existing_nullable=False) + + op.drop_table('manufacturers') + # ### end Alembic commands ### diff --git a/alembic/versions/a52e7ca7a925_add_scope_models_table.py b/alembic/versions/a52e7ca7a925_add_scope_models_table.py new file mode 100644 index 0000000..2429716 --- /dev/null +++ b/alembic/versions/a52e7ca7a925_add_scope_models_table.py @@ -0,0 +1,36 @@ +"""add_scope_models_table + +Revision ID: a52e7ca7a925 +Revises: a192abbf4091 +Create Date: 2026-06-25 10:35:40.227253 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a52e7ca7a925' +down_revision: Union[str, None] = 'a192abbf4091' +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('scope_models', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('manufacturer_id', sa.Integer(), nullable=False), + sa.Column('url', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['manufacturer_id'], ['manufacturers.id'], ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('scope_models') + # ### end Alembic commands ### diff --git a/app/api/manufacturers.py b/app/api/manufacturers.py new file mode 100644 index 0000000..1781a86 --- /dev/null +++ b/app/api/manufacturers.py @@ -0,0 +1,76 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.crud import manufacturer as manufacturer_crud +from app.deps import require_permission +from app.models.user import User +from app.schemas.manufacturer import ManufacturerCreate, ManufacturerRead, ManufacturerUpdate + +router = APIRouter() + + +@router.get("", response_model=list[ManufacturerRead]) +async def list_manufacturers( + skip: int = 0, + limit: int = 100, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("manufacturers:read")), +): + return await manufacturer_crud.list_all(db, skip=skip, limit=limit) + + +@router.post("", response_model=ManufacturerRead, status_code=status.HTTP_201_CREATED) +async def create_manufacturer( + payload: ManufacturerCreate, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("manufacturers:write")), +): + return await manufacturer_crud.create( + db, + name=payload.name, + url=str(payload.url), + logo_url=str(payload.logo_url) if payload.logo_url else None, + ) + + +@router.get("/{manufacturer_id}", response_model=ManufacturerRead) +async def get_manufacturer( + manufacturer_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("manufacturers:read")), +): + m = await manufacturer_crud.get(db, manufacturer_id) + if m is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Manufacturer not found") + return m + + +@router.patch("/{manufacturer_id}", response_model=ManufacturerRead) +async def update_manufacturer( + manufacturer_id: int, + payload: ManufacturerUpdate, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("manufacturers:write")), +): + m = await manufacturer_crud.get(db, manufacturer_id) + if m is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Manufacturer not found") + data = payload.model_dump(exclude_unset=True) + if "url" in data and data["url"] is not None: + data["url"] = str(data["url"]) + if "logo_url" in data and data["logo_url"] is not None: + data["logo_url"] = str(data["logo_url"]) + return await manufacturer_crud.update(db, m, data) + + +@router.delete("/{manufacturer_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_manufacturer( + manufacturer_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("manufacturers:delete")), +): + m = await manufacturer_crud.get(db, manufacturer_id) + if m is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Manufacturer not found") + await manufacturer_crud.delete(db, m) diff --git a/app/api/router.py b/app/api/router.py index 215b7cc..8fdd972 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, roles, unc_threads, users +from app.api import auth, iso_threads, manufacturers, roles, scope_models, unc_threads, users api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) @@ -8,3 +8,5 @@ api_router.include_router(users.router, prefix="/users", tags=["users"]) api_router.include_router(roles.router, prefix="/roles", tags=["roles"]) api_router.include_router(iso_threads.router, prefix="/iso-threads", tags=["iso-threads"]) 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"]) diff --git a/app/api/scope_models.py b/app/api/scope_models.py new file mode 100644 index 0000000..f7d6f9b --- /dev/null +++ b/app/api/scope_models.py @@ -0,0 +1,74 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.crud import scope_model as scope_model_crud +from app.deps import require_permission +from app.models.user import User +from app.schemas.scope_model import ScopeModelCreate, ScopeModelRead, ScopeModelUpdate + +router = APIRouter() + + +@router.get("", response_model=list[ScopeModelRead]) +async def list_scope_models( + skip: int = 0, + limit: int = 100, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("scope_models:read")), +): + return await scope_model_crud.list_all(db, skip=skip, limit=limit) + + +@router.post("", response_model=ScopeModelRead, status_code=status.HTTP_201_CREATED) +async def create_scope_model( + payload: ScopeModelCreate, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("scope_models:write")), +): + return await scope_model_crud.create( + db, + name=payload.name, + manufacturer_id=payload.manufacturer_id, + url=str(payload.url) if payload.url else None, + ) + + +@router.get("/{scope_model_id}", response_model=ScopeModelRead) +async def get_scope_model( + scope_model_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("scope_models:read")), +): + sm = await scope_model_crud.get(db, scope_model_id) + if sm is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scope model not found") + return sm + + +@router.patch("/{scope_model_id}", response_model=ScopeModelRead) +async def update_scope_model( + scope_model_id: int, + payload: ScopeModelUpdate, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("scope_models:write")), +): + sm = await scope_model_crud.get(db, scope_model_id) + if sm is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scope model not found") + data = payload.model_dump(exclude_unset=True) + if "url" in data and data["url"] is not None: + data["url"] = str(data["url"]) + return await scope_model_crud.update(db, sm, data) + + +@router.delete("/{scope_model_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_scope_model( + scope_model_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(require_permission("scope_models:delete")), +): + sm = await scope_model_crud.get(db, scope_model_id) + if sm is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scope model not found") + await scope_model_crud.delete(db, sm) diff --git a/app/crud/manufacturer.py b/app/crud/manufacturer.py new file mode 100644 index 0000000..d3e1579 --- /dev/null +++ b/app/crud/manufacturer.py @@ -0,0 +1,46 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.manufacturer import Manufacturer + + +async def get(db: AsyncSession, manufacturer_id: int) -> Manufacturer | None: + return await db.get(Manufacturer, manufacturer_id) + + +async def list_all( + db: AsyncSession, skip: int = 0, limit: int = 100 +) -> list[Manufacturer]: + result = await db.execute( + select(Manufacturer).order_by(Manufacturer.name).offset(skip).limit(limit) + ) + return list(result.scalars().all()) + + +async def create( + db: AsyncSession, + *, + name: str, + url: str, + logo_url: str | None = None, +) -> Manufacturer: + m = Manufacturer(name=name, url=url, logo_url=logo_url) + db.add(m) + await db.commit() + await db.refresh(m) + return m + + +async def update( + db: AsyncSession, manufacturer: Manufacturer, data: dict +) -> Manufacturer: + for field, value in data.items(): + setattr(manufacturer, field, value) + await db.commit() + await db.refresh(manufacturer) + return manufacturer + + +async def delete(db: AsyncSession, manufacturer: Manufacturer) -> None: + await db.delete(manufacturer) + await db.commit() diff --git a/app/crud/scope_model.py b/app/crud/scope_model.py new file mode 100644 index 0000000..6c82f2b --- /dev/null +++ b/app/crud/scope_model.py @@ -0,0 +1,44 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.scope_model import ScopeModel + + +async def get(db: AsyncSession, scope_model_id: int) -> ScopeModel | None: + return await db.get(ScopeModel, scope_model_id) + + +async def list_all( + db: AsyncSession, skip: int = 0, limit: int = 100 +) -> list[ScopeModel]: + result = await db.execute( + select(ScopeModel).order_by(ScopeModel.name).offset(skip).limit(limit) + ) + return list(result.scalars().all()) + + +async def create( + db: AsyncSession, + *, + name: str, + manufacturer_id: int, + url: str | None = None, +) -> ScopeModel: + sm = ScopeModel(name=name, manufacturer_id=manufacturer_id, url=url) + db.add(sm) + await db.commit() + await db.refresh(sm, attribute_names=["manufacturer"]) + return sm + + +async def update(db: AsyncSession, scope_model: ScopeModel, data: dict) -> ScopeModel: + for field, value in data.items(): + setattr(scope_model, field, value) + await db.commit() + await db.refresh(scope_model, attribute_names=["manufacturer"]) + return scope_model + + +async def delete(db: AsyncSession, scope_model: ScopeModel) -> None: + await db.delete(scope_model) + await db.commit() diff --git a/app/models/__init__.py b/app/models/__init__.py index f28f762..3559cee 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -2,7 +2,9 @@ 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.permission import Permission +from app.models.scope_model import ScopeModel from app.models.refresh_token import RefreshToken from app.models.role import Role from app.models.unc_thread import UncThread @@ -10,7 +12,9 @@ from app.models.user import User __all__ = [ "IsoThread", + "Manufacturer", "Permission", + "ScopeModel", "RefreshToken", "Role", "UncThread", diff --git a/app/models/manufacturer.py b/app/models/manufacturer.py new file mode 100644 index 0000000..907b477 --- /dev/null +++ b/app/models/manufacturer.py @@ -0,0 +1,12 @@ +from sqlalchemy.orm import Mapped, mapped_column + +from app.core.database import Base + + +class Manufacturer(Base): + __tablename__ = "manufacturers" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(nullable=False) + url: Mapped[str] = mapped_column(nullable=False) + logo_url: Mapped[str | None] = mapped_column(nullable=True, default=None) diff --git a/app/models/scope_model.py b/app/models/scope_model.py new file mode 100644 index 0000000..cf90b0c --- /dev/null +++ b/app/models/scope_model.py @@ -0,0 +1,18 @@ +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base +from app.models.manufacturer import Manufacturer + + +class ScopeModel(Base): + __tablename__ = "scope_models" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(nullable=False) + manufacturer_id: Mapped[int] = mapped_column( + ForeignKey("manufacturers.id", ondelete="RESTRICT"), nullable=False + ) + url: Mapped[str | None] = mapped_column(nullable=True, default=None) + + manufacturer: Mapped[Manufacturer] = relationship(lazy="selectin") diff --git a/app/schemas/manufacturer.py b/app/schemas/manufacturer.py new file mode 100644 index 0000000..2275352 --- /dev/null +++ b/app/schemas/manufacturer.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, ConfigDict, HttpUrl + + +class ManufacturerBase(BaseModel): + name: str + url: HttpUrl + logo_url: HttpUrl | None = None + + +class ManufacturerCreate(ManufacturerBase): + pass + + +class ManufacturerUpdate(BaseModel): + name: str | None = None + url: HttpUrl | None = None + logo_url: HttpUrl | None = None + + +class ManufacturerRead(ManufacturerBase): + model_config = ConfigDict(from_attributes=True) + + id: int diff --git a/app/schemas/scope_model.py b/app/schemas/scope_model.py new file mode 100644 index 0000000..16bbce6 --- /dev/null +++ b/app/schemas/scope_model.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel, ConfigDict, HttpUrl + +from app.schemas.manufacturer import ManufacturerRead + + +class ScopeModelBase(BaseModel): + name: str + manufacturer_id: int + url: HttpUrl | None = None + + +class ScopeModelCreate(ScopeModelBase): + pass + + +class ScopeModelUpdate(BaseModel): + name: str | None = None + manufacturer_id: int | None = None + url: HttpUrl | None = None + + +class ScopeModelRead(ScopeModelBase): + model_config = ConfigDict(from_attributes=True) + + id: int + manufacturer: ManufacturerRead diff --git a/app/templates/_nav.html b/app/templates/_nav.html index 3779ef9..eccc55d 100644 --- a/app/templates/_nav.html +++ b/app/templates/_nav.html @@ -14,6 +14,12 @@ {% if user.is_superuser or "unc_threads:read" in user.permissions %} UNC Threads {% endif %} + {% if user.is_superuser or "manufacturers:read" in user.permissions %} + Manufacturers + {% endif %} + {% if user.is_superuser or "scope_models:read" in user.permissions %} + Scope Models + {% endif %} API docs
diff --git a/app/templates/admin/_manufacturer_row.html b/app/templates/admin/_manufacturer_row.html new file mode 100644 index 0000000..38e690a --- /dev/null +++ b/app/templates/admin/_manufacturer_row.html @@ -0,0 +1,23 @@ + + {{ m.id }} + {{ m.name }} + {{ m.url }} + + {% if m.logo_url %} + {{ m.name }} logo + {% else %} + — + {% endif %} + + + + + + diff --git a/app/templates/admin/_manufacturer_row_edit.html b/app/templates/admin/_manufacturer_row_edit.html new file mode 100644 index 0000000..f79eb61 --- /dev/null +++ b/app/templates/admin/_manufacturer_row_edit.html @@ -0,0 +1,20 @@ + + + + #{{ m.id }} + + + +
+ + +
+
+ + diff --git a/app/templates/admin/_manufacturers_table.html b/app/templates/admin/_manufacturers_table.html new file mode 100644 index 0000000..600a46a --- /dev/null +++ b/app/templates/admin/_manufacturers_table.html @@ -0,0 +1,16 @@ +
+ + + + + + + + {% for m in manufacturers %} + {% include "admin/_manufacturer_row.html" %} + {% else %} + + {% endfor %} + +
IDNameHomepageLogo
No manufacturers recorded.
+
diff --git a/app/templates/admin/_scope_model_row.html b/app/templates/admin/_scope_model_row.html new file mode 100644 index 0000000..95abf58 --- /dev/null +++ b/app/templates/admin/_scope_model_row.html @@ -0,0 +1,23 @@ + + {{ sm.id }} + {{ sm.name }} + {{ sm.manufacturer.name }} + + {% if sm.url %} + {{ sm.url }} + {% else %} + — + {% endif %} + + + + + + diff --git a/app/templates/admin/_scope_model_row_edit.html b/app/templates/admin/_scope_model_row_edit.html new file mode 100644 index 0000000..8be6401 --- /dev/null +++ b/app/templates/admin/_scope_model_row_edit.html @@ -0,0 +1,26 @@ + + +
+ #{{ sm.id }} + + + +
+ + +
+
+ + diff --git a/app/templates/admin/_scope_models_table.html b/app/templates/admin/_scope_models_table.html new file mode 100644 index 0000000..f21a981 --- /dev/null +++ b/app/templates/admin/_scope_models_table.html @@ -0,0 +1,16 @@ +
+ + + + + + + + {% for sm in scope_models %} + {% include "admin/_scope_model_row.html" %} + {% else %} + + {% endfor %} + +
IDNameManufacturerProduct Page
No scope models recorded.
+
diff --git a/app/templates/admin/manufacturers.html b/app/templates/admin/manufacturers.html new file mode 100644 index 0000000..a342515 --- /dev/null +++ b/app/templates/admin/manufacturers.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block title %}Manufacturers · {{ app_name }}{% endblock %} +{% block body %} +{% include "_nav.html" %} +
+

Manufacturers

+ +
+

Add manufacturer

+
+ + + + +
+
+ + {% include "admin/_manufacturers_table.html" %} +
+{% endblock %} diff --git a/app/templates/admin/scope_models.html b/app/templates/admin/scope_models.html new file mode 100644 index 0000000..12d9bef --- /dev/null +++ b/app/templates/admin/scope_models.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block title %}Scope Models · {{ app_name }}{% endblock %} +{% block body %} +{% include "_nav.html" %} +
+

Scope Models

+ +
+

Add scope model

+
+ + + + +
+
+ + {% include "admin/_scope_models_table.html" %} +
+{% endblock %} diff --git a/app/web/admin.py b/app/web/admin.py index 0ab9c65..dc58105 100644 --- a/app/web/admin.py +++ b/app/web/admin.py @@ -6,7 +6,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.crud import iso_thread as iso_thread_crud +from app.crud import manufacturer as manufacturer_crud from app.crud import role as role_crud +from app.crud import scope_model as scope_model_crud from app.crud import unc_thread as unc_thread_crud from app.crud import user as user_crud from app.deps import get_current_web_user @@ -483,3 +485,223 @@ async def delete_unc_thread( if thread is not None: await unc_thread_crud.delete(db, thread) return Response(status_code=status.HTTP_200_OK) + + +# --- Manufacturers ----------------------------------------------------------- + + +@router.get("/manufacturers", response_class=HTMLResponse) +async def manufacturers_page( + request: Request, + db: AsyncSession = Depends(get_db), + user: User = Depends(web_require("manufacturers:read")), +): + return templates.TemplateResponse( + request, + "admin/manufacturers.html", + {"user": user, "manufacturers": await manufacturer_crud.list_all(db)}, + ) + + +@router.post("/manufacturers", response_class=HTMLResponse) +async def create_manufacturer( + request: Request, + name: str = Form(...), + url: str = Form(...), + logo_url: str = Form(""), + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("manufacturers:write")), +): + await manufacturer_crud.create( + db, + name=name, + url=url, + logo_url=logo_url.strip() or None, + ) + return templates.TemplateResponse( + request, + "admin/_manufacturers_table.html", + {"manufacturers": await manufacturer_crud.list_all(db)}, + ) + + +@router.get("/manufacturers/{manufacturer_id}/edit", response_class=HTMLResponse) +async def edit_manufacturer_row( + request: Request, + manufacturer_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("manufacturers:write")), +): + m = await manufacturer_crud.get(db, manufacturer_id) + if m is None: + raise HTTPException(status_code=404, detail="Manufacturer not found") + return templates.TemplateResponse( + request, "admin/_manufacturer_row_edit.html", {"m": m} + ) + + +@router.get("/manufacturers/{manufacturer_id}/row", response_class=HTMLResponse) +async def manufacturer_row( + request: Request, + manufacturer_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("manufacturers:read")), +): + m = await manufacturer_crud.get(db, manufacturer_id) + if m is None: + raise HTTPException(status_code=404, detail="Manufacturer not found") + return templates.TemplateResponse( + request, "admin/_manufacturer_row.html", {"m": m} + ) + + +@router.post("/manufacturers/{manufacturer_id}", response_class=HTMLResponse) +async def update_manufacturer( + request: Request, + manufacturer_id: int, + name: str = Form(...), + url: str = Form(...), + logo_url: str = Form(""), + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("manufacturers:write")), +): + m = await manufacturer_crud.get(db, manufacturer_id) + if m is None: + raise HTTPException(status_code=404, detail="Manufacturer not found") + await manufacturer_crud.update( + db, + m, + { + "name": name, + "url": url, + "logo_url": logo_url.strip() or None, + }, + ) + return templates.TemplateResponse( + request, "admin/_manufacturer_row.html", {"m": m} + ) + + +@router.delete("/manufacturers/{manufacturer_id}") +async def delete_manufacturer( + manufacturer_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("manufacturers:delete")), +): + m = await manufacturer_crud.get(db, manufacturer_id) + if m is not None: + await manufacturer_crud.delete(db, m) + return Response(status_code=status.HTTP_200_OK) + + +# --- Scope Models ------------------------------------------------------------ + + +@router.get("/scope-models", response_class=HTMLResponse) +async def scope_models_page( + request: Request, + db: AsyncSession = Depends(get_db), + user: User = Depends(web_require("scope_models:read")), +): + return templates.TemplateResponse( + request, + "admin/scope_models.html", + { + "user": user, + "scope_models": await scope_model_crud.list_all(db), + "manufacturers": await manufacturer_crud.list_all(db), + }, + ) + + +@router.post("/scope-models", response_class=HTMLResponse) +async def create_scope_model( + request: Request, + name: str = Form(...), + manufacturer_id: int = Form(...), + url: str = Form(""), + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("scope_models:write")), +): + await scope_model_crud.create( + db, + name=name, + manufacturer_id=manufacturer_id, + url=url.strip() or None, + ) + return templates.TemplateResponse( + request, + "admin/_scope_models_table.html", + {"scope_models": await scope_model_crud.list_all(db)}, + ) + + +@router.get("/scope-models/{scope_model_id}/edit", response_class=HTMLResponse) +async def edit_scope_model_row( + request: Request, + scope_model_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("scope_models:write")), +): + sm = await scope_model_crud.get(db, scope_model_id) + if sm is None: + raise HTTPException(status_code=404, detail="Scope model not found") + return templates.TemplateResponse( + request, + "admin/_scope_model_row_edit.html", + {"sm": sm, "manufacturers": await manufacturer_crud.list_all(db)}, + ) + + +@router.get("/scope-models/{scope_model_id}/row", response_class=HTMLResponse) +async def scope_model_row( + request: Request, + scope_model_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("scope_models:read")), +): + sm = await scope_model_crud.get(db, scope_model_id) + if sm is None: + raise HTTPException(status_code=404, detail="Scope model not found") + return templates.TemplateResponse( + request, "admin/_scope_model_row.html", {"sm": sm} + ) + + +@router.post("/scope-models/{scope_model_id}", response_class=HTMLResponse) +async def update_scope_model( + request: Request, + scope_model_id: int, + name: str = Form(...), + manufacturer_id: int = Form(...), + url: str = Form(""), + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("scope_models:write")), +): + sm = await scope_model_crud.get(db, scope_model_id) + if sm is None: + raise HTTPException(status_code=404, detail="Scope model not found") + await scope_model_crud.update( + db, + sm, + { + "name": name, + "manufacturer_id": manufacturer_id, + "url": url.strip() or None, + }, + ) + return templates.TemplateResponse( + request, "admin/_scope_model_row.html", {"sm": sm} + ) + + +@router.delete("/scope-models/{scope_model_id}") +async def delete_scope_model( + scope_model_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(web_require("scope_models:delete")), +): + sm = await scope_model_crud.get(db, scope_model_id) + if sm is not None: + await scope_model_crud.delete(db, sm) + return Response(status_code=status.HTTP_200_OK) diff --git a/scripts/seed.py b/scripts/seed.py index 207dc1e..93a70bf 100644 --- a/scripts/seed.py +++ b/scripts/seed.py @@ -27,12 +27,18 @@ PERMISSIONS = [ ("unc_threads:read", "View UNC threads"), ("unc_threads:write", "Create and update UNC threads"), ("unc_threads:delete", "Delete UNC threads"), + ("manufacturers:read", "View manufacturers"), + ("manufacturers:write", "Create and update manufacturers"), + ("manufacturers:delete", "Delete manufacturers"), + ("scope_models:read", "View scope models"), + ("scope_models:write", "Create and update scope models"), + ("scope_models:delete", "Delete scope models"), ] # Default roles -> permission names. "admin" gets everything. ROLES = { "admin": [name for name, _ in PERMISSIONS], - "user": ["users:read", "iso_threads:read", "unc_threads:read"], + "user": ["users:read", "iso_threads:read", "unc_threads:read", "manufacturers:read", "scope_models:read"], }