127 lines
5.9 KiB
Python
127 lines
5.9 KiB
Python
"""Nutzung und Kosten, Runs (→ Konzept 13, Phase 5 Administration)."""
|
|
|
|
import csv
|
|
import io
|
|
from datetime import date, datetime, time, timedelta
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import HTMLResponse, Response
|
|
|
|
from app import web
|
|
from app.core.errors import Invalid
|
|
from app.feedback import service as feedback
|
|
from app.identity import admin
|
|
from app.tasks import states
|
|
from app.usage import service as usage
|
|
from app.workspaces import service as workspaces
|
|
|
|
PERIODS = {"month": "Dieser Monat", "last_month": "Letzter Monat", "7d": "Letzte 7 Tage", "30d": "Letzte 30 Tage",
|
|
"custom": "Zeitraum"}
|
|
|
|
|
|
def period(params: dict[str, str]) -> tuple[str, date, date]:
|
|
"""Gewählter Zeitraum als [start, end] in Tagen (end einschließlich)."""
|
|
today = date.today()
|
|
key = params.get("period", "month")
|
|
if key == "last_month":
|
|
end = today.replace(day=1) - timedelta(days=1)
|
|
return key, end.replace(day=1), end
|
|
if key == "7d":
|
|
return key, today - timedelta(days=6), today
|
|
if key == "30d":
|
|
return key, today - timedelta(days=29), today
|
|
if key == "custom":
|
|
try:
|
|
start, end = date.fromisoformat(params.get("start", "")), date.fromisoformat(params.get("end", ""))
|
|
except ValueError:
|
|
raise Invalid("Bitte Start- und Enddatum angeben.") from None
|
|
if end < start:
|
|
raise Invalid("Das Enddatum liegt vor dem Start.")
|
|
return key, start, end
|
|
return "month", today.replace(day=1), today
|
|
|
|
|
|
def bounds(start: date, end: date) -> tuple[datetime, datetime]:
|
|
return (datetime.combine(start, time.min).astimezone(),
|
|
datetime.combine(end + timedelta(days=1), time.min).astimezone())
|
|
|
|
|
|
def router() -> APIRouter:
|
|
r = APIRouter()
|
|
|
|
def overview(request: Request, workspace=None) -> HTMLResponse:
|
|
params = dict(request.query_params)
|
|
key, start, end = period(params)
|
|
group = params.get("group", "team" if workspace is None else "user")
|
|
if group not in usage.GROUPS or (workspace is not None and group in ("team", "workspace")):
|
|
group = "user"
|
|
since, until = bounds(start, end)
|
|
ws_id = workspace.id if workspace else None
|
|
with web.db(request).session() as session:
|
|
rows = usage.report(session, since, until, group, workspace_id=ws_id)
|
|
context = {"rows": rows, "total": usage.total(rows), "group": group, "groups": usage.GROUPS,
|
|
"feedback": feedback.report(session, since, until, workspace_id=ws_id),
|
|
"period": key, "periods": PERIODS, "start": start, "end": end,
|
|
"stats": usage.run_stats(session, since, until, workspace_id=ws_id),
|
|
"labels": states.LABELS, "workspace": workspace, "current_workspace": workspace,
|
|
"base": f"/workspaces/{ws_id}" if workspace else "/admin"}
|
|
return web.render(request, "usage.html", context)
|
|
|
|
def run_list(request: Request, workspace=None) -> HTMLResponse:
|
|
params = dict(request.query_params)
|
|
key, start, end = period(params)
|
|
status = params.get("status", "")
|
|
since, until = bounds(start, end)
|
|
with web.db(request).session() as session:
|
|
rows = usage.runs(session, since, until, status=status if status in states.LABELS else "",
|
|
workspace_id=workspace.id if workspace else None)
|
|
return web.render(request, "runs.html", {
|
|
"rows": rows, "status": status, "labels": states.LABELS, "period": key, "periods": PERIODS,
|
|
"start": start, "end": end, "workspace": workspace, "current_workspace": workspace,
|
|
"base": f"/workspaces/{workspace.id}" if workspace else "/admin"})
|
|
|
|
def feedback_csv(request: Request, workspace=None) -> Response:
|
|
key, start, end = period(dict(request.query_params))
|
|
since, until = bounds(start, end)
|
|
with web.db(request).session() as session:
|
|
found = feedback.rows(session, since, until, workspace_id=workspace.id if workspace else None)
|
|
buffer = io.StringIO()
|
|
csv.writer(buffer, delimiter=";").writerows(feedback.csv_rows(found))
|
|
name = f"rueckmeldungen-{start}-{end}.csv"
|
|
return Response("\ufeff" + buffer.getvalue(), media_type="text/csv; charset=utf-8",
|
|
headers={"Content-Disposition": f'attachment; filename="{name}"'})
|
|
|
|
@r.get("/admin/usage/feedback.csv")
|
|
def platform_feedback(request: Request, principal: web.User) -> Response:
|
|
admin.require(principal)
|
|
return feedback_csv(request)
|
|
|
|
@r.get("/workspaces/{workspace_id}/usage/feedback.csv")
|
|
def workspace_feedback(request: Request, principal: web.User, workspace_id: int) -> Response:
|
|
with web.db(request).session() as session:
|
|
ws = workspaces.require_admin(session, principal, workspace_id).workspace
|
|
return feedback_csv(request, ws)
|
|
|
|
@r.get("/admin/usage")
|
|
def platform_usage(request: Request, principal: web.User) -> HTMLResponse:
|
|
admin.require(principal)
|
|
return overview(request)
|
|
|
|
@r.get("/admin/runs")
|
|
def platform_runs(request: Request, principal: web.User) -> HTMLResponse:
|
|
admin.require(principal)
|
|
return run_list(request)
|
|
|
|
@r.get("/workspaces/{workspace_id}/usage")
|
|
def workspace_usage(request: Request, principal: web.User, workspace_id: int) -> HTMLResponse:
|
|
with web.db(request).session() as session:
|
|
ws = workspaces.require_admin(session, principal, workspace_id).workspace
|
|
return overview(request, ws)
|
|
|
|
@r.get("/workspaces/{workspace_id}/runs")
|
|
def workspace_runs(request: Request, principal: web.User, workspace_id: int) -> HTMLResponse:
|
|
with web.db(request).session() as session:
|
|
ws = workspaces.require_admin(session, principal, workspace_id).workspace
|
|
return run_list(request, ws)
|
|
|
|
return r
|