Skip to content

Commit d98d360

Browse files
authored
Merge pull request #56 from crackhex/sub_order_bugfix
submission order bugfix
2 parents b652ff2 + adb3825 commit d98d360

2 files changed

Lines changed: 96 additions & 33 deletions

File tree

application/services/submission_services/base_submission_service.py

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

infrastructure/repositories/sqlalchemy_submission_repo.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,31 @@ async def add(self, sub: Submission) -> None:
6161
None
6262
"""
6363
async with self._sf() as sess:
64-
# Determine existing record by task + team/user to preserve primary key
65-
stmt = select(SubmissionORM).where(
66-
SubmissionORM.task_id == sub.task.id,
67-
SubmissionORM.team_id == (sub.team.id if sub.team else None),
68-
SubmissionORM.user_id == sub.submitted_by.discord_id,
69-
)
64+
if sub.team:
65+
stmt = select(SubmissionORM).where(
66+
SubmissionORM.task_id == sub.task.id,
67+
SubmissionORM.team_id == sub.team.id,
68+
)
69+
else:
70+
stmt = select(SubmissionORM).where(
71+
SubmissionORM.task_id == sub.task.id,
72+
SubmissionORM.user_id == sub.submitted_by.discord_id,
73+
)
74+
7075
existing = (await sess.scalars(stmt)).first()
7176

7277
orm = SubmissionORM.from_domain(sub)
7378
if existing:
74-
orm.id = existing.id # preserve PK for update
75-
await sess.merge(orm) # merge = insert or update
79+
# IMPORTANT:
80+
# We do NOT want to create a new row and get a new PK,
81+
# we want to UPDATE the existing row so ordering is stable.
82+
orm.id = existing.id
83+
84+
await sess.merge(orm)
7685
await sess.commit()
77-
sub.id = orm.id # propagate assigned ID back to domain
86+
87+
# Push the PK back to the domain entity
88+
sub.id = orm.id
7889

7990
async def save(self, sub: Submission) -> None:
8091
"""

0 commit comments

Comments
 (0)