Coverage for baibu/staff/views.py: 100%
97 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
1"""Staff screens: review queues and processing errors."""
3from datetime import timedelta
5from django.contrib import messages
6from django.core.paginator import Paginator
7from django.http import HttpRequest
8from django.http import HttpResponse
9from django.shortcuts import get_object_or_404
10from django.shortcuts import redirect
11from django.shortcuts import render
12from django.utils import timezone
13from django.utils.translation import gettext as _
14from django.views.decorators.cache import cache_control
15from django.views.decorators.http import require_GET
16from django.views.decorators.http import require_http_methods
18from baibu.chat import services as chat_services
19from baibu.chat.models import ChatFlag
20from baibu.chat.models import Run
21from baibu.chat.models import ToolInvocation
22from baibu.core import storage
23from baibu.submissions import services as submission_services
24from baibu.submissions.models import Submission
26from .access import staff_required
27from .forms import FlagDecisionForm
28from .forms import SubmissionDecisionForm
30QUEUE_STATUSES = (Submission.Status.NEEDS_REVIEW, Submission.Status.ISSUE)
31ERROR_WINDOW_DAYS = 7
34def _stored_text(key: str) -> str | None:
35 payload = storage.read_json(key) if key else None
36 return payload.get("text") if payload else None
39@staff_required
40@require_GET
41def dashboard(request: HttpRequest) -> HttpResponse:
42 since = timezone.now() - timedelta(days=ERROR_WINDOW_DAYS)
43 context = {
44 "needs_review": Submission.objects.filter(status=Submission.Status.NEEDS_REVIEW).count(),
45 "issues": Submission.objects.filter(status=Submission.Status.ISSUE).count(),
46 "open_flags": ChatFlag.objects.filter(status=ChatFlag.Status.OPEN).count(),
47 "failed_runs": Run.objects.filter(status=Run.Status.FAILED, created_at__gte=since).count(),
48 "failed_tools": ToolInvocation.objects.filter(
49 status=ToolInvocation.Status.FAILED, created_at__gte=since
50 ).count(),
51 "window_days": ERROR_WINDOW_DAYS,
52 }
53 return render(request, "staff/dashboard.html", context)
56# --- Submissions ------------------------------------------------------------
59def _submission_queue(request):
60 queryset = Submission.objects.filter(status__in=QUEUE_STATUSES).order_by("created_at")
61 status = request.GET.get("status")
62 if status in QUEUE_STATUSES:
63 queryset = queryset.filter(status=status)
64 return queryset
67@staff_required
68@cache_control(private=True, no_store=True)
69@require_GET
70def submission_queue(request: HttpRequest) -> HttpResponse:
71 page = Paginator(_submission_queue(request), 50).get_page(request.GET.get("page"))
72 return render(
73 request,
74 "staff/submission_queue.html",
75 {"page": page, "status": request.GET.get("status", ""), "statuses": QUEUE_STATUSES},
76 )
79def _allowed_decisions(submission):
80 return [d for d in ("verified", "rejected", "pending") if submission.can_transition_to(d)]
83@staff_required
84@cache_control(private=True, no_store=True)
85@require_http_methods(["GET", "POST"])
86def submission_detail(request: HttpRequest, pk) -> HttpResponse:
87 submission = get_object_or_404(Submission.objects.select_related("user", "reviewed_by"), pk=pk)
88 allowed = _allowed_decisions(submission)
89 form = SubmissionDecisionForm(
90 request.POST or None, allowed=allowed, initial={"staff_notes": submission.staff_notes}
91 )
92 if request.method == "POST" and form.is_valid():
93 decision = form.cleaned_data["decision"]
94 submission.staff_notes = form.cleaned_data["staff_notes"]
95 submission.save(update_fields=["staff_notes", "updated_at"])
96 if decision == "pending":
97 done = submission_services.requeue(submission)
98 else:
99 done = submission_services.review(submission, decision, reviewer=request.user)
100 if not done:
101 messages.warning(request, _("This submission was already decided. Nothing was changed."))
102 return redirect("staff:submission", pk=submission.pk)
103 messages.success(request, _("Decision saved."))
104 following = _submission_queue(request).exclude(pk=submission.pk).first()
105 return redirect("staff:submission", pk=following.pk) if following else redirect("staff:submissions")
106 return render(
107 request,
108 "staff/submission_detail.html",
109 {
110 "submission": submission,
111 "form": form,
112 "in_queue": submission.status in QUEUE_STATUSES,
113 "raw_text": _stored_text(submission.raw_key),
114 "clean_text": _stored_text(submission.clean_key),
115 },
116 )
119# --- Reported chat replies --------------------------------------------------
122@staff_required
123@cache_control(private=True, no_store=True)
124@require_GET
125def flag_queue(request: HttpRequest) -> HttpResponse:
126 flags = ChatFlag.objects.filter(status=ChatFlag.Status.OPEN).select_related("message", "conversation")
127 page = Paginator(flags, 50).get_page(request.GET.get("page"))
128 return render(request, "staff/flag_queue.html", {"page": page})
131@staff_required
132@cache_control(private=True, no_store=True)
133@require_http_methods(["GET", "POST"])
134def flag_detail(request: HttpRequest, pk) -> HttpResponse:
135 flag = get_object_or_404(ChatFlag.objects.select_related("conversation", "message", "decided_by"), pk=pk)
136 form = FlagDecisionForm(request.POST or None)
137 if request.method == "POST" and form.is_valid():
138 if not chat_services.decide_flag(
139 flag, form.cleaned_data["decision"], reviewer=request.user, note=form.cleaned_data["note"]
140 ):
141 messages.warning(request, _("This report was already decided. Nothing was changed."))
142 return redirect("staff:flag", pk=flag.pk)
143 messages.success(request, _("Decision saved."))
144 following = ChatFlag.objects.filter(status=ChatFlag.Status.OPEN).first()
145 return redirect("staff:flag", pk=following.pk) if following else redirect("staff:flags")
146 transcript = flag.conversation.messages.all()
147 return render(request, "staff/flag_detail.html", {"flag": flag, "form": form, "transcript": transcript})
150# --- Errors -----------------------------------------------------------------
153@staff_required
154@cache_control(private=True, no_store=True)
155@require_GET
156def errors(request: HttpRequest) -> HttpResponse:
157 since = timezone.now() - timedelta(days=ERROR_WINDOW_DAYS)
158 return render(
159 request,
160 "staff/errors.html",
161 {
162 "issues": Submission.objects.filter(status=Submission.Status.ISSUE).order_by("-updated_at")[:100],
163 "failed_runs": Run.objects.filter(status=Run.Status.FAILED, created_at__gte=since).order_by("-created_at")[
164 :100
165 ],
166 "failed_tools": ToolInvocation.objects.filter(
167 status=ToolInvocation.Status.FAILED, created_at__gte=since
168 ).order_by("-created_at")[:100],
169 "window_days": ERROR_WINDOW_DAYS,
170 },
171 )