Skip to content

Commit d1ba7d7

Browse files
committed
Merge branch 'main' into update/readme
# Conflicts: # pyproject.toml
2 parents e78db0f + d7de4da commit d1ba7d7

File tree

8 files changed

+520
-16
lines changed

8 files changed

+520
-16
lines changed

app/modules/sport_competition/cruds_sport_competition.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,6 +1404,20 @@ async def delete_team_by_id(
14041404
await db.flush()
14051405

14061406

1407+
async def load_all_teams(
1408+
edition_id: UUID,
1409+
db: AsyncSession,
1410+
) -> list[schemas_sport_competition.TeamComplete]:
1411+
teams = await db.execute(
1412+
select(models_sport_competition.CompetitionTeam)
1413+
.where(models_sport_competition.CompetitionTeam.edition_id == edition_id)
1414+
.options(
1415+
selectinload(models_sport_competition.CompetitionTeam.participants),
1416+
),
1417+
)
1418+
return [team_model_to_schema(team) for team in teams.scalars().all()]
1419+
1420+
14071421
async def load_team_by_id(
14081422
team_id,
14091423
db: AsyncSession,
@@ -1711,6 +1725,22 @@ async def delete_match_by_id(
17111725
await db.flush()
17121726

17131727

1728+
async def load_all_matches_by_edition_id(
1729+
edition_id: UUID,
1730+
db: AsyncSession,
1731+
) -> list[schemas_sport_competition.MatchComplete]:
1732+
matches = await db.execute(
1733+
select(models_sport_competition.Match)
1734+
.where(models_sport_competition.Match.edition_id == edition_id)
1735+
.options(
1736+
selectinload(models_sport_competition.Match.team1),
1737+
selectinload(models_sport_competition.Match.team2),
1738+
),
1739+
)
1740+
1741+
return [match_model_to_schema(match) for match in matches.scalars().all()]
1742+
1743+
17141744
async def load_match_by_id(
17151745
match_id,
17161746
db: AsyncSession,

app/modules/sport_competition/dependencies_sport_competition.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77

88
from app.core.groups import groups_type
99
from app.core.groups.groups_type import GroupType
10-
from app.dependencies import get_db, get_user_id_from_token_with_scopes
10+
from app.core.users import models_users
11+
from app.dependencies import (
12+
get_db,
13+
get_user_from_token_with_scopes,
14+
get_user_id_from_token_with_scopes,
15+
)
1116
from app.modules.sport_competition import (
1217
cruds_sport_competition,
1318
schemas_sport_competition,
@@ -119,6 +124,65 @@ async def is_user_a_member_of(
119124
return is_user_a_member_of
120125

121126

127+
def has_user_competition_access(
128+
group_id: GroupType | None = None,
129+
competition_group: CompetitionGroupType | None = None,
130+
exclude_external: bool = False,
131+
) -> Callable[
132+
[models_users.CoreUser],
133+
Coroutine[Any, Any, models_users.CoreUser],
134+
]:
135+
"""
136+
Generate a dependency which will:
137+
* check if the request header contains a valid API JWT token (a token that can be used to call endpoints from the API)
138+
* make sure the user making the request exists and is a member of the group with the given id (if provided)
139+
* make sure the user is a member of the competition group with the given id for the current edition (if provided)
140+
"""
141+
142+
async def is_user_a_member_of(
143+
user: models_users.CoreUser = Depends(
144+
get_user_from_token_with_scopes([[ScopeType.API]]),
145+
),
146+
edition: schemas_sport_competition.CompetitionEdition = Depends(
147+
get_current_edition,
148+
),
149+
db: AsyncSession = Depends(get_db),
150+
) -> models_users.CoreUser:
151+
"""
152+
A dependency that checks that user is a member of the group with the given id then returns the corresponding user.
153+
"""
154+
if exclude_external and user.account_type == groups_type.AccountType.external:
155+
raise HTTPException(
156+
status_code=403,
157+
detail="User is external",
158+
)
159+
if group_id is not None and not any(
160+
group.id == group_id for group in user.groups
161+
):
162+
raise HTTPException(
163+
status_code=403,
164+
detail="User is not a member of the group",
165+
)
166+
if competition_group is not None and not any(
167+
group.id == GroupType.competition_admin.value for group in user.groups
168+
):
169+
user_groups = (
170+
await cruds_sport_competition.load_user_competition_groups_memberships(
171+
db=db,
172+
user_id=user.id,
173+
edition_id=edition.id,
174+
)
175+
)
176+
if not any(group.group == competition_group for group in user_groups):
177+
raise HTTPException(
178+
status_code=403,
179+
detail="User is not a member of the competition group",
180+
)
181+
return user
182+
183+
return is_user_a_member_of
184+
185+
122186
async def get_current_edition(
123187
db: AsyncSession = Depends(get_db),
124188
) -> schemas_sport_competition.CompetitionEdition:

app/modules/sport_competition/endpoints_sport_competition.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
)
2121
from app.modules.sport_competition.dependencies_sport_competition import (
2222
get_current_edition,
23+
has_user_competition_access,
2324
is_competition_user,
2425
)
2526
from app.modules.sport_competition.types_sport_competition import (
@@ -1554,6 +1555,20 @@ async def delete_product_quota(
15541555
# region: Teams
15551556

15561557

1558+
@module.router.get(
1559+
"/competition/teams",
1560+
response_model=list[schemas_sport_competition.TeamComplete],
1561+
)
1562+
async def get_teams(
1563+
db: AsyncSession = Depends(get_db),
1564+
user: models_users.CoreUser = Depends(is_user()),
1565+
edition: schemas_sport_competition.CompetitionEdition = Depends(
1566+
get_current_edition,
1567+
),
1568+
) -> list[schemas_sport_competition.TeamComplete]:
1569+
return await cruds_sport_competition.load_all_teams(edition.id, db)
1570+
1571+
15571572
@module.router.get(
15581573
"/competition/teams/me",
15591574
response_model=schemas_sport_competition.TeamComplete,
@@ -2543,6 +2558,23 @@ async def delete_location(
25432558
# region: Matches
25442559

25452560

2561+
@module.router.get(
2562+
"/competition/matches",
2563+
response_model=list[schemas_sport_competition.MatchComplete],
2564+
)
2565+
async def get_all_matches_for_edition(
2566+
edition: schemas_sport_competition.CompetitionEdition = Depends(
2567+
get_current_edition,
2568+
),
2569+
db: AsyncSession = Depends(get_db),
2570+
user: models_users.CoreUser = Depends(is_user()),
2571+
) -> list[schemas_sport_competition.MatchComplete]:
2572+
return await cruds_sport_competition.load_all_matches_by_edition_id(
2573+
edition.id,
2574+
db,
2575+
)
2576+
2577+
25462578
@module.router.get(
25472579
"/competition/matches/sports/{sport_id}",
25482580
response_model=list[schemas_sport_competition.MatchComplete],
@@ -2638,7 +2670,9 @@ async def create_match(
26382670
match_info: schemas_sport_competition.MatchBase,
26392671
db: AsyncSession = Depends(get_db),
26402672
user: schemas_sport_competition.CompetitionUser = Depends(
2641-
is_competition_user(competition_group=CompetitionGroupType.sport_manager),
2673+
has_user_competition_access(
2674+
competition_group=CompetitionGroupType.sport_manager,
2675+
),
26422676
),
26432677
edition: schemas_sport_competition.CompetitionEdition = Depends(
26442678
get_current_edition,
@@ -2699,8 +2733,10 @@ async def edit_match(
26992733
match_id: UUID,
27002734
match_info: schemas_sport_competition.MatchEdit,
27012735
db: AsyncSession = Depends(get_db),
2702-
user: schemas_sport_competition.CompetitionUser = Depends(
2703-
is_competition_user(competition_group=CompetitionGroupType.sport_manager),
2736+
user: models_users.CoreUser = Depends(
2737+
has_user_competition_access(
2738+
competition_group=CompetitionGroupType.sport_manager,
2739+
),
27042740
),
27052741
edition: schemas_sport_competition.CompetitionEdition = Depends(
27062742
get_current_edition,
@@ -2736,7 +2772,7 @@ async def delete_match(
27362772
match_id: UUID,
27372773
db: AsyncSession = Depends(get_db),
27382774
user: schemas_users.CoreUser = Depends(
2739-
is_competition_user(
2775+
has_user_competition_access(
27402776
competition_group=CompetitionGroupType.sport_manager,
27412777
),
27422778
),
@@ -2773,7 +2809,7 @@ async def get_global_podiums(
27732809

27742810

27752811
@module.router.get(
2776-
"/competition/podiums/sport/{sport_id}",
2812+
"/competition/podiums/sports/{sport_id}",
27772813
response_model=list[schemas_sport_competition.TeamSportResultComplete],
27782814
status_code=200,
27792815
)
@@ -2798,7 +2834,7 @@ async def get_sport_podiums(
27982834

27992835

28002836
@module.router.get(
2801-
"/competition/podiums/school/{school_id}",
2837+
"/competition/podiums/schools/{school_id}",
28022838
response_model=list[schemas_sport_competition.TeamSportResultComplete],
28032839
status_code=200,
28042840
)
@@ -2823,7 +2859,7 @@ async def get_school_podiums(
28232859

28242860

28252861
@module.router.post(
2826-
"/competition/podiums/sport/{sport_id}",
2862+
"/competition/podiums/sports/{sport_id}",
28272863
response_model=list[schemas_sport_competition.TeamSportResult],
28282864
status_code=201,
28292865
)
@@ -2832,7 +2868,7 @@ async def create_sport_podium(
28322868
rankings: schemas_sport_competition.SportPodiumRankings,
28332869
db: AsyncSession = Depends(get_db),
28342870
user: models_users.CoreUser = Depends(
2835-
is_competition_user(
2871+
has_user_competition_access(
28362872
competition_group=CompetitionGroupType.sport_manager,
28372873
),
28382874
),
@@ -2873,14 +2909,14 @@ async def create_sport_podium(
28732909

28742910

28752911
@module.router.delete(
2876-
"/competition/podiums/sport/{sport_id}",
2912+
"/competition/podiums/sports/{sport_id}",
28772913
status_code=204,
28782914
)
28792915
async def delete_sport_podium(
28802916
sport_id: UUID,
28812917
db: AsyncSession = Depends(get_db),
28822918
user: models_users.CoreUser = Depends(
2883-
is_competition_user(
2919+
has_user_competition_access(
28842920
competition_group=CompetitionGroupType.sport_manager,
28852921
),
28862922
),

app/modules/sport_competition/models_sport_competition.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,9 +297,12 @@ class SportPodium(Base):
297297
ForeignKey("competition_school_extension.school_id"),
298298
primary_key=True,
299299
)
300+
team_id: Mapped[UUID] = mapped_column(
301+
ForeignKey("competition_team.id"),
302+
primary_key=True,
303+
)
300304
points: Mapped[int]
301305
rank: Mapped[int]
302-
team_id: Mapped[UUID] = mapped_column(ForeignKey("competition_team.id"))
303306

304307
team: Mapped[CompetitionTeam] = relationship(
305308
"CompetitionTeam",

app/utils/auth/providers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def get_userinfo(cls, user: models_users.CoreUser):
240240

241241
return {
242242
"sub": user.id,
243-
# "picture": f"https://hyperion.myecl.fr/users/{user.id}/profile-picture",
243+
"picture": f"https://hyperion.myecl.fr/users/{user.id}/profile-picture",
244244
# Matrix does not support special characters in username
245245
"username": username,
246246
"displayname": user.full_name,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Update_podium_foreign_key
2+
3+
Create Date: 2025-10-18 11:26:25.491895
4+
"""
5+
6+
from collections.abc import Sequence
7+
from typing import TYPE_CHECKING
8+
9+
if TYPE_CHECKING:
10+
from pytest_alembic import MigrationContext
11+
12+
import sqlalchemy as sa
13+
from alembic import op
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "d1079d6b8e6b"
17+
down_revision: str | None = "c4812e1ab108"
18+
branch_labels: str | Sequence[str] | None = None
19+
depends_on: str | Sequence[str] | None = None
20+
21+
22+
def upgrade() -> None:
23+
op.drop_constraint(
24+
type_="primary",
25+
table_name="competition_sport_podium",
26+
constraint_name="competition_sport_podium_pkey",
27+
)
28+
op.create_primary_key(
29+
constraint_name="competition_sport_podium_pkey",
30+
table_name="competition_sport_podium",
31+
columns=["team_id", "sport_id", "edition_id", "school_id"],
32+
)
33+
34+
35+
def downgrade() -> None:
36+
op.drop_constraint(
37+
type_="primary",
38+
table_name="competition_sport_podium",
39+
constraint_name="competition_sport_podium_pkey",
40+
)
41+
op.create_primary_key(
42+
constraint_name="competition_sport_podium_pkey",
43+
table_name="competition_sport_podium",
44+
columns=["sport_id", "edition_id", "school_id"],
45+
)
46+
47+
48+
def pre_test_upgrade(
49+
alembic_runner: "MigrationContext",
50+
alembic_connection: sa.Connection,
51+
) -> None:
52+
pass
53+
54+
55+
def test_upgrade(
56+
alembic_runner: "MigrationContext",
57+
alembic_connection: sa.Connection,
58+
) -> None:
59+
pass

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ authors = [{ name = "AEECL ECLAIR" }]
66

77
# Hyperion follows Semantic Versioning
88
# https://semver.org/
9-
version = "4.9.8"
9+
version = "4.9.9"
1010
requires-python = ">= 3.11, < 3.13"
1111

1212
license = "MIT"

0 commit comments

Comments
 (0)