Merge branch 'feat/demo'

This commit is contained in:
julius 2025-05-18 13:18:46 +02:00
commit 881e015c1f
Signed by: julius
GPG Key ID: C80A63E6A5FD7092
9 changed files with 135 additions and 9 deletions

View File

@ -1,4 +1,6 @@
import io import io
import itertools
import random
import base64 import base64
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, HTTPException, Security, status from fastapi import APIRouter, HTTPException, Security, status
@ -12,6 +14,7 @@ import numpy as np
import matplotlib import matplotlib
from cutt.security import TeamScopedRequest, verify_team_scope from cutt.security import TeamScopedRequest, verify_team_scope
from cutt.demo import demo_players
matplotlib.use("agg") matplotlib.use("agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@ -55,14 +58,65 @@ def sociogram_json():
def graph_json( def graph_json(
request: Annotated[ request: Annotated[TeamScopedRequest, Security(verify_team_scope)],
TeamScopedRequest, Security(verify_team_scope, scopes=["analysis"])
],
networkx_graph: bool = False, networkx_graph: bool = False,
): ):
nodes = [] nodes = []
edges = [] edges = []
player_map = {} 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: with Session(engine) as session:
players = session.exec( players = session.exec(
select(P) select(P)
@ -240,11 +294,26 @@ async def render_sociogram(params: Params):
def mvp( def mvp(
request: Annotated[ request: Annotated[TeamScopedRequest, Security(verify_team_scope)],
TeamScopedRequest, Security(verify_team_scope, scopes=["analysis"])
],
): ):
ranks = dict() 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: with Session(engine) as session:
players = session.exec( players = session.exec(
select(P) select(P)

27
cutt/demo.py Normal file
View File

@ -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)
]

View File

@ -76,6 +76,8 @@ def submit_mvps(
mvps: MVPRanking, mvps: MVPRanking,
user: Annotated[Player, Depends(get_current_active_user)], user: Annotated[Player, Depends(get_current_active_user)],
): ):
if mvps.team == 42:
return JSONResponse("DEMO team, nothing happens")
if user.id == mvps.user: if user.id == mvps.user:
with Session(engine) as session: with Session(engine) as session:
statement = select(Team).where(Team.id == mvps.team) statement = select(Team).where(Team.id == mvps.team)
@ -121,6 +123,8 @@ def get_mvps(
def submit_chemistry( def submit_chemistry(
chemistry: Chemistry, user: Annotated[Player, Depends(get_current_active_user)] 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: if user.id == chemistry.user:
with Session(engine) as session: with Session(engine) as session:
statement = select(Team).where(Team.id == chemistry.team) statement = select(Team).where(Team.id == chemistry.team)

View File

@ -12,6 +12,7 @@ from cutt.security import (
read_player_me, read_player_me,
verify_team_scope, verify_team_scope,
) )
from cutt.demo import demo_players
P = Player P = Player
@ -29,6 +30,12 @@ class PlayerRequest(BaseModel):
class AddPlayerRequest(PlayerRequest): ... class AddPlayerRequest(PlayerRequest): ...
DEMO_TEAM_REQUEST = HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="DEMO Team, nothing happens",
)
def add_player( def add_player(
r: AddPlayerRequest, r: AddPlayerRequest,
request: Annotated[TeamScopedRequest, Depends(verify_team_scope)], request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
@ -74,6 +81,8 @@ def modify_player(
r: ModifyPlayerRequest, r: ModifyPlayerRequest,
request: Annotated[TeamScopedRequest, Depends(verify_team_scope)], request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
): ):
if request.team_id == 42:
raise DEMO_TEAM_REQUEST
with Session(engine) as session: with Session(engine) as session:
player = session.exec( player = session.exec(
select(P) select(P)
@ -105,6 +114,8 @@ def disable_player(
r: DisablePlayerRequest, r: DisablePlayerRequest,
request: Annotated[TeamScopedRequest, Depends(verify_team_scope)], request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
): ):
if request.team_id == 42:
raise DEMO_TEAM_REQUEST
with Session(engine) as session: with Session(engine) as session:
player = session.exec( player = session.exec(
select(P) select(P)
@ -152,6 +163,13 @@ async def list_all_players():
async def list_players( async def list_players(
team_id: int, user: Annotated[Player, Depends(get_current_active_user)] 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: with Session(engine) as session:
current_user = session.exec( current_user = session.exec(
select(P) select(P)
@ -190,7 +208,9 @@ async def list_players(
def read_teams_me(user: Annotated[P, Depends(get_current_active_user)]): def read_teams_me(user: Annotated[P, Depends(get_current_active_user)]):
with Session(engine) as session: 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( player_router.add_api_route(

View File

@ -170,6 +170,8 @@ class TeamScopedRequest(BaseModel):
async def verify_team_scope( async def verify_team_scope(
team_id: int, user: Annotated[Player, Depends(get_current_active_user)] 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()) allowed_scopes = set(user.scopes.split())
if f"team:{team_id}" not in allowed_scopes: if f"team:{team_id}" not in allowed_scopes:
raise HTTPException( raise HTTPException(

View File

@ -4,10 +4,11 @@ import { useSession } from "./Session";
export default function Footer() { export default function Footer() {
const location = useLocation(); const location = useLocation();
const { user } = useSession(); const { user, teams } = useSession();
return ( return (
<footer className={location.pathname === "/network" ? "fixed-footer" : ""}> <footer className={location.pathname === "/network" ? "fixed-footer" : ""}>
{user?.scopes.split(" ").includes("analysis") && ( {(user?.scopes.split(" ").includes("analysis") ||
teams?.activeTeam === 42) && (
<div className="navbar"> <div className="navbar">
<Link to="/"> <Link to="/">
<span>Form</span> <span>Form</span>

View File

@ -15,6 +15,7 @@ const MVPChart = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
user?.scopes.includes(`team:${teams?.activeTeam}`) || user?.scopes.includes(`team:${teams?.activeTeam}`) ||
teams?.activeTeam === 42 ||
navigate("/", { replace: true }); navigate("/", { replace: true });
}, [user]); }, [user]);

View File

@ -50,6 +50,7 @@ export const GraphComponent = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
user?.scopes.includes(`team:${teams?.activeTeam}`) || user?.scopes.includes(`team:${teams?.activeTeam}`) ||
teams?.activeTeam === 42 ||
navigate("/", { replace: true }); navigate("/", { replace: true });
}, [user]); }, [user]);

View File

@ -9,6 +9,7 @@ const TeamPanel = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
user?.scopes.includes(`team:${teams?.activeTeam}`) || user?.scopes.includes(`team:${teams?.activeTeam}`) ||
teams?.activeTeam === 42 ||
navigate("/", { replace: true }); navigate("/", { replace: true });
}, [user]); }, [user]);
const newPlayerTemplate = { const newPlayerTemplate = {