Skip to content

Commit c372527

Browse files
committed
test loading data locally & add a script for migrating
1 parent 0c7f8b7 commit c372527

File tree

10 files changed

+256
-23
lines changed

10 files changed

+256
-23
lines changed

src/__init__.py

Whitespace-only changes.

src/alembic/versions/166f3772fce7_auth_officer_init.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,17 @@ def upgrade() -> None:
4545
sa.Column("start_date", sa.Date(), nullable=False),
4646
sa.Column("end_date", sa.Date(), nullable=True),
4747
sa.Column("nickname", sa.String(length=128), nullable=True),
48-
sa.Column("favourite_course_0", sa.String(length=32), nullable=True),
49-
sa.Column("favourite_course_1", sa.String(length=32), nullable=True),
50-
sa.Column("favourite_pl_0", sa.String(length=32), nullable=True),
51-
sa.Column("favourite_pl_1", sa.String(length=32), nullable=True),
48+
sa.Column("favourite_course_0", sa.String(length=64), nullable=True),
49+
sa.Column("favourite_course_1", sa.String(length=64), nullable=True),
50+
sa.Column("favourite_pl_0", sa.String(length=64), nullable=True),
51+
sa.Column("favourite_pl_1", sa.String(length=64), nullable=True),
5252
sa.Column("biography", sa.Text(), nullable=True),
5353
sa.Column("photo_url", sa.Text(), nullable=True),
5454
)
5555
op.create_table(
5656
"officer_info",
5757
sa.Column("legal_name", sa.String(length=128), nullable=False),
58-
sa.Column("discord_id", sa.String(length=18), nullable=True),
58+
sa.Column("discord_id", sa.String(length=32), nullable=True),
5959
sa.Column("discord_name", sa.String(length=32), nullable=True),
6060
sa.Column("discord_nickname", sa.String(length=32), nullable=True),
6161
sa.Column("computing_id", sa.String(length=32), sa.ForeignKey("site_user.computing_id"), primary_key=True),

src/auth/__init__.py

Whitespace-only changes.

src/constants.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
COMPUTING_ID_MAX = 8
1616

1717
# see https://support.discord.com/hc/en-us/articles/4407571667351-How-to-Find-User-IDs-for-Law-Enforcement#:~:text=Each%20Discord%20user%20is%20assigned,user%20and%20cannot%20be%20changed.
18-
DISCORD_ID_LEN = 18
18+
# NOTE: the length got updated to 19 in july 2024. See https://www.reddit.com/r/discordapp/comments/ucrp1r/only_3_months_until_discord_ids_hit_19_digits/
19+
# I set us to 32 just in case...
20+
DISCORD_ID_LEN = 32
1921

2022
# https://github.com/discord/discord-api-docs/blob/main/docs/resources/User.md
2123
DISCORD_NAME_LEN = 32

src/officers/constants.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ def position_list() -> list[str]:
3030
def length_in_semesters(position: str) -> int | None:
3131
# TODO (#101): ask the committee to maintain a json file with all the important details from the constitution
3232
"""How many semester position is active for, according to the CSSS Constitution"""
33-
return _LENGTH_MAP[position]
33+
if position not in _LENGTH_MAP:
34+
# this can occur for legacy positions
35+
return None
36+
else:
37+
return _LENGTH_MAP[position]
3438

3539
@staticmethod
3640
def to_email(position: str) -> str | None:

src/officers/tables.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ class OfficerTerm(Base):
4040
end_date = Column(Date, nullable=True)
4141

4242
nickname = Column(String(128), nullable=True)
43-
favourite_course_0 = Column(String(32), nullable=True)
44-
favourite_course_1 = Column(String(32), nullable=True)
43+
favourite_course_0 = Column(String(64), nullable=True)
44+
favourite_course_1 = Column(String(64), nullable=True)
4545
# programming language
46-
favourite_pl_0 = Column(String(32), nullable=True)
47-
favourite_pl_1 = Column(String(32), nullable=True)
46+
favourite_pl_0 = Column(String(64), nullable=True)
47+
favourite_pl_1 = Column(String(64), nullable=True)
4848
biography = Column(Text, nullable=True)
4949
photo_url = Column(Text, nullable=True) # some urls get big, best to let it be a string
5050

src/officers/urls.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from fastapi import APIRouter, Body, HTTPException, Request
44
from fastapi.responses import JSONResponse, PlainTextResponse
5-
from sqlalchemy.exc import IntegrityError
65

76
import auth.crud
87
import database

src/scripts/__init__.py

Whitespace-only changes.

src/scripts/load_officer_data_from_old_db.py

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import asyncio
2+
import os
3+
import sys
4+
from datetime import date, datetime, timedelta
5+
from pathlib import Path
6+
7+
import asyncpg
8+
import sqlalchemy
9+
10+
sys.path.append(str(Path(__file__).parent.parent.resolve()))
11+
12+
from auth.crud import site_user_exists
13+
from auth.tables import SiteUser
14+
from data import semesters
15+
from database import SQLALCHEMY_TEST_DATABASE_URL, DatabaseSessionManager
16+
from officers.constants import OfficerPosition
17+
from officers.types import OfficerInfo, OfficerTerm
18+
19+
# This loads officer data from the https://github.com/CSSS/csss-site database into the provided database
20+
21+
DB_PASSWORD = os.environ.get("DB_PASSWORD")
22+
# NOTE: pass either SQLALCHEMY_DATABASE_URL or SQLALCHEMY_TEST_DATABASE_URL
23+
DB_TARGET = os.environ.get("DB_TARGET")
24+
25+
async def main():
26+
conn = await asyncpg.connect(
27+
user="postgres",
28+
password=DB_PASSWORD,
29+
database="postgres",
30+
host="sfucsss.org", # NOTE: this should point to the old sfucsss.org server (made initially by jace)
31+
port=5432,
32+
)
33+
34+
# officer data in order of oldest term first
35+
officer_data = await conn.fetch(
36+
"""
37+
SELECT *
38+
FROM about_officer
39+
ORDER BY elected_term_id ASC
40+
"""
41+
)
42+
print(len(officer_data))
43+
44+
officer_956 = next(officer for officer in officer_data if officer["id"] == 956)
45+
46+
def get_key(officer):
47+
if officer["id"] == 966:
48+
# something weird happened w/ the start date which caused it to not be the same as the other 2.
49+
# here, we update it to be the same as it used to
50+
return (officer_956["start_date"], officer["sfu_computing_id"], officer["position_name"])
51+
else:
52+
return (officer["start_date"], officer["sfu_computing_id"], officer["position_name"])
53+
54+
# group by (officer.start_date, officer.sfu_computing_id, officer.position_name)
55+
unique_terms = {}
56+
for officer in officer_data:
57+
key = get_key(officer)
58+
if key not in unique_terms:
59+
unique_terms[key] = (officer["id"], 1)
60+
else:
61+
# if there is a term with the same start date, position, and computing_id, take only the last instance.
62+
unique_terms[key] = (officer["id"], unique_terms[key][1]+1)
63+
64+
# computing num_semesters
65+
num_semesters_map = {}
66+
consolidated_officer_data = [
67+
officer for officer in officer_data
68+
# include only latest info in a term
69+
if unique_terms[get_key(officer)][0] == officer["id"]
70+
]
71+
for officer in consolidated_officer_data:
72+
# add num_semesters
73+
key = get_key(officer)
74+
num_semesters_map[key] = unique_terms[key][1]
75+
76+
# Jace's many terms as sysadmin is a bit unusual, so we want to concatenate it into a single long officer term
77+
last_term_jace = None
78+
for officer in consolidated_officer_data:
79+
if officer["full_name"] == "Jace Manshadi":
80+
last_term_jace = officer
81+
num_semesters_jace = sum([
82+
num_semesters_map[get_key(officer)]
83+
for officer in consolidated_officer_data
84+
if officer["full_name"] == "Jace Manshadi"]
85+
)
86+
num_semesters_map[(
87+
last_term_jace["start_date"],
88+
last_term_jace["sfu_computing_id"],
89+
last_term_jace["position_name"]
90+
)] = num_semesters_jace
91+
92+
consolidated_officer_data = [
93+
officer for officer in consolidated_officer_data if officer["full_name"] != "Jace Manshadi"
94+
] + [last_term_jace]
95+
await conn.close()
96+
97+
#print("\n\n".join([str(x) for x in consolidated_officer_data[100:]]))
98+
99+
sessionmanager = DatabaseSessionManager(DB_TARGET, { "echo": False }, check_db=False)
100+
await DatabaseSessionManager.test_connection(DB_TARGET)
101+
async with sessionmanager.session() as db_session:
102+
# NOTE: keep an eye out for bugs with legacy officer position names, as any not in OfficerPosition should be considered inactive
103+
position_name_map = {
104+
"SFSS Council Representative": OfficerPosition.SFSS_COUNCIL_REPRESENTATIVE,
105+
"Frosh Chair": OfficerPosition.FROSH_WEEK_CHAIR,
106+
"General Election Officer": OfficerPosition.ELECTIONS_OFFICER,
107+
"First Year Representative": OfficerPosition.FIRST_YEAR_REPRESENTATIVE,
108+
"Director of Communications": OfficerPosition.DIRECTOR_OF_COMMUNICATIONS,
109+
"By-Elections Officer": OfficerPosition.ELECTIONS_OFFICER,
110+
"Director of Education Events": OfficerPosition.DIRECTOR_OF_EDUCATIONAL_EVENTS,
111+
"Director of Multi-media": OfficerPosition.DIRECTOR_OF_MULTIMEDIA,
112+
"Systems Administrator": OfficerPosition.SYSTEM_ADMINISTRATOR,
113+
"Elections Officer": OfficerPosition.ELECTIONS_OFFICER,
114+
"Executive at Large 1": OfficerPosition.EXECUTIVE_AT_LARGE,
115+
"Director of Resources": OfficerPosition.DIRECTOR_OF_RESOURCES,
116+
"Frosh Week Chair": OfficerPosition.FROSH_WEEK_CHAIR,
117+
"First Year Representative 1": OfficerPosition.FIRST_YEAR_REPRESENTATIVE,
118+
"First Year Representative 2": OfficerPosition.FIRST_YEAR_REPRESENTATIVE,
119+
"Executive at Large 2": OfficerPosition.EXECUTIVE_AT_LARGE,
120+
"Executive at Large": OfficerPosition.EXECUTIVE_AT_LARGE,
121+
"Director of Archives": OfficerPosition.DIRECTOR_OF_ARCHIVES,
122+
"By-Election Officer": OfficerPosition.ELECTIONS_OFFICER,
123+
"co-Frosh Chair 2": OfficerPosition.FROSH_WEEK_CHAIR,
124+
"Treasurer": OfficerPosition.TREASURER,
125+
"Assistant Director of Events": OfficerPosition.ASSISTANT_DIRECTOR_OF_EVENTS,
126+
"Director of Events": OfficerPosition.DIRECTOR_OF_EVENTS,
127+
"President": OfficerPosition.PRESIDENT,
128+
"Executive at Large [Surrey]": OfficerPosition.EXECUTIVE_AT_LARGE,
129+
"Webmaster": OfficerPosition.WEBMASTER,
130+
"co-Frosh Chair 1": OfficerPosition.FROSH_WEEK_CHAIR,
131+
"Vice-President": OfficerPosition.VICE_PRESIDENT,
132+
}
133+
134+
for officer in consolidated_officer_data:
135+
print(f"* doing {officer} ...")
136+
137+
if officer["id"] == 855:
138+
# some weird position truman had that didn't exist, but is stored in the db?
139+
continue
140+
elif officer["id"] == 883:
141+
# ditto with jasper
142+
continue
143+
144+
if not await site_user_exists(db_session, officer["sfu_computing_id"]):
145+
# if computing_id has not been created as a site_user yet, add them
146+
db_session.add(SiteUser(
147+
computing_id=officer["sfu_computing_id"],
148+
first_logged_in=datetime.now(),
149+
last_logged_in=datetime.now()
150+
))
151+
152+
# use the most up to date officer info
153+
# --------------------------------
154+
155+
new_officer_info = OfficerInfo(
156+
computing_id = officer["sfu_computing_id"],
157+
legal_name = officer["full_name"],
158+
phone_number = str(officer["phone_number"]),
159+
160+
discord_id = officer["discord_id"],
161+
discord_name = officer["discord_username"],
162+
discord_nickname = officer["discord_nickname"],
163+
164+
google_drive_email = officer["gmail"],
165+
github_username = officer["github_username"],
166+
)
167+
168+
existing_officer_info = await db_session.scalar(
169+
sqlalchemy
170+
.select(OfficerInfo)
171+
.where(OfficerInfo.computing_id == new_officer_info.computing_id)
172+
)
173+
if existing_officer_info is None:
174+
db_session.add(new_officer_info)
175+
else:
176+
await db_session.execute(
177+
sqlalchemy
178+
.update(OfficerInfo)
179+
.where(OfficerInfo.computing_id == new_officer_info.computing_id)
180+
.values(new_officer_info.to_update_dict())
181+
)
182+
183+
# now, create an officer term
184+
# --------------------------------
185+
186+
corrected_position_name = (
187+
position_name_map[officer["position_name"]]
188+
if officer["position_name"] in position_name_map
189+
else officer["position_name"]
190+
)
191+
position_length = OfficerPosition.length_in_semesters(corrected_position_name)
192+
num_semesters = num_semesters_map[get_key(officer)]
193+
194+
if position_length is not None:
195+
if (
196+
(officer["start_date"].date() < (date.today() - timedelta(days=365)))
197+
and (officer["start_date"].date() > (date.today() - timedelta(days=365*5)))
198+
and officer["id"] != 867 # sometimes people only run partial terms (elected_term_id=20222)
199+
and officer["id"] != 942 # sometimes people only run partial terms
200+
):
201+
# over the past few years, the semester length should be as expected
202+
if not (position_length == num_semesters):
203+
print(num_semesters)
204+
print(officer["start_date"].date())
205+
print(date.today() - timedelta(days=365))
206+
assert position_length == num_semesters
207+
computed_end_date = semesters.step_semesters(
208+
semesters.current_semester_start(officer["start_date"]),
209+
position_length,
210+
)
211+
else:
212+
computed_end_date = semesters.step_semesters(
213+
semesters.current_semester_start(officer["start_date"]),
214+
num_semesters,
215+
)
216+
217+
new_officer_term = OfficerTerm(
218+
computing_id = officer["sfu_computing_id"],
219+
position = corrected_position_name,
220+
221+
start_date = officer["start_date"],
222+
end_date = computed_end_date,
223+
224+
nickname = None,
225+
favourite_course_0 = officer["course1"],
226+
favourite_course_1 = officer["course2"],
227+
favourite_pl_0 = officer["language1"],
228+
favourite_pl_1 = officer["language2"],
229+
biography = officer["bio"],
230+
photo_url = officer["image"],
231+
)
232+
db_session.add(new_officer_term)
233+
234+
# data lgtm!
235+
await db_session.commit()
236+
237+
print("successfully loaded data!")
238+
239+
asyncio.run(main())

0 commit comments

Comments
 (0)