Skip to content

Commit fd23837

Browse files
authored
Various fixes on Elections API (#112)
* Refactored some imports and changed some return types * Added Election, Nominee Info, and Nominee Application model for better API documentation
1 parent 0dddbd9 commit fd23837

File tree

6 files changed

+77
-28
lines changed

6 files changed

+77
-28
lines changed

src/elections/crud.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import logging
2-
31
import sqlalchemy
42
from sqlalchemy.ext.asyncio import AsyncSession
53

64
from elections.tables import Election, NomineeApplication, NomineeInfo
75

8-
_logger = logging.getLogger(__name__)
96

10-
async def get_all_elections(db_session: AsyncSession) -> list[Election] | None:
7+
async def get_all_elections(db_session: AsyncSession) -> list[Election]:
118
# TODO: can this return None?
129
election_list = (await db_session.scalars(
1310
sqlalchemy
@@ -53,7 +50,7 @@ async def delete_election(db_session: AsyncSession, slug: str) -> None:
5350
# ------------------------------------------------------- #
5451

5552
# TODO: switch to only using one of application or registration
56-
async def get_all_registrations(
53+
async def get_all_registrations_of_user(
5754
db_session: AsyncSession,
5855
computing_id: str,
5956
election_slug: str

src/elections/models.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from enum import Enum
2+
3+
from pydantic import BaseModel
4+
5+
6+
class ElectionTypeEnum(str, Enum):
7+
GENERAL = "general_election"
8+
BY_ELECTION = "by_election"
9+
COUNCIL_REP = "council_rep_election"
10+
11+
class ElectionModel(BaseModel):
12+
slug: str
13+
name: str
14+
type: ElectionTypeEnum
15+
datetime_start_nominations: str
16+
datetime_start_voting: str
17+
datetime_end_voting: str
18+
available_positions: str
19+
survey_link: str | None
20+
21+
class NomineeInfoModel(BaseModel):
22+
computing_id: str
23+
full_name: str
24+
linked_in: str
25+
instagram: str
26+
email: str
27+
discord_username: str
28+
29+
class NomineeApplicationModel(BaseModel):
30+
computing_id: str
31+
nominee_election: str
32+
position: str
33+
speech: str

src/elections/urls.py

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import logging
21
import re
32
from datetime import datetime
43

@@ -7,15 +6,16 @@
76

87
import database
98
import elections
9+
import elections.crud
1010
import elections.tables
11+
from elections.models import ElectionModel, NomineeApplicationModel, NomineeInfoModel
1112
from elections.tables import Election, NomineeApplication, NomineeInfo, election_types
1213
from officers.constants import OfficerPosition
1314
from officers.crud import get_active_officer_terms
1415
from permission.types import ElectionOfficer, WebsiteAdmin
16+
from utils.shared_models import SuccessFailModel
1517
from utils.urls import is_logged_in
1618

17-
_logger = logging.getLogger(__name__)
18-
1919
router = APIRouter(
2020
prefix="/elections",
2121
tags=["elections"],
@@ -28,9 +28,9 @@ def _slugify(text: str) -> str:
2828
async def _validate_user(
2929
request: Request,
3030
db_session: database.DBSession,
31-
) -> tuple[bool, str, str]:
31+
) -> tuple[bool, str | None, str | None]:
3232
logged_in, session_id, computing_id = await is_logged_in(request, db_session)
33-
if not logged_in:
33+
if not logged_in or not computing_id:
3434
return False, None, None
3535

3636
# where valid means elections officer or website admin
@@ -44,7 +44,8 @@ async def _validate_user(
4444

4545
@router.get(
4646
"/list",
47-
description="Returns a list of all elections & their status"
47+
description="Returns a list of all elections & their status",
48+
response_model=list[ElectionModel]
4849
)
4950
async def list_elections(
5051
_: Request,
@@ -53,7 +54,7 @@ async def list_elections(
5354
election_list = await elections.crud.get_all_elections(db_session)
5455
if election_list is None or len(election_list) == 0:
5556
raise HTTPException(
56-
status_code=status.HTTP_404_INTERNAL_SERVER_ERROR,
57+
status_code=status.HTTP_404_NOT_FOUND,
5758
detail="no elections found"
5859
)
5960

@@ -71,7 +72,8 @@ async def list_elections(
7172
Retrieves the election data for an election by name.
7273
Returns private details when the time is allowed.
7374
If user is an admin or elections officer, returns computing ids for each candidate as well.
74-
"""
75+
""",
76+
response_model=ElectionModel
7577
)
7678
async def get_election(
7779
request: Request,
@@ -92,6 +94,11 @@ async def get_election(
9294

9395
election_json = election.private_details(current_time)
9496
all_nominations = await elections.crud.get_all_registrations_in_election(db_session, slugified_name)
97+
if not all_nominations:
98+
raise HTTPException(
99+
status_code=status.HTTP_404_NOT_FOUND,
100+
detail="no registrations found"
101+
)
95102
election_json["candidates"] = []
96103

97104
available_positions_list = election.available_positions.split(",")
@@ -166,6 +173,7 @@ def _raise_if_bad_election_data(
166173
@router.post(
167174
"/{election_name:str}",
168175
description="Creates an election and places it in the database. Returns election json on success",
176+
response_model=ElectionModel
169177
)
170178
async def create_election(
171179
request: Request,
@@ -251,7 +259,8 @@ async def create_election(
251259
name produces the same slug.
252260
253261
Returns election json on success.
254-
"""
262+
""",
263+
response_model=ElectionModel
255264
)
256265
async def update_election(
257266
request: Request,
@@ -310,7 +319,8 @@ async def update_election(
310319

311320
@router.delete(
312321
"/{election_name:str}",
313-
description="Deletes an election from the database. Returns whether the election exists after deletion."
322+
description="Deletes an election from the database. Returns whether the election exists after deletion.",
323+
response_model=SuccessFailModel
314324
)
315325
async def delete_election(
316326
request: Request,
@@ -337,7 +347,8 @@ async def delete_election(
337347

338348
@router.get(
339349
"/registration/{election_name:str}",
340-
description="get your election registration(s)"
350+
description="get your election registration(s)",
351+
response_model=list[NomineeApplicationModel]
341352
)
342353
async def get_election_registrations(
343354
request: Request,
@@ -358,7 +369,7 @@ async def get_election_registrations(
358369
detail=f"election with slug {slugified_name} does not exist"
359370
)
360371

361-
registration_list = await elections.crud.get_all_registrations(db_session, computing_id, slugified_name)
372+
registration_list = await elections.crud.get_all_registrations_of_user(db_session, computing_id, slugified_name)
362373
if registration_list is None:
363374
return JSONResponse([])
364375
return JSONResponse([
@@ -367,7 +378,7 @@ async def get_election_registrations(
367378

368379
@router.post(
369380
"/registration/{election_name:str}",
370-
description="register for a specific position in this election, but doesn't set a speech"
381+
description="register for a specific position in this election, but doesn't set a speech",
371382
)
372383
async def register_in_election(
373384
request: Request,
@@ -414,7 +425,7 @@ async def register_in_election(
414425
status_code=status.HTTP_400_BAD_REQUEST,
415426
detail="registrations can only be made during the nomination period"
416427
)
417-
elif await elections.crud.get_all_registrations(db_session, computing_id, slugified_name):
428+
elif await elections.crud.get_all_registrations_of_user(db_session, computing_id, slugified_name):
418429
raise HTTPException(
419430
status_code=status.HTTP_400_BAD_REQUEST,
420431
detail="you are already registered in this election"
@@ -482,7 +493,7 @@ async def update_registration(
482493
detail="speeches can only be updated during the nomination period"
483494
)
484495

485-
elif not await elections.crud.get_all_registrations(db_session, ccid_of_registrant, slugified_name):
496+
elif not await elections.crud.get_all_registrations_of_user(db_session, ccid_of_registrant, slugified_name):
486497
raise HTTPException(
487498
status_code=status.HTTP_404_NOT_FOUND,
488499
detail="applicant not yet registered in this election"
@@ -531,7 +542,7 @@ async def delete_registration(
531542
status_code=status.HTTP_400_BAD_REQUEST,
532543
detail="registration can only be revoked during the nomination period"
533544
)
534-
elif not await elections.crud.get_all_registrations(db_session, computing_id, slugified_name):
545+
elif not await elections.crud.get_all_registrations_of_user(db_session, computing_id, slugified_name):
535546
raise HTTPException(
536547
status_code=status.HTTP_404_NOT_FOUND,
537548
detail="you are not yet registered in this election"
@@ -544,7 +555,8 @@ async def delete_registration(
544555

545556
@router.get(
546557
"/nominee/info",
547-
description="Nominee info is always publically tied to elections, so be careful!"
558+
description="Nominee info is always publically tied to elections, so be careful!",
559+
response_model=NomineeInfoModel
548560
)
549561
async def get_nominee_info(
550562
request: Request,
@@ -568,7 +580,8 @@ async def get_nominee_info(
568580

569581
@router.put(
570582
"/nominee/info",
571-
description="Will create or update nominee info. Returns an updated copy of their nominee info."
583+
description="Will create or update nominee info. Returns an updated copy of their nominee info.",
584+
response_model=NomineeInfoModel
572585
)
573586
async def provide_nominee_info(
574587
request: Request,

src/utils/shared_models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from pydantic import BaseModel
2+
3+
4+
class SuccessFailModel(BaseModel):
5+
success: bool

src/utils/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from fastapi import HTTPException, Request
22

33
import auth
4+
import auth.crud
45
import database
56

67
# TODO: move other utils into this module
@@ -23,7 +24,7 @@ async def logged_in_or_raise(
2324
async def is_logged_in(
2425
request: Request,
2526
db_session: database.DBSession
26-
) -> tuple[str | None, str | None]:
27+
) -> tuple[bool, str | None, str | None]:
2728
"""gets the user's computing_id, or raises an exception if the current request is not logged in"""
2829
session_id = request.cookies.get("session_id", None)
2930
if session_id is None:

tests/integration/test_elections.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,25 @@
88
import load_test_db
99
from auth.crud import create_user_session, get_computing_id, update_site_user
1010
from database import SQLALCHEMY_TEST_DATABASE_URL, DatabaseSessionManager
11-
from elections.crud import (
11+
from main import app
12+
from src.elections.crud import (
1213
add_registration,
1314
create_election,
1415
create_nominee_info,
1516
delete_election,
1617
delete_registration,
1718
# election crud
1819
get_all_elections,
19-
# election registration crud
20-
get_all_registrations,
2120
get_all_registrations_in_election,
21+
# election registration crud
22+
get_all_registrations_of_user,
2223
get_election,
2324
# info crud
2425
get_nominee_info,
2526
update_election,
2627
update_nominee_info,
2728
update_registration,
2829
)
29-
from main import app
3030

3131

3232
@pytest.fixture(scope="session")

0 commit comments

Comments
 (0)