diff --git a/app/core/groups/groups_type.py b/app/core/groups/groups_type.py index 0cd291a92..361f58d80 100644 --- a/app/core/groups/groups_type.py +++ b/app/core/groups/groups_type.py @@ -26,6 +26,7 @@ class GroupType(str, Enum): # Module related groups amap = "70db65ee-d533-4f6b-9ffa-a4d70a17b7ef" BDE = "53a669d6-84b1-4352-8d7c-421c1fbd9c6a" + BDS = "0a728640-50d3-43a9-b26c-67a758698c44" CAA = "6c6d7e88-fdb8-4e42-b2b5-3d3cfd12e7d6" cinema = "ce5f36e6-5377-489f-9696-de70e2477300" raid_admin = "e9e6e3d3-9f5f-4e9b-8e5f-9f5f4e9b8e5f" diff --git a/app/modules/sports_results/cruds_sport_results.py b/app/modules/sports_results/cruds_sport_results.py new file mode 100644 index 000000000..5a43a4c27 --- /dev/null +++ b/app/modules/sports_results/cruds_sport_results.py @@ -0,0 +1,252 @@ +from collections.abc import Sequence + +from sqlalchemy import delete, select, update +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.sports_results import models_sport_results, schemas_sport_results + + +async def get_sports( + db: AsyncSession, +) -> Sequence[models_sport_results.Sport]: + result = await db.execute(select(models_sport_results.Sport)) + return result.scalars().all() + + +async def get_sport_by_id( + sport_id, + db: AsyncSession, +) -> models_sport_results.Sport | None: + result = await db.execute( + select(models_sport_results.Sport).where( + models_sport_results.Sport.id == sport_id, + ), + ) + return result.scalars().first() + + +async def get_captains_by_sport_id( + sport_id: str, + db: AsyncSession, +) -> Sequence[models_sport_results.Captain]: + result = await db.execute( + select(models_sport_results.Captain).where( + models_sport_results.Captain.sports.id == sport_id, + ), + ) + return result.scalars().all() + + +async def is_user_a_captain_of_a_sport( + user_id: str, + sport_id: str, + db: AsyncSession, +) -> bool: + result = await db.execute( + select( + models_sport_results.Captain, + ).where( + models_sport_results.Captain.sports.any( + models_sport_results.Captain.sports.id == sport_id, + ), + models_sport_results.Captain.user_id == user_id, + ), + ) + + return result.unique().scalars().first() is not None + + +async def is_user_a_captain( + user_id: str, + db: AsyncSession, +) -> bool: + result = await db.execute( + select( + models_sport_results.Captain, + ).where( + models_sport_results.Captain.user_id == user_id, + ), + ) + + return result.unique().scalars().first() is not None + + +async def get_captain_by_id( + captain_id, + db: AsyncSession, +) -> models_sport_results.Captain | None: + captain = await db.execute( + select(models_sport_results.Captain).where( + models_sport_results.Captain.id == captain_id, + ), + ) + return captain.scalars().first() + + +async def get_result_by_id( + result_id, + db: AsyncSession, +) -> models_sport_results.Result | None: + result = await db.execute( + select(models_sport_results.Result).where( + models_sport_results.Result.id == result_id, + ), + ) + return result.scalars().first() + + +async def get_results(db: AsyncSession) -> Sequence[models_sport_results.Result]: + result = await db.execute( + select(models_sport_results.Result).order_by( + models_sport_results.Result.match_date, + ), + ) + return result.scalars().all() + + +async def get_results_by_sport_id( + sport_id, + db: AsyncSession, +) -> Sequence[models_sport_results.Result]: + result = await db.execute( + select(models_sport_results.Result) + .where( + models_sport_results.Result.sport_id == sport_id, + ) + .order_by( + models_sport_results.Result.match_date, + ), + ) + return result.scalars().all() + + +async def add_result( + result: models_sport_results.Result, + db: AsyncSession, +) -> models_sport_results.Result: + db.add(result) + try: + await db.commit() + return result + except IntegrityError as error: + await db.rollback() + raise ValueError(error) + + +async def update_result( + result_id: str, + result_update: schemas_sport_results.ResultUpdate, + db: AsyncSession, +): + await db.execute( + update(models_sport_results.Result) + .where( + models_sport_results.Result.id == result_id, + ) + .values(**result_update.model_dump(exclude_none=True)), + ) + await db.commit() + + +async def delete_result( + result_id: str, + db: AsyncSession, +): + await db.execute( + delete(models_sport_results.Result).where( + models_sport_results.Result.id == result_id, + ), + ) + await db.commit() + + +async def add_captain( + captain: models_sport_results.Captain, + db: AsyncSession, +) -> models_sport_results.Captain: + db.add(captain) + try: + await db.commit() + return captain + except IntegrityError as error: + await db.rollback() + raise ValueError(error) + + +async def update_captain( + captain_id: str, + captain_update: schemas_sport_results.CaptainUpdate, + db: AsyncSession, +): + await db.execute( + update(models_sport_results.Captain) + .where( + models_sport_results.Captain.id == captain_id, + ) + .values(**captain_update.model_dump(exclude_none=True)), + ) + await db.commit() + + +async def delete_captain( + captain_id: str, + db: AsyncSession, +): + await db.execute( + delete(models_sport_results.Captain).where( + models_sport_results.Captain.id == captain_id, + ), + ) + await db.commit() + + +async def add_sport( + sport: models_sport_results.Sport, + db: AsyncSession, +) -> models_sport_results.Sport: + db.add(sport) + try: + await db.commit() + return sport + except IntegrityError as error: + await db.rollback() + raise ValueError(error) + + +async def update_sport( + sport_id: str, + sport_update: schemas_sport_results.SportUpdate, + db: AsyncSession, +): + await db.execute( + update(models_sport_results.Sport) + .where( + models_sport_results.Sport.id == sport_id, + ) + .values(**sport_update.model_dump(exclude_none=True)), + ) + await db.commit() + + +async def delete_sport( + sport_id: str, + db: AsyncSession, +): + await db.execute( + delete(models_sport_results.Result).where( + models_sport_results.Result.sport_id == sport_id, + ), + ) + await db.execute( + ##################################### + delete(models_sport_results.Captain).where( + models_sport_results.Captain.sports.id == sport_id, + ), + ) + await db.execute( + delete(models_sport_results.Sport).where( + models_sport_results.Sport.id == sport_id, + ), + ) + await db.commit() \ No newline at end of file diff --git a/app/modules/sports_results/endpoints_sport_results.py b/app/modules/sports_results/endpoints_sport_results.py new file mode 100644 index 000000000..da249102e --- /dev/null +++ b/app/modules/sports_results/endpoints_sport_results.py @@ -0,0 +1,359 @@ +import uuid + +from fastapi import Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core import models_core +from app.core.groups.groups_type import GroupType +from app.dependencies import get_db, is_user_a_member, is_user_a_member_of +from app.modules.sports_results import ( + cruds_sport_results, + models_sport_results, + schemas_sport_results, +) +from app.types.module import Module + +module = Module( + root="sport-results", + tag="Sport_results", + default_allowed_groups_ids=[GroupType.student], +) + + +@module.router.get( + "/sport-results/results/", + response_model=list[schemas_sport_results.ResultComplete], + status_code=200, +) +async def get_results( + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member), +): + return await cruds_sport_results.get_results(db=db) + + +@module.router.get( + "/sport-results/results/sport/{sport_id}", + response_model=list[schemas_sport_results.ResultComplete], + status_code=200, +) +async def get_results_by_sport_id( + sport_id: str, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member), +): + return await cruds_sport_results.get_results_by_sport_id(sport_id, db=db) + + +@module.router.get( + "/sport-results/results/{result_id}", + response_model=schemas_sport_results.ResultComplete, + status_code=200, +) +async def get_result_by_id( + result_id: str, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member), +): + return await cruds_sport_results.get_result_by_id(result_id, db=db) + + +@module.router.get( + "/sport-results/sports/", + response_model=list[schemas_sport_results.SportComplete], + status_code=200, +) +async def get_sports( + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member), +): + return await cruds_sport_results.get_sports(db=db) + + +@module.router.get( + "/sport-results/captain/{user_id}", + response_model=bool, + status_code=200, +) +async def is_user_a_captain( + user_id: str, + db: AsyncSession = Depends(get_db), +): + return await cruds_sport_results.is_user_a_captain(user_id, db=db) + + +@module.router.get( + "/sport-results/captain/{user_id}/{sport_id}", + response_model=bool, + status_code=200, +) +async def is_user_a_captain_of_a_sport( + user_id: str, + sport_id: str, + db: AsyncSession = Depends(get_db), +): + return await cruds_sport_results.is_user_a_captain_of_a_sport( + user_id, + sport_id, + db=db, + ) + + +@module.router.get( + "/sport-results/captain/sport/{sport_id}", + response_model=list[schemas_sport_results.CaptainComplete], + status_code=200, +) +async def get_captains_by_sport_id( + sport_id: str, + db: AsyncSession = Depends(get_db), +): + return await cruds_sport_results.get_captains_by_sport_id( + sport_id, + db=db, + ) + + +@module.router.post( + "/sport-results/captain", + response_model=schemas_sport_results.CaptainBase, + status_code=201, +) +async def add_captain( + captain: schemas_sport_results.CaptainBase, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDS)), +): + captain_complete = schemas_sport_results.CaptainComplete( + id=str(uuid.uuid4()), + **captain.model_dump(), + ) + try: + captain_db = models_sport_results.Captain( + id=captain_complete.id, + user_id=captain_complete.user_id, + sports=captain_complete.sports, + ) + return await cruds_sport_results.add_captain(captain=captain_db, db=db) + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) + + +@module.router.patch( + "/sport-results/captain/{captain_id}", + status_code=204, +) +async def update_captain( + captain_id: str, + captain_update: schemas_sport_results.CaptainUpdate, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDS)), +): + captain = await cruds_sport_results.get_captain_by_id(captain_id=captain_id, db=db) + if not captain: + raise HTTPException( + status_code=404, + detail="Invalid id", + ) + + await cruds_sport_results.update_captain( + captain_id=captain_id, + captain_update=captain_update, + db=db, + ) + + +@module.router.delete( + "/sport-results/{captain_id}", + status_code=204, +) +async def delete_captain( + captain_id: str, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDS)), +): + captain = await cruds_sport_results.get_captain_by_id(captain_id=captain_id, db=db) + if not captain: + raise HTTPException( + status_code=404, + detail="Invalid id", + ) + + await cruds_sport_results.delete_captain( + captain_id=captain_id, + db=db, + ) + + +@module.router.post( + "/sport-results/result", + response_model=schemas_sport_results.ResultComplete, + status_code=201, +) +async def add_result( + result: schemas_sport_results.ResultBase, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member), +): + if not cruds_sport_results.is_user_a_captain_of_a_sport( + user.id, + result.sport_id, + db, + ): + raise HTTPException(status_code=403, detail="Not a captain") + + result_complete = schemas_sport_results.ResultComplete( + id=str(uuid.uuid4()), + **result.model_dump(), + ) + try: + result_db = models_sport_results.Result( + id=result_complete.id, + sport_id=result_complete.sport_id, + score1=result_complete.score1, + score2=result_complete.score2, + rank=result_complete.rank, + location=result_complete.location, + match_date=result_complete.match_date, + ) + return await cruds_sport_results.add_result(result=result_db, db=db) + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) + + +@module.router.patch( + "/sport-results/result/{result_id}", + status_code=204, +) +async def update_result( + result_id: str, + result_update: schemas_sport_results.ResultUpdate, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member), +): + result = await cruds_sport_results.get_result_by_id(result_id=result_id, db=db) + if not result: + raise HTTPException( + status_code=404, + detail="Invalid id", + ) + + if not cruds_sport_results.is_user_a_captain_of_a_sport( + user.id, + result.sport_id, + db, + ): + raise HTTPException(status_code=403, detail="Not a captain") + + if result_update.sport_id and not cruds_sport_results.is_user_a_captain_of_a_sport( + user.id, + result_update.sport_id, + db, + ): + raise HTTPException(status_code=403, detail="Not a captain") + + await cruds_sport_results.update_result( + result_id=result_id, + result_update=result_update, + db=db, + ) + + +@module.router.delete( + "/sport-results/result/{result_id}", + status_code=204, +) +async def delete_result( + result_id: str, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member), +): + result = await cruds_sport_results.get_result_by_id(result_id=result_id, db=db) + if not result: + raise HTTPException( + status_code=404, + detail="Invalid id", + ) + + if not cruds_sport_results.is_user_a_captain_of_a_sport( + user.id, + result.sport_id, + db, + ): + raise HTTPException(status_code=403, detail="Not a captain") + + await cruds_sport_results.delete_result( + result_id=result_id, + db=db, + ) + + +@module.router.post( + "/sport-results/sport/", + response_model=schemas_sport_results.SportComplete, + status_code=201, +) +async def add_sport( + sport: schemas_sport_results.SportBase, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDS)), +): + sport_complete = schemas_sport_results.SportComplete( + id=str(uuid.uuid4()), + **sport.model_dump(), + ) + try: + sport_db = models_sport_results.Sport( + id=sport_complete.id, + name=sport_complete.name, + captains=sport_complete.captains, + ) + return await cruds_sport_results.add_sport(sport=sport_db, db=db) + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) + + +@module.router.patch( + "/sport-results/sport/{sport_id}", + status_code=204, +) +async def update_sport( + sport_id: str, + sport_update: schemas_sport_results.SportUpdate, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDS)), +): + sport = await cruds_sport_results.get_sport_by_id(sport_id=sport_id, db=db) + if not sport: + raise HTTPException( + status_code=404, + detail="Invalid id", + ) + + await cruds_sport_results.update_sport( + sport_id=sport_id, + sport_update=sport_update, + db=db, + ) + + +@module.router.delete( + "/sport-results/sport/{sport_id}", + status_code=204, +) +async def delete_sport( + sport_id: str, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDS)), +): + sport = await cruds_sport_results.get_sport_by_id(sport_id=sport_id, db=db) + if not sport: + raise HTTPException( + status_code=404, + detail="Invalid id", + ) + + await cruds_sport_results.delete_sport( + sport_id=sport_id, + db=db, + ) \ No newline at end of file diff --git a/app/modules/sports_results/models_sport_results.py b/app/modules/sports_results/models_sport_results.py new file mode 100644 index 000000000..afbee4310 --- /dev/null +++ b/app/modules/sports_results/models_sport_results.py @@ -0,0 +1,46 @@ +from datetime import date + +from sqlalchemy import Date, ForeignKey, Integer, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.types.sqlalchemy import Base + +class CaptainsSports(Base): + __tablename__ = "captains_sports" + + capitain_id: Mapped[str] = mapped_column(ForeignKey("sport_team_captains.id"), primary_key=True) + sport_id: Mapped[str] = mapped_column(ForeignKey("sport_sports.id"), primary_key=True) + +class Captain(Base): + __tablename__ = "sport_team_captains" + + id: Mapped[str] = mapped_column(String, primary_key=True) + user_id: Mapped[str] = mapped_column(ForeignKey("core_user.id"), nullable=False) + sports: Mapped[list["Sport"]] = relationship( + "Sport", + secondary="captains_sports", + back_populates="captains", + ) + + +class Sport(Base): + __tablename__ = "sport_sports" + + id: Mapped[str] = mapped_column(String, primary_key=True) + name: Mapped[str] = mapped_column(String, unique=True, nullable=False) + captains: Mapped[list["Captain"]] = relationship( + "Captain", + secondary="captains_sports", + back_populates="sports", + ) + +class Result(Base): + __tablename__ = "sport_results" + + id: Mapped[str] = mapped_column(String, primary_key=True) + sport_id: Mapped[str] = mapped_column(String, nullable=False) + score1: Mapped[int] = mapped_column(Integer, nullable=False) + score2: Mapped[int] = mapped_column(Integer, nullable=False) + rank: Mapped[int] = mapped_column(Integer, nullable=False) + location: Mapped[str] = mapped_column(String, nullable=False) + match_date: Mapped[date] = mapped_column(Date, nullable=False) \ No newline at end of file diff --git a/app/modules/sports_results/schemas_sport_results.py b/app/modules/sports_results/schemas_sport_results.py new file mode 100644 index 000000000..5f283694f --- /dev/null +++ b/app/modules/sports_results/schemas_sport_results.py @@ -0,0 +1,53 @@ +from datetime import date + +from pydantic import BaseModel + + +class CaptainBase(BaseModel): + user_id: str + sports: list["SportComplete"] + + +class CaptainUpdate(BaseModel): + user_id: str | None = None + sports: "list[SportComplete] | None" = None + + +class CaptainComplete(CaptainBase): + id: str + + +class SportBase(BaseModel): + name: str + captains: list[CaptainComplete] + + +class SportUpdate(BaseModel): + name: str | None = None + captains: list[CaptainComplete] | None = None + + +class SportComplete(SportBase): + id: str + + +class ResultBase(BaseModel): + sport_id: str + score1: int | None + score2: int | None + rank: int | None + location: str + match_date: date + + +class ResultUpdate(BaseModel): + sport_id: str | None = None + score1: int | None = None + score2: int | None = None + rank: int | None = None + location: str | None = None + match_date: date | None = None + + +class ResultComplete(ResultBase): + id: str \ No newline at end of file diff --git a/tests/test_sport_results.py b/tests/test_sport_results.py new file mode 100644 index 000000000..3daa126dc --- /dev/null +++ b/tests/test_sport_results.py @@ -0,0 +1,194 @@ +import datetime +import uuid + +import pytest_asyncio +from fastapi.testclient import TestClient + +from app.core import models_core +from app.core.groups.groups_type import GroupType +from app.modules.sports_results import models_sport_results +from tests.commons import ( + add_object_to_db, + create_api_access_token, + create_user_with_groups, +) + +user_simple: models_core.CoreUser +user_captain: models_core.CoreUser +bds_user: models_core.CoreUser + +token_simple: str +token_captain: str +token_bds: str + +sport1: models_sport_results.Sport +sport2: models_sport_results.Sport +result1: models_sport_results.Result +result2: models_sport_results.Result + +captain: models_sport_results.Captain + + +@pytest_asyncio.fixture(scope="module", autouse=True) +async def init_objects(client: TestClient) -> None: + global bds_user + bds_user = await create_user_with_groups([GroupType.BDS]) + + global token_bds + token_bds = create_api_access_token(bds_user) + + global user_simple + user_simple = await create_user_with_groups([GroupType.student]) + + global token_simple + token_simple = create_api_access_token(user_simple) + + global sport1 + sport1 = models_sport_results.Sport( + id=str(uuid.uuid4()), + name="volley", + ) + await add_object_to_db(sport1) + + global sport2 + sport2 = models_sport_results.Sport( + id=str(uuid.uuid4()), + name="pétanque", + ) + await add_object_to_db(sport2) + + global user_captain + user_captain = await create_user_with_groups([GroupType.student]) + + global captain + captain = models_sport_results.Captain( + id=str(uuid.uuid4()), + user_id=user_captain.id, + sports=[sport1], + ) + await add_object_to_db(captain) + + global token_captain + token_captain = create_api_access_token(user_captain) + + global result1 + result1 = models_sport_results.Result( + id=str(uuid.uuid4()), + sport_id=sport1.id, + score1=21, + score2=2, + rank=1, + location="Gymnase ECL", + match_date=datetime.date(2024, 5, 28), + ) + await add_object_to_db(result1) + + global result2 + result2 = models_sport_results.Result( + id=str(uuid.uuid4()), + sport_id=sport2.id, + score1=12, + score2=14, + rank=2, + location="Terrain ECL", + match_date=datetime.date(2024, 4, 27), + ) + await add_object_to_db(result2) + + +def test_get_results(client: TestClient) -> None: + response = client.get( + "/sport-results/results/", + headers={"Authorization": f"Bearer {token_simple}"}, + ) + response_json = response.json() + assert response.status_code == 200 + assert str(result1.id) in [ + response_result["id"] for response_result in response_json + ] + assert str(result2.id) in [ + response_result["id"] for response_result in response_json + ] + + +def test_get_results_by_sport_id(client: TestClient) -> None: + response = client.get( + f"/sport-results/results/sport/{sport1.id}", + headers={"Authorization": f"Bearer {token_simple}"}, + ) + response_json = response.json() + assert response.status_code == 200 + assert str(result1.id) in [ + response_result["id"] for response_result in response_json + ] + assert str(result2.id) in [ + response_result["id"] for response_result in response_json + ] + + +def test_get_result_by_id(client: TestClient) -> None: + response = client.get( + f"/sport-results/results/{result1.id}", + headers={"Authorization": f"Bearer {token_simple}"}, + ) + response_json = response.json() + assert response.status_code == 200 + assert str(result1.id) == response_json["id"] + + +def test_get_sports(client: TestClient) -> None: + response = client.get( + "/sport-results/sports/", + headers={"Authorization": f"Bearer {token_simple}"}, + ) + response_json = response.json() + assert response.status_code == 200 + assert str(sport1.id) in [response_sport["id"] for response_sport in response_json] + assert str(sport2.id) in [response_sport["id"] for response_sport in response_json] + + +def test_add_sport(client: TestClient) -> None: + response = client.post( + "/sport-results/sport", + json={ + "name": "badminton", + "captains": [], + }, + headers={"Authorization": f"Bearer {token_bds}"}, + ) + assert response.status_code == 201 + + +def test_update_sport(client: TestClient) -> None: + response = client.patch( + f"/sport-results/sport/{sport1.id}", + json={ + "name": "tennis", + }, + headers={"Authorization": f"Bearer {token_bds}"}, + ) + assert response.status_code == 204 + + +def test_delete_sport(client: TestClient) -> None: + response = client.delete( + f"/sport-results/sport/{sport1.id}", + headers={"Authtorization": f"Bearer {token_bds}"}, + ) + assert response.status_code == 204 + + +def test_add_result(client: TestClient) -> None: + response = client.post( + "/sport-results/result", + json={ + "sport_id": result1.id, + "score1": result1.score1, + "score2": result1.score2, + "rank": result1.rank, + "location": result1.location, + "match_date": result1.match_date, + }, + headers={"Authtorization": f"Bearer {token_captain}"}, + ) + assert response.status_code == 201 \ No newline at end of file