Files

77 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The activity log: everything users did, newest first (administrators)."""
import csv
import io
from fastapi import APIRouter, Depends
from fastapi.responses import Response
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session
from ..db import get_db
from ..models import AuditEntry, BudgetYear, Department, User
from ..security import require
from .mail import _date
router = APIRouter(prefix="/api", tags=["activity"])
def _query(q, user_id, entity, action, year_id, date_from, date_to):
query = select(AuditEntry)
if user_id:
query = query.where(AuditEntry.user_id == user_id)
if entity:
query = query.where(AuditEntry.entity == entity)
if action:
query = query.where(AuditEntry.action == action)
if year_id:
query = query.where(AuditEntry.year_id == year_id)
if date_from:
query = query.where(AuditEntry.at >= _date(date_from))
if date_to:
query = query.where(AuditEntry.at < _date(date_to, end=True))
if q:
like = f"%{q.strip().lower()}%"
query = query.where(or_(func.lower(AuditEntry.summary).like(like),
AuditEntry.user_id.in_(select(User.id).where(func.lower(User.name).like(like)))))
return query
def _rows(db: Session, entries: list[AuditEntry]) -> list[dict]:
year_ids = {a.year_id for a in entries if a.year_id}
labels = {y.id: f"{d.name} {y.year}" for y, d in db.execute(
select(BudgetYear, Department).join(Department).where(BudgetYear.id.in_(year_ids)))} if year_ids else {}
return [{"id": a.id, "at": a.at.isoformat(), "user": a.user.name if a.user else None, "user_id": a.user_id,
"entity": a.entity, "entity_id": a.entity_id, "action": a.action, "summary": a.summary,
"changes": a.changes, "year_id": a.year_id,
"budget": labels.get(a.year_id) if a.year_id else None,
"budget_exists": a.year_id in labels} for a in entries]
@router.get("/activity")
def activity(q: str | None = None, user_id: int | None = None, entity: str | None = None, action: str | None = None,
year_id: int | None = None, date_from: str | None = None, date_to: str | None = None,
limit: int = 100, offset: int = 0, db: Session = Depends(get_db), _: User = Depends(require("admin"))):
query = _query(q, user_id, entity, action, year_id, date_from, date_to)
total = db.scalar(select(func.count()).select_from(query.subquery()))
entries = db.scalars(query.order_by(AuditEntry.at.desc(), AuditEntry.id.desc())
.limit(min(max(limit, 1), 500)).offset(max(offset, 0))).all()
kinds = db.execute(select(AuditEntry.entity, AuditEntry.action).distinct()).all()
return {"total": total, "rows": _rows(db, entries),
"entities": sorted({e for e, _ in kinds}), "actions": sorted({a for _, a in kinds})}
@router.get("/activity.csv")
def activity_csv(q: str | None = None, user_id: int | None = None, entity: str | None = None,
action: str | None = None, year_id: int | None = None, date_from: str | None = None,
date_to: str | None = None, db: Session = Depends(get_db), _: User = Depends(require("admin"))):
entries = db.scalars(_query(q, user_id, entity, action, year_id, date_from, date_to)
.order_by(AuditEntry.at.desc(), AuditEntry.id.desc())).all()
out = io.StringIO()
w = csv.writer(out, delimiter=";")
w.writerow(["date", "user", "area", "action", "budget", "summary", "changes"])
for r in _rows(db, entries):
w.writerow([r["at"], r["user"] or "", r["entity"], r["action"], r["budget"] or "", r["summary"],
"" if not r["changes"] else str(r["changes"])])
return Response("" + out.getvalue(), media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": 'attachment; filename="activity_log.csv"'})