Skip to content

Commit 480c971

Browse files
authored
Add admin endpoints (#60)
* add endpoint & crud function for all officers * comment out unused endpoints * add website admin permission * add endpoint for admin permission
1 parent 554499b commit 480c971

File tree

8 files changed

+159
-27
lines changed

8 files changed

+159
-27
lines changed

src/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import auth.urls
44
import database
55
import officers.urls
6+
import permission.urls
67
from fastapi import FastAPI
78

89
import tests.urls
@@ -14,6 +15,8 @@
1415

1516
app.include_router(auth.urls.router)
1617
app.include_router(officers.urls.router)
18+
app.include_router(permission.urls.router)
19+
1720
app.include_router(tests.urls.router)
1821

1922
@app.get("/")

src/officers/crud.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@ async def most_recent_exec_term(db_session: database.DBSession, computing_id: st
3131
# TODO: can this be replaced with scalar to improve performance?
3232
return (await db_session.scalars(query)).first()
3333

34+
# TODO: test this function
35+
async def current_officer_position(db_session: database.DBSession, computing_id: str) -> str | None:
36+
"""
37+
Returns None if the user is not currently an officer
38+
"""
39+
query = sqlalchemy.select(OfficerTerm)
40+
query = query.where(OfficerTerm.computing_id == computing_id)
41+
# TODO: assert this constraint at the SQL level, so that we don't even have to check it.
42+
query = query.where(
43+
# TODO: turn this query into a utility function, so it can be reused
44+
OfficerTerm.is_filled_in
45+
and (
46+
# executives without a specified end_date are considered active
47+
OfficerTerm.end_date is None
48+
# check that today's timestamp is before (smaller than) the term's end date
49+
or (datetime.today() <= OfficerTerm.end_date)
50+
)
51+
)
52+
query = query.limit(1)
53+
54+
# TODO: can this be replaced with scalar to improve performance?
55+
return (await db_session.scalars(query)).first()
3456

3557
async def current_executive_team(db_session: database.DBSession, include_private: bool) -> dict[str, list[OfficerData]]:
3658
"""
@@ -88,7 +110,7 @@ async def current_executive_team(db_session: database.DBSession, include_private
88110

89111
officer_data[term.position] += [
90112
OfficerData(
91-
is_current_officer = True,
113+
is_active = True,
92114

93115
position = term.position,
94116
start_date = term.start_date,
@@ -136,6 +158,59 @@ async def current_executive_team(db_session: database.DBSession, include_private
136158
return officer_data
137159

138160

161+
async def all_officer_terms(db_session: database.DBSession, include_private: bool) -> list[OfficerData]:
162+
"""
163+
Orders officers recent first.
164+
165+
This could be a lot of data, so be careful.
166+
167+
TODO: optionally paginate data, so it's not so bad.
168+
"""
169+
query = sqlalchemy.select(OfficerTerm)
170+
query = query.order_by(OfficerTerm.start_date.desc())
171+
officer_terms = (await db_session.scalars(query)).all()
172+
173+
officer_data_list = []
174+
for term in officer_terms:
175+
officer_info_query = sqlalchemy.select(OfficerInfo)
176+
officer_info_query = officer_info_query.where(
177+
OfficerInfo.computing_id == term.computing_id
178+
)
179+
officer_info = (await db_session.scalars(officer_info_query)).first()
180+
181+
officer_data_list += [
182+
OfficerData(
183+
is_active = (term.end_date is None) or (datetime.today() <= term.end_date),
184+
185+
position = term.position,
186+
start_date = term.start_date,
187+
end_date = term.end_date,
188+
189+
legal_name = officer_info.legal_name,
190+
nickname = term.nickname,
191+
discord_name = officer_info.discord_name,
192+
discord_nickname = officer_info.discord_nickname,
193+
194+
favourite_course_0 = term.favourite_course_0,
195+
favourite_course_1 = term.favourite_course_1,
196+
favourite_language_0 = term.favourite_pl_0,
197+
favourite_language_1 = term.favourite_pl_1,
198+
199+
csss_email = OfficerPosition.from_string(term.position).to_email(),
200+
biography = term.biography,
201+
photo_url = term.photo_url,
202+
203+
private_data = OfficerPrivateData(
204+
computing_id = term.computing_id,
205+
phone_number = officer_info.phone_number,
206+
github_username = officer_info.github_username,
207+
google_drive_email = officer_info.google_drive_email,
208+
) if include_private else None,
209+
)
210+
]
211+
212+
return officer_data_list
213+
139214
# TODO: do we ever expect to need to remove officer info? Probably not? Just updating it.
140215
def update_officer_info(db_session: database.DBSession, officer_info_data: OfficerInfoData):
141216
"""

src/officers/schemas.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class OfficerPrivateData:
4848

4949
@dataclass
5050
class OfficerData:
51-
is_current_officer: bool
51+
is_active: bool
5252

5353
# an officer may have multiple positions, such as FroshWeekChair & DirectorOfEvents
5454
position: str
@@ -62,7 +62,6 @@ class OfficerData:
6262

6363
favourite_course_0: str | None
6464
favourite_course_1: str | None
65-
6665
favourite_language_0: str | None
6766
favourite_language_1: str | None
6867

@@ -128,7 +127,7 @@ class OfficerPositionData_Upload(BaseModel):
128127
"""
129128
# TODO: this is for another api call, where the doa should be able to change any aspect of the person's configuration, just in case
130129
class OfficerDataUpdate_Upload(BaseModel):
131-
is_current_officer: bool
130+
is_active: bool
132131
133132
# an officer may have multiple positions, such as FroshWeekChair & DirectorOfEvents
134133
position: str

src/officers/urls.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,37 @@ async def current_officers(
4343
return JSONResponse(json_current_executives)
4444

4545

46-
# TODO: test this error afterwards
47-
@router.get("/please_error", description="Raises an error & should send an email to the sysadmin")
48-
async def raise_error():
49-
raise ValueError("This is an error, you're welcome")
50-
51-
5246
@router.get(
53-
"/past",
54-
description="Information from past exec terms. If year is not included, all years will be returned. If semester is not included, all semesters that year will be returned. If semester is given, but year is not, return all years and all semesters.",
47+
"/all",
48+
description="Information from all exec terms. If year is not included, all years will be returned. If semester is not included, all semesters that year will be returned. If semester is given, but year is not, return all years and all semesters.",
5549
)
56-
async def past_officers():
57-
return {"officers": "none"}
50+
async def all_officers(
51+
request: Request,
52+
db_session: database.DBSession,
53+
):
54+
# determine if user has access to this private data
55+
session_id = request.cookies.get("session_id", None)
56+
if session_id is None:
57+
has_private_access = False
58+
else:
59+
computing_id = await auth.crud.get_computing_id(db_session, session_id)
60+
has_private_access = await OfficerPrivateInfo.has_permission(db_session, computing_id)
5861

62+
all_officers = await officers.crud.all_officers(db_session, include_private=has_private_access)
63+
"""
64+
json_current_executives = {
65+
position: [
66+
officer_data.serializable_dict() for officer_data in officer_data_list
67+
] for position, officer_data_list in all_officers.items()
68+
}
69+
"""
70+
return JSONResponse(all_officers)
71+
72+
"""
73+
# TODO: test this error later
74+
@router.get("/please_error", description="Raises an error & should send an email to the sysadmin")
75+
async def raise_error():
76+
raise ValueError("This is an error, you're welcome")
5977
6078
@router.post(
6179
"/enter_info",
@@ -68,15 +86,12 @@ async def enter_info():
6886
6987
return {}
7088
71-
72-
"""
7389
@router.get(
7490
"/my_info",
7591
description="Get info about whether you are still an executive or not / what your position is.",
7692
)
7793
async def my_info():
7894
return {}
79-
"""
8095
8196
8297
@router.post(
@@ -101,3 +116,4 @@ async def remove_officer():
101116
)
102117
async def update_officer():
103118
return {}
119+
"""

src/permission/permission.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/permission/types.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from datetime import UTC, datetime, timezone
2+
from typing import ClassVar
23

34
import database
45
import officers.crud
56
from data.semesters import current_semester_start, step_semesters
7+
from officers.constants import OfficerPosition
68

79

810
class OfficerPrivateInfo:
@@ -23,3 +25,23 @@ async def has_permission(db_session: database.DBSession, computing_id: str) -> b
2325
cutoff_date = step_semesters(semester_start, -NUM_SEMESTERS)
2426

2527
return most_recent_exec_term > cutoff_date
28+
29+
class WebsiteAdmin:
30+
WEBSITE_ADMIN_POSITIONS: ClassVar[list[OfficerPosition]] = [
31+
OfficerPosition.President,
32+
OfficerPosition.VicePresident,
33+
OfficerPosition.DirectorOfArchives,
34+
OfficerPosition.SystemAdministrator,
35+
OfficerPosition.Webmaster,
36+
]
37+
38+
@staticmethod
39+
async def has_permission(db_session: database.DBSession, computing_id: str) -> bool:
40+
"""
41+
A website admin has to be one of the following positions, and
42+
"""
43+
position = await officers.crud.current_officer_position(db_session, computing_id)
44+
if position is None:
45+
return False
46+
47+
return position in WebsiteAdmin.WEBSITE_ADMIN_POSITIONS

src/permission/urls.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
1-
from fastapi import APIRouter
1+
import auth.crud
2+
import database
3+
from fastapi import APIRouter, Request
4+
from fastapi.responses import JSONResponse
5+
6+
from permission.types import WebsiteAdmin
27

38
router = APIRouter(
49
prefix="/permission",
510
tags=["permission"],
611
)
712

813
# TODO: add an endpoint for viewing permissions that exist & what levels they can have & what levels each person has
14+
@router.get(
15+
"/is_admin",
16+
description="checks if the current user has the admin permission"
17+
)
18+
async def is_admin(
19+
request: Request,
20+
db_session: database.DBSession,
21+
):
22+
session_id = request.cookies.get("session_id", None)
23+
if session_id is None:
24+
return JSONResponse({"is_admin": False})
25+
26+
# what if user doesn't have a computing id?
27+
computing_id = await auth.crud.get_computing_id(db_session, session_id)
28+
is_admin_permission = await WebsiteAdmin.has_permission(db_session, computing_id)
29+
return JSONResponse({"is_admin": is_admin_permission})

tests/integration/test_officers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pytest
55
from database import SQLALCHEMY_TEST_DATABASE_URL, DatabaseSessionManager
66
from officers.constants import OfficerPosition
7-
from officers.crud import current_executive_team, most_recent_exec_term
7+
from officers.crud import all_officer_terms, current_executive_team, most_recent_exec_term
88

99
# TODO: setup a database on the CI machine & run this as a unit test then (since
1010
# this isn't really an integration test)
@@ -55,6 +55,10 @@ async def test__read_execs(database_setup):
5555
assert next(iter(current_exec_team.values()))[0].private_data is not None
5656
assert next(iter(current_exec_team.values()))[0].private_data.computing_id == "abc33"
5757

58+
all_terms = await all_officer_terms(db_session, include_private=True)
59+
assert len(all_terms) == 3
60+
61+
5862
#async def test__update_execs(database_setup):
5963
# # TODO: the second time an update_officer_info call occurs, the user should be updated with info
6064
# pass

0 commit comments

Comments
 (0)