@@ -66,27 +66,54 @@ async def submit(
6666 # 3) Detect / validate team
6767 team = await self ._resolve_team (task , user , team_id )
6868
69- # 4) Replace any previous run(s)
70- await self ._purge_previous_runs (task , user , team )
69+ # 4) Find the existing "authoritative" submission, if any, for this competitor
70+ # - if team exists: check the team's submission row
71+ # - else: check the solo row for this user
72+ existing_sub = await self ._get_existing_submission (task , user , team )
7173
72- # 5) --- game-specific parsing ------------------------------------
74+ # 5) Parse the uploaded file into a domain SubmissionFile
7375 submission_file : SubmissionFile = self .parse_file (file_bytes , now )
7476
75- # 6. Build domain entity
76- sub = Submission (
77- submitted_by = user ,
78- task = task ,
79- file = submission_file ,
80- team = team ,
81- url = file_url ,
82- )
83-
84- # Add the extra fields (exemple case: character and vehicle for mkwii)
85- self .populate_metadata (sub , submission_file )
86-
87- # 7. Validate + persist
88- sub .validate ()
89- await self ._sub_repo .add (sub )
77+ if existing_sub :
78+ # -------- RESUBMISSION --------
79+ # Reuse the same DB row (same PK), so ordering doesn't move.
80+ existing_sub .file = submission_file
81+ existing_sub .url = file_url
82+ existing_sub .submitted_by = user
83+ existing_sub .team = team
84+
85+ # Fill in per-game metadata (example, for mkwii, this is character and vehicle)
86+ self .populate_metadata (existing_sub , submission_file )
87+
88+ existing_sub .validate ()
89+ await self ._sub_repo .save (existing_sub )
90+ sub = existing_sub
91+
92+ else :
93+ # -------- FIRST SUBMISSION FOR THIS COMPETITOR --------
94+ # There's no row yet for this competitor (user or team),
95+ # so we create one.
96+ sub = Submission (
97+ submitted_by = user ,
98+ task = task ,
99+ file = submission_file ,
100+ team = team ,
101+ url = file_url ,
102+ )
103+
104+ # Fill in per-game metadata (example, for mkwii, this is character and vehicle)
105+ self .populate_metadata (sub , submission_file )
106+
107+ sub .validate ()
108+ await self ._sub_repo .add (sub )
109+
110+ # 6) Enforce exclusivity rules:
111+ # If this is a TEAM submission:
112+ # - This team is now the only valid entry for all its members.
113+ # - Therefore: remove any SOLO submissions for each of the team members.
114+ if team :
115+ await self ._cleanup_member_solo_runs (task , team )
116+
90117 return sub
91118
92119 # ------------------------------------------------------------------ #
@@ -100,7 +127,7 @@ def parse_file(self,file_bytes: bytes,uploaded_at_epoch: int) -> SubmissionFile:
100127 def populate_metadata (self ,sub : Submission ,file : SubmissionFile ) -> None : ...
101128
102129 # ------------------------------------------------------------------ #
103- # Helpers (shared across all comps) #
130+ # Helpers for this class #
104131 # ------------------------------------------------------------------ #
105132 async def _resolve_team (self , task : Task , user , team_id ):
106133 if task .team_size <= 1 or task .speed_task or self ._team_repo is None :
@@ -118,18 +145,42 @@ async def _resolve_team(self, task: Task, user, team_id):
118145 raise RuntimeError (f"Team #{ team_id } not found." )
119146 return team
120147
121- async def _purge_previous_runs (self , task : Task , user , team ):
148+ async def _cleanup_member_solo_runs (self , task : Task , team ) -> None :
149+ """
150+ After a team submits, there shouldn't be any standalone 'solo' runs
151+ from any of the members left in the DB for this task.
152+
153+ This guarantees:
154+ - If someone used to be 'solo' and is now on a team,
155+ their solo submission is invalidated.
156+ - The leaderboard won't show both "Player A (solo)" AND "Team XYZ (Player A & ...)".
157+ """
158+ for member in team .members :
159+ await self ._sub_repo .remove_user_submissions (task .id , member .discord_id )
160+
161+ async def _get_existing_submission (
162+ self ,
163+ task : Task ,
164+ user ,
165+ team ,
166+ ) -> Optional [Submission ]:
167+ """
168+ What is the "official" submission row this resubmission should update?
169+
170+ - If team exists: the team's shared submission row.
171+ - Else: the user's solo submission row.
172+ """
122173 if team :
123- await self ._sub_repo .remove_team_submissions (task .id , team .id )
124- for m in team .members :
125- await self ._sub_repo .remove_user_submissions (task .id , m .discord_id )
174+ return await self ._sub_repo .get_submission_by_team (task .id , team .id )
126175 else :
127- await self ._sub_repo .remove_user_submissions (task .id , user .discord_id )
176+ return await self ._sub_repo .get_submission_by_user (task .id , user .discord_id )
177+
128178
129179 # ------------------------------------------------------------------ #
130180 # Other methods (shared across all comps)
131181 # ------------------------------------------------------------------ #
132182
183+
133184 async def remove_submission (self , user_id : int ) -> Submission :
134185 """
135186 Delete the existing submission (solo or team) for the user,
@@ -174,6 +225,7 @@ async def remove_submission(self, user_id: int) -> Submission:
174225 # 5) Return the deleted entity for command feedback
175226 return sub
176227
228+
177229 async def get_submissions (self ) -> list [Submission ]:
178230 """
179231 List all submissions for the active competition,
0 commit comments