Add Division info to Teams

This commit is contained in:
Sebastiaan
2025-06-12 23:31:56 +02:00
parent 56b503751a
commit 9538b9067c
5 changed files with 480 additions and 8 deletions

View File

@@ -25,6 +25,12 @@ from app.models.user import (
PermissionPart, PermissionPart,
PermissionRight, PermissionRight,
) )
from app.models.division import (
DivisionTeamLink,
DivisionTeamLinkCreate,
DivisionTeamLinkUpdate,
DivisionTeamLinkPublic,
)
router = APIRouter(prefix="/teams", tags=[ApiTags.TEAMS]) router = APIRouter(prefix="/teams", tags=[ApiTags.TEAMS])
@@ -191,3 +197,113 @@ def delete_team(session: SessionDep,current_user: CurrentUser, id: RowId) -> Mes
return Message(message="Team deleted successfully") return Message(message="Team deleted successfully")
# endregion # endregion
# region # Teams / Division ####################################################
@router.get("/{id}/division", response_model=DivisionTeamLinkPublic, tags=[ApiTags.DIVISIONS])
def read_team_divisions(session: SessionDep, current_user: CurrentUser, id: RowId) -> Any:
"""
Get division from team by ID.
"""
team = session.get(Team, id)
if not team:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Team not found")
event = session.get(Event, team.event_id)
if not event: # pragma: no cover
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Event not found")
if not current_user.has_permissions(
module=PermissionModule.TEAM,
part=PermissionPart.ADMIN,
rights=PermissionRight.MANAGE_DIVISIONS,
) and not (event.user_has_rights(user=current_user, rights=PermissionRight.MANAGE_DIVISIONS)):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")
return team.division_link
@router.post("/{id}/division", response_model=DivisionTeamLinkPublic, tags=[ApiTags.DIVISIONS])
def create_team_division_link(
*, session: SessionDep, current_user: CurrentUser, team_in: DivisionTeamLinkCreate, id: RowId
) -> Any:
"""
Create new division link in team.
"""
team = session.get(Team, id)
if not team:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Team not found")
if team.division_link:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Team already linked to division")
event = session.get(Event, team.event_id)
if not event: # pragma: no cover
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Event not found")
if not current_user.has_permissions(
module=PermissionModule.TEAM,
part=PermissionPart.ADMIN,
rights=PermissionRight.MANAGE_DIVISIONS,
) and not (event.user_has_rights(user=current_user, rights=PermissionRight.MANAGE_DIVISIONS)):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")
division_team_link = DivisionTeamLink.create(create_obj=team_in, session=session, team=team)
return division_team_link
@router.put("/{id}/division", response_model=DivisionTeamLinkPublic, tags=[ApiTags.DIVISIONS])
def update_team_division_link(
*, session: SessionDep, current_user: CurrentUser, id: RowId, team_in: DivisionTeamLinkUpdate
) -> Any:
"""
Update division info inside team.
"""
team = session.get(Team, id)
if not team:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Team not found")
# Check user's permissions for the existing event
event = session.get(Event, team.event_id)
if not event: # pragma: no cover
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Event not found")
if not current_user.has_permissions(
module=PermissionModule.TEAM,
part=PermissionPart.ADMIN,
rights=PermissionRight.MANAGE_DIVISIONS,
) and not (event.user_has_rights(user=current_user, rights=PermissionRight.MANAGE_DIVISIONS)):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")
# Update the team
division_team_link = DivisionTeamLink.update(db_obj=team.division_link, in_obj=team_in, session=session)
return division_team_link
@router.delete("/{id}/division", tags=[ApiTags.DIVISIONS])
def delete_team_division_link(session: SessionDep, current_user: CurrentUser, id: RowId) -> Message:
"""
Delete a division link from a team.
"""
team = session.get(Team, id)
if not team:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Team not found")
event = session.get(Event, team.event_id)
if not event: # pragma: no cover
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Event not found")
if not current_user.has_permissions(
module=PermissionModule.TEAM,
part=PermissionPart.ADMIN,
rights=PermissionRight.MANAGE_DIVISIONS,
) and not (event.user_has_rights(user=current_user, rights=PermissionRight.MANAGE_DIVISIONS)):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")
session.delete(team.division_link)
session.commit()
return Message(message="Division deleted from team successfully")
# endregion

View File

@@ -1,5 +1,6 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy.orm.sync import update
from sqlmodel import ( from sqlmodel import (
Session, Session,
Field, Field,
@@ -14,6 +15,82 @@ from .base import (
if TYPE_CHECKING: if TYPE_CHECKING:
from .association import Association from .association import Association
from .team import Team
# region # Divisions / Team Link ######################################
class DivisionTeamLinkBase(
mixin.Name,
BaseSQLModel,
):
pass
class DivisionTeamLinkCreate(DivisionTeamLinkBase):
division_id: RowId | None = Field(default=None)
class DivisionTeamLinkUpdate(DivisionTeamLinkBase):
pass
class DivisionTeamLink(DivisionTeamLinkBase, table=True):
# --- database only items --------------------------------------------------
# --- read only items ------------------------------------------------------
team_id: RowId = Field(
default=None,
foreign_key="team.id",
nullable=False,
ondelete="CASCADE",
primary_key=True,
)
# --- back_populates links -------------------------------------------------
division_id: RowId = Field(
default=None,
foreign_key="division.id",
nullable=False,
ondelete="CASCADE",
)
division: "Division" = Relationship(back_populates="team_links")
team: "Team" = Relationship(back_populates="division_link")
# Members (1 lid > meerdere teams | many-to-one)
# --- CRUD actions ---------------------------------------------------------
@classmethod
def create(cls, *, session: Session, create_obj: DivisionTeamLinkCreate, team: "Team") -> "DivisionTeamLink":
data_obj = create_obj.model_dump(exclude_unset=True)
db_obj = cls.model_validate(data_obj, update={"team_id": team.id})
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
@classmethod
def update(
cls, *, session: Session, db_obj: "DivisionTeamLink", in_obj: DivisionTeamLinkUpdate
) -> "DivisionTeamLink":
data_obj = in_obj.model_dump(exclude_unset=True)
db_obj.sqlmodel_update(data_obj)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
# Properties to return via API, id is always required
class DivisionTeamLinkPublic(DivisionTeamLinkBase):
team_id: RowId
division_id: RowId
# endregion
# region # Divisions ########################################################### # region # Divisions ###########################################################
@@ -45,7 +122,8 @@ class Division(mixin.RowId, DivisionBase, table=True):
# --- read only items ------------------------------------------------------ # --- read only items ------------------------------------------------------
# --- back_populates links ------------------------------------------------- # --- back_populates links -------------------------------------------------
association: "Association" = Relationship(back_populates="divisions") # , cascade_delete=True) association: "Association" = Relationship(back_populates="divisions")
team_links: list["DivisionTeamLink"] = Relationship(back_populates="division", cascade_delete=True)
# --- CRUD actions --------------------------------------------------------- # --- CRUD actions ---------------------------------------------------------
@classmethod @classmethod

View File

@@ -102,7 +102,7 @@ class Event(mixin.RowId, EventBase, table=True):
# --- many-to-many links --------------------------------------------------- # --- many-to-many links ---------------------------------------------------
user_links: list["EventUserLink"] = Relationship(back_populates="event", cascade_delete=True) user_links: list["EventUserLink"] = Relationship(back_populates="event", cascade_delete=True)
team_links: list["Team"] = Relationship(back_populates="event", cascade_delete=True) teams: list["Team"] = Relationship(back_populates="event", cascade_delete=True)
# --- CRUD actions --------------------------------------------------------- # --- CRUD actions ---------------------------------------------------------
@classmethod @classmethod

View File

@@ -14,6 +14,7 @@ from .base import (
if TYPE_CHECKING: if TYPE_CHECKING:
from .event import Event from .event import Event
from .division import DivisionTeamLink
# region # Team ################################################################ # region # Team ################################################################
@@ -28,9 +29,6 @@ class TeamBase(
event_id: RowId = Field( event_id: RowId = Field(
foreign_key="event.id", nullable=False, ondelete="CASCADE" foreign_key="event.id", nullable=False, ondelete="CASCADE"
) )
# scouting_team_id: RowId | None = Field(
# foreign_key="ScoutingTeam.id", nullable=False, ondelete="CASCADE"
# )
# Properties to receive via API on creation # Properties to receive via API on creation
@@ -49,8 +47,8 @@ class Team(mixin.RowId, TeamBase, table=True):
# --- read only items ------------------------------------------------------ # --- read only items ------------------------------------------------------
# --- back_populates links ------------------------------------------------- # --- back_populates links -------------------------------------------------
event: "Event" = Relationship(back_populates="team_links")#, cascade_delete=True) event: "Event" = Relationship(back_populates="teams")
# team: "ScoutingTeam" = Relationship(back_populates="event_links", cascade_delete=True) division_link: "DivisionTeamLink" = Relationship(back_populates="team", cascade_delete=True)
# --- CRUD actions --------------------------------------------------------- # --- CRUD actions ---------------------------------------------------------
@classmethod @classmethod
@@ -85,4 +83,4 @@ class TeamsPublic(BaseSQLModel):
count: int count: int
# endregion # endregion

View File

@@ -5,8 +5,12 @@ from fastapi.testclient import TestClient
from sqlmodel import Session from sqlmodel import Session
from app.core.config import settings from app.core.config import settings
from app.models.division import DivisionTeamLink, DivisionTeamLinkCreate
from app.tests.conftest import EventUserHeader
from app.tests.utils.division import create_random_division from app.tests.utils.division import create_random_division
from app.tests.utils.association import create_random_association from app.tests.utils.association import create_random_association
from app.tests.utils.team import create_random_team
from app.tests.utils.utils import random_lower_string
def test_create_division(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None: def test_create_division(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None:
@@ -189,3 +193,279 @@ def test_delete_division_no_permissions(client: TestClient, normal_user_token_he
) )
assert response.status_code == status.HTTP_403_FORBIDDEN assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.json()["detail"] == "Not enough permissions" assert response.json()["detail"] == "Not enough permissions"
def test_create_team_division_link(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None:
team = create_random_team(db)
division = create_random_division(db)
data = {
"name": "Otters",
"division_id": str(division.id),
}
response = client.post(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=superuser_token_headers,
json=data,
)
assert response.status_code == status.HTTP_200_OK
content = response.json()
assert content["name"] == data["name"]
assert content["team_id"] == str(team.id)
assert content["division_id"] == str(division.id)
def test_create_team_division_link_unknown_team(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None:
division = create_random_division(db)
data = {
"name": "Vossen",
"division_id": str(division.id),
}
response = client.post(
f"{settings.API_V1_STR}/teams/{uuid.uuid4()}/division",
headers=superuser_token_headers,
json=data,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json()["detail"] == "Team not found"
def test_create_team_division_already_linked(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None:
team = create_random_team(db)
division1 = create_random_division(db)
name = random_lower_string()
DivisionTeamLink.create(session=db, create_obj=DivisionTeamLinkCreate(name=name, division_id=division1.id), team=team)
division2 = create_random_division(db)
data = {
"name": "Vossen",
"division_id": str(division2.id),
}
response = client.post(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=superuser_token_headers,
json=data,
)
assert response.status_code == status.HTTP_409_CONFLICT
assert response.json()["detail"] == "Team already linked to division"
def test_create_team_division_link_no_permissions(client: TestClient, normal_user_token_headers: dict[str, str], db: Session) -> None:
team = create_random_team(db)
division = create_random_division(db)
data = {
"name": "Arenden",
"division_id": str(division.id),
}
response = client.post(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=normal_user_token_headers,
json=data,
)
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.json()["detail"] == "Not enough permissions"
def test_create_team_division_link_event_user(client: TestClient, event_user_token_headers: EventUserHeader, db: Session) -> None:
team = create_random_team(db, event=event_user_token_headers.event)
division = create_random_division(db)
data = {
"name": "Sperwers",
"division_id": str(division.id),
}
response = client.post(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=event_user_token_headers.headers,
json=data,
)
assert response.status_code == status.HTTP_200_OK
content = response.json()
assert content["name"] == data["name"]
assert content["team_id"] == str(team.id)
assert content["division_id"] == str(division.id)
def test_read_team_division(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None:
team = create_random_team(db)
division = create_random_division(db)
name = random_lower_string()
DivisionTeamLink.create(session=db, create_obj=DivisionTeamLinkCreate(name=name, division_id=division.id), team=team)
response = client.get(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=superuser_token_headers,
)
assert response.status_code == status.HTTP_200_OK
content = response.json()
assert content["name"] == name
assert content["team_id"] == str(team.id)
assert content["division_id"] == str(division.id)
def test_read_team_division_unknown_team(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None:
response = client.get(
f"{settings.API_V1_STR}/teams/{uuid.uuid4()}/division",
headers=superuser_token_headers,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json()["detail"] == "Team not found"
def test_read_team_division_no_permissions(client: TestClient, normal_user_token_headers: dict[str, str], db: Session) -> None:
team = create_random_team(db)
response = client.get(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=normal_user_token_headers,
)
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.json()["detail"] == "Not enough permissions"
def test_read_team_division_event_user(client: TestClient, event_user_token_headers: EventUserHeader, db: Session) -> None:
team = create_random_team(db, event=event_user_token_headers.event)
division = create_random_division(db)
name = random_lower_string()
DivisionTeamLink.create(session=db, create_obj=DivisionTeamLinkCreate(name=name, division_id=division.id), team=team)
response = client.get(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=event_user_token_headers.headers,
)
assert response.status_code == status.HTTP_200_OK
content = response.json()
assert content["name"] == name
assert content["team_id"] == str(team.id)
assert content["division_id"] == str(division.id)
def test_update_team_division_link(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None:
team = create_random_team(db)
division = create_random_division(db)
name = random_lower_string()
DivisionTeamLink.create(session=db, create_obj=DivisionTeamLinkCreate(name=name, division_id=division.id), team=team)
data = {
"name": "Muizenoor",
}
response = client.put(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=superuser_token_headers,
json=data,
)
assert response.status_code == status.HTTP_200_OK
content = response.json()
assert content["name"] == data["name"]
assert content["team_id"] == str(team.id)
assert content["division_id"] == str(division.id)
def test_update_team_division_link_unknown_team(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None:
data = {
"name": "Leeuenbek",
}
response = client.put(
f"{settings.API_V1_STR}/teams/{uuid.uuid4()}/division",
headers=superuser_token_headers,
json=data,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json()["detail"] == "Team not found"
def test_update_team_division_link_no_permissions(client: TestClient, normal_user_token_headers: dict[str, str], db: Session) -> None:
team = create_random_team(db)
division = create_random_division(db)
name = random_lower_string()
DivisionTeamLink.create(session=db, create_obj=DivisionTeamLinkCreate(name=name, division_id=division.id), team=team)
data = {
"name": "Geitebaard",
}
response = client.put(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=normal_user_token_headers,
json=data,
)
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.json()["detail"] == "Not enough permissions"
def test_update_team_division_link_event_user(client: TestClient, event_user_token_headers: EventUserHeader, db: Session) -> None:
team = create_random_team(db, event=event_user_token_headers.event)
division = create_random_division(db)
name = random_lower_string()
DivisionTeamLink.create(session=db, create_obj=DivisionTeamLinkCreate(name=name, division_id=division.id), team=team)
data = {
"name": "Berenklouw",
}
response = client.put(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=event_user_token_headers.headers,
json=data,
)
assert response.status_code == status.HTTP_200_OK
content = response.json()
assert content["name"] == data["name"]
assert content["team_id"] == str(team.id)
assert content["division_id"] == str(division.id)
def test_delete_team_division_link(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None:
team = create_random_team(db)
division = create_random_division(db)
name = random_lower_string()
DivisionTeamLink.create(session=db, create_obj=DivisionTeamLinkCreate(name=name, division_id=division.id), team=team)
response = client.delete(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=superuser_token_headers,
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["message"] == "Division deleted from team successfully"
def test_delete_team_division_link_unknown_team(client: TestClient, superuser_token_headers: dict[str, str], db: Session) -> None:
response = client.delete(
f"{settings.API_V1_STR}/teams/{uuid.uuid4()}/division",
headers=superuser_token_headers,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json()["detail"] == "Team not found"
def test_delete_team_division_link_no_permissions(client: TestClient, normal_user_token_headers: dict[str, str], db: Session) -> None:
team = create_random_team(db)
division = create_random_division(db)
name = random_lower_string()
DivisionTeamLink.create(session=db, create_obj=DivisionTeamLinkCreate(name=name, division_id=division.id), team=team)
response = client.delete(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=normal_user_token_headers,
)
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.json()["detail"] == "Not enough permissions"
def test_delete_team_division_link_event_user(client: TestClient, event_user_token_headers: EventUserHeader, db: Session) -> None:
team = create_random_team(db, event=event_user_token_headers.event)
division = create_random_division(db)
name = random_lower_string()
DivisionTeamLink.create(session=db, create_obj=DivisionTeamLinkCreate(name=name, division_id=division.id), team=team)
response = client.delete(
f"{settings.API_V1_STR}/teams/{team.id}/division",
headers=event_user_token_headers.headers,
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["message"] == "Division deleted from team successfully"