Skip to content

Commit 2e084f1

Browse files
authored
Add foreign keys & update alembic revisions (#78)
* add foreign key & implied metadata * combine alembic revisions
1 parent a377d83 commit 2e084f1

8 files changed

+54
-190
lines changed

src/alembic/versions/04a41462e0a8_modify_site_user_table.py

-31
This file was deleted.

src/alembic/versions/066f3772fce6_create_user_session_table.py

-33
This file was deleted.

src/alembic/versions/0db2c57ce969_add_persistent_user_table.py

-31
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,44 @@
1-
"""introduce officer tables
1+
"""create user session table
22
3-
Revision ID: 75857bf0c826
4-
Revises: 0db2c57ce969
5-
Create Date: 2024-06-03 06:36:40.642476
3+
Revision ID: 166f3772fce7
4+
Revises:
5+
Create Date: 2024-02-23 00:58:50.320796
66
77
"""
88

99
from collections.abc import Sequence
10+
from datetime import datetime
1011
from typing import Union
1112

1213
import sqlalchemy as sa
1314
from alembic import op
1415

1516
# revision identifiers, used by Alembic.
16-
revision: str = "75857bf0c826"
17-
down_revision: str | None = "0db2c57ce969"
17+
revision: str = "166f3772fce7"
18+
down_revision: str | None = None
1819
branch_labels: str | Sequence[str] | None = None
1920
depends_on: str | Sequence[str] | None = None
2021

2122

2223
def upgrade() -> None:
23-
# drop all existing user session data
24-
# TODO: combine all past migrations into this one
25-
op.drop_table("user_session")
26-
op.create_table(
27-
"user_session",
28-
sa.Column("session_id", sa.String(512), nullable=False, primary_key=True),
29-
sa.Column("issue_time", sa.DateTime, nullable=False),
30-
sa.Column("computing_id", sa.String(32), nullable=False),
31-
)
32-
op.create_unique_constraint("unique__user_session__session_id", "user_session", ["session_id"])
33-
34-
# drop all existing site user data
35-
# TODO: combine all past migrations into this one
36-
op.drop_table("site_user")
3724
op.create_table(
3825
"site_user",
39-
sa.Column("computing_id", sa.String(32), nullable=False, primary_key=True),
26+
sa.Column("computing_id", sa.String(32), primary_key=True),
27+
sa.Column("first_logged_in", sa.DateTime, nullable=False, default=datetime(2024, 6, 16)),
28+
sa.Column("last_logged_in", sa.DateTime, nullable=False, default=datetime(2024, 6, 16)),
4029
)
41-
4230
op.create_table(
43-
"officer_info",
44-
sa.Column("is_filled_in", sa.Boolean(), nullable=False),
45-
sa.Column("legal_name", sa.String(length=128), nullable=False),
46-
sa.Column("discord_id", sa.String(length=18), nullable=True),
47-
sa.Column("discord_name", sa.String(length=32), nullable=True),
48-
sa.Column("discord_nickname", sa.String(length=32), nullable=True),
49-
sa.Column("computing_id", sa.String(length=32), nullable=False),
50-
sa.Column("phone_number", sa.String(length=24), nullable=True),
51-
sa.Column("github_username", sa.String(length=39), nullable=True),
52-
sa.Column("google_drive_email", sa.Text(), nullable=True),
53-
sa.PrimaryKeyConstraint("computing_id", name="pk__officer_info__computing_id"),
31+
"user_session",
32+
# NOTE: order is important; site_user must be created first!
33+
sa.Column("computing_id", sa.String(32), sa.ForeignKey("site_user.computing_id"), primary_key=True),
34+
sa.Column("issue_time", sa.DateTime, nullable=False),
35+
sa.Column("session_id", sa.String(512), nullable=False, unique=True),
5436
)
37+
5538
op.create_table(
5639
"officer_term",
57-
sa.Column("id", sa.Integer(), nullable=False),
58-
sa.Column("computing_id", sa.String(length=32), nullable=False),
40+
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
41+
sa.Column("computing_id", sa.String(length=32), sa.ForeignKey("site_user.computing_id"), nullable=False),
5942
sa.Column("is_filled_in", sa.Boolean(), nullable=False),
6043
sa.Column("position", sa.String(length=128), nullable=False),
6144
sa.Column("start_date", sa.DateTime(), nullable=False),
@@ -67,26 +50,23 @@ def upgrade() -> None:
6750
sa.Column("favourite_pl_1", sa.String(length=32), nullable=True),
6851
sa.Column("biography", sa.Text(), nullable=True),
6952
sa.Column("photo_url", sa.Text(), nullable=True),
70-
sa.PrimaryKeyConstraint("id", name="pk__officer_term__id"),
7153
)
72-
54+
op.create_table(
55+
"officer_info",
56+
sa.Column("is_filled_in", sa.Boolean(), nullable=False),
57+
sa.Column("legal_name", sa.String(length=128), nullable=False),
58+
sa.Column("discord_id", sa.String(length=18), nullable=True),
59+
sa.Column("discord_name", sa.String(length=32), nullable=True),
60+
sa.Column("discord_nickname", sa.String(length=32), nullable=True),
61+
sa.Column("computing_id", sa.String(length=32), sa.ForeignKey("user_session.computing_id"), primary_key=True),
62+
sa.Column("phone_number", sa.String(length=24), nullable=True),
63+
sa.Column("github_username", sa.String(length=39), nullable=True),
64+
sa.Column("google_drive_email", sa.String(length=256), nullable=True),
65+
)
7366

7467
def downgrade() -> None:
75-
op.drop_table("officer_term")
7668
op.drop_table("officer_info")
77-
78-
op.drop_table("site_user")
79-
op.create_table(
80-
"site_user",
81-
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
82-
sa.Column("computing_id", sa.String(32), nullable=False),
83-
)
69+
op.drop_table("officer_term")
8470

8571
op.drop_table("user_session")
86-
op.create_table(
87-
"user_session",
88-
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
89-
sa.Column("issue_time", sa.DateTime, nullable=False),
90-
sa.Column("session_id", sa.String(512), nullable=False),
91-
sa.Column("computing_id", sa.String(32), nullable=False),
92-
)
72+
op.drop_table("site_user")

src/alembic/versions/43f71e4bd6fc_.py

-26
This file was deleted.

src/auth/tables.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
from constants import COMPUTING_ID_LEN, SESSION_ID_LEN
44
from database import Base
55
from sqlalchemy import Column, DateTime, ForeignKey, String
6-
from sqlalchemy.orm import relationship
76

87

98
class UserSession(Base):
109
__tablename__ = "user_session"
1110

12-
# note: a primary key is required for every database table
1311
computing_id = Column(
14-
String(COMPUTING_ID_LEN), nullable=False, primary_key=True
12+
String(COMPUTING_ID_LEN),
13+
ForeignKey("site_user.computing_id"),
14+
# in psql pkey means non-null
15+
primary_key=True,
1516
)
1617

1718
# time the CAS ticket was issued
@@ -27,11 +28,8 @@ class SiteUser(Base):
2728
# see: https://stackoverflow.com/questions/22256124/cannot-create-a-database-table-named-user-in-postgresql
2829
__tablename__ = "site_user"
2930

30-
# note: a primary key is required for every database table
3131
computing_id = Column(
3232
String(COMPUTING_ID_LEN),
33-
#ForeignKey("user_session.computing_id"),
34-
nullable=False,
3533
primary_key=True,
3634
)
3735

src/database.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,27 @@
77
import asyncpg
88
import sqlalchemy
99
from fastapi import Depends, FastAPI
10+
from sqlalchemy import MetaData
1011
from sqlalchemy.ext.asyncio import (
1112
AsyncConnection,
1213
AsyncSession,
1314
)
1415

15-
# Base = sqlalchemy.ext.declarative.declarative_base()
16+
convention = {
17+
"ix": "ix_%(column_0_label)s", # index
18+
"uq": "uq_%(table_name)s_%(column_0_name)s", # unique
19+
"ck": "ck_%(table_name)s_%(constraint_name)s", # check
20+
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", # foreign key
21+
"pk": "pk_%(table_name)s", # primary key
22+
}
23+
1624
Base = sqlalchemy.orm.declarative_base()
25+
Base.metadata = MetaData(naming_convention=convention)
1726

1827
# from: https://medium.com/@tclaitken/setting-up-a-fastapi-app-with-async-sqlalchemy-2-0-pydantic-v2-e6c540be4308
1928
class DatabaseSessionManager:
2029
def __init__(self, db_url: str, engine_kwargs: dict[str, Any], check_db=True):
21-
# engine = sqlalchemy.create_engine(SQLALCHEMY_DATABASE_URL)
2230
self._engine = sqlalchemy.ext.asyncio.create_async_engine(db_url, **engine_kwargs)
23-
# SessionLocal = sqlalchemy.orm.sessionmaker(autocommit=False, autoflush=False, bind=engine)
2431
self._sessionmaker = sqlalchemy.ext.asyncio.async_sessionmaker(autocommit=False, bind=self._engine)
2532

2633
if check_db:

src/officers/tables.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Boolean,
1414
Column,
1515
DateTime,
16+
ForeignKey,
1617
Integer,
1718
String,
1819
Text,
@@ -29,6 +30,7 @@ class OfficerTerm(Base):
2930
id = Column(Integer, primary_key=True, autoincrement=True)
3031
computing_id = Column(
3132
String(COMPUTING_ID_LEN),
33+
ForeignKey("user_session.computing_id"),
3234
nullable=False,
3335
)
3436

@@ -127,23 +129,21 @@ class OfficerInfo(Base):
127129
# private info will be added last
128130
computing_id = Column(
129131
String(COMPUTING_ID_LEN),
132+
ForeignKey("user_session.computing_id"),
130133
primary_key=True,
131134
)
132135
phone_number = Column(String(24))
133136
github_username = Column(String(GITHUB_USERNAME_LEN))
134137

135-
# A comma separated list of emails
136-
# technically 320 is the most common max-size for emails
137-
# specifications for valid email addresses vary widely, but we will not
138-
# accept any that contain a comma
139-
google_drive_email = Column(Text)
138+
# Technically 320 is the most common max-size for emails, but we'll use 256 instead,
139+
# since it's reasonably large (input validate this too)
140+
google_drive_email = Column(String(256))
140141

141142
# NOTE: not sure if we'll need this, depending on implementation
143+
# TODO: get this data on the fly when requested, but rate limit users
144+
# to something like 1/s 100/hour
142145
# has_signed_into_bitwarden = Column(Boolean)
143146

144-
# TODO: can we represent more complicated data structures?
145-
# has_autheticated_github = Column(Boolean)
146-
147147
@staticmethod
148148
def from_data(is_filled_in: bool, officer_info_data: OfficerInfoData) -> OfficerTerm:
149149
return OfficerInfo(

0 commit comments

Comments
 (0)