Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d67b000
Added elections model to alembic/env.py
DerpyWasHere Aug 9, 2024
905170b
Added election tables revision to alembic
DerpyWasHere Aug 9, 2024
7cb496c
Initial elections model implementation
DerpyWasHere Aug 9, 2024
8b06dda
Redid revision, added position column to NomineeAplication, renamed N…
DerpyWasHere Aug 10, 2024
b5103ca
Added ElectionOfficer class, created has_permission() to check whethe…
DerpyWasHere Aug 12, 2024
b579e08
Changed date to be nonnull, officer_id in alembic revision
DerpyWasHere Aug 13, 2024
c89e659
Removed unique constraint on officer_id
DerpyWasHere Aug 13, 2024
117611a
Removed unique constraint on officer_id, removed nullability from date
DerpyWasHere Aug 13, 2024
3aded13
properly handled session validation on create_elections
DerpyWasHere Aug 29, 2024
fc8def5
created create_election stub
DerpyWasHere Aug 29, 2024
d9b8e52
added elections router to main
DerpyWasHere Aug 29, 2024
e239b9b
Working commit containing fns to create, delete, and update elections
DerpyWasHere Sep 27, 2024
8bf5dcb
Merge branch 'main' into dev-issue-25
EarthenSky Oct 3, 2024
06fe816
switch from models to tables
EarthenSky Oct 3, 2024
82685ef
fix small import bug
EarthenSky Oct 3, 2024
0c299d2
fix past alembic migration
EarthenSky Oct 3, 2024
5a2b886
Removed enum in urls.py, satisfied linter for tables.py
DerpyWasHere Jan 13, 2025
5faad35
Changed old ElectionTypes enum into a string array
DerpyWasHere Jan 13, 2025
7b465b5
Changed query in get_election() to be more SQL-like.
DerpyWasHere Jan 13, 2025
44d60f3
Changed list comprehension in create_election() to just a 'if not in'…
DerpyWasHere Jan 13, 2025
40eedd7
Made change referenced in pr 63 wrt committing transactions in get_el…
DerpyWasHere Jan 13, 2025
e2bb4db
Removed commits from crud.py and added commits to endpoints in urls.py
DerpyWasHere Jan 13, 2025
50302b1
Changed occurrences of websurvey to survey_link to match that websurv…
DerpyWasHere Jan 13, 2025
82ccc24
Changed election parameters from a list to a dedicated dataclass, ref…
DerpyWasHere Jan 13, 2025
a36eef6
Changed parameter orders to be consistent with other crud functions i…
DerpyWasHere Jan 13, 2025
057e405
Merge branch 'main' into dev-issue-25
DerpyWasHere Jan 13, 2025
ff2951c
Appeased linter
DerpyWasHere Jan 13, 2025
8d0e267
Reintroduced elections router into main.py
DerpyWasHere Jan 13, 2025
40041ad
update down revision to be blog posts
EarthenSky Jan 13, 2025
853038d
Added lost default param in elections table migration
DerpyWasHere Jan 13, 2025
2dee83a
Changed discord id length in election_nominee table from 18 to 32.
DerpyWasHere Jan 13, 2025
945fb29
Changed date -> start_date in elections table
DerpyWasHere Jan 13, 2025
57f4b2a
Changed date -> start_datetime, start_date -> start_datetime, end_dat…
DerpyWasHere Jan 13, 2025
1c61134
Changed date -> start_datetime, end_date -> end_datetime
DerpyWasHere Jan 13, 2025
def5346
update formatting & fix some small access bugs
EarthenSky Mar 8, 2025
dadd620
update comment
EarthenSky Apr 5, 2025
3a66f37
Update urls.py
EarthenSky Apr 5, 2025
80a6105
update POST election
EarthenSky Apr 5, 2025
bfbd082
Update urls.py
EarthenSky Apr 5, 2025
df870d0
fix import style
EarthenSky Apr 5, 2025
8e64f4d
complete election crud and election crud urls
EarthenSky Apr 6, 2025
551e73f
add test data, fix bugs, refactor
EarthenSky Apr 7, 2025
d54ec21
test all endpoints & fix bugs
EarthenSky Apr 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import auth.tables
import database
import elections.tables
import officers.tables
from alembic import context
from sqlalchemy import pool
Expand Down
61 changes: 61 additions & 0 deletions src/alembic/versions/243190df5588_create_election_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""create election tables

Revision ID: 243190df5588
Revises: 43f71e4bd6fc
Create Date: 2024-08-10 08:32:54.037614

"""
from collections.abc import Sequence
from typing import Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "243190df5588"
down_revision: str | None = "166f3772fce7"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
op.create_table("election",
sa.Column("slug", sa.String(length=32), nullable=False),
sa.Column("name", sa.String(length=32), nullable=False),
sa.Column("officer_id", sa.String(length=32), nullable=False),
sa.Column("type", sa.String(length=64), nullable=True),
sa.Column("date", sa.DateTime(), nullable=False),
sa.Column("end_date", sa.DateTime(), nullable=True),
sa.Column("websurvey", sa.String(length=300), nullable=True),
sa.PrimaryKeyConstraint("slug")
)
op.create_table("election_nominee",
sa.Column("computing_id", sa.String(length=32), nullable=False),
sa.Column("full_name", sa.String(length=64), nullable=False),
sa.Column("facebook", sa.String(length=64), nullable=True),
sa.Column("instagram", sa.String(length=64), nullable=True),
sa.Column("email", sa.String(length=64), nullable=True),
sa.Column("discord", sa.String(length=32), nullable=True),
sa.Column("discord_id", sa.String(length=18), nullable=True),
sa.Column("discord_username", sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint("computing_id")
)
op.create_table("nominee_application",
sa.Column("computing_id", sa.String(length=32), nullable=False),
sa.Column("nominee_election", sa.String(length=32), nullable=False),
sa.Column("speech", sa.Text(), nullable=True),
sa.Column("position", sa.String(length=64), nullable=False),
sa.ForeignKeyConstraint(["computing_id"], ["election_nominee.computing_id"], ),
sa.ForeignKeyConstraint(["nominee_election"], ["election.slug"], ),
sa.PrimaryKeyConstraint("computing_id", "nominee_election")
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("nominee_application")
op.drop_table("election_nominee")
op.drop_table("election")
# ### end Alembic commands ###
68 changes: 68 additions & 0 deletions src/elections/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import dataclasses
import logging
from datetime import datetime

import database
import sqlalchemy
from elections.tables import Election
from officers.constants import OfficerPosition
from officers.tables import OfficerInfo, OfficerTerm

_logger = logging.getLogger(__name__)

async def get_election(db_session: database.DBSession, election_slug: str) -> Election | None:
query = sqlalchemy.select(Election)
query = query.where(Election.slug == election_slug)
result = (await db_session.execute(query)).scalar()
Comment thread
DerpyWasHere marked this conversation as resolved.
Outdated
db_session.commit()
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
return result

async def create_election(params: dict[str, datetime], db_session: database.DBSession) -> None:
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
"""
Creates a new election with given parameters.
Does not validate if an election _already_ exists
"""
election = Election(slug=params["slug"],
name=params["name"],
officer_id=params["officer_id"],
type=params["type"],
date=params["date"],
end_date=params["end_date"],
websurvey=params["websurvey"])
db_session.add(election)
await db_session.commit()

async def delete_election(slug: str, db_session: database.DBSession) -> None:
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
"""
Deletes a given election by its slug.
Does not validate if an election exists
"""
query = sqlalchemy.delete(Election).where(Election.slug == slug)
await db_session.execute(query)
await db_session.commit()

async def update_election(params: dict[str, datetime], db_session: database.DBSession) -> None:
"""
Updates an election with the provided parameters.
Take care as this will replace values with None if not populated.
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
You _cannot_ change the name or slug, you should instead delete and create a new election.
Does not validate if an election _already_ exists
"""

election = (await db_session.execute(sqlalchemy.select(Election).filter_by(slug=params["slug"]))).scalar_one()

if params["date"] is not None:
election.date = params["date"]
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
if params["type"] is not None:
election.type = params["type"]
if params["end_date"] is not None:
election.end_date = params["end_date"]
if params["websurvey"] is not None:
election.websurvey = params["websurvey"]

await db_session.commit()


# query = sqlalchemy.update(Election).where(Election.slug == params["slug"]).values(election)
# await db_session.execute(query)

58 changes: 58 additions & 0 deletions src/elections/tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from datetime import datetime

from constants import (
COMPUTING_ID_LEN,
DISCORD_ID_LEN,
DISCORD_NAME_LEN,
DISCORD_NICKNAME_LEN,
)
from database import Base
from sqlalchemy import (
Column,
DateTime,
ForeignKey,
PrimaryKeyConstraint,
String,
Text,
)


# Each row represents an instance of an
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
class Election(Base):
__tablename__ = "election"

# Slugs are unique identifiers
slug = Column(String(32), primary_key=True)
name = Column(String(32), nullable=False)
officer_id = Column(String(COMPUTING_ID_LEN), nullable=False)
# Can be one of (general_election: General Election, by_election: By-Election, council_rep_election: Council Rep Election)
type = Column(String(64), default="general_election")
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
date = Column(DateTime, nullable=False)
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
end_date = Column(DateTime)
websurvey = Column(String(300))
Comment thread
EarthenSky marked this conversation as resolved.
Outdated

# Each row represents a nominee of a given election
class Nominee(Base):
__tablename__ = "election_nominee"

# Previously named sfuid
computing_id = Column(String(COMPUTING_ID_LEN), primary_key=True)
full_name = Column(String(64), nullable=False)
facebook = Column(String(64))
instagram = Column(String(64))
email = Column(String(64))
discord = Column(String(DISCORD_NAME_LEN))
discord_id = Column(String(DISCORD_ID_LEN))
Comment thread
EarthenSky marked this conversation as resolved.
discord_username = Column(String(DISCORD_NICKNAME_LEN))
Comment thread
EarthenSky marked this conversation as resolved.

class NomineeApplication(Base):
__tablename__ = "nominee_application"

computing_id = Column(ForeignKey("election_nominee.computing_id"), primary_key=True)
nominee_election = Column(ForeignKey("election.slug"), primary_key=True)
speech = Column(Text)
position = Column(String(64), nullable=False)

__table_args__ = (
PrimaryKeyConstraint(computing_id, nominee_election),
)
160 changes: 160 additions & 0 deletions src/elections/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import base64
import logging
import os
import re
import urllib.parse
from datetime import datetime
from enum import Enum

import auth
import auth.crud
import database
import elections
import requests # TODO: make this async
import xmltodict
from constants import root_ip_address
from fastapi import APIRouter, BackgroundTasks, FastAPI, HTTPException, Request, status
from fastapi.exceptions import RequestValidationError
from permission import types

_logger = logging.getLogger(__name__)

router = APIRouter(
prefix="/elections",
tags=["elections"],
)

class ElectionType(Enum):
GENERAL_ELECTION = "general_election"
BY_ELECTION = "by_election"
COUNCIL_REP_ELECTION = "council_rep_election"
Comment thread
EarthenSky marked this conversation as resolved.
Outdated

def _slugify(
text: str
) -> str:
"""
Creates a unique slug based on text passed in. Assumes non-unicode text.
"""
return re.sub(r"[\W_]+", "-", text)

async def _validate_user(
db_session: database.DBSession,
session_id: str
) -> dict:
computing_id = await auth.crud.get_computing_id(db_session, session_id)
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
# Assuming now user is validated
result = await types.ElectionOfficer.has_permission(db_session, computing_id)
return result

@router.get(
"/create_election",
description="Creates an election and places it in the database",
)
async def create_election(
request: Request,
db_session: database.DBSession,
name: str,
election_type: str,
date: datetime | None = None,
end_date: datetime | None = None,
websurvey: str | None = None
):
Comment thread
EarthenSky marked this conversation as resolved.
Comment thread
EarthenSky marked this conversation as resolved.
"""
aaa
"""
session_id = request.cookies.get("session_id", None)
user_auth = await _validate_user(db_session, session_id)
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
if user_auth is False:
# let's workshop how we actually wanna handle this
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="You do not have permission to access this resource",
headers={"WWW-Authenticate": "Basic"},
)
Comment thread
EarthenSky marked this conversation as resolved.

# Default start time should be now unless specified otherwise
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
if date is None:
date = datetime.now()

if election_type not in [e.value for e in ElectionType]:
raise RequestValidationError()

params = {
"slug" : _slugify(name),
"name": name,
"officer_id" : await auth.crud.get_computing_id(db_session, session_id),
"type": election_type,
"date": date,
"end_date": end_date,
"websurvey": websurvey
}

await elections.crud.create_election(params, db_session)

# TODO: create a suitable json response
return {}

@router.get(
"/delete_election",
description="Deletes an election from the database"
)
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
async def delete_election(
request: Request,
db_session: database.DBSession,
slug: str
):
session_id = request.cookies.get("session_id", None)
user_auth = await _validate_user(db_session, session_id)
if user_auth is False:
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
# let's workshop how we actually wanna handle this
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="You do not have permission to access this resource",
headers={"WWW-Authenticate": "Basic"},
)

if slug is not None:
await elections.crud.delete_election(slug, db_session)

@router.get(
"/update_election",
description="""Updates an election in the database.
Note that this does not allow you to change the _name_ of an election as this would generate a new slug."""
)
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
async def update_election(
request: Request,
db_session: database.DBSession,
slug: str,
name: str,
election_type: str,
date: datetime | None = None,
end_date: datetime | None = None,
websurvey: str | None = None
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
):
session_id = request.cookies.get("session_id", None)
user_auth = await _validate_user(db_session, session_id)
if user_auth is False:
# let's workshop how we actually wanna handle this
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="You do not have permission to access this resource",
headers={"WWW-Authenticate": "Basic"},
)
if slug is not None:
Comment thread
EarthenSky marked this conversation as resolved.
Outdated
params = {
"slug" : slug,
"name" : name,
"officer_id" : await auth.crud.get_computing_id(db_session, session_id),
"type": election_type,
"date": date,
"end_date": end_date,
"websurvey": websurvey
}
await elections.crud.update_election(params, db_session)


@router.get(
"/test"
)
async def test():
return {"error": "lol"}
20 changes: 19 additions & 1 deletion src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import auth.urls
import database
import elections.urls
import officers.urls
import permission.urls
from fastapi import FastAPI
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

import tests.urls
Comment thread
EarthenSky marked this conversation as resolved.
Outdated

Expand All @@ -18,7 +22,21 @@
app.include_router(permission.urls.router)

app.include_router(tests.urls.router)
app.include_router(elections.urls.router)

@app.get("/")
async def read_root():
return {"message": "Hello! You might be lost, this is actually the sfucsss.org's backend api."}

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request,
exception: RequestValidationError,
):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({
"detail": exception.errors(),
"body": exception.body,
})
)
Loading