Skip to content

Commit 5988ab6

Browse files
authored
Change team edition permissions (#865)
## Description ### Summary <!--Brief description of what this PR does.--> Allow school BDS to modify school's teams ### Related Issues <!-- If applicable --> Closes #<!--0--> ## Changes Made <!--Please describe the changes made in this pull request--> - ... ## Type of Change - [x] 🐛 Bug fix (non-breaking change which fixes an issue) - [x] ✨ New feature (non-breaking change which adds functionality) - [ ] 🔨 Refactor (non-breaking change that neither fixes a bug nor adds a feature) - [ ] 🔧 Infra CI/CD (changes to configs of workflows) - [ ] 💥 BREAKING CHANGE (fix or feature that require a new minimal version of the front-end) ## Impact & Scope - [ ] Core functionality changes - [x] Single module changes - [ ] Multiple modules changes - [ ] Database migrations required - [ ] Other ## Testing - [x] Added/modified tests that pass the CI - [ ] Tested in a pre-prod - [ ] Tested this locally ## Documentation - [ ] Updated docs accordingly (docs.myecl.fr) : <!--[Docs#0 - Title](https://github.com/aeecleclair/myecl-documentation/pull/0)--> - [ ] Code includes docstrings - [x] No documentation needed ## Checklist - [x] My code follows the style guidelines of this project - [x] I have commented my code, particularly in hard-to-understand areas - [x] Any dependent changes have been merged and published (_Indicate the linked PR for the dependent changes_) ## Additional Notes Add any other context, screenshots, or information about the pull request here.
1 parent 1581769 commit 5988ab6

File tree

5 files changed

+243
-8
lines changed

5 files changed

+243
-8
lines changed

app/modules/sport_competition/endpoints_sport_competition.py

Lines changed: 190 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,31 @@ async def get_teams_for_sport(
15271527
)
15281528

15291529

1530+
@module.router.get(
1531+
"/competition/teams/schools/{school_id}",
1532+
response_model=list[schemas_sport_competition.TeamComplete],
1533+
)
1534+
async def get_teams_for_school(
1535+
school_id: UUID,
1536+
db: AsyncSession = Depends(get_db),
1537+
edition: schemas_sport_competition.CompetitionEdition = Depends(
1538+
get_current_edition,
1539+
),
1540+
user: models_users.CoreUser = Depends(is_user()),
1541+
) -> list[schemas_sport_competition.TeamComplete]:
1542+
school = await cruds_sport_competition.load_school_by_id(school_id, db)
1543+
if school is None:
1544+
raise HTTPException(
1545+
status_code=404,
1546+
detail="School not found in the database",
1547+
)
1548+
return await cruds_sport_competition.load_all_teams_by_school_id(
1549+
school_id,
1550+
edition.id,
1551+
db,
1552+
)
1553+
1554+
15301555
@module.router.get(
15311556
"/competition/teams/sports/{sport_id}/schools/{school_id}",
15321557
response_model=list[schemas_sport_competition.TeamComplete],
@@ -1573,6 +1598,12 @@ async def create_team(
15731598
),
15741599
user: schemas_users.CoreUser = Depends(is_user()),
15751600
) -> schemas_sport_competition.Team:
1601+
if not edition.inscription_enabled:
1602+
raise HTTPException(
1603+
status_code=400,
1604+
detail="Inscriptions are not enabled for the current edition",
1605+
)
1606+
15761607
if GroupType.competition_admin.value not in [
15771608
group.id for group in user.groups
15781609
] and (user.id != team_info.captain_id or user.school_id != team_info.school_id):
@@ -1659,9 +1690,22 @@ async def edit_team(
16591690
status_code=404,
16601691
detail="Team not found in the database",
16611692
)
1662-
if user.id != stored.captain_id and GroupType.competition_admin.value not in [
1663-
group.id for group in user.groups
1664-
]:
1693+
user_competition_groups = (
1694+
await cruds_sport_competition.load_user_competition_groups_memberships(
1695+
user.id,
1696+
stored.edition_id,
1697+
db,
1698+
)
1699+
)
1700+
if (
1701+
user.id != stored.captain_id
1702+
and GroupType.competition_admin.value not in [group.id for group in user.groups]
1703+
and (
1704+
CompetitionGroupType.schools_bds
1705+
not in [group.group for group in user_competition_groups]
1706+
or user.school_id != stored.school_id
1707+
)
1708+
):
16651709
raise HTTPException(status_code=403, detail="Unauthorized action")
16661710
if team_info.captain_id is not None and team_info.captain_id != stored.captain_id:
16671711
sport = await cruds_sport_competition.load_sport_by_id(
@@ -1712,16 +1756,32 @@ async def delete_team(
17121756
team_id: UUID,
17131757
db: AsyncSession = Depends(get_db),
17141758
user: schemas_users.CoreUser = Depends(is_user()),
1759+
edition: schemas_sport_competition.CompetitionEdition = Depends(
1760+
get_current_edition,
1761+
),
17151762
) -> None:
17161763
stored = await cruds_sport_competition.load_team_by_id(team_id, db)
17171764
if stored is None:
17181765
raise HTTPException(
17191766
status_code=404,
17201767
detail="Team not found in the database",
17211768
)
1722-
if user.id != stored.captain_id and GroupType.competition_admin.value not in [
1723-
group.id for group in user.groups
1724-
]:
1769+
user_competition_groups = (
1770+
await cruds_sport_competition.load_user_competition_groups_memberships(
1771+
user.id,
1772+
edition.id,
1773+
db,
1774+
)
1775+
)
1776+
if (
1777+
user.id != stored.captain_id
1778+
and GroupType.competition_admin.value not in [group.id for group in user.groups]
1779+
and (
1780+
CompetitionGroupType.schools_bds
1781+
not in [group.group for group in user_competition_groups]
1782+
or user.school_id != stored.school_id
1783+
)
1784+
):
17251785
raise HTTPException(status_code=403, detail="Unauthorized action")
17261786
await cruds_sport_competition.delete_team_by_id(stored.id, db)
17271787

@@ -2092,6 +2152,12 @@ async def withdraw_from_sport(
20922152
get_current_edition,
20932153
),
20942154
) -> None:
2155+
if not edition.inscription_enabled:
2156+
raise HTTPException(
2157+
status_code=400,
2158+
detail="Inscriptions are not enabled for this edition",
2159+
)
2160+
20952161
participant = await cruds_sport_competition.load_participant_by_ids(
20962162
user.user_id,
20972163
sport_id,
@@ -2139,6 +2205,15 @@ async def delete_participant(
21392205
get_current_edition,
21402206
),
21412207
) -> None:
2208+
if (
2209+
GroupType.competition_admin.value
2210+
not in [group.id for group in user.user.groups]
2211+
and not edition.inscription_enabled
2212+
):
2213+
raise HTTPException(
2214+
status_code=403,
2215+
detail="Editions inscriptions are closed",
2216+
)
21422217
participant = await cruds_sport_competition.load_participant_by_ids(
21432218
user_id,
21442219
sport_id,
@@ -3076,6 +3151,49 @@ async def delete_product_variant(
30763151
# region: Purchases
30773152

30783153

3154+
@module.router.get(
3155+
"/competition/purchases/schools/{school_id}",
3156+
response_model=dict[str, list[schemas_sport_competition.PurchaseComplete]],
3157+
status_code=200,
3158+
)
3159+
async def get_purchases_by_school_id(
3160+
school_id: UUID,
3161+
db: AsyncSession = Depends(get_db),
3162+
user: schemas_sport_competition.CompetitionUser = Depends(
3163+
is_competition_user(competition_group=CompetitionGroupType.schools_bds),
3164+
),
3165+
edition: schemas_sport_competition.CompetitionEdition = Depends(
3166+
get_current_edition,
3167+
),
3168+
):
3169+
"""
3170+
Get a school's purchases.
3171+
3172+
**User must be competition admin to use this endpoint**
3173+
"""
3174+
school = await cruds_sport_competition.load_school_by_id(school_id, db)
3175+
if not school:
3176+
raise HTTPException(
3177+
status_code=404,
3178+
detail="School not found.",
3179+
)
3180+
users = await cruds_sport_competition.load_all_competition_users_by_school(
3181+
school_id,
3182+
edition.id,
3183+
db,
3184+
)
3185+
purchases_by_user: dict[str, list[schemas_sport_competition.PurchaseComplete]] = {}
3186+
for db_user in users:
3187+
purchases_by_user[
3188+
db_user.user_id
3189+
] = await cruds_sport_competition.load_purchases_by_user_id(
3190+
db_user.user_id,
3191+
edition.id,
3192+
db,
3193+
)
3194+
return purchases_by_user
3195+
3196+
30793197
@module.router.get(
30803198
"/competition/purchases/users/{user_id}",
30813199
response_model=list[schemas_sport_competition.Purchase],
@@ -3134,6 +3252,12 @@ async def create_purchase(
31343252
31353253
**User must create a purchase for themself**
31363254
"""
3255+
if not edition.inscription_enabled:
3256+
raise HTTPException(
3257+
status_code=403,
3258+
detail="You can't make a purchase when inscriptions are closed",
3259+
)
3260+
31373261
competition_user = await cruds_sport_competition.load_competition_user_by_id(
31383262
user.id,
31393263
edition.id,
@@ -3226,6 +3350,11 @@ async def delete_purchase(
32263350
32273351
**User must delete their own purchase**
32283352
"""
3353+
if not edition.inscription_enabled:
3354+
raise HTTPException(
3355+
status_code=403,
3356+
detail="You can't delete a purchase when inscriptions are closed",
3357+
)
32293358

32303359
db_purchase = await cruds_sport_competition.load_purchase_by_ids(
32313360
user.id,
@@ -3265,6 +3394,56 @@ async def delete_purchase(
32653394
# region: Payments
32663395

32673396

3397+
@module.router.get(
3398+
"/competition/payments/schools/{school_id}",
3399+
response_model=dict[str, list[schemas_sport_competition.PaymentComplete]],
3400+
status_code=200,
3401+
)
3402+
async def get_users_payments_by_school_id(
3403+
school_id: UUID,
3404+
db: AsyncSession = Depends(get_db),
3405+
user: schemas_sport_competition.CompetitionUser = Depends(
3406+
is_competition_user(competition_group=CompetitionGroupType.schools_bds),
3407+
),
3408+
edition: schemas_sport_competition.CompetitionEdition = Depends(
3409+
get_current_edition,
3410+
),
3411+
):
3412+
"""
3413+
Get a school's users payments.
3414+
3415+
**User must be competition admin to use this endpoint**
3416+
"""
3417+
school = await cruds_sport_competition.load_school_by_id(school_id, db)
3418+
if not school:
3419+
raise HTTPException(
3420+
status_code=404,
3421+
detail="The school does not exist.",
3422+
)
3423+
if user.user.school_id != school_id and GroupType.competition_admin.value not in [
3424+
group.id for group in user.user.groups
3425+
]:
3426+
raise HTTPException(
3427+
status_code=403,
3428+
detail="You're not allowed to see other schools payments.",
3429+
)
3430+
users = await cruds_sport_competition.load_all_competition_users_by_school(
3431+
school_id,
3432+
edition.id,
3433+
db,
3434+
)
3435+
payments_by_user: dict[str, list[schemas_sport_competition.PaymentComplete]] = {}
3436+
for db_user in users:
3437+
payments_by_user[
3438+
db_user.user_id
3439+
] = await cruds_sport_competition.load_user_payments(
3440+
db_user.user_id,
3441+
edition.id,
3442+
db,
3443+
)
3444+
return payments_by_user
3445+
3446+
32683447
@module.router.get(
32693448
"/competition/users/{user_id}/payments",
32703449
response_model=list[schemas_sport_competition.PaymentComplete],
@@ -3481,7 +3660,11 @@ async def get_payment_url(
34813660
"""
34823661
Get payment url
34833662
"""
3484-
3663+
if not edition.inscription_enabled:
3664+
raise HTTPException(
3665+
status_code=403,
3666+
detail="Inscriptions are not enabled for this edition.",
3667+
)
34853668
purchases = await cruds_sport_competition.load_purchases_by_user_id(
34863669
user.id,
34873670
edition.id,

app/modules/sport_competition/utils_sport_competition.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ async def validate_payment(
139139
)
140140
raise ValueError(f"User checkout {checkout_id} not found.") # noqa: TRY003
141141

142+
edition = await cruds_sport_competition.load_edition_by_id(
143+
checkout.edition_id,
144+
db,
145+
)
146+
if not edition or not edition.inscription_enabled:
147+
raise HTTPException(
148+
status_code=403,
149+
detail="Inscriptions are not enabled for this edition.",
150+
)
151+
142152
db_payment = schemas_sport_competition.PaymentComplete(
143153
id=uuid4(),
144154
user_id=checkout.user_id,

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.6"
9+
version = "4.9.7"
1010
minimal-titan-version-code = 139
1111
requires-python = ">= 3.11, < 3.13"
1212

tests/sport_competition/test_purchases.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,21 @@ async def test_delete_product_variant_with_purchases(
10491049
)
10501050

10511051

1052+
async def test_get_school_users_purchases(
1053+
client: TestClient,
1054+
):
1055+
response = client.get(
1056+
f"/competition/purchases/schools/{SchoolType.centrale_lyon.value}",
1057+
headers={"Authorization": f"Bearer {admin_token}"},
1058+
)
1059+
assert response.status_code == 200
1060+
data = response.json()
1061+
assert isinstance(data, dict)
1062+
assert admin_user.id in data
1063+
assert isinstance(data[admin_user.id], list)
1064+
assert len(data[admin_user.id]) == 2
1065+
1066+
10521067
async def get_user_purchases(
10531068
client: TestClient,
10541069
):
@@ -1204,6 +1219,21 @@ async def test_delete_validated_purchase(
12041219
)
12051220

12061221

1222+
async def test_get_school_users_payments(
1223+
client: TestClient,
1224+
):
1225+
response = client.get(
1226+
f"/competition/payments/schools/{SchoolType.centrale_lyon.value}",
1227+
headers={"Authorization": f"Bearer {admin_token}"},
1228+
)
1229+
assert response.status_code == 200
1230+
data = response.json()
1231+
assert isinstance(data, dict)
1232+
assert admin_user.id in data
1233+
assert isinstance(data[admin_user.id], list)
1234+
assert len(data[admin_user.id]) == 1
1235+
1236+
12071237
async def test_get_payments(
12081238
client: TestClient,
12091239
):

tests/sport_competition/test_sport_inscription.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,6 +1598,18 @@ async def test_get_sport_teams(
15981598
assert len(teams) == 2
15991599

16001600

1601+
async def test_get_school_teams(
1602+
client: TestClient,
1603+
) -> None:
1604+
response = client.get(
1605+
f"/competition/teams/schools/{school1.id}",
1606+
headers={"Authorization": f"Bearer {user3_token}"},
1607+
)
1608+
assert response.status_code == 200, response.json()
1609+
teams = response.json()
1610+
assert len(teams) == 1
1611+
1612+
16011613
async def test_get_sport_team_for_school(
16021614
client: TestClient,
16031615
) -> None:

0 commit comments

Comments
 (0)