From 7f07b77354ad5a8586af1a8eeb629d78f7f72720 Mon Sep 17 00:00:00 2001 From: Jon Andre Briones Date: Mon, 25 Aug 2025 17:49:29 -0700 Subject: [PATCH 1/6] fix: removed unused imports and changed some return types --- src/elections/crud.py | 5 +---- src/elections/urls.py | 16 +++++++++------- src/utils/urls.py | 3 ++- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/elections/crud.py b/src/elections/crud.py index 8a60e98..2dac6df 100644 --- a/src/elections/crud.py +++ b/src/elections/crud.py @@ -1,13 +1,10 @@ -import logging - import sqlalchemy from sqlalchemy.ext.asyncio import AsyncSession from elections.tables import Election, NomineeApplication, NomineeInfo -_logger = logging.getLogger(__name__) -async def get_all_elections(db_session: AsyncSession) -> list[Election] | None: +async def get_all_elections(db_session: AsyncSession) -> list[Election]: # TODO: can this return None? election_list = (await db_session.scalars( sqlalchemy diff --git a/src/elections/urls.py b/src/elections/urls.py index 2ab5fde..2532dd5 100644 --- a/src/elections/urls.py +++ b/src/elections/urls.py @@ -1,4 +1,3 @@ -import logging import re from datetime import datetime @@ -7,15 +6,13 @@ import database import elections +import elections.crud import elections.tables from elections.tables import Election, NomineeApplication, NomineeInfo, election_types from officers.constants import OfficerPosition -from officers.crud import get_active_officer_terms from permission.types import ElectionOfficer, WebsiteAdmin from utils.urls import is_logged_in -_logger = logging.getLogger(__name__) - router = APIRouter( prefix="/elections", tags=["elections"], @@ -28,9 +25,9 @@ def _slugify(text: str) -> str: async def _validate_user( request: Request, db_session: database.DBSession, -) -> tuple[bool, str, str]: +) -> tuple[bool, str | None, str | None]: logged_in, session_id, computing_id = await is_logged_in(request, db_session) - if not logged_in: + if not logged_in or not computing_id: return False, None, None # where valid means elections officer or website admin @@ -53,7 +50,7 @@ async def list_elections( 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_INTERNAL_SERVER_ERROR, + status_code=status.HTTP_404_NOT_FOUND, detail="no elections found" ) @@ -92,6 +89,11 @@ async def get_election( election_json = election.private_details(current_time) all_nominations = await elections.crud.get_all_registrations_in_election(db_session, slugified_name) + if not all_nominations: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="no registrations found" + ) election_json["candidates"] = [] available_positions_list = election.available_positions.split(",") diff --git a/src/utils/urls.py b/src/utils/urls.py index 13acb86..53f66dd 100644 --- a/src/utils/urls.py +++ b/src/utils/urls.py @@ -1,6 +1,7 @@ from fastapi import HTTPException, Request import auth +import auth.crud import database # TODO: move other utils into this module @@ -23,7 +24,7 @@ async def logged_in_or_raise( async def is_logged_in( request: Request, db_session: database.DBSession -) -> tuple[str | None, str | None]: +) -> tuple[bool, str | None, str | None]: """gets the user's computing_id, or raises an exception if the current request is not logged in""" session_id = request.cookies.get("session_id", None) if session_id is None: From e23cc4a3fc330dbd9cf223c2d83ff8ac8ea62d01 Mon Sep 17 00:00:00 2001 From: Jon Andre Briones Date: Mon, 25 Aug 2025 21:17:24 -0700 Subject: [PATCH 2/6] feat: add election model for better documentation --- src/elections/crud.py | 2 +- src/elections/models.py | 20 ++++++++++++++++++++ src/elections/urls.py | 22 ++++++++++++++-------- src/utils/shared_models.py | 5 +++++ 4 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 src/elections/models.py create mode 100644 src/utils/shared_models.py diff --git a/src/elections/crud.py b/src/elections/crud.py index 2dac6df..10695a4 100644 --- a/src/elections/crud.py +++ b/src/elections/crud.py @@ -50,7 +50,7 @@ async def delete_election(db_session: AsyncSession, slug: str) -> None: # ------------------------------------------------------- # # TODO: switch to only using one of application or registration -async def get_all_registrations( +async def get_all_registrations_of_user( db_session: AsyncSession, computing_id: str, election_slug: str diff --git a/src/elections/models.py b/src/elections/models.py new file mode 100644 index 0000000..4edebcb --- /dev/null +++ b/src/elections/models.py @@ -0,0 +1,20 @@ +from enum import Enum + +from pydantic import BaseModel + + +class ElectionTypeEnum(str, Enum): + GENERAL = "general_election" + BY_ELECTION = "by_election" + COUNCIL_REP = "council_rep_election" + +class ElectionModel(BaseModel): + slug: str + name: str + type: ElectionTypeEnum + datetime_start_nominations: str + datetime_start_voting: str + datetime_end_voting: str + available_positions: str + survey_link: str | None + diff --git a/src/elections/urls.py b/src/elections/urls.py index 2532dd5..dad7dd4 100644 --- a/src/elections/urls.py +++ b/src/elections/urls.py @@ -8,6 +8,7 @@ import elections import elections.crud import elections.tables +from elections.models import ElectionModel from elections.tables import Election, NomineeApplication, NomineeInfo, election_types from officers.constants import OfficerPosition from permission.types import ElectionOfficer, WebsiteAdmin @@ -41,7 +42,8 @@ async def _validate_user( @router.get( "/list", - description="Returns a list of all elections & their status" + description="Returns a list of all elections & their status", + response_model=list[ElectionModel] ) async def list_elections( _: Request, @@ -68,7 +70,8 @@ async def list_elections( Retrieves the election data for an election by name. Returns private details when the time is allowed. If user is an admin or elections officer, returns computing ids for each candidate as well. - """ + """, + response_model=ElectionModel ) async def get_election( request: Request, @@ -168,6 +171,7 @@ def _raise_if_bad_election_data( @router.post( "/{election_name:str}", description="Creates an election and places it in the database. Returns election json on success", + response_model=ElectionModel ) async def create_election( request: Request, @@ -253,7 +257,8 @@ async def create_election( name produces the same slug. Returns election json on success. - """ + """, + response_model=ElectionModel ) async def update_election( request: Request, @@ -312,7 +317,8 @@ async def update_election( @router.delete( "/{election_name:str}", - description="Deletes an election from the database. Returns whether the election exists after deletion." + description="Deletes an election from the database. Returns whether the election exists after deletion.", + response_model=SuccessFailModel ) async def delete_election( request: Request, @@ -360,7 +366,7 @@ async def get_election_registrations( detail=f"election with slug {slugified_name} does not exist" ) - registration_list = await elections.crud.get_all_registrations(db_session, computing_id, slugified_name) + registration_list = await elections.crud.get_all_registrations_of_user(db_session, computing_id, slugified_name) if registration_list is None: return JSONResponse([]) return JSONResponse([ @@ -416,7 +422,7 @@ async def register_in_election( status_code=status.HTTP_400_BAD_REQUEST, detail="registrations can only be made during the nomination period" ) - elif await elections.crud.get_all_registrations(db_session, computing_id, slugified_name): + elif await elections.crud.get_all_registrations_of_user(db_session, computing_id, slugified_name): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="you are already registered in this election" @@ -484,7 +490,7 @@ async def update_registration( detail="speeches can only be updated during the nomination period" ) - elif not await elections.crud.get_all_registrations(db_session, ccid_of_registrant, slugified_name): + elif not await elections.crud.get_all_registrations_of_user(db_session, ccid_of_registrant, slugified_name): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="applicant not yet registered in this election" @@ -533,7 +539,7 @@ async def delete_registration( status_code=status.HTTP_400_BAD_REQUEST, detail="registration can only be revoked during the nomination period" ) - elif not await elections.crud.get_all_registrations(db_session, computing_id, slugified_name): + elif not await elections.crud.get_all_registrations_of_user(db_session, computing_id, slugified_name): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="you are not yet registered in this election" diff --git a/src/utils/shared_models.py b/src/utils/shared_models.py new file mode 100644 index 0000000..ceaa2e2 --- /dev/null +++ b/src/utils/shared_models.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class SuccessFailModel(BaseModel): + success: bool From 72bf4108101a1f9e6efbc59419df0ada1c7f7a3b Mon Sep 17 00:00:00 2001 From: Jon Andre Briones Date: Mon, 25 Aug 2025 21:18:22 -0700 Subject: [PATCH 3/6] feat: add nominee info model --- src/elections/models.py | 7 +++++++ src/elections/urls.py | 11 +++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/elections/models.py b/src/elections/models.py index 4edebcb..7b3d951 100644 --- a/src/elections/models.py +++ b/src/elections/models.py @@ -18,3 +18,10 @@ class ElectionModel(BaseModel): available_positions: str survey_link: str | None +class NomineeInfoModel(BaseModel): + computing_id: str + full_name: str + linked_in: str + instagram: str + email: str + discord_username: str diff --git a/src/elections/urls.py b/src/elections/urls.py index dad7dd4..14f96ef 100644 --- a/src/elections/urls.py +++ b/src/elections/urls.py @@ -8,10 +8,11 @@ import elections import elections.crud import elections.tables -from elections.models import ElectionModel +from elections.models import ElectionModel, NomineeInfoModel from elections.tables import Election, NomineeApplication, NomineeInfo, election_types from officers.constants import OfficerPosition from permission.types import ElectionOfficer, WebsiteAdmin +from utils.shared_models import SuccessFailModel from utils.urls import is_logged_in router = APIRouter( @@ -375,7 +376,7 @@ async def get_election_registrations( @router.post( "/registration/{election_name:str}", - description="register for a specific position in this election, but doesn't set a speech" + description="register for a specific position in this election, but doesn't set a speech", ) async def register_in_election( request: Request, @@ -552,7 +553,8 @@ async def delete_registration( @router.get( "/nominee/info", - description="Nominee info is always publically tied to elections, so be careful!" + description="Nominee info is always publically tied to elections, so be careful!", + response_model=NomineeInfoModel ) async def get_nominee_info( request: Request, @@ -576,7 +578,8 @@ async def get_nominee_info( @router.put( "/nominee/info", - description="Will create or update nominee info. Returns an updated copy of their nominee info." + description="Will create or update nominee info. Returns an updated copy of their nominee info.", + response_model=NomineeInfoModel ) async def provide_nominee_info( request: Request, From f100add76b8e1744da3235b6222d2000835e4089 Mon Sep 17 00:00:00 2001 From: Jon Andre Briones Date: Mon, 25 Aug 2025 21:29:32 -0700 Subject: [PATCH 4/6] fix: add `get_active_officer_terms` import --- src/elections/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/elections/urls.py b/src/elections/urls.py index 14f96ef..08e5b6e 100644 --- a/src/elections/urls.py +++ b/src/elections/urls.py @@ -11,6 +11,7 @@ from elections.models import ElectionModel, NomineeInfoModel from elections.tables import Election, NomineeApplication, NomineeInfo, election_types from officers.constants import OfficerPosition +from officers.crud import get_active_officer_terms from permission.types import ElectionOfficer, WebsiteAdmin from utils.shared_models import SuccessFailModel from utils.urls import is_logged_in From bd0f996c6dbfb6e654e9f33dbe894edb11f3f193 Mon Sep 17 00:00:00 2001 From: Jon Andre Briones Date: Mon, 25 Aug 2025 23:07:28 -0700 Subject: [PATCH 5/6] feat: add NomineeApplicationModel --- src/elections/models.py | 6 ++++++ src/elections/urls.py | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/elections/models.py b/src/elections/models.py index 7b3d951..2b39614 100644 --- a/src/elections/models.py +++ b/src/elections/models.py @@ -25,3 +25,9 @@ class NomineeInfoModel(BaseModel): instagram: str email: str discord_username: str + +class NomineeApplicationModel(BaseModel): + computing_id: str + nominee_election: str + position: str + speech: str diff --git a/src/elections/urls.py b/src/elections/urls.py index 08e5b6e..65412d4 100644 --- a/src/elections/urls.py +++ b/src/elections/urls.py @@ -8,7 +8,7 @@ import elections import elections.crud import elections.tables -from elections.models import ElectionModel, NomineeInfoModel +from elections.models import ElectionModel, NomineeApplicationModel, NomineeInfoModel from elections.tables import Election, NomineeApplication, NomineeInfo, election_types from officers.constants import OfficerPosition from officers.crud import get_active_officer_terms @@ -347,7 +347,8 @@ async def delete_election( @router.get( "/registration/{election_name:str}", - description="get your election registration(s)" + description="get your election registration(s)", + response_model=list[NomineeApplicationModel] ) async def get_election_registrations( request: Request, From fb40c399990071b33d048b0ec3b3b22ad2b1aa01 Mon Sep 17 00:00:00 2001 From: Jon Andre Briones Date: Mon, 25 Aug 2025 23:17:45 -0700 Subject: [PATCH 6/6] fix: resolved import and changed renamed function --- tests/integration/test_elections.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_elections.py b/tests/integration/test_elections.py index 896887c..466c11b 100644 --- a/tests/integration/test_elections.py +++ b/tests/integration/test_elections.py @@ -8,7 +8,8 @@ import load_test_db from auth.crud import create_user_session, get_computing_id, update_site_user from database import SQLALCHEMY_TEST_DATABASE_URL, DatabaseSessionManager -from elections.crud import ( +from main import app +from src.elections.crud import ( add_registration, create_election, create_nominee_info, @@ -16,9 +17,9 @@ delete_registration, # election crud get_all_elections, - # election registration crud - get_all_registrations, get_all_registrations_in_election, + # election registration crud + get_all_registrations_of_user, get_election, # info crud get_nominee_info, @@ -26,7 +27,6 @@ update_nominee_info, update_registration, ) -from main import app @pytest.fixture(scope="session")