diff --git a/backend/src/apiserver/app/routers/ranking.py b/backend/src/apiserver/app/routers/ranking.py index 4a575d64..7ae49252 100644 --- a/backend/src/apiserver/app/routers/ranking.py +++ b/backend/src/apiserver/app/routers/ranking.py @@ -15,12 +15,18 @@ from apiserver.data.context.ranking import ( add_new_event, add_new_training, + context_modify_class, context_most_recent_class_points, + 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, @@ -98,22 +104,13 @@ async def get_classification_with_meta( return RawJSONResponse(RankingInfo.model_dump_json(ranking_info).encode()) -@ranking_members_router.get("/get_meta/{rank_type}/", response_model=RankingInfo) -async def member_classification_meta( - rank_type: str, dsrc: SourceDep, app_context: AppContext +@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: - return await get_classification_with_meta( - dsrc, app_context.rank_ctx, rank_type, False - ) - - -@ranking_admin_router.get("/get_meta/{rank_type}/", response_model=RankingInfo) -async def member_classification_admin_meta( - rank_type: str, dsrc: SourceDep, app_context: AppContext -) -> RawJSONResponse: - return await get_classification_with_meta( - dsrc, app_context.rank_ctx, rank_type, True - ) + 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]) @@ -205,3 +202,17 @@ 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) \ No newline at end of file diff --git a/backend/src/apiserver/data/api/classifications.py b/backend/src/apiserver/data/api/classifications.py index e9907a30..ffba5183 100644 --- a/backend/src/apiserver/data/api/classifications.py +++ b/backend/src/apiserver/data/api/classifications.py @@ -1,11 +1,15 @@ from datetime import date, timedelta from typing import Literal +from schema.model.model import CLASS_END_DATE from sqlalchemy import RowMapping from sqlalchemy.ext.asyncio import AsyncConnection from apiserver.lib.model.entities import ( ClassEvent, + ClassMeta, + ClassMetaList, + ClassUpdate, Classification, ClassView, EventDate, @@ -46,7 +50,9 @@ lit_model, select_some_join_where, select_some_where, + update_by_unique, update_column_by_unique, + upsert_by_unique, ) from store.error import DataError, NoDataError, DbError, DbErrors @@ -75,8 +81,8 @@ async def insert_classification( async def most_recent_class_of_type( - conn: AsyncConnection, class_type: Literal["training", "points"] -) -> ClassView: + conn: AsyncConnection, class_type: Literal["training", "points"], amount: int = 1 +) -> list[ClassMeta]: if class_type == "training": query_class_type = "training" elif class_type == "points": @@ -90,11 +96,11 @@ async def most_recent_class_of_type( largest_class_list = await get_largest_where( conn, CLASSIFICATION_TABLE, - {CLASS_ID, CLASS_LAST_UPDATED, CLASS_START_DATE, CLASS_HIDDEN_DATE}, + {CLASS_ID, CLASS_LAST_UPDATED, CLASS_START_DATE, CLASS_HIDDEN_DATE, CLASS_END_DATE}, CLASS_TYPE, query_class_type, CLASS_START_DATE, - 1, + amount, ) if len(largest_class_list) == 0: raise NoDataError( @@ -102,7 +108,7 @@ async def most_recent_class_of_type( "no_most_recent_training_class", ) - return ClassView.model_validate(largest_class_list[0]) + return ClassMetaList.validate_python(largest_class_list) async def all_points_in_class( @@ -260,3 +266,9 @@ async def class_update_last_updated( return await update_column_by_unique( conn, CLASSIFICATION_TABLE, CLASS_LAST_UPDATED, date, CLASS_ID, class_id ) + +async def update_classification( + conn: AsyncConnection, class_view: ClassUpdate +) -> None: + + await update_by_unique(conn, CLASSIFICATION_TABLE, lit_model(class_view), "classification_id", class_view.classification_id) diff --git a/backend/src/apiserver/data/context/app_context.py b/backend/src/apiserver/data/context/app_context.py index d06fdd74..1d96e744 100644 --- a/backend/src/apiserver/data/context/app_context.py +++ b/backend/src/apiserver/data/context/app_context.py @@ -18,6 +18,9 @@ from apiserver.data import Source from apiserver.lib.model.entities import ( ClassEvent, + ClassMeta, + ClassUpdate, + ClassView, NewEvent, RankingInfo, UserData, @@ -101,6 +104,24 @@ async def context_get_event_users( cls, dsrc: Source, event_id: str ) -> list[UserPointsNames]: raise ContextNotImpl() + + @classmethod + async def most_recent_classes( + cls, dsrc: Source, amount: int = 10 + ) -> list[ClassMeta]: + raise ContextNotImpl() + + @classmethod + async def context_new_classes( + cls, dsrc: Source + ) -> None: + raise ContextNotImpl() + + @classmethod + async def context_modify_class( + cls, dsrc: Source, class_update: ClassUpdate + ) -> None: + raise ContextNotImpl() class AuthorizeAppContext(Context): diff --git a/backend/src/apiserver/data/context/ranking.py b/backend/src/apiserver/data/context/ranking.py index f07c2b3f..ed074a7e 100644 --- a/backend/src/apiserver/data/context/ranking.py +++ b/backend/src/apiserver/data/context/ranking.py @@ -9,6 +9,8 @@ from apiserver.lib.model.entities import ( ClassEvent, + ClassMeta, + ClassUpdate, ClassView, NewEvent, NewTrainingEvent, @@ -25,7 +27,9 @@ class_update_last_updated, events_in_class, get_event_user_points, + insert_classification, most_recent_class_of_type, + update_classification, ) from apiserver.data.context import RankingContext from apiserver.data.source import get_conn @@ -49,7 +53,7 @@ async def add_new_event(dsrc: Source, new_event: NewEvent) -> None: date. Use the 'publish' function to force them to be equal.""" async with get_conn(dsrc) as conn: try: - classification = await most_recent_class_of_type(conn, new_event.class_type) + classification = (await most_recent_class_of_type(conn, new_event.class_type))[0] except DataError as e: if e.key != "incorrect_class_type": raise e @@ -135,17 +139,17 @@ async def context_most_recent_class_id_of_type( dsrc: Source, rank_type: Literal["points", "training"] ) -> int: async with get_conn(dsrc) as conn: - class_id = (await most_recent_class_of_type(conn, rank_type)).classification_id + class_id = (await most_recent_class_of_type(conn, rank_type))[0].classification_id return class_id @ctx_reg.register(RankingContext) async def context_most_recent_class_points( - dsrc: Source, rank_type: Literal["points", "training"], is_admin: bool + dsrc: Source, rank_type: Literal["points", "training"], is_admin: bool, ) -> RankingInfo: async with get_conn(dsrc) as conn: - class_view = await most_recent_class_of_type(conn, rank_type) + class_view = (await most_recent_class_of_type(conn, rank_type))[0] user_points = await all_points_in_class( conn, class_view.classification_id, is_admin ) @@ -161,8 +165,8 @@ async def context_most_recent_class_points( @ctx_reg.register(RankingContext) async def sync_publish_ranking(dsrc: Source, publish: bool) -> None: async with get_conn(dsrc) as conn: - training_class = await most_recent_class_of_type(conn, "training") - points_class = await most_recent_class_of_type(conn, "points") + training_class = (await most_recent_class_of_type(conn, "training"))[0] + points_class = (await most_recent_class_of_type(conn, "points"))[0] await update_class_points(conn, training_class.classification_id, publish) await update_class_points(conn, points_class.classification_id, publish) @@ -192,3 +196,38 @@ async def context_get_event_users(dsrc: Source, event_id: str) -> list[UserPoint events_points = await get_event_user_points(conn, event_id) return events_points + + +@ctx_reg.register(RankingContext) +async def most_recent_classes( + dsrc: Source, amount: int = 10 +) -> list[ClassMeta]: + if amount < 2 or amount % 2 != 0: + raise AppError( + ErrorKeys.DATA, + "Request at least 2 classes and make sure it is an even number!", + "most_recent_too_few", + ) + + async with get_conn(dsrc) as conn: + training_classes = (await most_recent_class_of_type(conn, "training", amount // 2)) + points_classes = (await most_recent_class_of_type(conn, "points", amount // 2)) + + return training_classes + points_classes + + +@ctx_reg.register(RankingContext) +async def context_new_classes( + dsrc: Source +) -> None: + async with get_conn(dsrc) as conn: + await insert_classification(conn, "training") + await insert_classification(conn, "points") + + +@ctx_reg.register(RankingContext) +async def context_modify_class( + dsrc: Source, class_update: ClassUpdate +) -> None: + async with get_conn(dsrc) as conn: + await update_classification(conn, class_update) \ No newline at end of file diff --git a/backend/src/apiserver/lib/model/entities.py b/backend/src/apiserver/lib/model/entities.py index d13d5786..f4a7e736 100644 --- a/backend/src/apiserver/lib/model/entities.py +++ b/backend/src/apiserver/lib/model/entities.py @@ -282,3 +282,16 @@ class NewTrainingEvent(BaseModel): class EventDate(BaseModel): date: date + + +class ClassMeta(ClassView): + end_date: date + + +class ClassUpdate(BaseModel): + classification_id: int + start_date: date + hidden_date: date + end_date: date + +ClassMetaList = TypeAdapter(List[ClassMeta]) \ No newline at end of file diff --git a/backend/src/store/db.py b/backend/src/store/db.py index 055730bc..311457f8 100644 --- a/backend/src/store/db.py +++ b/backend/src/store/db.py @@ -227,6 +227,27 @@ async def upsert_by_unique( return row_cnt(res) +async def update_by_unique( + conn: AsyncConnection, + table: LiteralString, + set_dict: LiteralDict, + unique_column: LiteralString, + value: Any, +) -> int: + """Note that while the values are safe from injection, the column names are not.""" + + _, _, row_keys_set = _row_keys_vars_set(set_dict) + + query = text( + f"UPDATE {table} SET {row_keys_set} WHERE {unique_column} = :val;" + ) + val_dict: LiteralDict = {"val": value} + params = set_dict | val_dict + + res = await execute_catch(conn, query, parameters=params) + return row_cnt(res) + + async def update_column_by_unique( conn: AsyncConnection, table: LiteralString, diff --git a/dev.nu b/dev.nu index eb9c9f84..26272273 100755 --- a/dev.nu +++ b/dev.nu @@ -10,7 +10,7 @@ def pull [envnmt: string, env_file: string, profile: string] { } def up [envnmt: string, env_file: string, profile: string] { - pull $envnmt $env_file $profile + # pull $envnmt $env_file $profile docker compose -f $"($deploy_dir)/use/($envnmt)/docker-compose.yml" --env-file $"($deploy_dir)/use/($envnmt)/($env_file).env" --profile $profile up -d } @@ -49,4 +49,4 @@ def "main backend" [] { # important for the command to be exposed to the outside # Useful development commands for starting and stopping databases -def main [] {} \ No newline at end of file +def main [] {}