From 076765e5c54cf8e666c1b04f9174c9c15a411d20 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 8 Nov 2025 12:19:34 +0100 Subject: [PATCH] Added base file for serie --- backend/app/api/main.py | 2 + backend/app/api/routes/series.py | 176 ++++++++++++++++++++ backend/app/core/db.py | 3 + backend/app/models/base.py | 1 + backend/app/models/user.py | 3 + backend/app/tests/api/routes/test_serie.py | 179 +++++++++++++++++++++ backend/app/tests/utils/serie.py | 12 ++ 7 files changed, 376 insertions(+) create mode 100644 backend/app/api/routes/series.py create mode 100644 backend/app/tests/api/routes/test_serie.py create mode 100644 backend/app/tests/utils/serie.py diff --git a/backend/app/api/main.py b/backend/app/api/main.py index 39ee9f5..b0192a6 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -10,6 +10,7 @@ from app.api.routes import ( private, users, utils, + series, ) from app.core.config import settings @@ -24,6 +25,7 @@ api_router.include_router(teams.router) api_router.include_router(associations.router) api_router.include_router(divisions.router) api_router.include_router(members.router) +api_router.include_router(series.router) if settings.ENVIRONMENT == "local": diff --git a/backend/app/api/routes/series.py b/backend/app/api/routes/series.py new file mode 100644 index 0000000..b8eb361 --- /dev/null +++ b/backend/app/api/routes/series.py @@ -0,0 +1,176 @@ +from typing import Any + +from fastapi import APIRouter, HTTPException, status +from sqlmodel import func, select + +from app.api.deps import CurrentUser, SessionDep +from app.models.base import ( + ApiTags, + Message, + RowId, +) +from app.models.serie import ( + Serie, + SerieCreate, + SerieUpdate, + SeriePublic, + SeriesPublic, +) +from app.models.division import ( + Division, + DivisionsPublic, +) +from app.models.user import ( + PermissionModule, + PermissionPart, + PermissionRight, +) + +router = APIRouter(prefix="/series", tags=[ApiTags.SERIES]) + + +# region # Series ######################################################## + +@router.get("/", response_model=SeriesPublic) +def read_series( + session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100 +) -> Any: + """ + Retrieve all series. + """ + + if current_user.has_permissions( + module=PermissionModule.SERIE, + part=PermissionPart.ADMIN, + rights=PermissionRight.READ, + ): + count_statement = select(func.count()).select_from(Serie) + count = session.exec(count_statement).one() + statement = select(Serie).offset(skip).limit(limit) + series = session.exec(statement).all() + return SeriesPublic(data=series, count=count) + + return SeriesPublic(data=[], count=0) + + +@router.get("/{id}", response_model=SeriePublic) +def read_serie(session: SessionDep, current_user: CurrentUser, id: RowId) -> Any: + """ + Get serie by ID. + """ + serie = session.get(Serie, id) + if not serie: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Serie not found") + + if not current_user.has_permissions( + module=PermissionModule.SERIE, + part=PermissionPart.ADMIN, + rights=PermissionRight.READ, + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions") + + return serie + + +@router.post("/", response_model=SeriePublic) +def create_serie( + *, session: SessionDep, current_user: CurrentUser, serie_in: SerieCreate +) -> Any: + """ + Create new serie. + """ + + if not current_user.has_permissions( + module=PermissionModule.SERIE, + part=PermissionPart.ADMIN, + rights=PermissionRight.CREATE, + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions") + + serie = Serie.create(create_obj=serie_in, session=session) + return serie + + +@router.put("/{id}", response_model=SeriePublic) +def update_serie( + *, session: SessionDep, current_user: CurrentUser, id: RowId, serie_in: SerieUpdate +) -> Any: + """ + Update a serie. + """ + serie = session.get(Serie, id) + if not serie: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Serie not found") + + if not current_user.has_permissions( + module=PermissionModule.SERIE, + part=PermissionPart.ADMIN, + rights=PermissionRight.UPDATE, + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions") + + serie = Serie.update(db_obj=serie, in_obj=serie_in, session=session) + return serie + + +@router.delete("/{id}") +def delete_serie(session: SessionDep,current_user: CurrentUser, id: RowId) -> Message: + """ + Delete a serie. + """ + serie = session.get(Serie, id) + if not serie: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Serie not found") + + if not current_user.has_permissions( + module=PermissionModule.SERIE, + part=PermissionPart.ADMIN, + rights=PermissionRight.DELETE, + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions") + + session.delete(serie) + session.commit() + return Message(message="Serie deleted successfully") + +# endregion + + +# region # Series / Divisions ############################################ + + +@router.get("/{series_id}/divisions/", response_model=DivisionsPublic) +def read_serie_division( + session: SessionDep, current_user: CurrentUser, series_id: RowId, skip: int = 0, limit: int = 100 +) -> Any: + """ + Retrieve all serie divisions. + """ + + serie = session.get(Serie, series_id) + if not serie: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Serie not found") + + if not current_user.has_permission( + module=PermissionModule.SERIE, + part=PermissionPart.ADMIN, + rights=(PermissionRight.MANAGE_DIVISIONS | PermissionRight.READ), + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions") + + count_statement = (select(func.count()) + .select_from(Division) + .where(Division.serie_id == serie.id) + ) + count = session.exec(count_statement).one() + statement = (select(Division) + .where(Division.serie_id == serie.id) + .offset(skip) + .limit(limit) + ) + divisions = session.exec(statement).all() + + return DivisionsPublic(data=divisions, count=count) + + +# endregion diff --git a/backend/app/core/db.py b/backend/app/core/db.py index 4fea078..650e375 100644 --- a/backend/app/core/db.py +++ b/backend/app/core/db.py @@ -30,6 +30,9 @@ from app.models.member import ( from app.models.apikey import ( ApiKey, ) +from app.models.serie import ( + Serie, +) engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) diff --git a/backend/app/models/base.py b/backend/app/models/base.py index 0b73771..01294ec 100644 --- a/backend/app/models/base.py +++ b/backend/app/models/base.py @@ -59,6 +59,7 @@ class ApiTags(DocumentedStrEnum): ASSOCIATIONS = "Associations" DIVISIONS = "Divisions" MEMBERS = "Members" + SERIES = "Series" # endregion diff --git a/backend/app/models/user.py b/backend/app/models/user.py index d0abd28..2611df8 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from .apikey import ApiKey from .event import EventUserLink from .member import Member + from .serie import SerieUserLink # region # User ################################################################ @@ -31,6 +32,7 @@ class PermissionModule(DocumentedStrEnum): ASSOCIATION = auto_enum() DIVISION = auto_enum() MEMBER = auto_enum() + SERIE = auto_enum() class PermissionPart(DocumentedStrEnum): @@ -136,6 +138,7 @@ class User(mixin.RowId, UserBase, table=True): # --- many-to-many links --------------------------------------------------- roles: list["Role"] = Relationship(back_populates="users", link_model=UserRoleLink) event_links: list["EventUserLink"] = Relationship(back_populates="user") + serie_links: list["SerieUserLink"] = Relationship(back_populates="user") # --- CRUD actions --------------------------------------------------------- @classmethod diff --git a/backend/app/tests/api/routes/test_serie.py b/backend/app/tests/api/routes/test_serie.py new file mode 100644 index 0000000..ad1e116 --- /dev/null +++ b/backend/app/tests/api/routes/test_serie.py @@ -0,0 +1,179 @@ +import uuid + +from fastapi import status +from fastapi.testclient import TestClient +from sqlmodel import Session + +from app.core.config import settings +from app.tests.utils.serie import create_random_serie + + +def test_create_serie(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None: + data = { + "name": "Zondag spel", + "contact": "Rick", + } + response = client.post( + f"{settings.API_V1_STR}/series/", + headers=superuser_token_headers, + json=data, + ) + assert response.status_code == status.HTTP_200_OK + content = response.json() + assert content["name"] == data["name"] + assert content["contact"] == data["contact"] + assert "id" in content + + +def test_create_serie_no_permissions(client: TestClient, normal_user_token_headers: dict[str, str], db: Session) -> None: + data = { + "name": "Zondag Spel", + "contact": "Rick", + } + response = client.post( + f"{settings.API_V1_STR}/series/", + headers=normal_user_token_headers, + json=data, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json()["detail"] == "Not enough permissions" + + +def test_read_serie(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None: + serie = create_random_serie(db) + response = client.get( + f"{settings.API_V1_STR}/series/{serie.id}", + headers=superuser_token_headers, + ) + assert response.status_code == status.HTTP_200_OK + content = response.json() + assert content["id"] == str(serie.id) + assert content["name"] == serie.name + assert content["contact"] == serie.contact + + +def test_read_serie_not_found(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None: + response = client.get( + f"{settings.API_V1_STR}/series/{uuid.uuid4()}", + headers=superuser_token_headers, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.json()["detail"] == "Serie not found" + + +def test_read_serie_no_permission(client: TestClient, normal_user_token_headers: dict[str, str], db: Session) -> None: + serie = create_random_serie(db) + response = client.get( + f"{settings.API_V1_STR}/series/{serie.id}", + headers=normal_user_token_headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json()["detail"] == "Not enough permissions" + + +def test_read_series(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None: + create_random_serie(db) + create_random_serie(db) + response = client.get( + f"{settings.API_V1_STR}/series/", + headers=superuser_token_headers, + ) + assert response.status_code == status.HTTP_200_OK + content = response.json() + assert "count" in content + assert content["count"] >= 2 + assert "data" in content + assert isinstance(content["data"], list) + assert len(content["data"]) <= content["count"] + + +def test_read_series_no_permissions(client: TestClient, normal_user_token_headers: dict[str, str], db: Session) -> None: + create_random_serie(db) + create_random_serie(db) + response = client.get( + f"{settings.API_V1_STR}/series/", + headers=normal_user_token_headers, + ) + assert response.status_code == status.HTTP_200_OK + content = response.json() + assert "count" in content + assert content["count"] == 0 + assert "data" in content + assert isinstance(content["data"], list) + assert len(content["data"]) == 0 + + +def test_update_serie(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None: + serie = create_random_serie(db) + data = { + "name": "Updated name", + "contact": "Updated contact", + } + response = client.put( + f"{settings.API_V1_STR}/series/{serie.id}", + headers=superuser_token_headers, + json=data, + ) + assert response.status_code == status.HTTP_200_OK + content = response.json() + assert content["id"] == str(serie.id) + assert content["name"] == data["name"] + assert content["contact"] == data["contact"] + + +def test_update_serie_not_found(client: TestClient, superuser_token_headers: dict[str, str]) -> None: + data = { + "name": "Not found", + "contact": "Not found", + } + response = client.put( + f"{settings.API_V1_STR}/series/{uuid.uuid4()}", + headers=superuser_token_headers, + json=data, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.json()["detail"] == "Serie not found" + + +def test_update_serie_no_permissions(client: TestClient, normal_user_token_headers: dict[str, str], db: Session) -> None: + serie = create_random_serie(db) + data = { + "name": "No permissions", + "contact": "No permissions", + } + response = client.put( + f"{settings.API_V1_STR}/series/{serie.id}", + headers=normal_user_token_headers, + json=data, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json()["detail"] == "Not enough permissions" + + +def test_delete_serie(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None: + serie = create_random_serie(db) + response = client.delete( + f"{settings.API_V1_STR}/series/{serie.id}", + headers=superuser_token_headers, + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["message"] == "Serie deleted successfully" + + +def test_delete_serie_not_found(client: TestClient, superuser_token_headers: dict[str, str]) -> None: + response = client.delete( + f"{settings.API_V1_STR}/series/{uuid.uuid4()}", + headers=superuser_token_headers, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.json()["detail"] == "Serie not found" + + +def test_delete_serie_no_permissions(client: TestClient, normal_user_token_headers: dict[str, str], db: Session) -> None: + serie = create_random_serie(db) + response = client.delete( + f"{settings.API_V1_STR}/series/{serie.id}", + headers=normal_user_token_headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json()["detail"] == "Not enough permissions" diff --git a/backend/app/tests/utils/serie.py b/backend/app/tests/utils/serie.py new file mode 100644 index 0000000..c3a8584 --- /dev/null +++ b/backend/app/tests/utils/serie.py @@ -0,0 +1,12 @@ +from sqlmodel import Session + +from app.models.serie import Serie, SerieCreate +from app.tests.utils.utils import random_lower_string + + +def create_random_serie(db: Session, name: str = None) -> Serie: + if not name: + name = random_lower_string() + + serie_in = SerieCreate(name=name) + return Serie.create(session=db, create_obj=serie_in)