Skip to content

Commit 0e9e2f6

Browse files
Fix bugs with candidates when moving election posts (#437)
* Attempted fix for candidates being left behind when moving election posts * Fixed bug where strange behaviour when moving posts
1 parent 598c6b6 commit 0e9e2f6

File tree

2 files changed

+144
-2
lines changed

2 files changed

+144
-2
lines changed

routes/sub_election_router.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from fastapi import APIRouter, HTTPException, status
2+
from db_models.candidate_model import Candidate_DB
3+
from db_models.candidate_post_model import Candidation_DB
24
from database import DB_dependency
35
from db_models.sub_election_model import SubElection_DB
46
from db_models.election_model import Election_DB
@@ -186,6 +188,11 @@ def move_election_post(sub_election_id: int, data: MovePostRequest, db: DB_depen
186188
status_code=status.HTTP_400_BAD_REQUEST, detail="New sub-election is not in the same election"
187189
)
188190

191+
if new_sub_election.sub_election_id == sub_election.sub_election_id:
192+
raise HTTPException(
193+
status_code=status.HTTP_400_BAD_REQUEST, detail="You are trying to move to the same sub-election"
194+
)
195+
189196
# Check if the post is already assigned to the new sub-election
190197
existing_post = (
191198
db.query(ElectionPost_DB)
@@ -201,14 +208,59 @@ def move_election_post(sub_election_id: int, data: MovePostRequest, db: DB_depen
201208
# Move the post to the new sub-election
202209
election_post.sub_election = new_sub_election
203210

204-
# Update all the candidations and candidates to point to the new sub-election
211+
# Update all the candidations to point to the new sub-election
205212
for candidation in election_post.candidations:
206213
candidation.sub_election = new_sub_election
207-
candidation.candidate.sub_election = new_sub_election
208214

209215
# Update all the nominations to point to the new sub-election
210216
for nomination in election_post.nominations:
211217
nomination.sub_election = new_sub_election
212218

219+
candidations_to_move = list(election_post.candidations)
220+
candidate_candidations: dict[int, list[Candidation_DB]] = {}
221+
candidate_lookup: dict[int, Candidate_DB] = {}
222+
for candidation in candidations_to_move:
223+
key = candidation.candidate_id
224+
candidate_candidations.setdefault(key, []).append(candidation)
225+
candidate_lookup.setdefault(key, candidation.candidate)
226+
227+
# Reassign candidates while keeping candidations tied to the correct sub-election
228+
for candidate_id, candidate_candidations_to_move in candidate_candidations.items():
229+
candidate = candidate_lookup[candidate_id]
230+
231+
# Check if a candidate from the same user already exists in the target sub-election
232+
existing_candidate = (
233+
db.query(Candidate_DB)
234+
.filter(Candidate_DB.sub_election_id == new_sub_election.sub_election_id)
235+
.filter(Candidate_DB.user_id == candidate.user_id)
236+
.one_or_none()
237+
)
238+
239+
# True if all candidations are being moved
240+
# candidate.candidations should be the same candidate mentioned
241+
# in candidate_candidations_to_move, so if lengths match, all are being moved
242+
moving_only_candidations = len(candidate.candidations) == len(candidate_candidations_to_move)
243+
244+
if existing_candidate is None and moving_only_candidations:
245+
# Move the candidate wholesale
246+
candidate.sub_election = new_sub_election
247+
target_candidate = candidate
248+
else:
249+
if existing_candidate is None:
250+
target_candidate = Candidate_DB(
251+
sub_election_id=new_sub_election.sub_election_id,
252+
user_id=candidate.user_id,
253+
)
254+
db.add(target_candidate)
255+
db.flush()
256+
else:
257+
target_candidate = existing_candidate
258+
259+
for candidation in candidate_candidations_to_move:
260+
candidation.candidate = target_candidate
261+
262+
if moving_only_candidations:
263+
db.delete(candidate)
264+
213265
db.commit()
214266
return new_sub_election

tests/test_elections.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,3 +553,93 @@ def test_move_election_post_retains_nominations(
553553
noms = moved_post.get("nominations", [])
554554
matching = [n for n in noms if n.get("nominee_email") == admin_user.email]
555555
assert len(matching) == 1
556+
557+
558+
def test_move_election_post_keeps_remaining_candidations_in_original_sub_election(
559+
admin_token, admin_user, client, admin_post, member_post, open_election
560+
):
561+
# Create sub-election with two posts
562+
resp_sub = create_sub_election(
563+
client,
564+
open_election.election_id,
565+
token=admin_token,
566+
title_sv="Source",
567+
title_en="Source",
568+
post_ids=[admin_post.id, member_post.id],
569+
)
570+
assert resp_sub.status_code in (200, 201), resp_sub.text
571+
sub_election = resp_sub.json()
572+
sub_election_id = sub_election["sub_election_id"]
573+
574+
# Create candidations for both posts for the same user
575+
resp_cand_admin = create_candidation(
576+
client,
577+
sub_election_id=sub_election_id,
578+
post_id=admin_post.id,
579+
token=admin_token,
580+
user_id=admin_user.id,
581+
)
582+
assert resp_cand_admin.status_code in (200, 201), resp_cand_admin.text
583+
584+
resp_cand_member = create_candidation(
585+
client,
586+
sub_election_id=sub_election_id,
587+
post_id=member_post.id,
588+
token=admin_token,
589+
user_id=admin_user.id,
590+
)
591+
assert resp_cand_member.status_code in (200, 201), resp_cand_member.text
592+
candidate = resp_cand_member.json()
593+
candidate_id = candidate["candidate_id"]
594+
post_ids_before_move = {candidation["post_id"] for candidation in candidate["candidations"]}
595+
assert post_ids_before_move == {admin_post.id, member_post.id}
596+
597+
# Destination sub-election without posts
598+
resp_dest = create_sub_election(
599+
client,
600+
open_election.election_id,
601+
token=admin_token,
602+
title_sv="Destination",
603+
title_en="Destination",
604+
)
605+
assert resp_dest.status_code in (200, 201), resp_dest.text
606+
dest_sub_election = resp_dest.json()
607+
dest_sub_election_id = dest_sub_election["sub_election_id"]
608+
609+
# Resolve election_post identifier for the admin post
610+
election_post_admin = next(ep for ep in sub_election["election_posts"] if ep["post_id"] == admin_post.id)
611+
election_post_id = election_post_admin["election_post_id"]
612+
613+
# Move election post to the destination sub-election
614+
resp_move = client.patch(
615+
f"/sub-election/{sub_election_id}/move-election-post",
616+
json={
617+
"election_post_id": election_post_id,
618+
"new_sub_election_id": dest_sub_election_id,
619+
},
620+
headers=auth_headers(admin_token),
621+
)
622+
assert resp_move.status_code in (200, 204), resp_move.text
623+
624+
# Original sub-election should still have the candidate with the remaining post
625+
resp_candidates_source = client.get(f"/candidate/sub-election/{sub_election_id}", headers=auth_headers(admin_token))
626+
assert resp_candidates_source.status_code == 200, resp_candidates_source.text
627+
candidates_source = resp_candidates_source.json()
628+
original_candidate = next(c for c in candidates_source if c["candidate_id"] == candidate_id)
629+
remaining_post_ids = {candidation["post_id"] for candidation in original_candidate["candidations"]}
630+
assert remaining_post_ids == {member_post.id}
631+
assert all(candidation["sub_election_id"] == sub_election_id for candidation in original_candidate["candidations"])
632+
633+
# Destination sub-election should have a candidate for the moved post only
634+
resp_candidates_dest = client.get(
635+
f"/candidate/sub-election/{dest_sub_election_id}", headers=auth_headers(admin_token)
636+
)
637+
assert resp_candidates_dest.status_code == 200, resp_candidates_dest.text
638+
candidates_dest = resp_candidates_dest.json()
639+
moved_candidate = next(c for c in candidates_dest if c["user_id"] == admin_user.id)
640+
moved_post_ids = {candidation["post_id"] for candidation in moved_candidate["candidations"]}
641+
assert moved_post_ids == {admin_post.id}
642+
assert all(
643+
candidation["sub_election_id"] == dest_sub_election_id for candidation in moved_candidate["candidations"]
644+
)
645+
assert moved_candidate["candidate_id"] != candidate_id

0 commit comments

Comments
 (0)