Skip to content

Commit 43a943f

Browse files
authored
Update: Officers API (#121)
Tests - added Webmaster test db entries for easier testing - refactored the test fixtures so they can be re-used - fixed some warnings that would become errors in the future - this patch breaks the elections tests, but will be fixed afterwards - broke down the Officer units tests into smaller tests Officer - closes #2: updated the officers API for basic CRUD usage - updated the HTTP decorators to update the documentation - added appropriate models Officer Term - closes #76: added a unique constraint between `computing_id`, `position`, and `start_date` - closes #100: return `{ success: True }` if the delete doesn't fail - updated the HTTP decorators to update the documentation - added appropriate models Elections - the datetime fields should be timezone unaware now - we may need to update old timestamps to this time
1 parent 6e5c539 commit 43a943f

30 files changed

+885
-398
lines changed

.github/workflows/pytest_unit.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,4 @@ jobs:
2929
pip install -r requirements.txt
3030
3131
- name: Run unit tests
32-
run: |
33-
source ./venv/bin/activate
34-
pytest ./tests/unit -v
32+
run: PYTHONPATH=src ./venv/bin/python -m pytest ./tests/unit -v

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ requires-python = ">= 3.11" # older versions untested, but we use new features o
77
Homepage = "https://api.sfucsss.org/"
88

99
[tool.pytest.ini_options]
10-
pythonpath = "./src/"
10+
pythonpath = ["src"]
1111
log_cli = true
1212
log_cli_level = "INFO"
1313
testpaths = [
1414
"tests",
15-
]
15+
]
16+
norecursedirs = "tests/wip"
17+
asyncio_mode = "auto"
18+
asyncio_default_fixture_loop_scope = "function"
1619

1720
[tool.ruff]
1821
line-length = 120

src/alembic/env.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import blog.tables
1010
import database
1111
import elections.tables
12+
import nominees.tables
1213
import officers.tables
14+
import registrations.tables
1315
from alembic import context
1416

1517
# this is the Alembic Config object, which provides
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""site_user_timestamps_nullable
2+
3+
Revision ID: 0a2c458d1ddd
4+
Revises: a5c42bcdda5c
5+
Create Date: 2025-09-28 20:52:02.486734
6+
7+
"""
8+
from collections.abc import Sequence
9+
10+
from sqlalchemy.dialects import postgresql
11+
12+
from alembic import op
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = "0a2c458d1ddd"
16+
down_revision: str | None = "a5c42bcdda5c"
17+
branch_labels: str | Sequence[str] | None = None
18+
depends_on: str | Sequence[str] | None = None
19+
20+
21+
def upgrade() -> None:
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.alter_column("site_user", "first_logged_in",
24+
existing_type=postgresql.TIMESTAMP(),
25+
nullable=True)
26+
op.alter_column("site_user", "last_logged_in",
27+
existing_type=postgresql.TIMESTAMP(),
28+
nullable=True)
29+
# ### end Alembic commands ###
30+
31+
32+
def downgrade() -> None:
33+
# ### commands auto generated by Alembic - please adjust! ###
34+
op.alter_column("site_user", "last_logged_in",
35+
existing_type=postgresql.TIMESTAMP(),
36+
nullable=False)
37+
op.alter_column("site_user", "first_logged_in",
38+
existing_type=postgresql.TIMESTAMP(),
39+
nullable=False)
40+
# ### end Alembic commands ###
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""elections_timestamp_datetime
2+
3+
Revision ID: 0c717bd88d06
4+
Revises: 0a2c458d1ddd
5+
Create Date: 2025-09-28 22:25:28.864945
6+
7+
"""
8+
from collections.abc import Sequence
9+
from typing import Union
10+
11+
import sqlalchemy as sa
12+
13+
from alembic import op
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "0c717bd88d06"
17+
down_revision: str | None = "0a2c458d1ddd"
18+
branch_labels: str | Sequence[str] | None = None
19+
depends_on: str | Sequence[str] | None = None
20+
21+
22+
def upgrade() -> None:
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.alter_column("election", "datetime_start_nominations",
25+
existing_type=sa.DATE(),
26+
type_=sa.DateTime(),
27+
existing_nullable=False)
28+
op.alter_column("election", "datetime_start_voting",
29+
existing_type=sa.DATE(),
30+
type_=sa.DateTime(),
31+
existing_nullable=False)
32+
op.alter_column("election", "datetime_end_voting",
33+
existing_type=sa.DATE(),
34+
type_=sa.DateTime(),
35+
existing_nullable=False)
36+
# ### end Alembic commands ###
37+
38+
39+
def downgrade() -> None:
40+
# ### commands auto generated by Alembic - please adjust! ###
41+
op.alter_column("election", "datetime_end_voting",
42+
existing_type=sa.DateTime(),
43+
type_=sa.DATE(),
44+
existing_nullable=False)
45+
op.alter_column("election", "datetime_start_voting",
46+
existing_type=sa.DateTime(),
47+
type_=sa.DATE(),
48+
existing_nullable=False)
49+
op.alter_column("election", "datetime_start_nominations",
50+
existing_type=sa.DateTime(),
51+
type_=sa.DATE(),
52+
existing_nullable=False)
53+
# ### end Alembic commands ###
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""elections-officers-datetime-to-date
2+
3+
Revision ID: 876041e5b41c
4+
Revises: 243190df5588
5+
Create Date: 2025-09-28 18:01:03.913302
6+
7+
"""
8+
from collections.abc import Sequence
9+
from typing import Union
10+
11+
import sqlalchemy as sa
12+
from sqlalchemy.dialects import postgresql
13+
14+
from alembic import op
15+
16+
# revision identifiers, used by Alembic.
17+
revision: str = "876041e5b41c"
18+
down_revision: str | None = "243190df5588"
19+
branch_labels: str | Sequence[str] | None = None
20+
depends_on: str | Sequence[str] | None = None
21+
22+
23+
def upgrade() -> None:
24+
# ### commands auto generated by Alembic - please adjust! ###
25+
op.alter_column("election", "datetime_start_nominations",
26+
existing_type=postgresql.TIMESTAMP(),
27+
type_=sa.Date(),
28+
existing_nullable=False)
29+
op.alter_column("election", "datetime_start_voting",
30+
existing_type=postgresql.TIMESTAMP(),
31+
type_=sa.Date(),
32+
existing_nullable=False)
33+
op.alter_column("election", "datetime_end_voting",
34+
existing_type=postgresql.TIMESTAMP(),
35+
type_=sa.Date(),
36+
existing_nullable=False)
37+
# ### end Alembic commands ###
38+
39+
40+
def downgrade() -> None:
41+
# ### commands auto generated by Alembic - please adjust! ###
42+
op.alter_column("election", "datetime_end_voting",
43+
existing_type=sa.Date(),
44+
type_=postgresql.TIMESTAMP(),
45+
existing_nullable=False)
46+
op.alter_column("election", "datetime_start_voting",
47+
existing_type=sa.Date(),
48+
type_=postgresql.TIMESTAMP(),
49+
existing_nullable=False)
50+
op.alter_column("election", "datetime_start_nominations",
51+
existing_type=sa.Date(),
52+
type_=postgresql.TIMESTAMP(),
53+
existing_nullable=False)
54+
# ### end Alembic commands ###
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""update_officer_term_constraint_71
2+
3+
Revision ID: a5c42bcdda5c
4+
Revises: 876041e5b41c
5+
Create Date: 2025-09-28 18:03:54.856781
6+
7+
"""
8+
from collections.abc import Sequence
9+
10+
import sqlalchemy as sa
11+
12+
from alembic import op
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = "a5c42bcdda5c"
16+
down_revision: str | None = "876041e5b41c"
17+
branch_labels: str | Sequence[str] | None = None
18+
depends_on: str | Sequence[str] | None = None
19+
20+
21+
def upgrade() -> None:
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.alter_column("election", "type",
24+
existing_type=sa.VARCHAR(length=64),
25+
type_=sa.String(length=32),
26+
nullable=False)
27+
op.create_unique_constraint(op.f("uq_officer_term_computing_id"), "officer_term", ["computing_id", "position", "start_date"])
28+
# ### end Alembic commands ###
29+
30+
31+
def downgrade() -> None:
32+
# ### commands auto generated by Alembic - please adjust! ###
33+
op.drop_constraint(op.f("uq_officer_term_computing_id"), "officer_term", type_="unique")
34+
op.alter_column("election", "type",
35+
existing_type=sa.String(length=32),
36+
type_=sa.VARCHAR(length=64),
37+
nullable=True)
38+
# ### end Alembic commands ###

src/auth/tables.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,21 @@ class SiteUser(Base):
3737
)
3838

3939
# first and last time logged into the CSSS API
40-
first_logged_in: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now())
41-
last_logged_in: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now())
40+
first_logged_in: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
41+
last_logged_in: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
4242

4343
# optional user information for display purposes
4444
profile_picture_url: Mapped[str | None] = mapped_column(Text, nullable=True)
4545

4646
def serialize(self) -> dict[str, str | int | bool | None]:
47-
return {
47+
48+
res = {
4849
"computing_id": self.computing_id,
49-
"first_logged_in": self.first_logged_in.isoformat(),
50-
"last_logged_in": self.last_logged_in.isoformat(),
5150
"profile_picture_url": self.profile_picture_url
5251
}
52+
if self.first_logged_in is not None:
53+
res["first_logged_in"] = self.first_logged_in.isoformat()
54+
if self.last_logged_in is not None:
55+
res["last_logged_in"] = self.last_logged_in.isoformat()
56+
57+
return res

src/cron/daily.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import github
77
import google_api
88
import utils
9-
from database import _db_session
9+
from database import get_db_session
1010
from officers.crud import all_officers, get_user_by_username
1111

1212
_logger = logging.getLogger(__name__)
@@ -55,7 +55,7 @@ async def update_github_permissions(db_session):
5555
_logger.info("updated github permissions")
5656

5757
async def update_permissions():
58-
db_session = _db_session()
58+
db_session = get_db_session()
5959

6060
update_google_permissions(db_session)
6161
db_session.commit()

src/database.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def setup_database():
102102
# TODO: where is sys.stdout piped to? I want all these to go to a specific logs folder
103103
sessionmanager = DatabaseSessionManager(
104104
SQLALCHEMY_TEST_DATABASE_URL if os.environ.get("LOCAL") else SQLALCHEMY_DATABASE_URL,
105-
{ "echo": True },
105+
{ "echo": False },
106106
)
107107

108108
@contextlib.asynccontextmanager
@@ -116,10 +116,9 @@ async def lifespan(app: FastAPI):
116116
await sessionmanager.close()
117117

118118

119-
async def _db_session():
119+
async def get_db_session():
120120
async with sessionmanager.session() as session:
121121
yield session
122122

123123

124-
# TODO: what does this do again?
125-
DBSession = Annotated[AsyncSession, Depends(_db_session)]
124+
DBSession = Annotated[AsyncSession, Depends(get_db_session)]

0 commit comments

Comments
 (0)