Skip to content

Commit 822c0dc

Browse files
committed
add support for nominee info
1 parent 45ca097 commit 822c0dc

File tree

4 files changed

+194
-16
lines changed

4 files changed

+194
-16
lines changed

src/alembic/versions/243190df5588_create_election_tables.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ def upgrade() -> None:
3636
"election_nominee_info",
3737
sa.Column("computing_id", sa.String(length=32), nullable=False),
3838
sa.Column("full_name", sa.String(length=64), nullable=False),
39-
sa.Column("facebook", sa.String(length=128), nullable=True),
39+
sa.Column("linked_in", sa.String(length=128), nullable=True),
4040
sa.Column("instagram", sa.String(length=128), nullable=True),
4141
sa.Column("email", sa.String(length=64), nullable=True),
42-
sa.Column("discord", sa.String(length=32), nullable=True),
43-
sa.Column("discord_id", sa.String(length=32), nullable=True),
42+
#sa.Column("discord", sa.String(length=32), nullable=True),
43+
#sa.Column("discord_id", sa.String(length=32), nullable=True),
4444
sa.Column("discord_username", sa.String(length=32), nullable=True),
4545
sa.PrimaryKeyConstraint("computing_id")
4646
)

src/elections/crud.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sqlalchemy
44
from sqlalchemy.ext.asyncio import AsyncSession
55

6-
from elections.tables import Election, NomineeApplication
6+
from elections.tables import Election, NomineeApplication, NomineeInfo
77

88
_logger = logging.getLogger(__name__)
99

@@ -50,6 +50,8 @@ async def delete_election(db_session: AsyncSession, slug: str) -> None:
5050
.where(Election.slug == slug)
5151
)
5252

53+
# ------------------------------------------------------- #
54+
5355
# TODO: switch to only using one of application or registration
5456
async def get_all_registrations(
5557
db_session: AsyncSession,
@@ -66,6 +68,19 @@ async def get_all_registrations(
6668
)).all()
6769
return registrations
6870

71+
async def get_all_registrations_in_election(
72+
db_session: AsyncSession,
73+
election_slug: str,
74+
) -> list[NomineeApplication] | None:
75+
registrations = (await db_session.scalars(
76+
sqlalchemy
77+
.select(NomineeApplication)
78+
.where(
79+
NomineeApplication.election_slug == election_slug
80+
)
81+
)).all()
82+
return registrations
83+
6984
async def add_registration(
7085
db_session: AsyncSession,
7186
initial_application: NomineeApplication
@@ -102,3 +117,32 @@ async def delete_registration(
102117
and NomineeApplication.position == position
103118
)
104119
)
120+
121+
# ------------------------------------------------------- #
122+
123+
async def get_nominee_info(
124+
db_session: AsyncSession,
125+
computing_id: str,
126+
) -> NomineeInfo | None:
127+
return await db_session.scalar(
128+
sqlalchemy
129+
.select(NomineeInfo)
130+
.where(NomineeInfo.computing_id == computing_id)
131+
)
132+
133+
async def create_nominee_info(
134+
db_session: AsyncSession,
135+
info: NomineeInfo,
136+
):
137+
db_session.add(info)
138+
139+
async def update_nominee_info(
140+
db_session: AsyncSession,
141+
info: NomineeInfo,
142+
):
143+
await db_session.execute(
144+
sqlalchemy
145+
.update(NomineeInfo)
146+
.where(NomineeInfo.computing_id == info.computing_id)
147+
.values(info.to_update_dict())
148+
)

src/elections/tables.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,39 @@ class NomineeInfo(Base):
122122

123123
computing_id = Column(String(COMPUTING_ID_LEN), primary_key=True)
124124
full_name = Column(String(64), nullable=False)
125-
facebook = Column(String(128))
125+
linked_in = Column(String(128))
126126
instagram = Column(String(128))
127127
email = Column(String(64))
128-
discord = Column(String(DISCORD_NAME_LEN))
129-
discord_id = Column(String(DISCORD_ID_LEN))
128+
#discord = Column(String(DISCORD_NAME_LEN))
129+
#discord_id = Column(String(DISCORD_ID_LEN))
130130
discord_username = Column(String(DISCORD_NICKNAME_LEN))
131131

132+
def to_update_dict(self) -> dict:
133+
return {
134+
"computing_id": self.computing_id,
135+
"full_name": self.full_name,
136+
137+
"linked_in": self.linked_in,
138+
"instagram": self.instagram,
139+
"email": self.email,
140+
"discord_username": self.discord_username,
141+
}
142+
143+
def as_serializable(self) -> dict:
144+
# NOTE: this function is currently the same as to_update_dict since the contents
145+
# have a different invariant they're upholding, which may cause them to change if a
146+
# new property is introduced. For example, dates must be converted into strings
147+
# to be serialized, but must not for update dictionaries.
148+
return {
149+
"computing_id": self.computing_id,
150+
"full_name": self.full_name,
151+
152+
"linked_in": self.linked_in,
153+
"instagram": self.instagram,
154+
"email": self.email,
155+
"discord_username": self.discord_username,
156+
}
157+
132158
class NomineeApplication(Base):
133159
__tablename__ = "election_nominee_application"
134160

src/elections/urls.py

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import database
99
import elections
1010
import elections.tables
11-
from elections.tables import Election, NomineeApplication, election_types
11+
from elections.tables import Election, NomineeApplication, NomineeInfo, election_types
1212
from officers.constants import OfficerPosition
1313
from permission.types import ElectionOfficer, WebsiteAdmin
1414
from utils.urls import is_logged_in
@@ -32,6 +32,7 @@ async def _validate_user(
3232
if not logged_in:
3333
return False, None, None
3434

35+
# where valid means elections officer or website admin
3536
has_permission = await ElectionOfficer.has_permission(db_session, computing_id)
3637
if not has_permission:
3738
has_permission = await WebsiteAdmin.has_permission(db_session, computing_id)
@@ -65,7 +66,11 @@ async def list_elections(
6566

6667
@router.get(
6768
"/by_name/{name:str}",
68-
description="Retrieves the election data for an election by name. Returns private details when the time is allowed."
69+
description="""
70+
Retrieves the election data for an election by name.
71+
Returns private details when the time is allowed.
72+
If user is an admin or elections officer, returns computing ids for each candidate as well.
73+
"""
6974
)
7075
async def get_election(
7176
request: Request,
@@ -80,16 +85,45 @@ async def get_election(
8085
status_code=status.HTTP_400_BAD_REQUEST,
8186
detail=f"election with slug {_slugify(name)} does not exist"
8287
)
83-
elif current_time >= election.datetime_start_voting:
84-
# after the voting period starts, all election data becomes public
85-
return JSONResponse(election.private_details(current_time))
86-
87-
# TODO: include nominees and speeches
88-
# TODO: ignore any empty mappings
8988

9089
is_valid_user, _, _ = await _validate_user(request, db_session)
91-
if is_valid_user:
90+
if current_time >= election.datetime_start_voting or is_valid_user:
91+
9292
election_json = election.private_details(current_time)
93+
all_nominations = elections.crud.get_all_registrations_in_election(db_session, _slugify(name))
94+
election_json["candidates"] = []
95+
96+
avaliable_positions_list = election.avaliable_positions.split(",")
97+
for nomination in all_nominations:
98+
if nomination.position not in avaliable_positions_list:
99+
# ignore any positions that are **no longer** active
100+
continue
101+
102+
# NOTE: if a nominee does not input their legal name, they are not considered a nominee
103+
nominee_info = elections.crud.get_nominee_info(db_session, nomination.computing_id)
104+
if nominee_info is None:
105+
print("unreachable")
106+
continue
107+
108+
candidate_entry = {
109+
"position": nomination.position,
110+
"full_name": nominee_info.full_name,
111+
"linked_in": nominee_info.linked_in,
112+
"instagram": nominee_info.instagram,
113+
"email": nominee_info.email,
114+
"discord_username": nominee_info.discord_username,
115+
"speech": (
116+
"No speech provided by this candidate"
117+
if nomination.speech is None
118+
else nomination.speech
119+
),
120+
}
121+
if is_valid_user:
122+
candidate_entry["computing_id"] = nomination.computing_id
123+
election_json["candidates"].append(candidate_entry)
124+
125+
# after the voting period starts, all election data becomes public
126+
return JSONResponse()
93127
else:
94128
election_json = election.public_details(current_time)
95129

@@ -353,6 +387,13 @@ async def register_in_election(
353387
detail=f"invalid position {position}"
354388
)
355389

390+
if await elections.crud.get_nominee_info(db_session, computing_id) is None:
391+
# ensure that the user has a nominee info entry before allowing registration to occur.
392+
raise HTTPException(
393+
status_code=status.HTTP_400_BAD_REQUEST,
394+
detail="must have submitted nominee info before registering"
395+
)
396+
356397
current_time = datetime.now()
357398
election_slug = _slugify(election_name)
358399
election = await elections.crud.get_election(db_session, election_slug)
@@ -388,6 +429,7 @@ async def register_in_election(
388429
position=position,
389430
speech=None
390431
))
432+
await db_session.commit()
391433

392434
@router.patch(
393435
"/register/{election_name:str}",
@@ -437,6 +479,7 @@ async def update_registration(
437479
position=position,
438480
speech=speech
439481
))
482+
await db_session.commit()
440483

441484
@router.delete(
442485
"/register/{election_name:str}",
@@ -480,3 +523,68 @@ async def delete_registration(
480523
)
481524

482525
await elections.crud.delete_registration(db_session, computing_id, election_slug, position)
526+
await db_session.commit()
527+
528+
# nominee info ------------------------------------------------------------- #
529+
530+
@router.get(
531+
"/nominee_info",
532+
description="Nominee info is always publically tied to elections, so be careful!"
533+
)
534+
async def get_nominee_info(
535+
request: Request,
536+
db_session: database.DBSession,
537+
):
538+
logged_in, _, computing_id = await is_logged_in(request, db_session)
539+
if not logged_in:
540+
raise HTTPException(
541+
status_code=status.HTTP_401_UNAUTHORIZED,
542+
detail="must be logged in to get your nominee info"
543+
)
544+
545+
nominee_info = await elections.crud.get_nominee_info(db_session, computing_id)
546+
if nominee_info is None:
547+
raise HTTPException(
548+
status_code=status.HTTP_400_BAD_REQUEST,
549+
detail="You don't have any nominee info yet"
550+
)
551+
552+
return JSONResponse(nominee_info.as_serializable())
553+
554+
@router.patch(
555+
"/nominee_info",
556+
description="Will create or update nominee info. Returns an updated copy of their nominee info."
557+
)
558+
async def provide_nominee_info(
559+
request: Request,
560+
db_session: database.DBSession,
561+
full_name: str,
562+
linked_in: str | None,
563+
instagram: str | None,
564+
email: str | None,
565+
discord_username: str | None,
566+
):
567+
logged_in, _, computing_id = await is_logged_in(request, db_session)
568+
if not logged_in:
569+
raise HTTPException(
570+
status_code=status.HTTP_401_UNAUTHORIZED,
571+
detail="must be logged in to update nominee info"
572+
)
573+
574+
pending_nominee_info = NomineeInfo(
575+
computing_id = computing_id,
576+
full_name = full_name,
577+
linked_in = linked_in,
578+
instagram = instagram,
579+
email = email,
580+
discord_username = discord_username,
581+
)
582+
if await elections.crud.get_nominee_info(db_session, computing_id) is None:
583+
await elections.crud.create_nominee_info(db_session, pending_nominee_info)
584+
else:
585+
await elections.crud.update_nominee_info(db_session, pending_nominee_info)
586+
587+
await db_session.commit()
588+
589+
new_nominee_info = await elections.crud.get_nominee_info(db_session, computing_id)
590+
return JSONResponse(new_nominee_info.as_serializable())

0 commit comments

Comments
 (0)