Skip to content

Commit f596d82

Browse files
committed
wip: add endpoint information for officer terms and officer info POST
1 parent b59fffd commit f596d82

File tree

7 files changed

+127
-56
lines changed

7 files changed

+127
-56
lines changed

src/nominees/urls.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
NomineeInfoUpdateParams,
99
)
1010
from nominees.tables import NomineeInfo
11-
from utils.urls import admin_or_raise
11+
from utils.urls import AdminTypeEnum, admin_or_raise
1212

1313
router = APIRouter(
1414
prefix="/nominee",
@@ -30,7 +30,7 @@ async def get_nominee_info(
3030
computing_id: str
3131
):
3232
# Putting this one behind the admin wall since it has contact information
33-
await admin_or_raise(request, db_session)
33+
await admin_or_raise(request, db_session, AdminTypeEnum.Election)
3434
nominee_info = await nominees.crud.get_nominee_info(db_session, computing_id)
3535
if nominee_info is None:
3636
raise HTTPException(
@@ -56,7 +56,7 @@ async def provide_nominee_info(
5656
computing_id: str
5757
):
5858
# TODO: There needs to be a lot more validation here.
59-
await admin_or_raise(request, db_session)
59+
await admin_or_raise(request, db_session, AdminTypeEnum.Election)
6060

6161
updated_data = {}
6262
# Only update fields that were provided

src/officers/crud.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ async def get_officer_info_or_raise(db_session: database.DBSession, computing_id
9898
raise HTTPException(status_code=404, detail=f"officer_info for computing_id={computing_id} does not exist yet")
9999
return officer_term
100100

101+
async def get_new_officer_info_or_raise(db_session: database.DBSession, computing_id: str) -> OfficerInfo:
102+
"""
103+
This check is for after a create/update
104+
"""
105+
officer_term = await db_session.scalar(
106+
sqlalchemy
107+
.select(OfficerInfo)
108+
.where(OfficerInfo.computing_id == computing_id)
109+
)
110+
if officer_term is None:
111+
raise HTTPException(status_code=500, detail=f"failed to fetch {computing_id} after update")
112+
return officer_term
113+
101114
async def get_officer_terms(
102115
db_session: database.DBSession,
103116
computing_id: str,

src/officers/models.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
from officers.constants import OFFICER_LEGAL_NAME_MAX, OfficerPositionEnum
66

77

8-
class OfficerBaseModel(BaseModel):
8+
class OfficerInfoBaseModel(BaseModel):
99
# TODO (#71): compute this using SFU's API & remove from being uploaded
1010
legal_name: str = Field(..., max_length=OFFICER_LEGAL_NAME_MAX)
1111
position: OfficerPositionEnum
1212
start_date: date
1313
end_date: date | None = None
1414

15-
class PublicOfficerResponse(OfficerBaseModel):
15+
class PublicOfficerInfoResponse(OfficerInfoBaseModel):
1616
"""
1717
Response when fetching public officer data
1818
"""
@@ -24,7 +24,7 @@ class PublicOfficerResponse(OfficerBaseModel):
2424
biography: str | None = None
2525
csss_email: str
2626

27-
class PrivateOfficerResponse(PublicOfficerResponse):
27+
class PrivateOfficerInfoResponse(PublicOfficerInfoResponse):
2828
"""
2929
Response when fetching private officer data
3030
"""
@@ -33,11 +33,40 @@ class PrivateOfficerResponse(PublicOfficerResponse):
3333
github_username: str | None = None
3434
google_drive_email: str | None = None
3535

36+
class OfficerSelfUpdate(BaseModel):
37+
"""
38+
Used when an Officer is updating their own information
39+
"""
40+
nickname: str | None = None
41+
discord_id: str | None = None
42+
discord_name: str | None = None
43+
discord_nickname: str | None = None
44+
biography: str | None = None
45+
phone_number: str | None = None
46+
github_username: str | None = None
47+
google_drive_email: str | None = None
48+
49+
class OfficerUpdate(OfficerSelfUpdate):
50+
"""
51+
Used when an admin is updating an Officer's info
52+
"""
53+
legal_name: str | None = Field(None, max_length=OFFICER_LEGAL_NAME_MAX)
54+
position: OfficerPositionEnum | None = None
55+
start_date: date | None = None
56+
end_date: date | None = None
57+
3658
class OfficerTermBaseModel(BaseModel):
3759
computing_id: str
3860
position: OfficerPositionEnum
3961
start_date: date
4062

63+
class OfficerTermCreate(OfficerTermBaseModel):
64+
"""
65+
Params to create a new Officer Term
66+
"""
67+
legal_name: str
68+
69+
4170
class OfficerTermResponse(OfficerTermBaseModel):
4271
id: int
4372
end_date: date | None = None

src/officers/tables.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
)
2222
from database import Base
2323
from officers.constants import OFFICER_LEGAL_NAME_MAX, OFFICER_POSITION_MAX, OfficerPositionEnum
24+
from officers.models import OfficerSelfUpdate, OfficerUpdate
2425

2526

2627
# A row represents an assignment of a person to a position.
@@ -149,6 +150,11 @@ def serializable_dict(self) -> dict:
149150
"google_drive_email": self.google_drive_email,
150151
}
151152

153+
def update_from_params(self, params: OfficerUpdate | OfficerSelfUpdate):
154+
update_data = params.model_dump(exclude_unset=True)
155+
for k, v in update_data.items():
156+
setattr(update_data, k, v)
157+
152158
def is_filled_in(self):
153159
return (
154160
self.computing_id is not None

src/officers/urls.py

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@
55
import database
66
import officers.crud
77
import utils
8-
from officers.models import OfficerTermResponse, PrivateOfficerResponse, PublicOfficerResponse
8+
from officers.models import (
9+
OfficerSelfUpdate,
10+
OfficerTermCreate,
11+
OfficerTermResponse,
12+
OfficerUpdate,
13+
PrivateOfficerInfoResponse,
14+
PublicOfficerInfoResponse,
15+
)
916
from officers.tables import OfficerInfo, OfficerTerm
1017
from officers.types import InitialOfficerInfo, OfficerInfoUpload, OfficerTermUpload
1118
from permission.types import OfficerPrivateInfo, WebsiteAdmin
12-
from utils.shared_models import DetailModel
13-
from utils.urls import logged_in_or_raise
19+
from utils.shared_models import DetailModel, SuccessResponse
20+
from utils.urls import admin_or_raise, is_website_admin, logged_in_or_raise
1421

1522
router = APIRouter(
1623
prefix="/officers",
@@ -42,7 +49,7 @@ async def _has_officer_private_info_access(
4249
@router.get(
4350
"/current",
4451
description="Get information about the current officers. With no authorization, only get basic info.",
45-
response_model=list[PrivateOfficerResponse] | list[PublicOfficerResponse],
52+
response_model=list[PrivateOfficerInfoResponse] | list[PublicOfficerInfoResponse],
4653
operation_id="get_current_officers"
4754
)
4855
async def current_officers(
@@ -62,9 +69,9 @@ async def current_officers(
6269
@router.get(
6370
"/all",
6471
description="Information for all execs from all exec terms",
65-
response_model=list[PrivateOfficerResponse] | list[PublicOfficerResponse],
72+
response_model=list[PrivateOfficerInfoResponse] | list[PublicOfficerInfoResponse],
6673
responses={
67-
401: { "description": "not authorized to view private info", "model": DetailModel }
74+
403: { "description": "not authorized to view private info", "model": DetailModel }
6875
},
6976
operation_id="get_all_officers"
7077
)
@@ -97,7 +104,7 @@ async def all_officers(
97104
responses={
98105
401: { "description": "not authorized to view private info", "model": DetailModel }
99106
},
100-
operation_id="get_all_officers"
107+
operation_id="get_officer_terms_by_id"
101108
)
102109
async def get_officer_terms(
103110
request: Request,
@@ -121,8 +128,13 @@ async def get_officer_terms(
121128
])
122129

123130
@router.get(
124-
"/info/{computing_id}",
131+
"/info/{computing_id:str}",
125132
description="Get officer info for the current user, if they've ever been an exec. Only admins can get info about another user.",
133+
response_model=PrivateOfficerInfoResponse,
134+
responses={
135+
403: { "description": "not authorized to view author user info", "model": DetailModel }
136+
},
137+
operation_id="get_officer_info_by_id"
126138
)
127139
async def get_officer_info(
128140
request: Request,
@@ -145,28 +157,27 @@ async def get_officer_info(
145157
Only the sysadmin, president, or DoA can submit this request. It will usually be the DoA.
146158
Updates the system with a new officer, and enables the user to login to the system to input their information.
147159
""",
160+
response_model=SuccessResponse,
161+
responses={
162+
403: { "description": "must be a website admin", "model": DetailModel },
163+
500: { "model": DetailModel },
164+
},
165+
operation_id="create_officer_term"
148166
)
149167
async def new_officer_term(
150168
request: Request,
151169
db_session: database.DBSession,
152-
officer_info_list: list[InitialOfficerInfo] = Body(), # noqa: B008
170+
officer_info_list: list[OfficerTermCreate],
153171
):
154-
"""
155-
If the current computing_id is not already an officer, officer_info will be created for them.
156-
"""
157-
for officer_info in officer_info_list:
158-
officer_info.valid_or_raise()
159-
160-
_, session_computing_id = await logged_in_or_raise(request, db_session)
161-
await WebsiteAdmin.has_permission_or_raise(db_session, session_computing_id)
172+
await admin_or_raise(request, db_session)
162173

163174
for officer_info in officer_info_list:
164175
# if user with officer_info.computing_id has never logged into the website before,
165176
# a site_user tuple will be created for them.
166177
await officers.crud.create_new_officer_info(db_session, OfficerInfo(
167178
computing_id = officer_info.computing_id,
168179
# TODO (#71): use sfu api to get legal name from officer_info.computing_id
169-
legal_name = "default name",
180+
legal_name = officer_info.legal_name,
170181
phone_number = None,
171182

172183
discord_id = None,
@@ -183,47 +194,43 @@ async def new_officer_term(
183194
))
184195

185196
await db_session.commit()
186-
return PlainTextResponse("ok")
197+
return JSONResponse({ "success": True })
187198

188199
@router.patch(
189-
"/info/{computing_id}",
200+
"/info/{computing_id:str}",
190201
description="""
191202
After election, officer computing ids are input into our system.
192203
If you have been elected as a new officer, you may authenticate with SFU CAS,
193204
then input your information & the valid token for us. Admins may update this info.
194-
"""
205+
""",
206+
response_model=OfficerPrivateInfo,
207+
responses={
208+
403: { "description": "must be a website admin", "model": DetailModel },
209+
500: { "description": "failed to fetch after update", "model": DetailModel },
210+
},
211+
operation_id="create_officer_term"
195212
)
196213
async def update_info(
197214
request: Request,
198215
db_session: database.DBSession,
199216
computing_id: str,
200-
officer_info_upload: OfficerInfoUpload = Body() # noqa: B008
217+
officer_info_upload: OfficerUpdate | OfficerSelfUpdate
201218
):
202-
officer_info_upload.valid_or_raise()
203-
_, session_computing_id = await logged_in_or_raise(request, db_session)
219+
is_site_admin, _, session_computing_id = await is_website_admin(request, db_session)
204220

205-
if computing_id != session_computing_id:
206-
await WebsiteAdmin.has_permission_or_raise(
207-
db_session, session_computing_id,
208-
errmsg="must have website admin permissions to update another user"
209-
)
221+
if computing_id != session_computing_id and not is_site_admin:
222+
raise HTTPException(status_code=403, detail="you may not update other officers")
210223

211224
old_officer_info = await officers.crud.get_officer_info_or_raise(db_session, computing_id)
212-
validation_failures, corrected_officer_info = await officer_info_upload.validate(computing_id, old_officer_info)
225+
old_officer_info.update_from_params(officer_info_upload)
226+
await officers.crud.update_officer_info(db_session, old_officer_info)
213227

214228
# TODO (#27): log all important changes just to a .log file & persist them for a few years
215229

216-
success = await officers.crud.update_officer_info(db_session, corrected_officer_info)
217-
if not success:
218-
raise HTTPException(status_code=400, detail="officer_info does not exist yet, please create the officer info entry first")
219-
220230
await db_session.commit()
221231

222-
updated_officer_info = await officers.crud.get_officer_info_or_raise(db_session, computing_id)
223-
return JSONResponse({
224-
"officer_info": updated_officer_info.serializable_dict(),
225-
"validation_failures": validation_failures,
226-
})
232+
updated_officer_info = await officers.crud.get_new_officer_info_or_raise(db_session, computing_id)
233+
return JSONResponse(updated_officer_info)
227234

228235
@router.patch(
229236
"/term/{term_id}",

src/registrations/urls.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
)
1919
from registrations.tables import NomineeApplication
2020
from utils.shared_models import DetailModel, SuccessResponse
21-
from utils.urls import admin_or_raise, logged_in_or_raise, slugify
21+
from utils.urls import AdminTypeEnum, admin_or_raise, logged_in_or_raise, slugify
2222

2323
router = APIRouter(
2424
prefix="/registration",
@@ -74,7 +74,7 @@ async def register_in_election(
7474
body: NomineeApplicationParams,
7575
election_name: str
7676
):
77-
await admin_or_raise(request, db_session)
77+
await admin_or_raise(request, db_session, AdminTypeEnum.Election)
7878

7979
if body.position not in OfficerPositionEnum:
8080
raise HTTPException(
@@ -157,7 +157,7 @@ async def update_registration(
157157
computing_id: str,
158158
position: OfficerPositionEnum
159159
):
160-
await admin_or_raise(request, db_session)
160+
await admin_or_raise(request, db_session, AdminTypeEnum.Election)
161161

162162
if body.position not in OfficerPositionEnum:
163163
raise HTTPException(
@@ -221,7 +221,7 @@ async def delete_registration(
221221
position: OfficerPositionEnum,
222222
computing_id: str
223223
):
224-
await admin_or_raise(request, db_session)
224+
await admin_or_raise(request, db_session, AdminTypeEnum.Election)
225225

226226
if position not in OfficerPositionEnum:
227227
raise HTTPException(

src/utils/urls.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from enum import Enum
23

34
from fastapi import HTTPException, Request, status
45

@@ -8,6 +9,11 @@
89
from permission.types import ElectionOfficer, WebsiteAdmin
910

1011

12+
class AdminTypeEnum(Enum):
13+
Full = 1
14+
Election = 2
15+
16+
1117
# TODO: move other utils into this module
1218
def slugify(text: str) -> str:
1319
"""Creates a unique slug based on text passed in. Assumes non-unicode text."""
@@ -49,7 +55,8 @@ async def get_current_user(request: Request, db_session: database.DBSession) ->
4955

5056
return session_id, session_computing_id
5157

52-
async def admin_or_raise(request: Request, db_session: database.DBSession) -> tuple[str, str]:
58+
# TODO: Add an election admin version that checks the election attempting to be modified as well
59+
async def admin_or_raise(request: Request, db_session: database.DBSession, admintype: AdminTypeEnum = AdminTypeEnum.Full) -> tuple[str, str]:
5360
session_id, computing_id = await get_current_user(request, db_session)
5461
if not session_id or not computing_id:
5562
raise HTTPException(
@@ -58,11 +65,20 @@ async def admin_or_raise(request: Request, db_session: database.DBSession) -> tu
5865
)
5966

6067
# where valid means election officer or website admin
61-
if (await ElectionOfficer.has_permission(db_session, computing_id)) or (await WebsiteAdmin.has_permission(db_session, computing_id)):
68+
if (await WebsiteAdmin.has_permission(db_session, computing_id)) or (admintype is AdminTypeEnum.Election and await ElectionOfficer.has_permission(db_session, computing_id)):
6269
return session_id, computing_id
63-
else:
64-
raise HTTPException(
65-
status_code=status.HTTP_403_FORBIDDEN,
66-
detail="must be an admin"
67-
)
6870

71+
raise HTTPException(
72+
status_code=status.HTTP_403_FORBIDDEN,
73+
detail="must be an admin"
74+
)
75+
76+
async def is_website_admin(request: Request, db_session: database.DBSession) -> tuple[bool, str | None, str | None]:
77+
session_id, computing_id = await get_current_user(request, db_session)
78+
if session_id is None or computing_id is None:
79+
return False, session_id, computing_id
80+
81+
if (await WebsiteAdmin.has_permission(db_session, computing_id)):
82+
return True, session_id, computing_id
83+
84+
return False, session_id, computing_id

0 commit comments

Comments
 (0)