From db1613413baa38e56bedecb93d598bdad029a4fe Mon Sep 17 00:00:00 2001 From: Maxime Roucher Date: Thu, 11 Sep 2025 18:46:07 +0200 Subject: [PATCH 1/6] fix: loosing up payment constraint --- app/modules/raid/endpoints_raid.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/modules/raid/endpoints_raid.py b/app/modules/raid/endpoints_raid.py index 114ce87aeb..d9856b1ee7 100644 --- a/app/modules/raid/endpoints_raid.py +++ b/app/modules/raid/endpoints_raid.py @@ -1130,12 +1130,6 @@ async def get_payment_url( if not participant: raise HTTPException(status_code=404, detail="Participant not found.") - if participant.validation_progress != 100: - raise HTTPException( - status_code=400, - detail="You must complete your registration before paying.", - ) - if not participant.payment: if ( participant.student_card is not None From da5f78dcba37afdd7ea6ec821c18234b9ff6e7da Mon Sep 17 00:00:00 2001 From: Maxime Roucher Date: Thu, 11 Sep 2025 19:40:57 +0200 Subject: [PATCH 2/6] feat: adding clearing document api route --- app/modules/raid/endpoints_raid.py | 105 ++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 31 deletions(-) diff --git a/app/modules/raid/endpoints_raid.py b/app/modules/raid/endpoints_raid.py index d9856b1ee7..dd2eb14257 100644 --- a/app/modules/raid/endpoints_raid.py +++ b/app/modules/raid/endpoints_raid.py @@ -1,6 +1,7 @@ import logging import uuid from datetime import UTC, date, datetime +from pathlib import Path from fastapi import Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse @@ -96,13 +97,11 @@ async def create_participant( """ Create a participant """ - # If the user is already a participant, return an error + if await cruds_raid.is_user_a_participant(user.id, db): raise HTTPException(status_code=403, detail="You are already a participant.") raid_information = await get_core_data(coredata_raid.RaidInformation, db) - # If the start_date is not set, we will use January the first of next year to determine if participants - # are minors. We can safely assume that the RAID will occurre before Jan 1 of next year is_minor = will_participant_be_minor_on( participant=participant, @@ -132,11 +131,10 @@ async def update_participant( """ Update a participant """ - # If the user is not a participant, return an error + if not await cruds_raid.is_user_a_participant(participant_id, db): raise HTTPException(status_code=403, detail="You are not a participant.") - # If the user is not the participant, return an error if not await cruds_raid.are_user_in_the_same_team(user.id, participant_id, db): raise HTTPException(status_code=403, detail="You are not the participant.") @@ -147,14 +145,12 @@ async def update_participant( day=1, ) - # We only want to change the is_minor value if the birthday is changed is_minor = None if participant.birthday: is_minor = will_participant_be_minor_on(participant, raid_start_date) saved_participant = await get_participant(participant_id, db) - # If the t_shirt_payment is set, we cannot remove the t_shirt_size if saved_participant.t_shirt_payment and participant.t_shirt_size == Size.None_: participant.t_shirt_size = saved_participant.t_shirt_size @@ -244,11 +240,10 @@ async def create_team( """ Create a team """ - # If the user is not a participant, return an error + if not await cruds_raid.is_user_a_participant(user.id, db): raise HTTPException(status_code=403, detail="You are not a participant.") - # If the user already has a team, return an error if await cruds_raid.get_team_by_participant_id(user.id, db): raise HTTPException(status_code=403, detail="You already have a team.") @@ -261,7 +256,7 @@ async def create_team( difficulty=None, ) await cruds_raid.create_team(db_team, db) - # We need to get the team from the db to have access to relationships + created_team = await cruds_raid.get_team_by_id(team_id=db_team.id, db=db) if created_team: await post_update_actions( @@ -312,12 +307,11 @@ async def get_team_by_participant_id( if user.id != participant_id: raise HTTPException(status_code=403, detail="You are not the participant.") - # If the user is not a participant, return an error if not await cruds_raid.is_user_a_participant(participant_id, db): raise HTTPException(status_code=403, detail="You are not a participant.") participant_team = await cruds_raid.get_team_by_participant_id(participant_id, db) - # If the user does not have a team, return an error + if not participant_team: raise HTTPException(status_code=404, detail="You do not have a team.") return participant_team @@ -403,7 +397,7 @@ async def delete_team( raise HTTPException(status_code=403, detail="This team does not exists") await cruds_raid.delete_team_invite_tokens(team_id, db) await cruds_raid.delete_team(team_id, db) - # We will try to delete PDF associated with the team from the Google Drive + if team.file_id: async with DriveGoogleAPI(db, settings) as google_api: google_api.delete_file(team.file_id) @@ -429,24 +423,22 @@ async def delete_all_teams( """ Delete all teams """ - # First get all teams to access their file IDs + teams = await cruds_raid.get_all_teams(db) try: - # Delete files associated with each team from Google Drive async with DriveGoogleAPI(db, settings) as google_api: for team in teams: - # Delete team PDF if team.file_id: google_api.delete_file(team.file_id) - # Delete captain's security file if exists + if ( team.captain and team.captain.security_file and team.captain.security_file.file_id ): google_api.delete_file(team.captain.security_file.file_id) - # Delete second member's security file if exists + if ( team.second and team.second.security_file @@ -458,13 +450,10 @@ async def delete_all_teams( "Google API configuration is missing in the .env file, raid-registering will not delete files from Google Drive.", ) - # Delete team invite tokens await cruds_raid.delete_all_invite_tokens(db) - # Delete all teams from the database await cruds_raid.delete_all_teams(db) - # Delete all participants from the database await cruds_raid.delete_all_participant(db) delete_all_folder_from_data("raid") @@ -490,13 +479,13 @@ async def upload_document( upload_file=file, directory="raid", filename=document_id, - max_file_size=50 * 1024 * 1024, # TODO : Change this value + max_file_size=50 * 1024 * 1024, accepted_content_types=[ ContentType.jpg, ContentType.png, ContentType.webp, ContentType.pdf, - ], # TODO : Change this value + ], ) model_document = models_raid.Document( @@ -546,7 +535,6 @@ async def read_document( participant = await cruds_raid.get_user_by_document_id(document_id, db) if not participant: - # The document can be a global document information = await get_core_data(coredata_raid.RaidInformation, db) if document_id in {information.raid_rules_id, information.raid_information_id}: return get_file_from_data( @@ -570,7 +558,7 @@ async def read_document( ) return get_file_from_data( - default_asset="assets/images/default_advert.png", # TODO: get a default document + default_asset="assets/images/default_advert.png", directory="raid", filename=str(document_id), ) @@ -631,8 +619,6 @@ async def set_security_file( raise HTTPException(status_code=403, detail="The participant does not exist") if participant.security_file_id: - # The participant already has a security file - # We want to delete it to replace it by the new one await cruds_raid.update_security_file( security_file_id=participant.security_file_id, security_file=security_file, @@ -820,8 +806,6 @@ async def join_team( user_team = await cruds_raid.get_team_by_participant_id(user.id, db) - # An user that is in a team without a second participant will quit its teams to joint the other - # If there are already two participants in the user's team, we want to raise an error if user_team: if user_team.second_id: raise HTTPException(status_code=403, detail="You are already in a team.") @@ -983,7 +967,7 @@ async def update_raid_information( """ Update raid information """ - # Checking the last saved information is a temporary fix for core data not supporting exclude None on update + last_information = await get_core_data(coredata_raid.RaidInformation, db) await set_core_data(raid_information, db) if ( @@ -1163,7 +1147,6 @@ async def get_payment_url( models_raid.ParticipantCheckout( id=str(uuid.uuid4()), participant_id=user.id, - # TODO: use UUID checkout_id=str(checkout.id), ), db=db, @@ -1171,3 +1154,63 @@ async def get_payment_url( return schemas_raid.PaymentUrl( url=checkout.payment_url, ) + + +@module.router.post( + "/raid/clear_documents", + status_code=204, +) +async def clear_documents( + db: AsyncSession = Depends(get_db), + user: models_users.CoreUser = Depends(is_user_in(GroupType.raid_admin)), +): + """ + For each participant, for all their document, check if this document exists, if not, clear the document from the participant + """ + + raid_data_path = Path("data/raid") + existing_file_ids = set() + + if raid_data_path.exists(): + for file_path in raid_data_path.glob("*.*"): + if file_path.is_file(): + existing_file_ids.add(file_path.stem) + + hyperion_error_logger.info( + f"RAID: Found {len(existing_file_ids)} existing files", + ) + + participants = await cruds_raid.get_all_participants(db) + cleared_documents_count = 0 + checked_documents_count = 0 + + for participant in participants: + if not participant: + continue + documents = { + "id_card_id": participant.id_card_id, + "medical_certificate_id": participant.medical_certificate_id, + "student_card_id": participant.student_card_id, + "raid_rules_id": participant.raid_rules_id, + "parent_authorization_id": participant.parent_authorization_id, + } + for document_key, document_id in documents.items(): + if document_id: + checked_documents_count += 1 + + file_exists = document_id in existing_file_ids + + if not file_exists: + await cruds_raid.assign_document( + participant.id, + None, + document_key, + db, + ) + cleared_documents_count += 1 + + hyperion_error_logger.info( + f"RAID: Document cleanup completed. " + f"Checked {checked_documents_count} document references, " + f"cleared {cleared_documents_count} invalid ones.", + ) From 9b2393f44bde5dc1d69aecfa207c611ca8d27710 Mon Sep 17 00:00:00 2001 From: Maxime Roucher Date: Thu, 11 Sep 2025 23:49:58 +0200 Subject: [PATCH 3/6] fix: putting back comments --- app/modules/raid/cruds_raid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modules/raid/cruds_raid.py b/app/modules/raid/cruds_raid.py index f490ff4439..7edac69bf3 100644 --- a/app/modules/raid/cruds_raid.py +++ b/app/modules/raid/cruds_raid.py @@ -304,7 +304,7 @@ async def create_document( async def assign_document( participant_id: str, - document_id: str, + document_id: str | None, document_key: str, db: AsyncSession, ) -> None: From c7d87d60e92b59a9480e045e641093b7eb180fb1 Mon Sep 17 00:00:00 2001 From: Maxime Roucher Date: Thu, 11 Sep 2025 23:50:11 +0200 Subject: [PATCH 4/6] fix: making document id nullable --- app/modules/raid/endpoints_raid.py | 46 +++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/app/modules/raid/endpoints_raid.py b/app/modules/raid/endpoints_raid.py index dd2eb14257..98d446400a 100644 --- a/app/modules/raid/endpoints_raid.py +++ b/app/modules/raid/endpoints_raid.py @@ -1,7 +1,7 @@ import logging +from pathlib import Path import uuid from datetime import UTC, date, datetime -from pathlib import Path from fastapi import Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse @@ -97,11 +97,13 @@ async def create_participant( """ Create a participant """ - + # If the user is already a participant, return an error if await cruds_raid.is_user_a_participant(user.id, db): raise HTTPException(status_code=403, detail="You are already a participant.") raid_information = await get_core_data(coredata_raid.RaidInformation, db) + # If the start_date is not set, we will use January the first of next year to determine if participants + # are minors. We can safely assume that the RAID will occurre before Jan 1 of next year is_minor = will_participant_be_minor_on( participant=participant, @@ -131,10 +133,11 @@ async def update_participant( """ Update a participant """ - + # If the user is not a participant, return an error if not await cruds_raid.is_user_a_participant(participant_id, db): raise HTTPException(status_code=403, detail="You are not a participant.") + # If the user is not the participant, return an error if not await cruds_raid.are_user_in_the_same_team(user.id, participant_id, db): raise HTTPException(status_code=403, detail="You are not the participant.") @@ -145,12 +148,14 @@ async def update_participant( day=1, ) + # We only want to change the is_minor value if the birthday is changed is_minor = None if participant.birthday: is_minor = will_participant_be_minor_on(participant, raid_start_date) saved_participant = await get_participant(participant_id, db) + # If the t_shirt_payment is set, we cannot remove the t_shirt_size if saved_participant.t_shirt_payment and participant.t_shirt_size == Size.None_: participant.t_shirt_size = saved_participant.t_shirt_size @@ -240,10 +245,11 @@ async def create_team( """ Create a team """ - + # If the user is not a participant, return an error if not await cruds_raid.is_user_a_participant(user.id, db): raise HTTPException(status_code=403, detail="You are not a participant.") + # If the user already has a team, return an error if await cruds_raid.get_team_by_participant_id(user.id, db): raise HTTPException(status_code=403, detail="You already have a team.") @@ -256,7 +262,7 @@ async def create_team( difficulty=None, ) await cruds_raid.create_team(db_team, db) - + # We need to get the team from the db to have access to relationships created_team = await cruds_raid.get_team_by_id(team_id=db_team.id, db=db) if created_team: await post_update_actions( @@ -307,11 +313,12 @@ async def get_team_by_participant_id( if user.id != participant_id: raise HTTPException(status_code=403, detail="You are not the participant.") + # If the user is not a participant, return an error if not await cruds_raid.is_user_a_participant(participant_id, db): raise HTTPException(status_code=403, detail="You are not a participant.") participant_team = await cruds_raid.get_team_by_participant_id(participant_id, db) - + # If the user does not have a team, return an error if not participant_team: raise HTTPException(status_code=404, detail="You do not have a team.") return participant_team @@ -397,7 +404,7 @@ async def delete_team( raise HTTPException(status_code=403, detail="This team does not exists") await cruds_raid.delete_team_invite_tokens(team_id, db) await cruds_raid.delete_team(team_id, db) - + # We will try to delete PDF associated with the team from the Google Drive if team.file_id: async with DriveGoogleAPI(db, settings) as google_api: google_api.delete_file(team.file_id) @@ -423,22 +430,24 @@ async def delete_all_teams( """ Delete all teams """ - + # First get all teams to access their file IDs teams = await cruds_raid.get_all_teams(db) try: + # Delete files associated with each team from Google Drive async with DriveGoogleAPI(db, settings) as google_api: for team in teams: + # Delete team PDF if team.file_id: google_api.delete_file(team.file_id) - + # Delete captain's security file if exists if ( team.captain and team.captain.security_file and team.captain.security_file.file_id ): google_api.delete_file(team.captain.security_file.file_id) - + # Delete second member's security file if exists if ( team.second and team.second.security_file @@ -450,10 +459,13 @@ async def delete_all_teams( "Google API configuration is missing in the .env file, raid-registering will not delete files from Google Drive.", ) + # Delete team invite tokens await cruds_raid.delete_all_invite_tokens(db) + # Delete all teams from the database await cruds_raid.delete_all_teams(db) + # Delete all participants from the database await cruds_raid.delete_all_participant(db) delete_all_folder_from_data("raid") @@ -479,13 +491,13 @@ async def upload_document( upload_file=file, directory="raid", filename=document_id, - max_file_size=50 * 1024 * 1024, + max_file_size=50 * 1024 * 1024, # TODO : Change this value accepted_content_types=[ ContentType.jpg, ContentType.png, ContentType.webp, ContentType.pdf, - ], + ], # TODO : Change this value ) model_document = models_raid.Document( @@ -535,6 +547,7 @@ async def read_document( participant = await cruds_raid.get_user_by_document_id(document_id, db) if not participant: + # The document can be a global document information = await get_core_data(coredata_raid.RaidInformation, db) if document_id in {information.raid_rules_id, information.raid_information_id}: return get_file_from_data( @@ -558,7 +571,7 @@ async def read_document( ) return get_file_from_data( - default_asset="assets/images/default_advert.png", + default_asset="assets/images/default_advert.png", # TODO: get a default document directory="raid", filename=str(document_id), ) @@ -619,6 +632,8 @@ async def set_security_file( raise HTTPException(status_code=403, detail="The participant does not exist") if participant.security_file_id: + # The participant already has a security file + # We want to delete it to replace it by the new one await cruds_raid.update_security_file( security_file_id=participant.security_file_id, security_file=security_file, @@ -806,6 +821,8 @@ async def join_team( user_team = await cruds_raid.get_team_by_participant_id(user.id, db) + # An user that is in a team without a second participant will quit its teams to joint the other + # If there are already two participants in the user's team, we want to raise an error if user_team: if user_team.second_id: raise HTTPException(status_code=403, detail="You are already in a team.") @@ -967,7 +984,7 @@ async def update_raid_information( """ Update raid information """ - + # Checking the last saved information is a temporary fix for core data not supporting exclude None on update last_information = await get_core_data(coredata_raid.RaidInformation, db) await set_core_data(raid_information, db) if ( @@ -1147,6 +1164,7 @@ async def get_payment_url( models_raid.ParticipantCheckout( id=str(uuid.uuid4()), participant_id=user.id, + # TODO: use UUID checkout_id=str(checkout.id), ), db=db, From 934252a182e90869cb694e4c90d703db58d42ec8 Mon Sep 17 00:00:00 2001 From: Maxime Roucher Date: Thu, 11 Sep 2025 23:50:45 +0200 Subject: [PATCH 5/6] fix: import order --- app/modules/raid/endpoints_raid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modules/raid/endpoints_raid.py b/app/modules/raid/endpoints_raid.py index 98d446400a..7e2df424ab 100644 --- a/app/modules/raid/endpoints_raid.py +++ b/app/modules/raid/endpoints_raid.py @@ -1,7 +1,7 @@ import logging -from pathlib import Path import uuid from datetime import UTC, date, datetime +from pathlib import Path from fastapi import Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse From f8c101229dcef9367e59ba12ef48057a98fa2f6e Mon Sep 17 00:00:00 2001 From: Maxime Roucher Date: Fri, 12 Sep 2025 00:03:00 +0200 Subject: [PATCH 6/6] feat: bumping version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 821dd158d8..ae79e411dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [{ name = "AEECL ECLAIR" }] # Hyperion follows Semantic Versioning # https://semver.org/ -version = "4.7.8" +version = "4.7.9" minimal-titan-version-code = 139 requires-python = ">= 3.11, < 3.13"