Skip to content

Feat 144 grouped election endpoints#148

Open
VerrillAng wants to merge 4 commits into
mainfrom
feat_144_grouped-election-endpoints
Open

Feat 144 grouped election endpoints#148
VerrillAng wants to merge 4 commits into
mainfrom
feat_144_grouped-election-endpoints

Conversation

@VerrillAng
Copy link
Copy Markdown

Feature: Grouped Election Endpoints
closes #144

Description:
Adds optional nominees to GET /election and GET /election/{name} via with_nominees=true.
Guests sees:

  1. name
  2. position
  3. speech.

Admins also get:

  1. computing_id
  2. linked_in
  3. instagram
  4. email
  5. discord_username

Features:

  • Added with_nominees query param on list and single election GET endpoints (default false)
  • Integration tests in test_elections.py for guest/admin list and single endpoints (with and without with_nominees), checking response shape and that private fields are hidden from guests

@jbriones1 jbriones1 self-requested a review June 4, 2026 21:36
Copy link
Copy Markdown
Contributor

@jbriones1 jbriones1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR and for writing tests! Lmk if you have any questions about the changes requests.

Comment thread src/elections/urls.py
@router.get(
"",
description="Returns a list of all election & their status",
description="Returns a list of all election, their status and nominees (if requested)",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pluralize. "Return a list of all elections,..."

Comment thread src/elections/urls.py
Comment on lines 123 to +144
election_list = await elections.crud.get_all_elections(db_session)
if election_list is None or len(election_list) == 0:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="no election found")

current_time = datetime.datetime.now(datetime.UTC)
if await is_user_election_admin(computing_id, db_session):
election_metadata_list = [election.private_details(current_time) for election in election_list]
election_metadata_list = []
has_permission = await is_user_election_admin(computing_id, db_session)

if has_permission:
for election in election_list:
election_data = election.private_details(current_time)
# Get the nominees for the election
if with_nominees:
election_data["candidates"] = await _get_election_nominees(db_session, election, has_permission)
election_metadata_list.append(election_data)
else:
election_metadata_list = [election.public_details(current_time) for election in election_list]
for election in election_list:
election_data = election.public_details(current_time)
# Get the nominees for the election
if with_nominees:
election_data["candidates"] = await _get_election_nominees(db_session, election, has_permission)
election_metadata_list.append(election_data)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will query the database once for each election, which is expensive. It'd be better to create a new CRUD operation that gets all the election and nominee information in one query (if it's requested). Something like:
SELECT *
FROM election
LEFT JOIN election_nominee_application
ON election.slug=election_nominee_info.nominee_election
So either the query on line 123 is executed or the query I suggested is executed. Make sure to use LEFT JOIN so elections with no candidates still appear.

You could even go one step further and define the fields to get from the database in the query i.e. don't fetch private details if the user does not have permission, but you don't have to.
If you do decide to remove the private details at the query level you can add response_model_exclude_none=True in the decorator, under response_model on line 114 and that will automatically strip away all fields that have None as their value.

Comment thread src/elections/urls.py
Retrieves the election data for an election by name.
Returns private details when the time is allowed.
If user is an admin or election officer, returns computing ids for each candidate as well.
Returns private details when user is admin or election officer.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove this line, since the next one explains this as well.

Comment thread src/elections/urls.py
Comment on lines +75 to +108
async def _get_election_nominees(
db_session: database.DBSession,
election_row: ElectionDB,
has_permission: bool,
) -> list[dict]:
candidates_list = []
all_nominations = await candidates.crud.get_all_candidates_in_election(db_session, election_row.slug)
if not all_nominations:
return []
for nomination in all_nominations:
# NOTE: if a nominee does not input their legal name, they are not considered a nominee
nominee_info = await nominees.crud.get_nominee_info(db_session, nomination.computing_id)
if nominee_info is None:
continue

if has_permission:
candidate_entry = {
"full_name": nominee_info.full_name,
"position": nomination.position,
"speech": ("No speech provided by this candidate" if nomination.speech is None else nomination.speech),
"computing_id": nomination.computing_id,
"linked_in": nominee_info.linked_in,
"instagram": nominee_info.instagram,
"email": nominee_info.email,
"discord_username": nominee_info.discord_username,
}
else:
candidate_entry = {
"full_name": nominee_info.full_name,
"position": nomination.position,
"speech": ("No speech provided by this candidate" if nomination.speech is None else nomination.speech),
}
candidates_list.append(candidate_entry)
return candidates_list
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to elections/crud.py.

Comment thread src/elections/urls.py
db_session: database.DBSession,
election_row: ElectionDB,
has_permission: bool,
) -> list[dict]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the return type to list[ElectionNomineeSummary] and modify candidate_entry to use the Pydantic model.

list[dict] is too ambiguous as a type and could lead to bugs later.

Comment thread src/elections/urls.py
Comment on lines +177 to +178
if with_nominees:
election_json["candidates"] = await _get_election_nominees(db_session, election, has_permission)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can move these statements outside the if has_permission block to prevent the duplicate on line 181-182.

Comment on lines +77 to +85
for candidate in election_2_response["candidates"]:
assert "full_name" in candidate
assert "position" in candidate
assert "speech" in candidate
assert "computing_id" not in candidate
assert "linked_in" not in candidate
assert "instagram" not in candidate
assert "email" not in candidate
assert "discord_username" not in candidate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write a helper function for this. If the shape of the response changes in the future we would need to update it on line 105 as well.

Also, you don't need to test the fields that should always appear (full_name, position, speech).

Comment on lines +105 to +113
for candidate in election_2_response["candidates"]:
assert "full_name" in candidate
assert "position" in candidate
assert "speech" in candidate
assert "computing_id" not in candidate
assert "linked_in" not in candidate
assert "instagram" not in candidate
assert "email" not in candidate
assert "discord_username" not in candidate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment.

Comment on lines +223 to +225
assert "full_name" in candidate
assert "position" in candidate
assert "speech" in candidate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to test the fields that should always appear.

Comment on lines +251 to +253
assert "full_name" in candidate
assert "position" in candidate
assert "speech" in candidate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Grouped election endpoints

2 participants