Skip to content

Commit 513a563

Browse files
authored
Challenger data exporter (#878)
## Description ### Summary Allow admin to export data as xlsx ## Changes Made <!--Please describe the changes made in this pull request--> - ... ## Type of Change - [ ] 🐛 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 - [x] Tested in a pre-prod - [x] 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 d029157 commit 513a563

File tree

10 files changed

+1083
-322
lines changed

10 files changed

+1083
-322
lines changed

app/modules/sport_competition/cruds_sport_competition.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,14 @@ async def remove_user_from_group(
313313
async def load_all_competition_users(
314314
edition_id: UUID,
315315
db: AsyncSession,
316+
exclude_non_validated: bool = False,
316317
) -> list[schemas_sport_competition.CompetitionUser]:
317318
competition_users = await db.execute(
318319
select(models_sport_competition.CompetitionUser).where(
319320
models_sport_competition.CompetitionUser.edition_id == edition_id,
321+
models_sport_competition.CompetitionUser.validated
322+
if exclude_non_validated
323+
else and_(True),
320324
),
321325
)
322326
return [
@@ -2219,6 +2223,27 @@ async def delete_product_variant_by_id(
22192223
# region: Purchases
22202224

22212225

2226+
async def load_all_purchases(
2227+
edition_id: UUID,
2228+
db: AsyncSession,
2229+
) -> dict[str, list[schemas_sport_competition.PurchaseComplete]]:
2230+
purchases = await db.execute(
2231+
select(models_sport_competition.CompetitionPurchase)
2232+
.where(
2233+
models_sport_competition.CompetitionPurchase.edition_id == edition_id,
2234+
)
2235+
.options(
2236+
selectinload(models_sport_competition.CompetitionPurchase.product_variant),
2237+
),
2238+
)
2239+
users_purchases: dict[str, list[schemas_sport_competition.PurchaseComplete]] = {}
2240+
for purchase in purchases.scalars().all():
2241+
if purchase.user_id not in users_purchases:
2242+
users_purchases[purchase.user_id] = []
2243+
users_purchases[purchase.user_id].append(purchase_model_to_schema(purchase))
2244+
return users_purchases
2245+
2246+
22222247
async def load_purchases_by_user_id(
22232248
user_id: str,
22242249
edition_id: UUID,
@@ -2388,6 +2413,30 @@ async def delete_purchase(
23882413
# region: Payments
23892414

23902415

2416+
async def load_all_payments(
2417+
edition_id: UUID,
2418+
db: AsyncSession,
2419+
) -> dict[str, list[schemas_sport_competition.PaymentComplete]]:
2420+
payments = await db.execute(
2421+
select(models_sport_competition.CompetitionPayment).where(
2422+
models_sport_competition.CompetitionPayment.edition_id == edition_id,
2423+
),
2424+
)
2425+
users_payments: dict[str, list[schemas_sport_competition.PaymentComplete]] = {}
2426+
for payment in payments.scalars().all():
2427+
if payment.user_id not in users_payments:
2428+
users_payments[payment.user_id] = []
2429+
users_payments[payment.user_id].append(
2430+
schemas_sport_competition.PaymentComplete(
2431+
id=payment.id,
2432+
user_id=payment.user_id,
2433+
edition_id=payment.edition_id,
2434+
total=payment.total,
2435+
),
2436+
)
2437+
return users_payments
2438+
2439+
23912440
async def load_user_payments(
23922441
user_id: str,
23932442
edition_id: UUID,

app/modules/sport_competition/endpoints_sport_competition.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import logging
22
from datetime import UTC, datetime
3+
from io import BytesIO
34
from uuid import UUID, uuid4
45

5-
from fastapi import Body, Depends, File, HTTPException, UploadFile
6+
from fastapi import Body, Depends, File, HTTPException, Query, Response, UploadFile
67
from fastapi.responses import FileResponse
78
from sqlalchemy.ext.asyncio import AsyncSession
89

@@ -23,10 +24,16 @@
2324
)
2425
from app.modules.sport_competition.types_sport_competition import (
2526
CompetitionGroupType,
27+
ExcelExportParams,
2628
ProductSchoolType,
2729
)
28-
from app.modules.sport_competition.utils_sport_competition import (
30+
from app.modules.sport_competition.utils.data_exporter import (
31+
construct_users_excel_with_parameters,
32+
)
33+
from app.modules.sport_competition.utils.validation_checker import (
2934
check_validation_consistency,
35+
)
36+
from app.modules.sport_competition.utils_sport_competition import (
3037
checksport_category_compatibility,
3138
get_public_type_from_user,
3239
validate_payment,
@@ -344,6 +351,77 @@ async def get_current_user_competition(
344351
return competition_user
345352

346353

354+
@module.router.get(
355+
"/competition/users/data-export",
356+
response_class=FileResponse,
357+
status_code=200,
358+
)
359+
async def export_competition_users_data(
360+
included_fields: list[ExcelExportParams] = Query(default=[]),
361+
exclude_non_validated: bool = False,
362+
db: AsyncSession = Depends(get_db),
363+
user: schemas_sport_competition.CompetitionUser = Depends(
364+
is_user_in(group_id=GroupType.competition_admin),
365+
),
366+
edition: schemas_sport_competition.CompetitionEdition = Depends(
367+
get_current_edition,
368+
),
369+
):
370+
"""
371+
Export competition users data for the current edition as a CSV file.
372+
"""
373+
users = await cruds_sport_competition.load_all_competition_users(
374+
edition.id,
375+
db,
376+
exclude_non_validated=exclude_non_validated,
377+
)
378+
products = await cruds_sport_competition.load_products(
379+
edition.id,
380+
db,
381+
)
382+
sports = await cruds_sport_competition.load_all_sports(db)
383+
schools = await cruds_sport_competition.load_all_schools(edition.id, db)
384+
385+
participants = None
386+
if ExcelExportParams.participants in included_fields:
387+
all_participants = await cruds_sport_competition.load_all_participants(
388+
edition.id,
389+
db,
390+
)
391+
participants = {p.user_id: p for p in all_participants}
392+
payments = None
393+
if ExcelExportParams.payments in included_fields:
394+
payments = await cruds_sport_competition.load_all_payments(edition.id, db)
395+
purchases = await cruds_sport_competition.load_all_purchases(edition.id, db)
396+
397+
excel_io = BytesIO()
398+
399+
construct_users_excel_with_parameters(
400+
parameters=included_fields,
401+
sports=sports,
402+
schools=schools,
403+
users=users,
404+
products=products,
405+
users_participant=participants,
406+
users_payments=payments,
407+
users_purchases=purchases,
408+
export_io=excel_io,
409+
)
410+
411+
res = excel_io.getvalue()
412+
413+
excel_io.close()
414+
415+
headers = {
416+
"Content-Disposition": f'attachment; filename="competition_users_{edition.name}.xlsx"',
417+
}
418+
return Response(
419+
res,
420+
headers=headers,
421+
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
422+
)
423+
424+
347425
@module.router.get(
348426
"/competition/users/{user_id}",
349427
response_model=schemas_sport_competition.CompetitionUser,

app/modules/sport_competition/types_sport_competition.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@ class ProductSchoolType(Enum):
3535
centrale = "centrale"
3636
from_lyon = "from_lyon"
3737
others = "others"
38+
39+
40+
class ExcelExportParams(Enum):
41+
participants = "participants"
42+
purchases = "purchases"
43+
payments = "payments"

0 commit comments

Comments
 (0)