Skip to content

Commit f765ac9

Browse files
First Elections PR (#63)
* Added election tables revision to alembic * Initial elections model implementation * Added ElectionOfficer class, created has_permission() to check whether a user is a current elections officer * Working commit containing fns to create, delete, and update elections * Changed parameter orders to be consistent with other crud functions in the project. Changed database.DBSession -> AsyncSession to match other crud functions. --------- Co-authored-by: Sean Chan <[email protected]>
1 parent 2ba2e46 commit f765ac9

File tree

11 files changed

+622
-21
lines changed

11 files changed

+622
-21
lines changed

src/alembic/env.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import auth.tables
99
import blog.tables
1010
import database
11+
import elections.tables
1112
import officers.tables
1213
from alembic import context
1314

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""create election tables
2+
3+
Revision ID: 243190df5588
4+
Revises: 43f71e4bd6fc
5+
Create Date: 2024-08-10 08:32:54.037614
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 = "243190df5588"
17+
down_revision: str | None = "2a6ea95342dc"
18+
branch_labels: str | Sequence[str] | None = None
19+
depends_on: str | Sequence[str] | None = None
20+
21+
22+
def upgrade() -> None:
23+
op.create_table(
24+
"election",
25+
sa.Column("slug", sa.String(length=64), nullable=False),
26+
sa.Column("name", sa.String(length=64), nullable=False),
27+
sa.Column("type", sa.String(length=64), default="general_election"),
28+
sa.Column("datetime_start_nominations", sa.DateTime(), nullable=False),
29+
sa.Column("datetime_start_voting", sa.DateTime(), nullable=False),
30+
sa.Column("datetime_end_voting", sa.DateTime(), nullable=False),
31+
sa.Column("survey_link", sa.String(length=300), nullable=True),
32+
sa.PrimaryKeyConstraint("slug")
33+
)
34+
op.create_table(
35+
"election_nominee",
36+
sa.Column("computing_id", sa.String(length=32), nullable=False),
37+
sa.Column("full_name", sa.String(length=64), nullable=False),
38+
sa.Column("facebook", sa.String(length=128), nullable=True),
39+
sa.Column("instagram", sa.String(length=128), nullable=True),
40+
sa.Column("email", sa.String(length=64), nullable=True),
41+
sa.Column("discord", sa.String(length=32), nullable=True),
42+
sa.Column("discord_id", sa.String(length=32), nullable=True),
43+
sa.Column("discord_username", sa.String(length=32), nullable=True),
44+
sa.PrimaryKeyConstraint("computing_id")
45+
)
46+
op.create_table(
47+
"nominee_application",
48+
sa.Column("computing_id", sa.String(length=32), nullable=False),
49+
sa.Column("nominee_election", sa.String(length=32), nullable=False),
50+
sa.Column("speech", sa.Text(), nullable=True),
51+
sa.Column("position", sa.String(length=64), nullable=False),
52+
sa.ForeignKeyConstraint(["computing_id"], ["election_nominee.computing_id"]),
53+
sa.ForeignKeyConstraint(["nominee_election"], ["election.slug"]),
54+
sa.PrimaryKeyConstraint("computing_id", "nominee_election")
55+
)
56+
57+
58+
def downgrade() -> None:
59+
op.drop_table("nominee_application")
60+
op.drop_table("election_nominee")
61+
op.drop_table("election")

src/elections/crud.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import logging
2+
3+
import sqlalchemy
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
6+
from elections.tables import Election
7+
8+
_logger = logging.getLogger(__name__)
9+
10+
async def get_election(db_session: AsyncSession, election_slug: str) -> Election | None:
11+
return await db_session.scalar(
12+
sqlalchemy
13+
.select(Election)
14+
.where(Election.slug == election_slug)
15+
)
16+
17+
async def create_election(db_session: AsyncSession, election: Election) -> None:
18+
"""
19+
Creates a new election with given parameters.
20+
Does not validate if an election _already_ exists
21+
"""
22+
db_session.add(election)
23+
24+
async def delete_election(db_session: AsyncSession, slug: str) -> None:
25+
"""
26+
Deletes a given election by its slug.
27+
Does not validate if an election exists
28+
"""
29+
await db_session.execute(
30+
sqlalchemy
31+
.delete(Election)
32+
.where(Election.slug == slug)
33+
)
34+
35+
async def update_election(db_session: AsyncSession, new_election: Election) -> bool:
36+
"""
37+
You attempting to change the name or slug will fail. Instead, you must create a new election.
38+
"""
39+
target_slug = new_election.slug
40+
target_election = await get_election(db_session, target_slug)
41+
42+
if target_election is None:
43+
return False
44+
else:
45+
await db_session.execute(
46+
sqlalchemy
47+
.update(Election)
48+
.where(Election.slug == target_slug)
49+
.values(new_election.to_update_dict())
50+
)
51+
return True

src/elections/tables.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from sqlalchemy import (
2+
Column,
3+
DateTime,
4+
ForeignKey,
5+
PrimaryKeyConstraint,
6+
String,
7+
Text,
8+
)
9+
10+
from constants import (
11+
COMPUTING_ID_LEN,
12+
DISCORD_ID_LEN,
13+
DISCORD_NAME_LEN,
14+
DISCORD_NICKNAME_LEN,
15+
)
16+
from database import Base
17+
18+
election_types = ["general_election", "by_election", "council_rep_election"]
19+
20+
MAX_ELECTION_NAME = 64
21+
MAX_ELECTION_SLUG = 64
22+
23+
class Election(Base):
24+
__tablename__ = "election"
25+
26+
# Slugs are unique identifiers
27+
slug = Column(String(MAX_ELECTION_SLUG), primary_key=True)
28+
name = Column(String(MAX_ELECTION_NAME), nullable=False)
29+
type = Column(String(64), default="general_election")
30+
datetime_start_nominations = Column(DateTime, nullable=False)
31+
datetime_start_voting = Column(DateTime, nullable=False)
32+
datetime_end_voting = Column(DateTime, nullable=False)
33+
survey_link = Column(String(300))
34+
35+
def serializable_dict(self) -> dict:
36+
return {
37+
"slug": self.slug,
38+
"name": self.name,
39+
"type": self.type,
40+
41+
"datetime_start_nominations": self.datetime_start_nominations.isoformat(),
42+
"datetime_start_voting": self.datetime_start_voting.isoformat(),
43+
"datetime_end_voting": self.datetime_end_voting.isoformat(),
44+
45+
"survey_link": self.survey_link,
46+
}
47+
48+
def public_details(self) -> dict:
49+
return {
50+
"slug": self.slug,
51+
"name": self.name,
52+
"type": self.type,
53+
54+
"datetime_start_nominations": self.datetime_start_nominations.isoformat(),
55+
"datetime_start_voting": self.datetime_start_voting.isoformat(),
56+
"datetime_end_voting": self.datetime_end_voting.isoformat(),
57+
}
58+
59+
def to_update_dict(self) -> dict:
60+
return {
61+
"slug": self.slug,
62+
"name": self.name,
63+
"type": self.type,
64+
65+
"datetime_start_nominations": self.datetime_start_nominations,
66+
"datetime_start_voting": self.datetime_start_voting,
67+
"datetime_end_voting": self.datetime_end_voting,
68+
69+
"survey_link": self.survey_link,
70+
}
71+
72+
# Each row represents a nominee of a given election
73+
class Nominee(Base):
74+
__tablename__ = "election_nominee"
75+
76+
# Previously named sfuid
77+
computing_id = Column(String(COMPUTING_ID_LEN), primary_key=True)
78+
full_name = Column(String(64), nullable=False)
79+
facebook = Column(String(128))
80+
instagram = Column(String(128))
81+
email = Column(String(64))
82+
discord = Column(String(DISCORD_NAME_LEN))
83+
discord_id = Column(String(DISCORD_ID_LEN))
84+
discord_username = Column(String(DISCORD_NICKNAME_LEN))
85+
86+
class NomineeApplication(Base):
87+
__tablename__ = "nominee_application"
88+
89+
computing_id = Column(ForeignKey("election_nominee.computing_id"), primary_key=True)
90+
nominee_election = Column(ForeignKey("election.slug"), primary_key=True)
91+
speech = Column(Text)
92+
position = Column(String(64), nullable=False)
93+
94+
__table_args__ = (
95+
PrimaryKeyConstraint(computing_id, nominee_election),
96+
)

0 commit comments

Comments
 (0)