Skip to content
This repository was archived by the owner on Dec 20, 2023. It is now read-only.

More klassement routes: user per event, events per class and events per user #66

Merged
merged 2 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion actions/local_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ async def test_update_points(local_dsrc):

@pytest.mark.asyncio
async def test_add_event(local_dsrc, faker: Faker):
faker.seed_instance(242424)
faker.seed_instance(3)
async with get_conn(local_dsrc) as conn:
users = await data.ud.get_all_usernames(conn)

Expand Down
1 change: 1 addition & 0 deletions src/apiserver/app/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class ErrorKeys(StrEnum):
REGISTER = "invalid_register"
RANKING_UPDATE = "invalid_ranking_update"
DATA = "invalid_data_load"
GET_CLASS = "invalid_get_class"


class AppError(Exception):
Expand Down
28 changes: 27 additions & 1 deletion src/apiserver/app/modules/ranking.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Literal
from typing import Literal, Optional

from pydantic import BaseModel
from datetime import date
Expand Down Expand Up @@ -77,3 +77,29 @@ async def sync_publish_ranking(dsrc: Source, publish: bool):
await data.special.update_class_points(
conn, points_class.classification_id, publish
)


async def class_id_or_recent(
dsrc: Source, class_id: Optional[int], rank_type: Optional[str]
) -> int:
if class_id is None and rank_type is None:
reason = "Provide either class_id or rank_type query parameter!"
raise AppError(
err_type=ErrorKeys.GET_CLASS,
err_desc=reason,
debug_key="user_events_invalid_class",
)
elif class_id is None and rank_type not in {"training", "points"}:
reason = f"Ranking {rank_type} is unknown!"
raise AppError(
err_type=ErrorKeys.GET_CLASS,
err_desc=reason,
debug_key="user_events_bad_ranking",
)
elif class_id is None:
async with data.get_conn(dsrc) as conn:
class_id = (
await data.classifications.most_recent_class_of_type(conn, rank_type)
).classification_id

return class_id
77 changes: 71 additions & 6 deletions src/apiserver/app/routers/ranking.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
from typing import Optional

from fastapi import APIRouter
from starlette.requests import Request

from apiserver import data
from apiserver.app.error import ErrorResponse, AppError
from apiserver.app.modules.ranking import add_new_event, NewEvent, sync_publish_ranking
from apiserver.app.modules.ranking import (
add_new_event,
NewEvent,
sync_publish_ranking,
class_id_or_recent,
)
from apiserver.app.ops.header import Authorization
from apiserver.app.response import RawJSONResponse
from apiserver.app.routers.helper import require_admin, require_member
from apiserver.data import Source
from apiserver.lib.model.entities import UserPointsNamesList
from apiserver.data import Source, get_conn
from apiserver.data.api.classifications import events_in_class, get_event_user_points
from apiserver.data.special import user_events_in_class
from apiserver.lib.model.entities import UserPointsNamesList, UserEventsList, EventsList

router = APIRouter()

Expand Down Expand Up @@ -65,12 +74,68 @@ async def member_classification_admin(
return await get_classification(dsrc, rank_type, True)


@router.get("/admin/classification/sync/{publish}/")
@router.post("/admin/class/sync/")
async def sync_publish_classification(
publish: str, request: Request, authorization: Authorization
request: Request, authorization: Authorization, publish: Optional[str] = None
):
dsrc: Source = request.state.dsrc
await require_admin(authorization, dsrc)

do_publish = publish == "publish"
return await sync_publish_ranking(dsrc, do_publish)
await sync_publish_ranking(dsrc, do_publish)


@router.get("/admin/class/events/user/{user_id}/")
async def get_user_events_in_class(
user_id: str,
request: Request,
authorization: Authorization,
class_id: Optional[int] = None,
rank_type: Optional[str] = None,
):
dsrc: Source = request.state.dsrc
await require_admin(authorization, dsrc)

try:
sure_class_id = await class_id_or_recent(dsrc, class_id, rank_type)
except AppError as e:
raise ErrorResponse(400, e.err_type, e.err_desc, e.debug_key)

async with get_conn(dsrc) as conn:
user_events = await user_events_in_class(conn, user_id, sure_class_id)

return RawJSONResponse(UserEventsList.dump_json(user_events))


@router.get("/admin/class/events/")
async def get_events_in_class(
request: Request,
authorization: Authorization,
class_id: Optional[int] = None,
rank_type: Optional[str] = None,
):
dsrc: Source = request.state.dsrc
await require_admin(authorization, dsrc)

try:
sure_class_id = await class_id_or_recent(dsrc, class_id, rank_type)
except AppError as e:
raise ErrorResponse(400, e.err_type, e.err_desc, e.debug_key)

async with get_conn(dsrc) as conn:
events = await events_in_class(conn, sure_class_id)

return RawJSONResponse(EventsList.dump_json(events))


@router.get("/admin/class/events/{event_id}/")
async def get_event_users(
event_id: str, request: Request, authorization: Authorization
):
dsrc: Source = request.state.dsrc
await require_admin(authorization, dsrc)

async with get_conn(dsrc) as conn:
event_users = await get_event_user_points(conn, event_id)

return RawJSONResponse(UserPointsNamesList.dump_json(event_users))
57 changes: 30 additions & 27 deletions src/apiserver/data/api/classifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ClassView,
UserPointsNames,
UserPointsNamesList,
EventsList,
)
from apiserver.lib.utilities import usp_hex
from schema.model import (
Expand Down Expand Up @@ -40,6 +41,7 @@
insert,
insert_many,
select_some_join_where,
select_some_where,
)
from store.error import DataError, NoDataError, DbError, DbErrors

Expand Down Expand Up @@ -188,30 +190,31 @@ async def add_users_to_event(
)


# async def check_user_in_class(
# conn: AsyncConnection,
# user_id: str,
# classification_id: int,
# ) -> bool:
# data = await select_some_two_where(
# conn,
# CLASS_POINTS_TABLE,
# {TRUE_POINTS},
# USER_ID,
# user_id,
# CLASS_ID,
# classification_id,
# )
#
# return data is not None


# async def get_hidden_date(conn: AsyncConnection, classification_id: int) -> str:
# hidden_date_data = await select_some_where(
# conn, CLASSIFICATION_TABLE, {CLASS_HIDDEN_DATE}, CLASS_ID, classification_id
# )
#
# print(
# ORJSONResponse([hidden_date.model_dump() for hidden_date in hidden_date_data])
# )
# return "str"
async def events_in_class(conn: AsyncConnection, class_id: int) -> bool:
events = await select_some_where(
conn,
CLASS_EVENTS_TABLE,
{C_EVENTS_ID, C_EVENTS_CATEGORY, C_EVENTS_DESCRIPTION, C_EVENTS_DATE},
CLASS_ID,
class_id,
)

return EventsList.validate_python(events)


async def get_event_user_points(
conn: AsyncConnection, event_id: str
) -> list[UserPointsNames]:
user_id_select = f"{USERDATA_TABLE}.{USER_ID}"
user_points = await select_some_join_where(
conn,
{user_id_select, UD_FIRSTNAME, UD_LASTNAME, C_EVENTS_POINTS},
CLASS_EVENTS_POINTS_TABLE,
USERDATA_TABLE,
USER_ID,
USER_ID,
C_EVENTS_ID,
event_id,
)

return UserPointsNamesList.validate_python(user_points)
29 changes: 25 additions & 4 deletions src/apiserver/data/special.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from sqlalchemy import text
from sqlalchemy import text, RowMapping
from sqlalchemy.ext.asyncio import AsyncConnection

from apiserver.lib.model.entities import UserEventsList, UserEvent
from schema.model import (
CLASSIFICATION_TABLE,
C_EVENTS_DATE,
Expand All @@ -16,8 +17,10 @@
C_EVENTS_POINTS,
USERDATA_TABLE,
CLASS_EVENTS_POINTS_TABLE,
C_EVENTS_CATEGORY,
C_EVENTS_DESCRIPTION,
)
from store.db import execute_catch_conn, row_cnt
from store.db import execute_catch_conn, row_cnt, all_rows


async def update_class_points(
Expand Down Expand Up @@ -124,11 +127,11 @@ async def update_class_points(
SELECT
{C_EVENTS_ID},
(ev_all.{C_EVENTS_DATE} < clss.{CLASS_HIDDEN_DATE})::int as visible,
(ev_all.{C_EVENTS_DATE} >= clss.{CLASS_HIDDEN_DATE})::int as hidden,
(ev_all.{C_EVENTS_DATE} >= clss.{CLASS_HIDDEN_DATE})::int as hidden
FROM {CLASS_EVENTS_TABLE} as ev_all
JOIN {CLASSIFICATION_TABLE} as clss
ON ev_all.{CLASS_ID} = clss.{CLASS_ID}
WHERE {CLASS_ID} = :id
WHERE ev_all.{CLASS_ID} = :id
) as ev
ON uev.{C_EVENTS_ID} = ev.{C_EVENTS_ID}
) as ce
Expand All @@ -145,3 +148,21 @@ async def update_class_points(

res = await execute_catch_conn(conn, query, params={"id": class_id})
return row_cnt(res)


def parse_user_events(user_events: list[RowMapping]) -> list[UserEvent]:
if len(user_events) == 0:
return []
return UserEventsList.validate_python(user_events)


async def user_events_in_class(
conn: AsyncConnection, user_id: str, class_id: int
) -> list[UserEvent]:
query = text(f"""
SELECT cp.{C_EVENTS_ID}, {C_EVENTS_CATEGORY}, {C_EVENTS_DESCRIPTION}, {C_EVENTS_DATE}, {C_EVENTS_POINTS}
FROM {CLASS_EVENTS_POINTS_TABLE} as cp
JOIN {CLASS_EVENTS_TABLE} as ce on cp.{C_EVENTS_ID} = ce.{C_EVENTS_ID}
WHERE ce.{CLASS_ID} = :class AND cp.{USER_ID} = :user;""")
res = await conn.execute(query, parameters={"class": class_id, "user": user_id})
return parse_user_events(all_rows(res))
27 changes: 24 additions & 3 deletions src/apiserver/lib/model/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,31 @@ class UserPointsNames(BaseModel):
firstname: str
lastname: str
# In the database it is 'display_points'/'true_points', but we want to export as points
points: int = Field(validation_alias=AliasChoices("display_points", "true_points"))
# It might also just be points in the event_points table
points: int = Field(
validation_alias=AliasChoices("display_points", "true_points", "points")
)


UserPointsNamesList = TypeAdapter(List[UserPointsNames])

# class PointsData(BaseModel):
# points: int

class ClassEvent(BaseModel):
event_id: str
category: str
description: str
date: date


EventsList = TypeAdapter(List[ClassEvent])


class UserEvent(BaseModel):
event_id: str
category: str
description: str
date: date
points: int


UserEventsList = TypeAdapter(List[UserEvent])