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

1"""Staff screens: review queues and processing errors.""" 

2 

3from datetime import timedelta 

4 

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 

17 

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 

25 

26from .access import staff_required 

27from .forms import FlagDecisionForm 

28from .forms import SubmissionDecisionForm 

29 

30QUEUE_STATUSES = (Submission.Status.NEEDS_REVIEW, Submission.Status.ISSUE) 

31ERROR_WINDOW_DAYS = 7 

32 

33 

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 

37 

38 

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) 

54 

55 

56# --- Submissions ------------------------------------------------------------ 

57 

58 

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 

65 

66 

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 ) 

77 

78 

79def _allowed_decisions(submission): 

80 return [d for d in ("verified", "rejected", "pending") if submission.can_transition_to(d)] 

81 

82 

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 ) 

117 

118 

119# --- Reported chat replies -------------------------------------------------- 

120 

121 

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}) 

129 

130 

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}) 

148 

149 

150# --- Errors ----------------------------------------------------------------- 

151 

152 

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 )