Skip to content

Commit f2e1704

Browse files
committed
fix: updating parameters works for election dates
1 parent cdeacb0 commit f2e1704

File tree

6 files changed

+58
-66
lines changed

6 files changed

+58
-66
lines changed

src/elections/models.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@ class ElectionResponse(BaseModel):
3939
candidates: list[CandidateModel] | None = Field(None, description="Only available to admins")
4040

4141
class ElectionParams(BaseModel):
42-
slug: str
4342
name: str
44-
type: ElectionTypeEnum | None = None
43+
type: ElectionTypeEnum
4544
datetime_start_nominations: str
4645
datetime_start_voting: str
4746
datetime_end_voting: str
@@ -57,7 +56,6 @@ class ElectionUpdateParams(BaseModel):
5756
survey_link: str | None = None
5857

5958
class NomineeApplicationParams(BaseModel):
60-
election_name: str
6159
computing_id: str
6260
position: OfficerPositionEnum
6361

@@ -69,7 +67,7 @@ class NomineeApplicationModel(BaseModel):
6967
computing_id: str
7068
nominee_election: str
7169
position: str
72-
speech: str
70+
speech: str | None = None
7371

7472
class NomineeInfoModel(BaseModel):
7573
computing_id: str
@@ -79,7 +77,7 @@ class NomineeInfoModel(BaseModel):
7977
email: str
8078
discord_username: str
8179

82-
class NomineeUpdateParams(BaseModel):
80+
class NomineeInfoUpdateParams(BaseModel):
8381
full_name: str | None = None
8482
linked_in: str | None = None
8583
instagram: str | None = None

src/elections/tables.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime
1+
from datetime import date, datetime
22

33
from sqlalchemy import (
44
DateTime,
@@ -21,7 +21,6 @@
2121
ElectionStatusEnum,
2222
ElectionUpdateParams,
2323
NomineeApplicationUpdateParams,
24-
NomineeUpdateParams,
2524
)
2625
from officers.types import OfficerPositionEnum
2726

@@ -53,7 +52,6 @@ def available_positions(self, value: str | list[str]) -> None:
5352
value = ",".join(value)
5453
self._available_positions = value
5554

56-
5755
def private_details(self, at_time: datetime) -> dict:
5856
# is serializable
5957
return {
@@ -105,18 +103,27 @@ def to_update_dict(self) -> dict:
105103
"name": self.name,
106104
"type": self.type,
107105

108-
"datetime_start_nominations": self.datetime_start_nominations,
109-
"datetime_start_voting": self.datetime_start_voting,
110-
"datetime_end_voting": self.datetime_end_voting,
106+
"datetime_start_nominations": self.datetime_start_nominations.date(),
107+
"datetime_start_voting": self.datetime_start_voting.date(),
108+
"datetime_end_voting": self.datetime_end_voting.date(),
111109

112110
"available_positions": self._available_positions,
113111
"survey_link": self.survey_link,
114112
}
115113

116114
def update_from_params(self, params: ElectionUpdateParams):
117-
update_data = params.model_dump(exclude_unset=True)
115+
update_data = params.model_dump(
116+
exclude_unset=True,
117+
exclude={"datetime_start_nominations", "datetime_start_voting", "datetime_end_voting"}
118+
)
118119
for k, v in update_data.items():
119120
setattr(self, k, v)
121+
if params.datetime_start_nominations:
122+
self.datetime_start_nominations = datetime.fromisoformat(params.datetime_start_nominations)
123+
if params.datetime_start_voting:
124+
self.datetime_start_voting = datetime.fromisoformat(params.datetime_start_voting)
125+
if params.datetime_end_voting:
126+
self.datetime_end_voting = datetime.fromisoformat(params.datetime_end_voting)
120127

121128
def status(self, at_time: datetime) -> str:
122129
if at_time <= self.datetime_start_nominations:

src/elections/urls.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
NomineeApplicationParams,
1919
NomineeApplicationUpdateParams,
2020
NomineeInfoModel,
21-
NomineeUpdateParams,
21+
NomineeInfoUpdateParams,
2222
)
2323
from elections.tables import Election, NomineeApplication, NomineeInfo
2424
from officers.constants import COUNCIL_REP_ELECTION_POSITIONS, GENERAL_ELECTION_POSITIONS
@@ -97,7 +97,7 @@ def _raise_if_bad_election_data(
9797
# elections ------------------------------------------------------------- #
9898

9999
@router.get(
100-
"/list",
100+
"",
101101
description="Returns a list of all elections & their status",
102102
response_model=list[ElectionResponse],
103103
responses={
@@ -220,12 +220,6 @@ async def create_election(
220220
body: ElectionParams,
221221
db_session: database.DBSession,
222222
):
223-
if body.name == "list":
224-
raise HTTPException(
225-
status_code=status.HTTP_400_BAD_REQUEST,
226-
detail="cannot use that election name",
227-
)
228-
229223
if body.available_positions is None:
230224
if body.type not in ElectionTypeEnum:
231225
raise HTTPException(
@@ -238,14 +232,17 @@ async def create_election(
238232

239233
slugified_name = _slugify(body.name)
240234
current_time = datetime.now()
235+
start_nominations = datetime.fromisoformat(body.datetime_start_nominations)
236+
start_voting = datetime.fromisoformat(body.datetime_start_voting)
237+
end_voting = datetime.fromisoformat(body.datetime_end_voting)
241238

242239
# TODO: We might be able to just use a validation function from Pydantic or SQLAlchemy to check this
243240
_raise_if_bad_election_data(
244241
slugified_name,
245242
body.type,
246-
datetime.fromisoformat(body.datetime_start_voting),
247-
datetime.fromisoformat(body.datetime_start_voting),
248-
datetime.fromisoformat(body.datetime_end_voting),
243+
start_nominations,
244+
start_voting,
245+
end_voting,
249246
",".join(available_positions),
250247
)
251248

@@ -268,11 +265,10 @@ async def create_election(
268265
slug = slugified_name,
269266
name = body.name,
270267
type = body.type,
271-
datetime_start_nominations = body.datetime_start_nominations,
272-
datetime_start_voting = body.datetime_start_voting,
273-
datetime_end_voting = body.datetime_end_voting,
274-
# TODO: Make this automatically concatenate the string and set it to lowercase if supplied with a list[str]
275-
available_positions = ",".join(available_positions),
268+
datetime_start_nominations = start_nominations,
269+
datetime_start_voting = start_voting,
270+
datetime_end_voting = end_voting,
271+
available_positions = available_positions,
276272
survey_link = body.survey_link
277273
)
278274
)
@@ -413,7 +409,7 @@ async def get_election_registrations(
413409
])
414410

415411
@router.post(
416-
"/register",
412+
"/registration/{election_name:str}",
417413
description="Register for a specific position in this election, but doesn't set a speech. Returns the created entry.",
418414
response_model=NomineeApplicationModel,
419415
responses={
@@ -428,6 +424,7 @@ async def register_in_election(
428424
request: Request,
429425
db_session: database.DBSession,
430426
body: NomineeApplicationParams,
427+
election_name: str
431428
):
432429
await admin_or_raise(request, db_session)
433430

@@ -444,7 +441,7 @@ async def register_in_election(
444441
detail="must have submitted nominee info before registering"
445442
)
446443

447-
slugified_name = _slugify(body.election_name)
444+
slugified_name = _slugify(election_name)
448445
election = await elections.crud.get_election(db_session, slugified_name)
449446
if election is None:
450447
raise HTTPException(
@@ -545,16 +542,11 @@ async def update_registration(
545542

546543
registration.update_from_params(body)
547544

548-
await elections.crud.update_registration(db_session, NomineeApplication(
549-
computing_id=computing_id,
550-
nominee_election=slugified_name,
551-
position=body.position,
552-
speech=body.speech
553-
))
545+
await elections.crud.update_registration(db_session, registration)
554546
await db_session.commit()
555547

556548
registrant = await elections.crud.get_one_registration_in_election(
557-
db_session, registration.computing_id, slugified_name, body.position
549+
db_session, registration.computing_id, slugified_name, registration.position
558550
)
559551
if not registrant:
560552
raise HTTPException(
@@ -654,7 +646,7 @@ async def get_nominee_info(
654646
async def provide_nominee_info(
655647
request: Request,
656648
db_session: database.DBSession,
657-
body: NomineeUpdateParams,
649+
body: NomineeInfoUpdateParams,
658650
computing_id: str
659651
):
660652
# TODO: There needs to be a lot more validation here.

src/permission/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,4 @@ async def has_permission_or_raise(
7777
) -> bool:
7878
if not await WebsiteAdmin.has_permission(db_session, computing_id):
7979
raise HTTPException(status_code=401, detail=errmsg)
80+
return True

src/utils/urls.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ async def admin_or_raise(request: Request, db_session: database.DBSession) -> tu
5353

5454
# where valid means elections officer or website admin
5555
if (await ElectionOfficer.has_permission(db_session, computing_id)) or (await WebsiteAdmin.has_permission(db_session, computing_id)):
56+
return session_id, computing_id
57+
else:
5658
raise HTTPException(
57-
status_code=status.HTTP_403_UNAUTHORIZED,
59+
status_code=status.HTTP_403_FORBIDDEN,
5860
detail="must be an admin"
5961
)
6062

61-
return session_id, computing_id

tests/integration/test_elections.py

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async def test_read_elections(database_setup):
8787
# API endpoint testing (without AUTH)--------------------------------------
8888
@pytest.mark.anyio
8989
async def test_endpoints(client, database_setup):
90-
response = await client.get("/elections/list")
90+
response = await client.get("/elections")
9191
assert response.status_code == 200
9292
assert response.json() != {}
9393

@@ -120,9 +120,8 @@ async def test_endpoints(client, database_setup):
120120
})
121121
assert response.status_code == 401 # unauthorized access to create an election
122122

123-
response = await client.post("/elections/register", json={
123+
response = await client.post("/elections/registration/{test-election-1}", json={
124124
"computing_id": "1234567",
125-
"election_name": "test-election-1",
126125
"position": "president",
127126
})
128127
assert response.status_code == 401 # unauthorized access to register candidates
@@ -171,7 +170,7 @@ async def test_endpoints_admin(client, database_setup):
171170
client.cookies = { "session_id": session_id }
172171

173172
# test that more info is given if logged in & with access to it
174-
response = await client.get("/elections/list")
173+
response = await client.get("/elections")
175174
assert response.status_code == 200
176175
assert response.json() != {}
177176

@@ -190,54 +189,46 @@ async def test_endpoints_admin(client, database_setup):
190189
assert response.status_code == 200
191190

192191
# ensure that authorized users can create an election
193-
response = await client.post("/elections/testElection4", json={
194-
"election_type": "general_election",
192+
response = await client.post("/elections", json={
193+
"name": "testElection4",
194+
"type": "general_election",
195195
"datetime_start_nominations": (datetime.now() - timedelta(days=1)).isoformat(),
196196
"datetime_start_voting": (datetime.now() + timedelta(days=7)).isoformat(),
197197
"datetime_end_voting": (datetime.now() + timedelta(days=14)).isoformat(),
198-
"available_positions": "president,treasurer",
198+
"available_positions": ["president", "treasurer"],
199199
"survey_link": "https://youtu.be/dQw4w9WgXcQ?si=kZROi2tu-43MXPM5"
200200
})
201201
assert response.status_code == 200
202202
# ensure that user can create elections without knowing each position type
203-
response = await client.post("/elections/byElection4", json={
204-
"election_type": "by_election",
203+
response = await client.post("/elections", json={
204+
"name": "byElection4",
205+
"type": "by_election",
205206
"datetime_start_nominations": (datetime.now() - timedelta(days=1)).isoformat(),
206207
"datetime_start_voting": (datetime.now() + timedelta(days=7)).isoformat(),
207208
"datetime_end_voting": (datetime.now() + timedelta(days=14)).isoformat(),
208209
"survey_link": "https://youtu.be/dQw4w9WgXcQ?si=kZROi2tu-43MXPM5"
209210
})
210211
assert response.status_code == 200
211212

212-
# try creating an invalid election name
213-
response = await client.post("/elections/list", json={
214-
"election_type": "by_election",
215-
"datetime_start_nominations": (datetime.now() - timedelta(days=1)).isoformat(),
216-
"datetime_start_voting": (datetime.now() + timedelta(days=7)).isoformat(),
217-
"datetime_end_voting": (datetime.now() + timedelta(days=14)).isoformat(),
218-
"survey_link": "https://youtu.be/dQw4w9WgXcQ?si=kZROi2tu-43MXPM5"
219-
})
220-
assert response.status_code == 400
221-
222-
223-
224-
225213
# try to register for a past election -> should say nomination period expired
226-
response = await client.post("/elections/registration/test election 1", json={
214+
testElection1 = "test election 1"
215+
response = await client.post(f"/elections/registration/{testElection1}", json={
216+
"computing_id": load_test_db.SYSADMIN_COMPUTING_ID,
227217
"position": "president",
228218
})
229219
assert response.status_code == 400
230220
assert "nomination period" in response.json()["detail"]
231221

232-
# try to register for an invalid position
222+
# try to register for an invalid position will just throw a 422
233223
response = await client.post(f"/elections/registration/{election_name}", json={
224+
"computing_id": load_test_db.SYSADMIN_COMPUTING_ID,
234225
"position": "CEO",
235226
})
236-
assert response.status_code == 400
237-
assert "invalid position" in response.json()["detail"]
227+
assert response.status_code == 422
238228

239229
# try to register in an unknown election
240230
response = await client.post("/elections/registration/unknownElection12345", json={
231+
"computing_id": load_test_db.SYSADMIN_COMPUTING_ID,
241232
"position": "president",
242233
})
243234
assert response.status_code == 404
@@ -247,6 +238,7 @@ async def test_endpoints_admin(client, database_setup):
247238

248239
# register for an election correctly
249240
response = await client.post(f"/elections/registration/{election_name}", json={
241+
"computing_id": "jdo12",
250242
"position": "president",
251243
})
252244
assert response.status_code == 200
@@ -256,6 +248,7 @@ async def test_endpoints_admin(client, database_setup):
256248

257249
# duplicate registration
258250
response = await client.post(f"/elections/registration/{election_name}", json={
251+
"computing_id": "jdo12",
259252
"position": "president",
260253
})
261254
assert response.status_code == 400

0 commit comments

Comments
 (0)