Skip to content

Commit 80a4f93

Browse files
committed
wip: move Nominee Application stuff to its own directory
1 parent 4b6a34b commit 80a4f93

File tree

16 files changed

+336
-302
lines changed

16 files changed

+336
-302
lines changed

src/elections/crud.py

Lines changed: 1 addition & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import sqlalchemy
44
from sqlalchemy.ext.asyncio import AsyncSession
55

6-
from elections.tables import Election, NomineeApplication, NomineeInfo
7-
from officers.constants import OfficerPositionEnum
6+
from elections.tables import Election, NomineeInfo
87

98

109
async def get_all_elections(db_session: AsyncSession) -> Sequence[Election]:
@@ -51,91 +50,6 @@ async def delete_election(db_session: AsyncSession, slug: str) -> None:
5150

5251
# ------------------------------------------------------- #
5352

54-
# TODO: switch to only using one of application or registration
55-
async def get_all_registrations_of_user(
56-
db_session: AsyncSession,
57-
computing_id: str,
58-
election_slug: str
59-
) -> Sequence[NomineeApplication] | None:
60-
registrations = (await db_session.scalars(
61-
sqlalchemy
62-
.select(NomineeApplication)
63-
.where(
64-
(NomineeApplication.computing_id == computing_id)
65-
& (NomineeApplication.nominee_election == election_slug)
66-
)
67-
)).all()
68-
return registrations
69-
70-
async def get_one_registration_in_election(
71-
db_session: AsyncSession,
72-
computing_id: str,
73-
election_slug: str,
74-
position: OfficerPositionEnum,
75-
) -> NomineeApplication | None:
76-
registration = (await db_session.scalar(
77-
sqlalchemy
78-
.select(NomineeApplication)
79-
.where(
80-
NomineeApplication.computing_id == computing_id,
81-
NomineeApplication.nominee_election == election_slug,
82-
NomineeApplication.position == position
83-
)
84-
))
85-
return registration
86-
87-
async def get_all_registrations_in_election(
88-
db_session: AsyncSession,
89-
election_slug: str,
90-
) -> Sequence[NomineeApplication] | None:
91-
registrations = (await db_session.scalars(
92-
sqlalchemy
93-
.select(NomineeApplication)
94-
.where(
95-
NomineeApplication.nominee_election == election_slug
96-
)
97-
)).all()
98-
return registrations
99-
100-
async def add_registration(
101-
db_session: AsyncSession,
102-
initial_application: NomineeApplication
103-
):
104-
db_session.add(initial_application)
105-
106-
async def update_registration(
107-
db_session: AsyncSession,
108-
initial_application: NomineeApplication
109-
):
110-
await db_session.execute(
111-
sqlalchemy
112-
.update(NomineeApplication)
113-
.where(
114-
(NomineeApplication.computing_id == initial_application.computing_id)
115-
& (NomineeApplication.nominee_election == initial_application.nominee_election)
116-
& (NomineeApplication.position == initial_application.position)
117-
)
118-
.values(initial_application.to_update_dict())
119-
)
120-
121-
async def delete_registration(
122-
db_session: AsyncSession,
123-
computing_id: str,
124-
election_slug: str,
125-
position: OfficerPositionEnum
126-
):
127-
await db_session.execute(
128-
sqlalchemy
129-
.delete(NomineeApplication)
130-
.where(
131-
(NomineeApplication.computing_id == computing_id)
132-
& (NomineeApplication.nominee_election == election_slug)
133-
& (NomineeApplication.position == position)
134-
)
135-
)
136-
137-
# ------------------------------------------------------- #
138-
13953
async def get_nominee_info(
14054
db_session: AsyncSession,
14155
computing_id: str,

src/elections/models.py

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pydantic import BaseModel, Field
44

55
from officers.constants import OfficerPositionEnum
6+
from registrations.models import RegistrationModel
67

78

89
class ElectionTypeEnum(StrEnum):
@@ -16,15 +17,6 @@ class ElectionStatusEnum(StrEnum):
1617
VOTING = "voting"
1718
AFTER_VOTING = "after_voting"
1819

19-
class CandidateModel(BaseModel):
20-
position: str
21-
full_name: str
22-
linked_in: str
23-
instagram: str
24-
email: str
25-
discord_username: str
26-
speech: str
27-
2820
class ElectionResponse(BaseModel):
2921
slug: str
3022
name: str
@@ -36,7 +28,7 @@ class ElectionResponse(BaseModel):
3628
status: ElectionStatusEnum
3729

3830
survey_link: str | None = Field(None, description="Only available to admins")
39-
candidates: list[CandidateModel] | None = Field(None, description="Only available to admins")
31+
candidates: list[RegistrationModel] | None = Field(None, description="Only available to admins")
4032

4133
class ElectionParams(BaseModel):
4234
name: str
@@ -55,20 +47,6 @@ class ElectionUpdateParams(BaseModel):
5547
available_positions: list[OfficerPositionEnum] | None = None
5648
survey_link: str | None = None
5749

58-
class NomineeApplicationParams(BaseModel):
59-
computing_id: str
60-
position: OfficerPositionEnum
61-
62-
class NomineeApplicationUpdateParams(BaseModel):
63-
position: OfficerPositionEnum | None = None
64-
speech: str | None = None
65-
66-
class NomineeApplicationModel(BaseModel):
67-
computing_id: str
68-
nominee_election: str
69-
position: OfficerPositionEnum
70-
speech: str | None = None
71-
7250
class NomineeInfoModel(BaseModel):
7351
computing_id: str
7452
full_name: str

src/elections/tables.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22

33
from sqlalchemy import (
44
DateTime,
5-
ForeignKey,
6-
PrimaryKeyConstraint,
75
String,
8-
Text,
96
)
107
from sqlalchemy.orm import Mapped, mapped_column
118

@@ -17,7 +14,6 @@
1714
from elections.models import (
1815
ElectionStatusEnum,
1916
ElectionUpdateParams,
20-
NomineeApplicationUpdateParams,
2117
)
2218
from officers.constants import OfficerPositionEnum
2319
from utils.types import StringList
@@ -162,40 +158,3 @@ def serialize(self) -> dict:
162158
"discord_username": self.discord_username,
163159
}
164160

165-
class NomineeApplication(Base):
166-
__tablename__ = "election_nominee_application"
167-
168-
computing_id: Mapped[str] = mapped_column(ForeignKey("election_nominee_info.computing_id"), primary_key=True)
169-
nominee_election: Mapped[str] = mapped_column(ForeignKey("election.slug"), primary_key=True)
170-
position: Mapped[OfficerPositionEnum] = mapped_column(String(64), primary_key=True)
171-
172-
speech: Mapped[str | None] = mapped_column(Text)
173-
174-
__table_args__ = (
175-
PrimaryKeyConstraint(computing_id, nominee_election, position),
176-
)
177-
178-
def serialize(self) -> dict:
179-
return {
180-
"computing_id": self.computing_id,
181-
"nominee_election": self.nominee_election,
182-
"position": self.position,
183-
184-
"speech": self.speech,
185-
}
186-
187-
def to_update_dict(self) -> dict:
188-
return {
189-
"computing_id": self.computing_id,
190-
"nominee_election": self.nominee_election,
191-
"position": self.position,
192-
193-
"speech": self.speech,
194-
}
195-
196-
def update_from_params(self, params: NomineeApplicationUpdateParams):
197-
update_data = params.model_dump(exclude_unset=True)
198-
for k, v in update_data.items():
199-
setattr(self, k, v)
200-
201-

src/elections/urls.py

Lines changed: 12 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,24 @@
44
from fastapi.responses import JSONResponse
55

66
import database
7-
import elections
87
import elections.crud
98
import elections.tables
9+
import registrations.crud
1010
from elections.models import (
1111
ElectionParams,
1212
ElectionResponse,
1313
ElectionTypeEnum,
1414
ElectionUpdateParams,
15-
NomineeInfoModel,
16-
NomineeInfoUpdateParams,
1715
)
18-
from elections.tables import Election, NomineeInfo
16+
from elections.tables import Election
1917
from officers.constants import COUNCIL_REP_ELECTION_POSITIONS, GENERAL_ELECTION_POSITIONS, OfficerPositionEnum
2018
from permission.types import ElectionOfficer, WebsiteAdmin
2119
from utils.shared_models import DetailModel, SuccessResponse
22-
from utils.urls import admin_or_raise, get_current_user, slugify
20+
from utils.urls import get_current_user, slugify
2321

2422
router = APIRouter(
25-
prefix="/elections",
26-
tags=["elections"],
23+
prefix="/election",
24+
tags=["election"],
2725
)
2826

2927
async def get_user_permissions(
@@ -34,7 +32,7 @@ async def get_user_permissions(
3432
if not session_id or not computing_id:
3533
return False, None, None
3634

37-
# where valid means elections officer or website admin
35+
# where valid means election officer or website admin
3836
has_permission = await ElectionOfficer.has_permission(db_session, computing_id)
3937
if not has_permission:
4038
has_permission = await WebsiteAdmin.has_permission(db_session, computing_id)
@@ -84,14 +82,14 @@ def _raise_if_bad_election_data(
8482
detail=f"election slug '{slug}' is too long",
8583
)
8684

87-
# elections ------------------------------------------------------------- #
85+
# election ------------------------------------------------------------- #
8886

8987
@router.get(
9088
"",
91-
description="Returns a list of all elections & their status",
89+
description="Returns a list of all election & their status",
9290
response_model=list[ElectionResponse],
9391
responses={
94-
404: { "description": "No elections found" }
92+
404: { "description": "No election found" }
9593
},
9694
operation_id="get_all_elections"
9795
)
@@ -104,7 +102,7 @@ async def list_elections(
104102
if election_list is None or len(election_list) == 0:
105103
raise HTTPException(
106104
status_code=status.HTTP_404_NOT_FOUND,
107-
detail="no elections found"
105+
detail="no election found"
108106
)
109107

110108
current_time = datetime.now()
@@ -126,7 +124,7 @@ async def list_elections(
126124
description="""
127125
Retrieves the election data for an election by name.
128126
Returns private details when the time is allowed.
129-
If user is an admin or elections officer, returns computing ids for each candidate as well.
127+
If user is an admin or election officer, returns computing ids for each candidate as well.
130128
""",
131129
response_model=ElectionResponse,
132130
responses={
@@ -152,7 +150,7 @@ async def get_election(
152150
if current_time >= election.datetime_start_voting or is_valid_user:
153151

154152
election_json = election.private_details(current_time)
155-
all_nominations = await elections.crud.get_all_registrations_in_election(db_session, slugified_name)
153+
all_nominations = await registrations.crud.get_all_registrations_in_election(db_session, slugified_name)
156154
if not all_nominations:
157155
raise HTTPException(
158156
status_code=status.HTTP_404_NOT_FOUND,
@@ -364,93 +362,3 @@ async def delete_election(
364362

365363
old_election = await elections.crud.get_election(db_session, slugified_name)
366364
return JSONResponse({"success": old_election is None})
367-
368-
# nominee info ------------------------------------------------------------- #
369-
370-
@router.get(
371-
"/nominee/{computing_id:str}",
372-
description="Nominee info is always publically tied to elections, so be careful!",
373-
response_model=NomineeInfoModel,
374-
responses={
375-
404: { "description": "nominee doesn't exist" }
376-
},
377-
operation_id="get_nominee"
378-
)
379-
async def get_nominee_info(
380-
request: Request,
381-
db_session: database.DBSession,
382-
computing_id: str
383-
):
384-
# Putting this one behind the admin wall since it has contact information
385-
await admin_or_raise(request, db_session)
386-
nominee_info = await elections.crud.get_nominee_info(db_session, computing_id)
387-
if nominee_info is None:
388-
raise HTTPException(
389-
status_code=status.HTTP_404_NOT_FOUND,
390-
detail="nominee doesn't exist"
391-
)
392-
393-
return JSONResponse(nominee_info.serialize())
394-
395-
@router.patch(
396-
"/nominee/{computing_id:str}",
397-
description="Will create or update nominee info. Returns an updated copy of their nominee info.",
398-
response_model=NomineeInfoModel,
399-
responses={
400-
500: { "description": "Failed to retrieve updated nominee." }
401-
},
402-
operation_id="update_nominee"
403-
)
404-
async def provide_nominee_info(
405-
request: Request,
406-
db_session: database.DBSession,
407-
body: NomineeInfoUpdateParams,
408-
computing_id: str
409-
):
410-
# TODO: There needs to be a lot more validation here.
411-
await admin_or_raise(request, db_session)
412-
413-
updated_data = {}
414-
# Only update fields that were provided
415-
if body.full_name is not None:
416-
updated_data["full_name"] = body.full_name
417-
if body.linked_in is not None:
418-
updated_data["linked_in"] = body.linked_in
419-
if body.instagram is not None:
420-
updated_data["instagram"] = body.instagram
421-
if body.email is not None:
422-
updated_data["email"] = body.email
423-
if body.discord_username is not None:
424-
updated_data["discord_username"] = body.discord_username
425-
426-
existing_info = await elections.crud.get_nominee_info(db_session, computing_id)
427-
# if not already existing, create it
428-
if not existing_info:
429-
# unpack dictionary and expand into NomineeInfo class
430-
new_nominee_info = NomineeInfo(computing_id=computing_id, **updated_data)
431-
# create a new nominee
432-
await elections.crud.create_nominee_info(db_session, new_nominee_info)
433-
# else just update the partial data
434-
else:
435-
merged_data = {
436-
"computing_id": computing_id,
437-
"full_name": existing_info.full_name,
438-
"linked_in": existing_info.linked_in,
439-
"instagram": existing_info.instagram,
440-
"email": existing_info.email,
441-
"discord_username": existing_info.discord_username,
442-
}
443-
# update the dictionary with new data
444-
merged_data.update(updated_data)
445-
updated_nominee_info = NomineeInfo(**merged_data)
446-
await elections.crud.update_nominee_info(db_session, updated_nominee_info)
447-
448-
await db_session.commit()
449-
450-
nominee_info = await elections.crud.get_nominee_info(db_session, computing_id)
451-
if not nominee_info:
452-
raise HTTPException(
453-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
454-
detail="failed to get updated nominee"
455-
)
456-
return JSONResponse(nominee_info.serialize())

0 commit comments

Comments
 (0)