Skip to content

Commit a32f905

Browse files
committed
add avaliable positions & make it configurable. clean code too
1 parent 7926e48 commit a32f905

File tree

5 files changed

+147
-68
lines changed

5 files changed

+147
-68
lines changed

src/alembic/versions/243190df5588_create_election_tables.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def upgrade() -> None:
2828
sa.Column("datetime_start_nominations", sa.DateTime(), nullable=False),
2929
sa.Column("datetime_start_voting", sa.DateTime(), nullable=False),
3030
sa.Column("datetime_end_voting", sa.DateTime(), nullable=False),
31+
sa.Column("avaliable_positions", sa.Text(), nullable=False),
3132
sa.Column("survey_link", sa.String(length=300), nullable=True),
3233
sa.PrimaryKeyConstraint("slug")
3334
)

src/elections/crud.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,27 @@ async def get_election(db_session: AsyncSession, election_slug: str) -> Election
2222
.where(Election.slug == election_slug)
2323
)
2424

25-
async def create_election(db_session: AsyncSession, election: Election) -> None:
25+
async def create_election(db_session: AsyncSession, election: Election):
2626
"""
2727
Creates a new election with given parameters.
2828
Does not validate if an election _already_ exists
2929
"""
3030
db_session.add(election)
3131

32-
async def update_election(db_session: AsyncSession, new_election: Election) -> bool:
32+
async def update_election(db_session: AsyncSession, new_election: Election):
3333
"""
34-
You attempting to change the name or slug will fail. Instead, you must create a new election.
34+
Attempting to change slug will fail. Instead, you must create a new election.
3535
"""
36-
target_slug = new_election.slug
37-
# TODO: does this check need to be performed?
38-
target_election = await get_election(db_session, target_slug)
39-
40-
if target_election is None:
41-
return False
42-
else:
43-
await db_session.execute(
44-
sqlalchemy
45-
.update(Election)
46-
.where(Election.slug == target_slug)
47-
.values(new_election.to_update_dict())
48-
)
49-
return True
36+
await db_session.execute(
37+
sqlalchemy
38+
.update(Election)
39+
.where(Election.slug == new_election.slug)
40+
.values(new_election.to_update_dict())
41+
)
5042

5143
async def delete_election(db_session: AsyncSession, slug: str) -> None:
5244
"""
53-
Deletes a given election by its slug.
54-
Does not validate if an election exists
45+
Deletes a given election by its slug. Does not validate if an election exists
5546
"""
5647
await db_session.execute(
5748
sqlalchemy

src/elections/tables.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@
1616
DISCORD_NICKNAME_LEN,
1717
)
1818
from database import Base
19+
from officers.constants import COUNCIL_REP_ELECTION_POSITIONS, GENERAL_ELECTION_POSITIONS
1920

21+
# If you wish to add more elections & defaults, please see `create_election`
2022
election_types = ["general_election", "by_election", "council_rep_election"]
2123

24+
DEFAULT_POSITIONS_GENERAL_ELECTION = ",".join(GENERAL_ELECTION_POSITIONS)
25+
DEFAULT_POSITIONS_BY_ELECTION = ",".join(GENERAL_ELECTION_POSITIONS)
26+
DEFAULT_POSITIONS_COUNCIL_REP_ELECTION = ",".join(COUNCIL_REP_ELECTION_POSITIONS)
27+
2228
STATUS_BEFORE_NOMINATIONS = "before_nominations"
2329
STATUS_NOMINATIONS = "nominations"
2430
STATUS_VOTING = "voting"
@@ -37,6 +43,9 @@ class Election(Base):
3743
datetime_start_nominations = Column(DateTime, nullable=False)
3844
datetime_start_voting = Column(DateTime, nullable=False)
3945
datetime_end_voting = Column(DateTime, nullable=False)
46+
47+
# a csv list of positions which must be elements of OfficerPosition
48+
avaliable_positions = Column(Text, nullable=False)
4049
survey_link = Column(String(300))
4150

4251
def private_details(self, at_time: datetime) -> dict:
@@ -51,10 +60,26 @@ def private_details(self, at_time: datetime) -> dict:
5160
"datetime_end_voting": self.datetime_end_voting.isoformat(),
5261

5362
"status": self.status(at_time),
63+
"avaliable_positions": self.avaliable_positions,
5464
"survey_link": self.survey_link,
5565
}
5666

5767
def public_details(self, at_time: datetime) -> dict:
68+
# is serializable
69+
return {
70+
"slug": self.slug,
71+
"name": self.name,
72+
"type": self.type,
73+
74+
"datetime_start_nominations": self.datetime_start_nominations.isoformat(),
75+
"datetime_start_voting": self.datetime_start_voting.isoformat(),
76+
"datetime_end_voting": self.datetime_end_voting.isoformat(),
77+
78+
"status": self.status(at_time),
79+
"avaliable_positions": self.avaliable_positions,
80+
}
81+
82+
def public_metadata(self, at_time: datetime) -> dict:
5883
# is serializable
5984
return {
6085
"slug": self.slug,
@@ -78,6 +103,7 @@ def to_update_dict(self) -> dict:
78103
"datetime_start_voting": self.datetime_start_voting,
79104
"datetime_end_voting": self.datetime_end_voting,
80105

106+
"avaliable_positions": self.avaliable_positions,
81107
"survey_link": self.survey_link,
82108
}
83109

src/elections/urls.py

Lines changed: 91 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import database
99
import elections
10+
import elections.tables
1011
from elections.tables import Election, NomineeApplication, election_types
1112
from officers.constants import OfficerPosition
1213
from permission.types import ElectionOfficer, WebsiteAdmin
@@ -56,7 +57,7 @@ async def list_elections(
5657

5758
current_time = datetime.now()
5859
election_metadata_list = [
59-
election.public_details(current_time)
60+
election.public_metadata(current_time)
6061
for election in election_list
6162
]
6263

@@ -94,6 +95,45 @@ async def get_election(
9495

9596
return JSONResponse(election_json)
9697

98+
def _raise_if_bad_election_data(
99+
name: str,
100+
election_type: str,
101+
datetime_start_nominations: datetime,
102+
datetime_start_voting: datetime,
103+
datetime_end_voting: datetime,
104+
avaliable_positions: str | None,
105+
):
106+
if election_type not in election_types:
107+
raise HTTPException(
108+
status_code=status.HTTP_400_BAD_REQUEST,
109+
detail=f"unknown election type {election_type}",
110+
)
111+
elif not (
112+
(datetime_start_nominations <= datetime_start_voting)
113+
and (datetime_start_voting <= datetime_end_voting)
114+
):
115+
raise HTTPException(
116+
status_code=status.HTTP_400_BAD_REQUEST,
117+
detail="dates must be in order from earliest to latest",
118+
)
119+
elif avaliable_positions is not None:
120+
for position in avaliable_positions.split(","):
121+
if position not in OfficerPosition.position_list():
122+
raise HTTPException(
123+
status_code=status.HTTP_400_BAD_REQUEST,
124+
detail=f"unknown position found in position list {position}",
125+
)
126+
elif len(name) > elections.tables.MAX_ELECTION_NAME:
127+
raise HTTPException(
128+
status_code=status.HTTP_400_BAD_REQUEST,
129+
detail=f"election name {name} is too long",
130+
)
131+
elif len(_slugify(name)) > elections.tables.MAX_ELECTION_SLUG:
132+
raise HTTPException(
133+
status_code=status.HTTP_400_BAD_REQUEST,
134+
detail=f"election slug {_slugify(name)} is too long",
135+
)
136+
97137
@router.post(
98138
"/by_name/{name:str}",
99139
description="Creates an election and places it in the database. Returns election json on success",
@@ -106,15 +146,19 @@ async def create_election(
106146
datetime_start_nominations: datetime,
107147
datetime_start_voting: datetime,
108148
datetime_end_voting: datetime,
149+
# allows None, which assigns it to the default
150+
avaliable_positions: str | None,
109151
survey_link: str | None,
110152
):
111153
current_time = datetime.now()
112-
113-
if election_type not in election_types:
114-
raise HTTPException(
115-
status_code=status.HTTP_400_BAD_REQUEST,
116-
detail=f"unknown election type {election_type}",
117-
)
154+
_raise_if_bad_election_data(
155+
name,
156+
election_type,
157+
datetime_start_nominations,
158+
datetime_start_voting,
159+
datetime_end_voting,
160+
avaliable_positions,
161+
)
118162

119163
is_valid_user, _, _ = await _validate_user(request, db_session)
120164
if not is_valid_user:
@@ -124,30 +168,25 @@ async def create_election(
124168
# TODO: is this header actually required?
125169
headers={"WWW-Authenticate": "Basic"},
126170
)
127-
elif len(name) > elections.tables.MAX_ELECTION_NAME:
128-
raise HTTPException(
129-
status_code=status.HTTP_400_BAD_REQUEST,
130-
detail=f"election name {name} is too long",
131-
)
132-
elif len(_slugify(name)) > elections.tables.MAX_ELECTION_SLUG:
133-
raise HTTPException(
134-
status_code=status.HTTP_400_BAD_REQUEST,
135-
detail=f"election slug {_slugify(name)} is too long",
136-
)
137171
elif await elections.crud.get_election(db_session, _slugify(name)) is not None:
138172
# don't overwrite a previous election
139173
raise HTTPException(
140174
status_code=status.HTTP_400_BAD_REQUEST,
141175
detail="would overwrite previous election",
142176
)
143-
elif not (
144-
(datetime_start_nominations <= datetime_start_voting)
145-
and (datetime_start_voting <= datetime_end_voting)
146-
):
147-
raise HTTPException(
148-
status_code=status.HTTP_400_BAD_REQUEST,
149-
detail="dates must be in order from earliest to latest",
150-
)
177+
178+
if avaliable_positions is None:
179+
if election_type == "general_election":
180+
avaliable_positions = elections.tables.DEFAULT_POSITIONS_GENERAL_ELECTION
181+
elif election_type == "by_election":
182+
avaliable_positions = elections.tables.DEFAULT_POSITIONS_BY_ELECTION
183+
elif election_type == "council_rep_election":
184+
avaliable_positions = elections.tables.DEFAULT_POSITIONS_COUNCIL_REP_ELECTION
185+
else:
186+
raise HTTPException(
187+
status_code=status.HTTP_400_BAD_REQUEST,
188+
detail=f"invalid election type {election_type} for avaliable positions"
189+
)
151190

152191
await elections.crud.create_election(
153192
db_session,
@@ -158,6 +197,7 @@ async def create_election(
158197
datetime_start_nominations = datetime_start_nominations,
159198
datetime_start_voting = datetime_start_voting,
160199
datetime_end_voting = datetime_end_voting,
200+
avaliable_positions = avaliable_positions,
161201
survey_link = survey_link
162202
)
163203
)
@@ -185,47 +225,49 @@ async def update_election(
185225
datetime_start_nominations: datetime,
186226
datetime_start_voting: datetime,
187227
datetime_end_voting: datetime,
228+
avaliable_positions: str,
188229
survey_link: str | None,
189230
):
190231
current_time = datetime.now()
232+
_raise_if_bad_election_data(
233+
name,
234+
election_type,
235+
datetime_start_nominations,
236+
datetime_start_voting,
237+
datetime_end_voting,
238+
avaliable_positions,
239+
)
191240

192241
is_valid_user, _, _ = await _validate_user(request, db_session)
193242
if not is_valid_user:
194-
# let's workshop how we actually wanna handle this
195243
raise HTTPException(
196244
status_code=status.HTTP_401_UNAUTHORIZED,
197245
detail="must have election officer or admin permission",
198246
headers={"WWW-Authenticate": "Basic"},
199247
)
200-
elif not (
201-
(datetime_start_nominations <= datetime_start_voting)
202-
and (datetime_start_voting <= datetime_end_voting)
203-
):
248+
elif await elections.crud.get_election(db_session, _slugify(name)) is None:
204249
raise HTTPException(
205250
status_code=status.HTTP_400_BAD_REQUEST,
206-
detail="dates must be in order from earliest to latest",
251+
detail=f"election with slug {_slugify(name)} does not exist",
207252
)
208253

209-
new_election = Election(
210-
slug = _slugify(name),
211-
name = name,
212-
type = election_type,
213-
datetime_start_nominations = datetime_start_nominations,
214-
datetime_start_voting = datetime_start_voting,
215-
datetime_end_voting = datetime_end_voting,
216-
survey_link = survey_link
217-
)
218-
success = await elections.crud.update_election(db_session, new_election)
219-
if not success:
220-
raise HTTPException(
221-
status_code=status.HTTP_400_BAD_REQUEST,
222-
detail=f"election with slug {_slugify(name)} does not exist",
254+
await elections.crud.update_election(
255+
db_session,
256+
Election(
257+
slug = _slugify(name),
258+
name = name,
259+
type = election_type,
260+
datetime_start_nominations = datetime_start_nominations,
261+
datetime_start_voting = datetime_start_voting,
262+
datetime_end_voting = datetime_end_voting,
263+
avaliable_positions = avaliable_positions,
264+
survey_link = survey_link
223265
)
224-
else:
225-
await db_session.commit()
266+
)
267+
await db_session.commit()
226268

227-
election = await elections.crud.get_election(db_session, _slugify(name))
228-
return JSONResponse(election.private_details(current_time))
269+
election = await elections.crud.get_election(db_session, _slugify(name))
270+
return JSONResponse(election.private_details(current_time))
229271

230272
@router.delete(
231273
"/by_name/{name:str}",

src/officers/constants.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,22 @@ def expected_positions() -> list[str]:
175175
OfficerPosition.WEBMASTER,
176176
OfficerPosition.SOCIAL_MEDIA_MANAGER,
177177
]
178+
179+
GENERAL_ELECTION_POSITIONS = [
180+
OfficerPosition.PRESIDENT,
181+
OfficerPosition.VICE_PRESIDENT,
182+
OfficerPosition.TREASURER,
183+
184+
OfficerPosition.DIRECTOR_OF_RESOURCES,
185+
OfficerPosition.DIRECTOR_OF_EVENTS,
186+
OfficerPosition.DIRECTOR_OF_EDUCATIONAL_EVENTS,
187+
OfficerPosition.ASSISTANT_DIRECTOR_OF_EVENTS,
188+
OfficerPosition.DIRECTOR_OF_COMMUNICATIONS,
189+
#OfficerPosition.DIRECTOR_OF_OUTREACH,
190+
OfficerPosition.DIRECTOR_OF_MULTIMEDIA,
191+
OfficerPosition.DIRECTOR_OF_ARCHIVES,
192+
]
193+
194+
COUNCIL_REP_ELECTION_POSITIONS = [
195+
OfficerPosition.SFSS_COUNCIL_REPRESENTATIVE,
196+
]

0 commit comments

Comments
 (0)