Skip to content

Commit

Permalink
Merge branch 'main' into training_event_router
Browse files Browse the repository at this point in the history
  • Loading branch information
SenneDrent authored Mar 8, 2024
2 parents e2254bb + 358b759 commit 5d1b466
Show file tree
Hide file tree
Showing 39 changed files with 873 additions and 420 deletions.
8 changes: 4 additions & 4 deletions backend/actions/local_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,12 @@ async def test_add_classification(local_dsrc):
@pytest.mark.asyncio
async def test_update_points(local_dsrc):
async with get_conn(local_dsrc) as conn:
training_class = await data.classifications.most_recent_class_of_type(
training_class = (await data.classifications.most_recent_class_of_type(
conn, "training"
)
points_class = await data.classifications.most_recent_class_of_type(
))[0]
points_class = (await data.classifications.most_recent_class_of_type(
conn, "points"
)
))[0]
await update_class_points(conn, training_class.classification_id)
await update_class_points(conn, points_class.classification_id)

Expand Down
609 changes: 306 additions & 303 deletions backend/poetry.lock

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ packages = [

[tool.poetry.dependencies]
python = ">=3.11, <3.12"
cryptography = "41.0.4"
cryptography = "41.0.7"
PyJWT = "2.8.0"
fastapi = "0.103.2"
fastapi = "0.105.0"
gunicorn = "21.2.0"
uvicorn = { extras = ["standard"], version = "0.23.2" }
asyncpg = "0.28.0"
uvicorn = { extras = ["standard"], version = "0.24.0" }
asyncpg = "0.29.0"
psycopg = { extras = ["binary"], version="^3.1.0" }
pydantic = "2.4.2"
pydantic = "2.5.2"
redis = "5.0.1"
sqlalchemy = { extras = ["asyncio"], version = "2.0.22" }
sqlalchemy = { extras = ["asyncio"], version = "2.0.23" }
opaquepy = "0.3.5"
jinja2 = "^3.1.2"
anyio = "^3.7.1"
Expand All @@ -39,7 +39,7 @@ backend = "apiserver.dev:run"

[tool.poetry.group.dev.dependencies]
pytest = "^7.0.1"
pytest-asyncio = "^0.20.3"
pytest-asyncio = "^0.23.2"
pytest-mock = "^3.7.0"
pre-commit = "^2.20.0"
httpx = "^0.24.1"
Expand Down
26 changes: 23 additions & 3 deletions backend/src/apiserver/app/ops/header.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import Literal
from fastapi import HTTPException, Request
from fastapi.security.base import SecurityBase
from fastapi.openapi.models import HTTPBase as HTTPBaseModel
from fastapi.datastructures import Headers

from apiserver.define import DEFINE, grace_period
Expand All @@ -16,12 +19,29 @@
www_authenticate = f"Bearer realm={DEFINE.realm}"


def auth_header(request: Request) -> str:
# This is so we don't have to instantiate a Request object, which can be annoying
return parse_auth_header(request.headers)
class AuthBearerHeader(SecurityBase):
"""This allows using the OpenAPI docs (/docs) and enter the access token. Note that it already prepends 'Bearer'
to the value, so that's not necessary to add when using the docs."""

scheme: Literal["bearer"] = "bearer"

def __init__(self) -> None:
self.model = HTTPBaseModel(
scheme="bearer",
description="Provide the access token like 'Bearer <encoded token>'.",
)
self.scheme_name = "Bearer authorization scheme."

async def __call__(self, request: Request) -> str:
"""Returns the full value of the Authorization header, including the 'Bearer' part."""
return parse_auth_header(request.headers)


auth_header = AuthBearerHeader()


def parse_auth_header(headers: Headers) -> str:
"""This only checks and returns the Authorization header, it doesn't look at the scheme."""
authorization = headers.get("Authorization")
if not authorization:
# Conforms to RFC6750 https://www.rfc-editor.org/rfc/rfc6750.html
Expand Down
8 changes: 6 additions & 2 deletions backend/src/apiserver/app/ops/startup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from apiserver.app.error import AppError, ErrorKeys
from apiserver.data.special import update_class_points
from loguru import logger
from asyncio import sleep
from datetime import date
Expand Down Expand Up @@ -199,8 +200,11 @@ async def initial_population(dsrc: Source, config: Config) -> None:
user_id = await data.user.insert_return_user_id(conn, fake_user)
assert user_id == "1_fakerecord"

await insert_classification(conn, "training")
await insert_classification(conn, "points")
new_training_id = await insert_classification(conn, "training")
new_points_id = await insert_classification(conn, "points")

await update_class_points(conn, new_training_id, False)
await update_class_points(conn, new_points_id, False)


async def get_keystate(dsrc: Source) -> KeyState:
Expand Down
84 changes: 79 additions & 5 deletions backend/src/apiserver/app/routers/ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,31 @@
mod_user_events_in_class,
)
from apiserver.app.response import RawJSONResponse
from apiserver.data.api.classifications import get_event_user_points
from apiserver.data.api.classifications import (
get_event_user_points,
remove_classification,
)
from apiserver.data import Source
from apiserver.data.context.app_context import RankingContext, conn_wrap
from apiserver.data.context.ranking import (
add_new_event,
add_new_training,
context_most_recent_class_points,
get_all_upcoming_training_events,
context_modify_class,
context_new_classes,
most_recent_classes,
sync_publish_ranking,
)
from apiserver.lib.logic.ranking import is_rank_type
from apiserver.lib.model.entities import (
ClassEvent,
ClassMetaList,
ClassUpdate,
ClassView,
NewEvent,
NewTrainingEvent,
RankingInfo,
UserEvent,
UserPointsNames,
UserPointsNamesList,
Expand All @@ -37,7 +47,7 @@
ranking_members_router = APIRouter(prefix="/class", tags=["ranking"])


@old_router.get("/admin/ranking/update/")
@old_router.post("/admin/ranking/update/")
async def admin_update_ranking_old(
new_event: NewEvent, dsrc: SourceDep, app_context: AppContext
) -> None:
Expand All @@ -63,7 +73,6 @@ async def admin_update_ranking_add_training(
except AppError as e:
raise ErrorResponse(400, "invalid_ranking_update", e.err_desc, e.debug_key)


@ranking_members_router.get("/training/upcoming")
async def member_get_upcoming_training_events(
dsrc: SourceDep, app_context: AppContext
Expand All @@ -74,7 +83,6 @@ async def member_get_upcoming_training_events(
except AppError as e:
raise ErrorResponse(400, "invalid_training_request", e.err_desc, e.debug_key)


async def get_classification(
dsrc: Source, ctx: RankingContext, rank_type: str, admin: bool = False
) -> RawJSONResponse:
Expand All @@ -87,10 +95,48 @@ async def get_classification(
debug_key="bad_ranking",
)

user_points = await context_most_recent_class_points(ctx, dsrc, rank_type, admin)
user_points = (
await context_most_recent_class_points(ctx, dsrc, rank_type, admin)
).points
return RawJSONResponse(UserPointsNamesList.dump_json(user_points))


async def get_classification_with_info(
dsrc: Source, ctx: RankingContext, rank_type: str, admin: bool = False
) -> RawJSONResponse:
if not is_rank_type(rank_type):
reason = f"Ranking {rank_type} is unknown!"
raise ErrorResponse(
status_code=400,
err_type="invalid_ranking",
err_desc=reason,
debug_key="bad_ranking",
)

ranking_info = await context_most_recent_class_points(ctx, dsrc, rank_type, admin)
return RawJSONResponse(RankingInfo.model_dump_json(ranking_info).encode())


@ranking_members_router.get("/get_with_info/{rank_type}/", response_model=RankingInfo)
async def member_classification_with_info(
rank_type: str, dsrc: SourceDep, app_context: AppContext
) -> RawJSONResponse:
return await get_classification_with_info(
dsrc, app_context.rank_ctx, rank_type, False
)


@ranking_admin_router.get("/get_meta/{recent_number}/", response_model=list[ClassView])
async def get_classifications(
recent_number: int, dsrc: SourceDep, app_context: AppContext
) -> RawJSONResponse:
recent_classes = await most_recent_classes(
app_context.rank_ctx, dsrc, recent_number
)

return RawJSONResponse(ClassMetaList.dump_json(recent_classes))


@ranking_members_router.get("/get/{rank_type}/", response_model=list[UserPointsNames])
async def member_classification(
rank_type: str, dsrc: SourceDep, app_context: AppContext
Expand Down Expand Up @@ -180,3 +226,31 @@ async def get_event_users(
)

return RawJSONResponse(UserPointsNamesList.dump_json(event_users))


@ranking_admin_router.post("/new/")
async def new_classes(
dsrc: SourceDep,
app_context: AppContext,
) -> None:
await context_new_classes(app_context.rank_ctx, dsrc)


@ranking_admin_router.post("/modify/")
async def modify_class(
updated_class: ClassUpdate,
dsrc: SourceDep,
app_context: AppContext,
) -> None:
await context_modify_class(app_context.rank_ctx, dsrc, updated_class)


@ranking_admin_router.post("/remove/{class_id}/")
async def remove_class(
class_id: int,
dsrc: SourceDep,
app_context: AppContext,
) -> None:
await ctxlize_wrap(remove_classification, conn_wrap)(
app_context.rank_ctx, dsrc, class_id
)
9 changes: 9 additions & 0 deletions backend/src/apiserver/app_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# its top-level run
from apiserver.resources import res_path
from apiserver.app.error import (
AppEnvironmentError,
AppError,
error_response_return,
ErrorResponse,
Expand Down Expand Up @@ -55,6 +56,14 @@ async def validation_exception_handler(


def define_static_routes() -> list[Mount]:
static_credential_path = res_path.joinpath("static/credentials")
if not static_credential_path.exists():
raise AppEnvironmentError(
f"Could not find the static HTML files at {static_credential_path}. Did you"
" build "
+ "the files for the authpage?"
)

credential_mount = Mount(
"/credentials",
app=StaticFiles(directory=res_path.joinpath("static/credentials"), html=True),
Expand Down
Loading

0 comments on commit 5d1b466

Please sign in to comment.