Skip to content

Commit 4b6a34b

Browse files
committed
fix: moved registration to its own endpoint
1 parent 9243988 commit 4b6a34b

File tree

9 files changed

+345
-300
lines changed

9 files changed

+345
-300
lines changed

src/elections/models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class ElectionResponse(BaseModel):
3232
datetime_start_nominations: str
3333
datetime_start_voting: str
3434
datetime_end_voting: str
35-
available_positions: list[str]
35+
available_positions: list[OfficerPositionEnum]
3636
status: ElectionStatusEnum
3737

3838
survey_link: str | None = Field(None, description="Only available to admins")
@@ -44,15 +44,15 @@ class ElectionParams(BaseModel):
4444
datetime_start_nominations: str
4545
datetime_start_voting: str
4646
datetime_end_voting: str
47-
available_positions: list[str] | None = None
47+
available_positions: list[OfficerPositionEnum] | None = None
4848
survey_link: str | None = None
4949

5050
class ElectionUpdateParams(BaseModel):
5151
type: ElectionTypeEnum | None = None
5252
datetime_start_nominations: str | None = None
5353
datetime_start_voting: str | None = None
5454
datetime_end_voting: str | None = None
55-
available_positions: list[str] | None = None
55+
available_positions: list[OfficerPositionEnum] | None = None
5656
survey_link: str | None = None
5757

5858
class NomineeApplicationParams(BaseModel):
@@ -66,7 +66,7 @@ class NomineeApplicationUpdateParams(BaseModel):
6666
class NomineeApplicationModel(BaseModel):
6767
computing_id: str
6868
nominee_election: str
69-
position: str
69+
position: OfficerPositionEnum
7070
speech: str | None = None
7171

7272
class NomineeInfoModel(BaseModel):

src/elections/tables.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ class Election(Base):
3232
slug: Mapped[str] = mapped_column(String(MAX_ELECTION_SLUG), primary_key=True)
3333
name: Mapped[str] = mapped_column(String(MAX_ELECTION_NAME), nullable=False)
3434
type: Mapped[str] = mapped_column(String(64), default="general_election")
35-
datetime_start_nominations: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
36-
datetime_start_voting: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
37-
datetime_end_voting: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
35+
datetime_start_nominations: Mapped[datetime] = mapped_column(DateTime(), nullable=False)
36+
datetime_start_voting: Mapped[datetime] = mapped_column(DateTime(), nullable=False)
37+
datetime_end_voting: Mapped[datetime] = mapped_column(DateTime(), nullable=False)
3838

3939
# a comma-separated string of positions which must be elements of OfficerPosition
4040
# By giving it the type `StringList`, the database entry will automatically be marshalled to the correct form

src/elections/urls.py

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

43
from fastapi import APIRouter, HTTPException, Request, status
@@ -11,31 +10,23 @@
1110
from elections.models import (
1211
ElectionParams,
1312
ElectionResponse,
14-
ElectionStatusEnum,
1513
ElectionTypeEnum,
1614
ElectionUpdateParams,
17-
NomineeApplicationModel,
18-
NomineeApplicationParams,
19-
NomineeApplicationUpdateParams,
2015
NomineeInfoModel,
2116
NomineeInfoUpdateParams,
2217
)
23-
from elections.tables import Election, NomineeApplication, NomineeInfo
18+
from elections.tables import Election, NomineeInfo
2419
from officers.constants import COUNCIL_REP_ELECTION_POSITIONS, GENERAL_ELECTION_POSITIONS, OfficerPositionEnum
2520
from permission.types import ElectionOfficer, WebsiteAdmin
2621
from utils.shared_models import DetailModel, SuccessResponse
27-
from utils.urls import admin_or_raise, get_current_user, logged_in_or_raise
22+
from utils.urls import admin_or_raise, get_current_user, slugify
2823

2924
router = APIRouter(
3025
prefix="/elections",
3126
tags=["elections"],
3227
)
3328

34-
def _slugify(text: str) -> str:
35-
"""Creates a unique slug based on text passed in. Assumes non-unicode text."""
36-
return re.sub(r"[\W_]+", "-", text.strip().replace("/", "").replace("&", ""))
37-
38-
async def _get_user_permissions(
29+
async def get_user_permissions(
3930
request: Request,
4031
db_session: database.DBSession,
4132
) -> tuple[bool, str | None, str | None]:
@@ -108,7 +99,7 @@ async def list_elections(
10899
request: Request,
109100
db_session: database.DBSession,
110101
):
111-
is_admin, _, _ = await _get_user_permissions(request, db_session)
102+
is_admin, _, _ = await get_user_permissions(request, db_session)
112103
election_list = await elections.crud.get_all_elections(db_session)
113104
if election_list is None or len(election_list) == 0:
114105
raise HTTPException(
@@ -149,15 +140,15 @@ async def get_election(
149140
election_name: str
150141
):
151142
current_time = datetime.now()
152-
slugified_name = _slugify(election_name)
143+
slugified_name = slugify(election_name)
153144
election = await elections.crud.get_election(db_session, slugified_name)
154145
if election is None:
155146
raise HTTPException(
156147
status_code=status.HTTP_404_NOT_FOUND,
157148
detail=f"election with slug {slugified_name} does not exist"
158149
)
159150

160-
is_valid_user, _, _ = await _get_user_permissions(request, db_session)
151+
is_valid_user, _, _ = await get_user_permissions(request, db_session)
161152
if current_time >= election.datetime_start_voting or is_valid_user:
162153

163154
election_json = election.private_details(current_time)
@@ -229,7 +220,7 @@ async def create_election(
229220
else:
230221
available_positions = body.available_positions
231222

232-
slugified_name = _slugify(body.name)
223+
slugified_name = slugify(body.name)
233224
current_time = datetime.now()
234225
start_nominations = datetime.fromisoformat(body.datetime_start_nominations)
235226
start_voting = datetime.fromisoformat(body.datetime_start_voting)
@@ -245,7 +236,7 @@ async def create_election(
245236
available_positions
246237
)
247238

248-
is_valid_user, _, _ = await _get_user_permissions(request, db_session)
239+
is_valid_user, _, _ = await get_user_permissions(request, db_session)
249240
if not is_valid_user:
250241
raise HTTPException(
251242
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -305,14 +296,14 @@ async def update_election(
305296
db_session: database.DBSession,
306297
election_name: str,
307298
):
308-
is_valid_user, _, _ = await _get_user_permissions(request, db_session)
299+
is_valid_user, _, _ = await get_user_permissions(request, db_session)
309300
if not is_valid_user:
310301
raise HTTPException(
311302
status_code=status.HTTP_401_UNAUTHORIZED,
312303
detail="must have election officer or admin permission"
313304
)
314305

315-
slugified_name = _slugify(election_name)
306+
slugified_name = slugify(election_name)
316307
election = await elections.crud.get_election(db_session, slugified_name)
317308
if not election:
318309
raise HTTPException(
@@ -360,8 +351,8 @@ async def delete_election(
360351
db_session: database.DBSession,
361352
election_name: str
362353
):
363-
slugified_name = _slugify(election_name)
364-
is_valid_user, _, _ = await _get_user_permissions(request, db_session)
354+
slugified_name = slugify(election_name)
355+
is_valid_user, _, _ = await get_user_permissions(request, db_session)
365356
if not is_valid_user:
366357
raise HTTPException(
367358
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -374,237 +365,6 @@ async def delete_election(
374365
old_election = await elections.crud.get_election(db_session, slugified_name)
375366
return JSONResponse({"success": old_election is None})
376367

377-
# registration ------------------------------------------------------------- #
378-
379-
@router.get(
380-
"/registration/{election_name:str}",
381-
description="get all the registrations of a single election",
382-
response_model=list[NomineeApplicationModel],
383-
responses={
384-
401: { "description": "Not logged in", "model": DetailModel },
385-
404: { "description": "Election with slug does not exist", "model": DetailModel }
386-
},
387-
operation_id="get_election_registrations"
388-
)
389-
async def get_election_registrations(
390-
request: Request,
391-
db_session: database.DBSession,
392-
election_name: str
393-
):
394-
_, computing_id = await logged_in_or_raise(request, db_session)
395-
396-
slugified_name = _slugify(election_name)
397-
if await elections.crud.get_election(db_session, slugified_name) is None:
398-
raise HTTPException(
399-
status_code=status.HTTP_404_NOT_FOUND,
400-
detail=f"election with slug {slugified_name} does not exist"
401-
)
402-
403-
registration_list = await elections.crud.get_all_registrations_of_user(db_session, computing_id, slugified_name)
404-
if registration_list is None:
405-
return JSONResponse([])
406-
return JSONResponse([
407-
item.serialize() for item in registration_list
408-
])
409-
410-
@router.post(
411-
"/registration/{election_name:str}",
412-
description="Register for a specific position in this election, but doesn't set a speech. Returns the created entry.",
413-
response_model=NomineeApplicationModel,
414-
responses={
415-
400: { "description": "Bad request", "model": DetailModel },
416-
401: { "description": "Not logged in", "model": DetailModel },
417-
403: { "description": "Not an admin", "model": DetailModel },
418-
404: { "description": "No election found", "model": DetailModel },
419-
},
420-
operation_id="register"
421-
)
422-
async def register_in_election(
423-
request: Request,
424-
db_session: database.DBSession,
425-
body: NomineeApplicationParams,
426-
election_name: str
427-
):
428-
await admin_or_raise(request, db_session)
429-
430-
if body.position not in OfficerPositionEnum:
431-
raise HTTPException(
432-
status_code=status.HTTP_400_BAD_REQUEST,
433-
detail=f"invalid position {body.position}"
434-
)
435-
436-
if await elections.crud.get_nominee_info(db_session, body.computing_id) is None:
437-
# ensure that the user has a nominee info entry before allowing registration to occur.
438-
raise HTTPException(
439-
status_code=status.HTTP_400_BAD_REQUEST,
440-
detail="must have submitted nominee info before registering"
441-
)
442-
443-
slugified_name = _slugify(election_name)
444-
election = await elections.crud.get_election(db_session, slugified_name)
445-
if election is None:
446-
raise HTTPException(
447-
status_code=status.HTTP_404_NOT_FOUND,
448-
detail=f"election with slug {slugified_name} does not exist"
449-
)
450-
451-
if body.position not in election.available_positions:
452-
# NOTE: We only restrict creating a registration for a position that doesn't exist,
453-
# not updating or deleting one
454-
raise HTTPException(
455-
status_code=status.HTTP_400_BAD_REQUEST,
456-
detail=f"{body.position} is not available to register for in this election"
457-
)
458-
459-
if election.status(datetime.now()) != ElectionStatusEnum.NOMINATIONS:
460-
raise HTTPException(
461-
status_code=status.HTTP_400_BAD_REQUEST,
462-
detail="registrations can only be made during the nomination period"
463-
)
464-
465-
if await elections.crud.get_all_registrations_of_user(db_session, body.computing_id, slugified_name):
466-
raise HTTPException(
467-
status_code=status.HTTP_400_BAD_REQUEST,
468-
detail="person is already registered in this election"
469-
)
470-
471-
# TODO: associate specific elections officers with specific elections, then don't
472-
# allow any elections officer running an election to register for it
473-
await elections.crud.add_registration(db_session, NomineeApplication(
474-
computing_id=body.computing_id,
475-
nominee_election=slugified_name,
476-
position=body.position,
477-
speech=None
478-
))
479-
await db_session.commit()
480-
481-
registrant = await elections.crud.get_one_registration_in_election(
482-
db_session, body.computing_id, slugified_name, body.position
483-
)
484-
if not registrant:
485-
raise HTTPException(
486-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
487-
detail="failed to find new registrant"
488-
)
489-
return registrant
490-
491-
@router.patch(
492-
"/registration/{election_name:str}/{position:str}/{computing_id:str}",
493-
description="update the application of a specific registrant and return the changed entry",
494-
response_model=NomineeApplicationModel,
495-
responses={
496-
400: { "description": "Bad request", "model": DetailModel },
497-
401: { "description": "Not logged in", "model": DetailModel },
498-
403: { "description": "Not an admin", "model": DetailModel },
499-
404: { "description": "No election found", "model": DetailModel },
500-
},
501-
operation_id="update_registration"
502-
)
503-
async def update_registration(
504-
request: Request,
505-
db_session: database.DBSession,
506-
body: NomineeApplicationUpdateParams,
507-
election_name: str,
508-
computing_id: str,
509-
position: OfficerPositionEnum
510-
):
511-
await admin_or_raise(request, db_session)
512-
513-
if body.position not in OfficerPositionEnum:
514-
raise HTTPException(
515-
status_code=status.HTTP_400_BAD_REQUEST,
516-
detail=f"invalid position {body.position}"
517-
)
518-
519-
slugified_name = _slugify(election_name)
520-
election = await elections.crud.get_election(db_session, slugified_name)
521-
if election is None:
522-
raise HTTPException(
523-
status_code=status.HTTP_404_NOT_FOUND,
524-
detail=f"election with slug {slugified_name} does not exist"
525-
)
526-
527-
# self updates can only be done during nomination period. Officer updates can be done whenever
528-
if election.status(datetime.now()) != ElectionStatusEnum.NOMINATIONS:
529-
raise HTTPException(
530-
status_code=status.HTTP_400_BAD_REQUEST,
531-
detail="speeches can only be updated during the nomination period"
532-
)
533-
534-
registration = await elections.crud.get_one_registration_in_election(db_session, computing_id, slugified_name, position)
535-
if not registration:
536-
raise HTTPException(
537-
status_code=status.HTTP_404_NOT_FOUND,
538-
detail="no registration record found"
539-
)
540-
541-
registration.update_from_params(body)
542-
543-
await elections.crud.update_registration(db_session, registration)
544-
await db_session.commit()
545-
546-
registrant = await elections.crud.get_one_registration_in_election(
547-
db_session, registration.computing_id, slugified_name, registration.position
548-
)
549-
if not registrant:
550-
raise HTTPException(
551-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
552-
detail="failed to find changed registrant"
553-
)
554-
return registrant
555-
556-
@router.delete(
557-
"/registration/{election_name:str}/{position:str}/{computing_id:str}",
558-
description="delete the registration of a person",
559-
response_model=SuccessResponse,
560-
responses={
561-
400: { "description": "Bad request", "model": DetailModel },
562-
401: { "description": "Not logged in", "model": DetailModel },
563-
403: { "description": "Not an admin", "model": DetailModel },
564-
404: { "description": "No election or registrant found", "model": DetailModel },
565-
},
566-
operation_id="delete_registration"
567-
)
568-
async def delete_registration(
569-
request: Request,
570-
db_session: database.DBSession,
571-
election_name: str,
572-
position: OfficerPositionEnum,
573-
computing_id: str
574-
):
575-
await admin_or_raise(request, db_session)
576-
577-
if position not in OfficerPositionEnum:
578-
raise HTTPException(
579-
status_code=status.HTTP_400_BAD_REQUEST,
580-
detail=f"invalid position {position}"
581-
)
582-
583-
slugified_name = _slugify(election_name)
584-
election = await elections.crud.get_election(db_session, slugified_name)
585-
if election is None:
586-
raise HTTPException(
587-
status_code=status.HTTP_404_NOT_FOUND,
588-
detail=f"election with slug {slugified_name} does not exist"
589-
)
590-
591-
if election.status(datetime.now()) != ElectionStatusEnum.NOMINATIONS:
592-
raise HTTPException(
593-
status_code=status.HTTP_400_BAD_REQUEST,
594-
detail="registration can only be revoked during the nomination period"
595-
)
596-
597-
if not await elections.crud.get_all_registrations_of_user(db_session, computing_id, slugified_name):
598-
raise HTTPException(
599-
status_code=status.HTTP_404_NOT_FOUND,
600-
detail=f"{computing_id} was not registered in election {slugified_name} for {position}"
601-
)
602-
603-
await elections.crud.delete_registration(db_session, computing_id, slugified_name, position)
604-
await db_session.commit()
605-
old_election = await elections.crud.get_one_registration_in_election(db_session, computing_id, slugified_name, position)
606-
return JSONResponse({"success": old_election is None})
607-
608368
# nominee info ------------------------------------------------------------- #
609369

610370
@router.get(

0 commit comments

Comments
 (0)