-
Notifications
You must be signed in to change notification settings - Fork 0
First Elections PR #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 30 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 905170b
Added election tables revision to alembic
DerpyWasHere 7cb496c
Initial elections model implementation
DerpyWasHere 8b06dda
Redid revision, added position column to NomineeAplication, renamed N…
DerpyWasHere b5103ca
Added ElectionOfficer class, created has_permission() to check whethe…
DerpyWasHere b579e08
Changed date to be nonnull, officer_id in alembic revision
DerpyWasHere c89e659
Removed unique constraint on officer_id
DerpyWasHere 117611a
Removed unique constraint on officer_id, removed nullability from date
DerpyWasHere 3aded13
properly handled session validation on create_elections
DerpyWasHere fc8def5
created create_election stub
DerpyWasHere d9b8e52
added elections router to main
DerpyWasHere e239b9b
Working commit containing fns to create, delete, and update elections
DerpyWasHere 8bf5dcb
Merge branch 'main' into dev-issue-25
EarthenSky 06fe816
switch from models to tables
EarthenSky 82685ef
fix small import bug
EarthenSky 0c299d2
fix past alembic migration
EarthenSky 5a2b886
Removed enum in urls.py, satisfied linter for tables.py
DerpyWasHere 5faad35
Changed old ElectionTypes enum into a string array
DerpyWasHere 7b465b5
Changed query in get_election() to be more SQL-like.
DerpyWasHere 44d60f3
Changed list comprehension in create_election() to just a 'if not in'…
DerpyWasHere 40eedd7
Made change referenced in pr 63 wrt committing transactions in get_el…
DerpyWasHere e2bb4db
Removed commits from crud.py and added commits to endpoints in urls.py
DerpyWasHere 50302b1
Changed occurrences of websurvey to survey_link to match that websurv…
DerpyWasHere 82ccc24
Changed election parameters from a list to a dedicated dataclass, ref…
DerpyWasHere a36eef6
Changed parameter orders to be consistent with other crud functions i…
DerpyWasHere 057e405
Merge branch 'main' into dev-issue-25
DerpyWasHere ff2951c
Appeased linter
DerpyWasHere 8d0e267
Reintroduced elections router into main.py
DerpyWasHere 40041ad
update down revision to be blog posts
EarthenSky 853038d
Added lost default param in elections table migration
DerpyWasHere 2dee83a
Changed discord id length in election_nominee table from 18 to 32.
DerpyWasHere 945fb29
Changed date -> start_date in elections table
DerpyWasHere 57f4b2a
Changed date -> start_datetime, start_date -> start_datetime, end_dat…
DerpyWasHere 1c61134
Changed date -> start_datetime, end_date -> end_datetime
DerpyWasHere def5346
update formatting & fix some small access bugs
EarthenSky dadd620
update comment
EarthenSky 3a66f37
Update urls.py
EarthenSky 80a6105
update POST election
EarthenSky bfbd082
Update urls.py
EarthenSky df870d0
fix import style
EarthenSky 8e64f4d
complete election crud and election crud urls
EarthenSky 551e73f
add test data, fix bugs, refactor
EarthenSky d54ec21
test all endpoints & fix bugs
EarthenSky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
src/alembic/versions/243190df5588_create_election_tables.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
"""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 = "2a6ea95342dc" | ||
branch_labels: str | Sequence[str] | None = None | ||
depends_on: str | Sequence[str] | None = None | ||
|
||
|
||
def upgrade() -> None: | ||
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, default="general_election"), | ||
sa.Column("date", sa.DateTime(), nullable=False), | ||
sa.Column("end_date", sa.DateTime(), nullable=True), | ||
sa.Column("survey_link", 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") | ||
) | ||
|
||
|
||
def downgrade() -> None: | ||
op.drop_table("nominee_application") | ||
op.drop_table("election_nominee") | ||
op.drop_table("election") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import logging | ||
from dataclasses import dataclass | ||
from datetime import datetime | ||
|
||
import sqlalchemy | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
|
||
import database | ||
from elections.tables import Election | ||
from officers.constants import OfficerPosition | ||
from officers.tables import OfficerInfo, OfficerTerm | ||
|
||
|
||
@dataclass | ||
class ElectionParameters: | ||
""" | ||
Dataclass encompassing the data that can go into an Election. | ||
""" | ||
slug: str | ||
name: str | ||
officer_id: str | ||
type: str | ||
date: datetime | ||
end_date: datetime | ||
survey_link: str | ||
|
||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
async def get_election(db_session: AsyncSession, election_slug: str) -> Election | None: | ||
query = ( | ||
sqlalchemy | ||
.select(Election) | ||
.where(Election.slug == election_slug) | ||
) | ||
result = await db_session.scalar(query) | ||
return result | ||
|
||
async def create_election(db_session: AsyncSession, params: ElectionParameters) -> None: | ||
""" | ||
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, | ||
survey_link=params.survey_link) | ||
db_session.add(election) | ||
|
||
async def delete_election(db_session: AsyncSession, slug: str) -> None: | ||
""" | ||
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) | ||
|
||
async def update_election(db_session: AsyncSession, params: ElectionParameters) -> None: | ||
""" | ||
Updates an election with the provided parameters. | ||
Take care as this will replace values with None if not populated. | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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() | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if params.date is not None: | ||
election.date = params.date | ||
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.survey_link is not None: | ||
election.survey_link = params.survey_link | ||
|
||
query = sqlalchemy.update(Election).where(Election.slug == params.slug).values(election) | ||
await db_session.execute(query) | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
from datetime import datetime | ||
|
||
from sqlalchemy import ( | ||
Column, | ||
DateTime, | ||
ForeignKey, | ||
PrimaryKeyConstraint, | ||
String, | ||
Text, | ||
) | ||
|
||
from constants import ( | ||
COMPUTING_ID_LEN, | ||
DISCORD_ID_LEN, | ||
DISCORD_NAME_LEN, | ||
DISCORD_NICKNAME_LEN, | ||
) | ||
from database import Base | ||
|
||
election_types = ["general_election", "by_election", "council_rep_election"] | ||
|
||
# Each row represents an instance of an | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
type = Column(String(64), default="general_election") | ||
date = Column(DateTime, nullable=False) | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end_date = Column(DateTime) | ||
survey_link = Column(String(300)) | ||
|
||
# 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)) | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
discord_username = Column(String(DISCORD_NICKNAME_LEN)) | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide 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), | ||
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
import requests # TODO: make this async | ||
import xmltodict | ||
from crud import ElectionParameters | ||
from fastapi import APIRouter, BackgroundTasks, FastAPI, HTTPException, Request, status | ||
from fastapi.exceptions import RequestValidationError | ||
from tables import election_types | ||
|
||
import auth | ||
import auth.crud | ||
import database | ||
import elections | ||
from constants import root_ip_address | ||
from permission import types | ||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
router = APIRouter( | ||
prefix="/elections", | ||
tags=["elections"], | ||
) | ||
|
||
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) | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# 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, | ||
survey_link: str | None = None | ||
): | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
aaa | ||
""" | ||
session_id = request.cookies.get("session_id", None) | ||
user_auth = await _validate_user(db_session, session_id) | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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"}, | ||
) | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Default start time should be now unless specified otherwise | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if date is None: | ||
date = datetime.now() | ||
|
||
if election_type not in election_types: | ||
raise RequestValidationError() | ||
|
||
params = ElectionParameters( | ||
_slugify(name), | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
name, | ||
await auth.crud.get_computing_id(db_session, session_id), | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
election_type, | ||
date, | ||
end_date, | ||
survey_link | ||
) | ||
|
||
await elections.crud.create_election(params, db_session) | ||
await db_session.commit() | ||
|
||
# TODO: create a suitable json response | ||
return {} | ||
|
||
@router.get( | ||
"/delete_election", | ||
description="Deletes an election from the database" | ||
) | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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: | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# 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) | ||
await db_session.commit() | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@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.""" | ||
) | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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, | ||
survey_link: str | None = None | ||
): | ||
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: | ||
EarthenSky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
params = ElectionParameters( | ||
_slugify(name), | ||
name, | ||
await auth.crud.get_computing_id(db_session, session_id), | ||
election_type, | ||
date, | ||
end_date, | ||
survey_link | ||
) | ||
await elections.crud.update_election(params, db_session) | ||
await db_session.commit() | ||
|
||
|
||
@router.get( | ||
"/test" | ||
) | ||
async def test(): | ||
return {"error": "lol"} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.