diff --git a/cutt/analysis.py b/cutt/analysis.py index 30da656..3b27240 100644 --- a/cutt/analysis.py +++ b/cutt/analysis.py @@ -1,4 +1,6 @@ import io +import itertools +import random import base64 from typing import Annotated from fastapi import APIRouter, HTTPException, Security, status @@ -12,6 +14,7 @@ import numpy as np import matplotlib from cutt.security import TeamScopedRequest, verify_team_scope +from cutt.demo import demo_players matplotlib.use("agg") import matplotlib.pyplot as plt @@ -55,14 +58,65 @@ def sociogram_json(): def graph_json( - request: Annotated[ - TeamScopedRequest, Security(verify_team_scope, scopes=["analysis"]) - ], + request: Annotated[TeamScopedRequest, Security(verify_team_scope)], networkx_graph: bool = False, ): nodes = [] edges = [] player_map = {} + if request.team_id == 42: + players = [request.user] + demo_players + random.seed(42) + for p in players: + nodes.append({"id": p.display_name, "label": p.display_name}) + for p, other in itertools.permutations(players, 2): + value = random.random() + if value > 0.5: + edges.append( + { + "id": f"{p.display_name}->{other.display_name}", + "source": p.display_name, + "target": other.display_name, + "size": max(value, 0.3), + "data": { + "relation": 2, + "origSize": max(value, 0.3), + "origFill": "#bed4ff", + }, + } + ) + elif value < 0.1: + edges.append( + { + "id": f"{p.display_name}-x>{other.display_name}", + "source": p.display_name, + "target": other.display_name, + "size": 0.3, + "data": {"relation": 0, "origSize": 0.3, "origFill": "#ff7c7c"}, + "fill": "#ff7c7c", + } + ) + G = nx.DiGraph() + G.add_nodes_from([n["id"] for n in nodes]) + G.add_weighted_edges_from( + [ + ( + e["source"], + e["target"], + e["size"] if e["data"]["relation"] == 2 else -e["size"], + ) + for e in edges + ] + ) + in_degrees = G.in_degree(weight="weight") + nodes = [ + dict(node, **{"data": {"inDegree": in_degrees[node["id"]]}}) + for node in nodes + ] + if networkx_graph: + return G + return JSONResponse({"nodes": nodes, "edges": edges}) + with Session(engine) as session: players = session.exec( select(P) @@ -240,11 +294,26 @@ async def render_sociogram(params: Params): def mvp( - request: Annotated[ - TeamScopedRequest, Security(verify_team_scope, scopes=["analysis"]) - ], + request: Annotated[TeamScopedRequest, Security(verify_team_scope)], ): ranks = dict() + if request.team_id == 42: + random.seed(42) + players = [request.user] + demo_players + for p in players: + random.shuffle(players) + for i, p in enumerate(players): + ranks[p.display_name] = ranks.get(p.display_name, []) + [i + 1] + return [ + { + "name": p, + "rank": f"{np.mean(v):.02f}", + "std": f"{np.std(v):.02f}", + "n": len(v), + } + for p, v in ranks.items() + ] + with Session(engine) as session: players = session.exec( select(P) diff --git a/cutt/demo.py b/cutt/demo.py new file mode 100644 index 0000000..626d95c --- /dev/null +++ b/cutt/demo.py @@ -0,0 +1,27 @@ +import random +from cutt.db import Player + +names = [ + "August", + "Beate", + "Ceasar", + "Daedalus", + "Elli", + "Ford P.", + "Gabriel", + "Hugo", + "Ivar Johansson", + "Jürgen Gordon Malinauskas", +] +demo_players = [ + Player.model_validate( + { + "id": i, + "display_name": name, + "username": name.lower().replace(" ", "").replace(".", ""), + "number": str(random.randint(0, 100)), + "email": name.lower().replace(" ", "").replace(".", "") + "@example.org", + } + ) + for i, name in enumerate(names) +] diff --git a/cutt/main.py b/cutt/main.py index 5e50eb4..1dba77e 100644 --- a/cutt/main.py +++ b/cutt/main.py @@ -76,6 +76,8 @@ def submit_mvps( mvps: MVPRanking, user: Annotated[Player, Depends(get_current_active_user)], ): + if mvps.team == 42: + return JSONResponse("DEMO team, nothing happens") if user.id == mvps.user: with Session(engine) as session: statement = select(Team).where(Team.id == mvps.team) @@ -121,6 +123,8 @@ def get_mvps( def submit_chemistry( chemistry: Chemistry, user: Annotated[Player, Depends(get_current_active_user)] ): + if chemistry.team == 42: + return JSONResponse("DEMO team, nothing happens") if user.id == chemistry.user: with Session(engine) as session: statement = select(Team).where(Team.id == chemistry.team) diff --git a/cutt/player.py b/cutt/player.py index 49f4808..9f16b98 100644 --- a/cutt/player.py +++ b/cutt/player.py @@ -12,6 +12,7 @@ from cutt.security import ( read_player_me, verify_team_scope, ) +from cutt.demo import demo_players P = Player @@ -29,6 +30,12 @@ class PlayerRequest(BaseModel): class AddPlayerRequest(PlayerRequest): ... +DEMO_TEAM_REQUEST = HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="DEMO Team, nothing happens", +) + + def add_player( r: AddPlayerRequest, request: Annotated[TeamScopedRequest, Depends(verify_team_scope)], @@ -74,6 +81,8 @@ def modify_player( r: ModifyPlayerRequest, request: Annotated[TeamScopedRequest, Depends(verify_team_scope)], ): + if request.team_id == 42: + raise DEMO_TEAM_REQUEST with Session(engine) as session: player = session.exec( select(P) @@ -105,6 +114,8 @@ def disable_player( r: DisablePlayerRequest, request: Annotated[TeamScopedRequest, Depends(verify_team_scope)], ): + if request.team_id == 42: + raise DEMO_TEAM_REQUEST with Session(engine) as session: player = session.exec( select(P) @@ -152,6 +163,13 @@ async def list_all_players(): async def list_players( team_id: int, user: Annotated[Player, Depends(get_current_active_user)] ): + if team_id == 42: + return [ + user.model_dump( + include={"id", "display_name", "username", "number", "email"} + ) + ] + demo_players + with Session(engine) as session: current_user = session.exec( select(P) @@ -190,7 +208,9 @@ async def list_players( def read_teams_me(user: Annotated[P, Depends(get_current_active_user)]): with Session(engine) as session: - return [p.teams for p in session.exec(select(P).where(P.id == user.id))][0] + return [p.teams for p in session.exec(select(P).where(P.id == user.id))][0] + [ + {"country": "nowhere", "id": 42, "location": "everywhere", "name": "DEMO"} + ] player_router.add_api_route( diff --git a/cutt/security.py b/cutt/security.py index b23ffb0..61f572e 100644 --- a/cutt/security.py +++ b/cutt/security.py @@ -170,6 +170,8 @@ class TeamScopedRequest(BaseModel): async def verify_team_scope( team_id: int, user: Annotated[Player, Depends(get_current_active_user)] ): + if team_id == 42: + return TeamScopedRequest(user=user, team_id=team_id) allowed_scopes = set(user.scopes.split()) if f"team:{team_id}" not in allowed_scopes: raise HTTPException( diff --git a/src/Footer.tsx b/src/Footer.tsx index ee46271..04578a1 100644 --- a/src/Footer.tsx +++ b/src/Footer.tsx @@ -4,10 +4,11 @@ import { useSession } from "./Session"; export default function Footer() { const location = useLocation(); - const { user } = useSession(); + const { user, teams } = useSession(); return (