diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 1b7bd44..35d3402 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -7,4 +7,4 @@ jobs: - uses: actions/checkout@v4 - uses: chartboost/ruff-action@v1 with: - version: 0.4.4 \ No newline at end of file + version: 0.6.9 diff --git a/.gitignore b/.gitignore index 12a75c6..1399d74 100755 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ google_key.json __pycache__/ *.py[cod] *$py.class + +.venv +.DS_Store diff --git a/requirements.txt b/requirements.txt index 7b350cf..c6e07d7 100755 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ fastapi==0.109.1 gunicorn==21.2.0 uvicorn[standard]==0.27.1 -sqlalchemy==2.0.27 +sqlalchemy[asyncio]==2.0.27 asyncpg==0.29.0 alembic==1.13.1 google-api-python-client==2.143.0 @@ -13,7 +13,7 @@ xmltodict==0.13.0 # for parsing responses from sfu it's auth api requests==2.31.0 # dev -ruff +ruff==0.6.9 # pre-commit # test diff --git a/src/alembic/env.py b/src/alembic/env.py index 955cdb3..387b0dd 100644 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -1,14 +1,15 @@ import asyncio from logging.config import fileConfig +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + import auth.tables import blog.tables import database import officers.tables from alembic import context -from sqlalchemy import pool -from sqlalchemy.engine import Connection -from sqlalchemy.ext.asyncio import async_engine_from_config # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/src/alembic/versions/166f3772fce7_auth_officer_init.py b/src/alembic/versions/166f3772fce7_auth_officer_init.py index 7e1a345..fa436c7 100644 --- a/src/alembic/versions/166f3772fce7_auth_officer_init.py +++ b/src/alembic/versions/166f3772fce7_auth_officer_init.py @@ -11,6 +11,7 @@ from typing import Union import sqlalchemy as sa + from alembic import op # revision identifiers, used by Alembic. @@ -26,6 +27,7 @@ def upgrade() -> None: sa.Column("computing_id", sa.String(32), primary_key=True), sa.Column("first_logged_in", sa.DateTime, nullable=False, default=datetime(2024, 6, 16)), sa.Column("last_logged_in", sa.DateTime, nullable=False, default=datetime(2024, 6, 16)), + sa.Column("profile_picture_url", sa.Text(), nullable=True), ) op.create_table( "user_session", @@ -56,7 +58,7 @@ def upgrade() -> None: sa.Column("discord_id", sa.String(length=18), nullable=True), sa.Column("discord_name", sa.String(length=32), nullable=True), sa.Column("discord_nickname", sa.String(length=32), nullable=True), - sa.Column("computing_id", sa.String(length=32), sa.ForeignKey("user_session.computing_id"), primary_key=True), + sa.Column("computing_id", sa.String(length=32), sa.ForeignKey("site_user.computing_id"), primary_key=True), sa.Column("phone_number", sa.String(length=24), nullable=True), sa.Column("github_username", sa.String(length=39), nullable=True), sa.Column("google_drive_email", sa.String(length=256), nullable=True), diff --git a/src/alembic/versions/2a6ea95342dc_blog_posts.py b/src/alembic/versions/2a6ea95342dc_blog_posts.py index 8ae89fb..c1c826f 100644 --- a/src/alembic/versions/2a6ea95342dc_blog_posts.py +++ b/src/alembic/versions/2a6ea95342dc_blog_posts.py @@ -9,11 +9,12 @@ from typing import Union import sqlalchemy as sa + from alembic import op # revision identifiers, used by Alembic. revision: str = "2a6ea95342dc" -down_revision: str | None = "43f71e4bd6fc" +down_revision: str | None = "166f3772fce7" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None diff --git a/src/auth/crud.py b/src/auth/crud.py index 2d12e1f..57fb119 100644 --- a/src/auth/crud.py +++ b/src/auth/crud.py @@ -1,11 +1,12 @@ import logging from datetime import datetime, timedelta -from typing import Optional import sqlalchemy -from auth.tables import SiteUser, UserSession from sqlalchemy.ext.asyncio import AsyncSession +from auth.tables import SiteUser, UserSession +from auth.types import SiteUserData + async def create_user_session(db_session: AsyncSession, session_id: str, computing_id: str): """ @@ -35,13 +36,6 @@ async def create_user_session(db_session: AsyncSession, session_id: str, computi # update the last time the user logged in to now existing_user.last_logged_in=datetime.now() else: - new_user_session = UserSession( - issue_time=datetime.now(), - session_id=session_id, - computing_id=computing_id, - ) - db_session.add(new_user_session) - # add new user to User table if it's their first time logging in query = sqlalchemy.select(SiteUser).where(SiteUser.computing_id == computing_id) existing_user = (await db_session.scalars(query)).first() @@ -53,6 +47,13 @@ async def create_user_session(db_session: AsyncSession, session_id: str, computi ) db_session.add(new_user) + new_user_session = UserSession( + issue_time=datetime.now(), + session_id=session_id, + computing_id=computing_id, + ) + db_session.add(new_user_session) + async def remove_user_session(db_session: AsyncSession, session_id: str) -> dict: query = sqlalchemy.select(UserSession).where(UserSession.session_id == session_id) @@ -60,23 +61,6 @@ async def remove_user_session(db_session: AsyncSession, session_id: str) -> dict await db_session.delete(user_session.first()) -async def check_user_session(db_session: AsyncSession, session_id: str) -> dict: - query = sqlalchemy.select(UserSession).where(UserSession.session_id == session_id) - existing_user_session = (await db_session.scalars(query)).first() - - if existing_user_session: - query = sqlalchemy.select(SiteUser).where(SiteUser.computing_id == existing_user_session.computing_id) - existing_user = (await db_session.scalars(query)).first() - return { - "is_valid": True, - "computing_id": existing_user_session.computing_id, - "first_logged_in": existing_user.first_logged_in.isoformat(), - "last_logged_in": existing_user.last_logged_in.isoformat() - } - else: - return {"is_valid": False} - - async def get_computing_id(db_session: AsyncSession, session_id: str) -> str | None: query = sqlalchemy.select(UserSession).where(UserSession.session_id == session_id) existing_user_session = (await db_session.scalars(query)).first() @@ -92,7 +76,8 @@ async def task_clean_expired_user_sessions(db_session: AsyncSession): await db_session.commit() -async def user_info(db_session: AsyncSession, session_id: str) -> None | dict: +# get the site user given a session ID; returns None when session is invalid +async def get_site_user(db_session: AsyncSession, session_id: str) -> None | SiteUserData: query = ( sqlalchemy .select(UserSession) @@ -111,8 +96,43 @@ async def user_info(db_session: AsyncSession, session_id: str) -> None | dict: if user is None: return None - return { - "computing_id": user_session.computing_id, - "first_logged_in": user.first_logged_in.isoformat(), - "last_logged_in": user.last_logged_in.isoformat() - } + return SiteUserData( + user_session.computing_id, + user.first_logged_in.isoformat(), + user.last_logged_in.isoformat(), + user.profile_picture_url + ) + + +# update the optional user info for a given site user (e.g., display name, profile picture, ...) +async def update_site_user( + db_session: AsyncSession, + session_id: str, + profile_picture_url: str +) -> None | SiteUserData: + query = ( + sqlalchemy + .select(UserSession) + .where(UserSession.session_id == session_id) + ) + user_session = await db_session.scalar(query) + if user_session is None: + return None + + query = ( + sqlalchemy + .update(SiteUser) + .where(SiteUser.computing_id == user_session.computing_id) + .values(profile_picture_url = profile_picture_url) + .returning(SiteUser) # returns all columns of SiteUser + ) + user = await db_session.scalar(query) + if user is None: + return None + + return SiteUserData( + user_session.computing_id, + user.first_logged_in.isoformat(), + user.last_logged_in.isoformat(), + user.profile_picture_url + ) diff --git a/src/auth/tables.py b/src/auth/tables.py index 43e994b..9599018 100644 --- a/src/auth/tables.py +++ b/src/auth/tables.py @@ -1,8 +1,9 @@ from datetime import datetime +from sqlalchemy import Column, DateTime, ForeignKey, String, Text + from constants import COMPUTING_ID_LEN, SESSION_ID_LEN from database import Base -from sqlalchemy import Column, DateTime, ForeignKey, String class UserSession(Base): @@ -37,3 +38,6 @@ class SiteUser(Base): # note: default date (for pre-existing columns) is June 16th, 2024 first_logged_in = Column(DateTime, nullable=False, default=datetime(2024, 6, 16)) last_logged_in = Column(DateTime, nullable=False, default=datetime(2024, 6, 16)) + + # optional user information for display purposes + profile_picture_url = Column(Text, nullable=True) diff --git a/src/auth/types.py b/src/auth/types.py new file mode 100644 index 0000000..6587ca3 --- /dev/null +++ b/src/auth/types.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + + +@dataclass +class SiteUserData: + computing_id: str + first_logged_in: str + last_logged_in: str + profile_picture_url: None | str + + def serializable_dict(self): + return { + "computing_id": self.computing_id, + "first_logged_in": self.first_logged_in, + "last_logged_in": self.last_logged_in, + "profile_picture_url": self.profile_picture_url + } diff --git a/src/auth/urls.py b/src/auth/urls.py index 9e6dbcb..8e5a06a 100644 --- a/src/auth/urls.py +++ b/src/auth/urls.py @@ -3,14 +3,15 @@ import os import urllib.parse -import database import requests # TODO: make this async import xmltodict -from auth import crud -from constants import root_ip_address from fastapi import APIRouter, BackgroundTasks, HTTPException, Request from fastapi.responses import JSONResponse, RedirectResponse +import database +from auth import crud +from constants import FRONTEND_ROOT_URL + _logger = logging.getLogger(__name__) # ----------------------- # @@ -37,25 +38,20 @@ def generate_session_id_b64(num_bytes: int) -> str: description="Login to the sfucsss.org. Must redirect to this endpoint from SFU's cas authentication service for correct parameters", ) async def login_user( - next_url: str, + redirect_path: str, + redirect_fragment: str, ticket: str, db_session: database.DBSession, background_tasks: BackgroundTasks, ): - # TODO: test this - if not next_url.startswith(root_ip_address): - raise HTTPException(status_code=400, detail=f"invalid next_url={next_url}, must be a page of our site") - # verify the ticket is valid - url = ( - f"https://cas.sfu.ca/cas/serviceValidate?service={urllib.parse.quote(root_ip_address)}" - f"/api/auth/login%3Fnext_url%3D{urllib.parse.quote(next_url)}&ticket={ticket}" - ) - cas_response = xmltodict.parse(requests.get(url).text) + service = urllib.parse.quote(f"{FRONTEND_ROOT_URL}/api/auth/login?redirect_path={redirect_path}&redirect_fragment={redirect_fragment}") + service_validate_url = f"https://cas.sfu.ca/cas/serviceValidate?service={service}&ticket={ticket}" + cas_response = xmltodict.parse(requests.get(service_validate_url).text) if "cas:authenticationFailure" in cas_response["cas:serviceResponse"]: _logger.info(f"User failed to login, with response {cas_response}") - raise HTTPException(status_code=400, detail="authentication error, ticket likely invalid") + raise HTTPException(status_code=401, detail="authentication error, ticket likely invalid") else: session_id = generate_session_id_b64(256) @@ -67,72 +63,75 @@ async def login_user( # clean old sessions after sending the response background_tasks.add_task(crud.task_clean_expired_user_sessions, db_session) - response = RedirectResponse(next_url) + response = RedirectResponse(FRONTEND_ROOT_URL + redirect_path + "#" + redirect_fragment) response.set_cookie( key="session_id", value=session_id ) # this overwrites any past, possibly invalid, session_id return response -# TODO: deprecate this when possible @router.get( - "/check", - description="Check if the current user is logged in based on session_id from cookies", + "/logout", + description="Logs out the current user by invalidating the session_id cookie", ) -async def check_authentication( - request: Request, # NOTE: these are the request headers +async def logout_user( + request: Request, db_session: database.DBSession, ): session_id = request.cookies.get("session_id", None) if session_id: - await crud.task_clean_expired_user_sessions(db_session) - response_dict = await crud.check_user_session(db_session, session_id) + await crud.remove_user_session(db_session, session_id) + await db_session.commit() + response_dict = {"message": "logout successful"} else: - response_dict = {"is_valid": False} + response_dict = {"message": "user was not logged in"} - return JSONResponse(response_dict) + response = JSONResponse(response_dict) + response.delete_cookie(key="session_id") + return response @router.get( - "/info", + "/user", description="Get info about the current user. Only accessible by that user", ) -async def get_info( +async def get_user( request: Request, db_session: database.DBSession, ): """ - Currently this endpoint only returns the info stored in tables in the auth module. + Returns the info stored in the site_user table in the auth module, if the user is logged in. """ session_id = request.cookies.get("session_id", None) if session_id is None: raise HTTPException(status_code=401, detail="User must be authenticated to get their info") - user_info = await crud.user_info(db_session, session_id) + user_info = await crud.get_site_user(db_session, session_id) if user_info is None: raise HTTPException(status_code=401, detail="Could not find user with session_id, please log in") - return JSONResponse(user_info) + return JSONResponse(user_info.serializable_dict()) -@router.post( - "/logout", - description="Logs out the current user by invalidating the session_id cookie", +@router.patch( + "/user", + description="Update information for the currently logged in user. Only accessible by that user", ) -async def logout_user( +async def update_user( + profile_picture_url: str, request: Request, db_session: database.DBSession, ): + """ + Returns the info stored in the site_user table in the auth module, if the user is logged in. + """ session_id = request.cookies.get("session_id", None) + if session_id is None: + raise HTTPException(status_code=401, detail="User must be authenticated to get their info") - if session_id: - await crud.remove_user_session(db_session, session_id) - await db_session.commit() - response_dict = {"message": "logout successful"} - else: - response_dict = {"message": "user was not logged in"} + user_info = await crud.update_site_user(db_session, session_id, profile_picture_url) + if user_info is None: + raise HTTPException(status_code=401, detail="Could not find user with session_id, please log in") - response = JSONResponse(response_dict) - response.delete_cookie(key="session_id") - return response + return JSONResponse(user_info.serializable_dict()) diff --git a/src/blog/crud.py b/src/blog/crud.py index 1aa621e..b5e5eef 100644 --- a/src/blog/crud.py +++ b/src/blog/crud.py @@ -3,11 +3,12 @@ from datetime import date, datetime from typing import Optional -import database import sqlalchemy -from blog.models import BlogPosts from sqlalchemy import func +import database +from blog.models import BlogPosts + _logger = logging.getLogger(__name__) async def create_new_entry( diff --git a/src/blog/tables.py b/src/blog/tables.py index 90e7b0c..67505fc 100644 --- a/src/blog/tables.py +++ b/src/blog/tables.py @@ -1,7 +1,8 @@ +from sqlalchemy import Column, DateTime, ForeignKey, String, Text + from constants import COMPUTING_ID_LEN from database import Base from officers import tables -from sqlalchemy import Column, DateTime, ForeignKey, String, Text # blog table diff --git a/src/blog/url.py b/src/blog/url.py index 376615a..6ad995f 100644 --- a/src/blog/url.py +++ b/src/blog/url.py @@ -1,10 +1,11 @@ import logging +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse + import auth import blog.crud import database -from fastapi import APIRouter, Request -from fastapi.responses import JSONResponse from permission.types import OfficerPrivateInfo _logger = logging.getLogger(__name__) diff --git a/src/constants.py b/src/constants.py index 60915da..9b19e2f 100644 --- a/src/constants.py +++ b/src/constants.py @@ -1,6 +1,8 @@ import os -root_ip_address = "http://localhost:8080" if os.environ.get("LOCAL") == "true" else "https://api.sfucsss.org" +# TODO(future): replace new.sfucsss.org with sfucsss.org during migration +# TODO(far-future): branch-specific root IP addresses (e.g., devbranch.sfucsss.org) +FRONTEND_ROOT_URL = "http://127.0.0.1:8000" if os.environ.get("LOCAL") == "true" else "https://new.sfucsss.org" GITHUB_ORG_NAME = "CSSS-Test-Organization" if os.environ.get("LOCAL") == "true" else "CSSS" W3_GUILD_ID = "1260652618875797504" diff --git a/src/discord/discord.py b/src/discord/discord.py index b2ebba2..6dc484f 100644 --- a/src/discord/discord.py +++ b/src/discord/discord.py @@ -3,9 +3,10 @@ from time import sleep import requests -from constants import ACTIVE_GUILD_ID from requests import Response +from constants import ACTIVE_GUILD_ID + # ----------------------- # # api diff --git a/src/github/__init__.py b/src/github/__init__.py index 67fb7bc..f53e592 100644 --- a/src/github/__init__.py +++ b/src/github/__init__.py @@ -1,12 +1,12 @@ # TODO: does this allow importing anything from the module? import logging -#from admin.email import send_email -from officers.constants import OfficerPosition - from github.internals import add_user_to_team, list_members, list_team_members, list_teams, remove_user_from_team from github.types import GithubUserPermissions +#from admin.email import send_email +from officers.constants import OfficerPosition + # Rules: # - all past officers will be members of the github org # - all past officers will be put in past_officers team diff --git a/src/github/internals.py b/src/github/internals.py index 15c1768..c74ec3d 100644 --- a/src/github/internals.py +++ b/src/github/internals.py @@ -3,9 +3,9 @@ from typing import Any import requests -from constants import GITHUB_ORG_NAME from requests import Response +from constants import GITHUB_ORG_NAME from github.types import GithubTeam, GithubUser GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") diff --git a/src/load_test_db.py b/src/load_test_db.py index c784428..25685fe 100644 --- a/src/load_test_db.py +++ b/src/load_test_db.py @@ -6,12 +6,13 @@ from datetime import date, datetime, timedelta import sqlalchemy -from auth.crud import create_user_session +from sqlalchemy.ext.asyncio import AsyncSession + +from auth.crud import create_user_session, update_site_user from database import SQLALCHEMY_TEST_DATABASE_URL, Base, DatabaseSessionManager from officers.constants import OfficerPosition from officers.crud import create_new_officer_info, create_new_officer_term, update_officer_info, update_officer_term from officers.tables import OfficerInfo, OfficerTerm -from sqlalchemy.ext.asyncio import AsyncSession async def reset_db(engine): @@ -56,8 +57,9 @@ async def reset_db(engine): else: print(f"new tables: {table_list}") -async def load_test_auth_data(): - pass +async def load_test_auth_data(db_session: AsyncSession): + await create_user_session(db_session, "temp_id_314", "abc314") + await update_site_user(db_session, "temp_id_314", "www.my_profile_picture_url.ca/test") async def load_test_officers_data(db_session: AsyncSession): print("login the 3 users, putting them in the site users table") @@ -215,6 +217,7 @@ async def load_sysadmin(db_session: AsyncSession): # put your computing id here for testing purposes SYSADMIN_COMPUTING_ID = "gsa92" + await create_user_session(db_session, "temp_id_4", SYSADMIN_COMPUTING_ID) await create_new_officer_info(db_session, OfficerInfo( legal_name="Gabe Schulz", discord_id=None, @@ -248,7 +251,7 @@ async def load_sysadmin(db_session: AsyncSession): async def async_main(sessionmanager): await reset_db(sessionmanager._engine) async with sessionmanager.session() as db_session: - # load_test_auth_data + await load_test_auth_data(db_session) await load_test_officers_data(db_session) await load_sysadmin(db_session) diff --git a/src/main.py b/src/main.py index 91fcac5..d2a01fa 100755 --- a/src/main.py +++ b/src/main.py @@ -1,11 +1,11 @@ import logging +from fastapi import FastAPI + import auth.urls import database import officers.urls import permission.urls -from fastapi import FastAPI - import tests.urls logging.basicConfig(level=logging.DEBUG) diff --git a/src/officers/crud.py b/src/officers/crud.py index 682359a..7b457d2 100644 --- a/src/officers/crud.py +++ b/src/officers/crud.py @@ -2,12 +2,12 @@ import logging from datetime import datetime -import database import sqlalchemy -import utils -from auth.tables import SiteUser from fastapi import HTTPException +import database +import utils +from auth.tables import SiteUser from officers.constants import OfficerPosition from officers.tables import OfficerInfo, OfficerTerm from officers.types import ( @@ -186,7 +186,7 @@ async def create_new_officer_info(db_session: database.DBSession, new_officer_in Return False if the officer already exists """ query = sqlalchemy.select(OfficerInfo) - query = query.where(OfficerInfo.computing_id == officer_info.computing_id) + query = query.where(OfficerInfo.computing_id == new_officer_info.computing_id) stored_officer_info = await db_session.scalar(query) if stored_officer_info is not None: return False diff --git a/src/officers/tables.py b/src/officers/tables.py index d5ab995..64ac9f5 100644 --- a/src/officers/tables.py +++ b/src/officers/tables.py @@ -1,14 +1,5 @@ from __future__ import annotations -# from sqlalchemy.orm import relationship -from constants import ( - COMPUTING_ID_LEN, - DISCORD_ID_LEN, - DISCORD_NAME_LEN, - DISCORD_NICKNAME_LEN, - GITHUB_USERNAME_LEN, -) -from database import Base from sqlalchemy import ( # Boolean, Column, @@ -21,6 +12,16 @@ and_, ) +# from sqlalchemy.orm import relationship +from constants import ( + COMPUTING_ID_LEN, + DISCORD_ID_LEN, + DISCORD_NAME_LEN, + DISCORD_NICKNAME_LEN, + GITHUB_USERNAME_LEN, +) +from database import Base + # A row represents an assignment of a person to a position. # An officer with multiple positions, such as Frosh Chair & DoE, is broken up into multiple assignments. @@ -30,7 +31,7 @@ class OfficerTerm(Base): id = Column(Integer, primary_key=True, autoincrement=True) computing_id = Column( String(COMPUTING_ID_LEN), - ForeignKey("user_session.computing_id"), + ForeignKey("site_user.computing_id"), nullable=False, ) @@ -123,7 +124,7 @@ class OfficerInfo(Base): computing_id = Column( String(COMPUTING_ID_LEN), - ForeignKey("user_session.computing_id"), + ForeignKey("site_user.computing_id"), primary_key=True, ) diff --git a/src/officers/types.py b/src/officers/types.py index 5b8dc07..7dfd4a0 100644 --- a/src/officers/types.py +++ b/src/officers/types.py @@ -3,9 +3,9 @@ from dataclasses import asdict, dataclass from datetime import date, datetime -from constants import COMPUTING_ID_MAX from fastapi import HTTPException +from constants import COMPUTING_ID_MAX from officers.constants import OfficerPosition from officers.tables import OfficerInfo, OfficerTerm diff --git a/src/officers/urls.py b/src/officers/urls.py index 30dee1a..6d8ede7 100755 --- a/src/officers/urls.py +++ b/src/officers/urls.py @@ -2,21 +2,21 @@ from dataclasses import dataclass from datetime import date, datetime +import sqlalchemy +from fastapi import APIRouter, Body, HTTPException, Request +from fastapi.responses import JSONResponse, PlainTextResponse + import auth.crud import database import github -import sqlalchemy +import officers.crud import utils from constants import COMPUTING_ID_MAX from discord import discord -from fastapi import APIRouter, Body, HTTPException, Request -from fastapi.responses import JSONResponse, PlainTextResponse -from permission.types import OfficerPrivateInfo, WebsiteAdmin - -import officers.crud from officers.constants import OfficerPosition from officers.tables import OfficerInfo, OfficerTerm from officers.types import OfficerInfoUpload, OfficerTermUpload +from permission.types import OfficerPrivateInfo, WebsiteAdmin _logger = logging.getLogger(__name__) diff --git a/src/permission/types.py b/src/permission/types.py index c9df28c..1725a6e 100644 --- a/src/permission/types.py +++ b/src/permission/types.py @@ -1,11 +1,12 @@ from datetime import UTC, datetime, timezone from typing import ClassVar +from fastapi import HTTPException, Request + import auth.crud import database import officers.crud from data.semesters import current_semester_start, step_semesters -from fastapi import HTTPException, Request from officers.constants import OfficerPosition diff --git a/src/permission/urls.py b/src/permission/urls.py index a50318c..0fdf9c5 100644 --- a/src/permission/urls.py +++ b/src/permission/urls.py @@ -1,8 +1,8 @@ -import auth.crud -import database from fastapi import APIRouter, HTTPException, Request from fastapi.responses import JSONResponse +import auth.crud +import database from permission.types import WebsiteAdmin router = APIRouter( diff --git a/src/utils.py b/src/utils.py index 7b7a762..4836dec 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,12 +1,13 @@ import re from datetime import datetime -from officers.tables import OfficerInfo, OfficerTerm from sqlalchemy import Select # we can't use and/or in sql expressions, so we must use these functions from sqlalchemy.sql.expression import and_, or_ +from officers.tables import OfficerInfo, OfficerTerm + def is_iso_format(date_str: str) -> bool: try: diff --git a/tests/integration/test_discord.py b/tests/integration/test_discord.py index 31f69a0..e1d087c 100644 --- a/tests/integration/test_discord.py +++ b/tests/integration/test_discord.py @@ -1,4 +1,5 @@ import pytest + from discord import discord diff --git a/tests/integration/test_github.py b/tests/integration/test_github.py index 9484af9..671aefc 100644 --- a/tests/integration/test_github.py +++ b/tests/integration/test_github.py @@ -1,6 +1,7 @@ -import github.internals import pytest +import github.internals + # NOTE: must export API key to use github api (mostly...) @pytest.mark.asyncio diff --git a/tests/integration/test_officers.py b/tests/integration/test_officers.py index 5f582a0..0aa3d09 100644 --- a/tests/integration/test_officers.py +++ b/tests/integration/test_officers.py @@ -1,7 +1,8 @@ import asyncio -import load_test_db import pytest + +import load_test_db from database import SQLALCHEMY_TEST_DATABASE_URL, DatabaseSessionManager from officers.constants import OfficerPosition from officers.crud import all_officer_terms, current_executive_team, most_recent_exec_term diff --git a/tests/login.html b/tests/login.html index 56ada40..3d98ae3 100644 --- a/tests/login.html +++ b/tests/login.html @@ -6,28 +6,30 @@
-

please log in here

-

current status:

-

please log out here

+

+ You may + log in or + log out. + Update profile picture. +

+ Profile picture +