Coverage for src / mesh / models / roles / reviewer.py: 91%
75 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-11 09:47 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-11 09:47 +0000
1from typing import ClassVar
3from django.db.models import Case, CharField, Exists, OuterRef, Q, Subquery, Value, When
5# from django.urls import reverse
6# from django.urls import reverse_lazy
7from django.utils.translation import gettext as _
8from opentelemetry import trace
10from mesh.app_settings import BlindMode, app_settings
11from mesh.models.orm.review_models import Review, ReviewAdditionalFile
12from mesh.models.orm.submission_models import (
13 Submission,
14 SubmissionAuthor,
15 SubmissionState,
16 SubmissionVersion,
17)
19# from ..views.components.button import Button
20from mesh.models.roles.base_role import Role
21from mesh.models.submission_status import SubmissionStatus, SubmissionStatusData
23# from ptf.url_utils import add_query_parameters_to_url
25tracer = trace.get_tracer(__name__)
28class ReviewerRights:
29 """
30 Rights implementation for reviewer role.
31 """
34class Reviewer(Role):
35 """
36 Reviewer role.
37 """
39 _CODE: ClassVar[str] = "reviewer"
40 _NAME: ClassVar[str] = _("Reviewer")
41 _ICON_CLASS: ClassVar[str] = "fa-user-group"
42 _SUBMISSION_LIST_TITLE: ClassVar[str] = _("My assignments")
43 rights: ReviewerRights
45 @tracer.start_as_current_span("Reviewer.__init__")
46 def __init__(self, user) -> None:
47 super().__init__(user)
48 if app_settings.BLIND_MODE == BlindMode.DOUBLE_BLIND:
49 self.created_by_censored_annotate = Value("**** ****")
50 self.authors_string_annotate = Value("**** ****")
51 self.authors_queryset = SubmissionAuthor.objects.none()
52 self.reviews_queryset = Review.objects.filter(reviewer=self.user)
53 self.versions_queryset = SubmissionVersion.objects.filter(reviews__reviewer=self.user)
54 self.submissions_queryset = Submission.objects.filter(
55 versions__reviews__reviewer=self.user
56 )
58 def _get_is_active(self):
59 return Review.objects.filter(reviewer=self.user).exists()
61 def get_submissions(self):
62 return self._annotate_submission_query(self.submissions_queryset).annotate(
63 user_is_editor=Value(False)
64 )
66 def can_access_submission(self, submission: Submission) -> bool:
67 return submission.versions.filter(reviews__reviewer=self.user).exists()
69 def can_access_reviews(self, version: SubmissionVersion) -> bool:
70 # Reviews should be censored anyways
71 return True
73 def can_access_review(self, submission, review) -> bool:
74 return review.reviewer == self.user
76 def can_access_review_file(self, submission, file: ReviewAdditionalFile) -> bool:
77 return self.can_access_review(submission, file.attached_to)
79 def can_access_review_details(self, submission, review: Review) -> bool:
80 return self.can_access_review(submission, review)
82 def get_current_open_review(self, version) -> Review | None:
83 return next(
84 (r for r in version.reviews_censored if r.version.review_open and not r.submitted),
85 None,
86 )
88 def can_edit_submission(self, submission: Submission) -> bool:
89 return False
91 def can_edit_review(self, submission, review: Review) -> bool:
92 return review.is_editable() and self.can_access_review(submission, review)
94 def can_submit_review(self, submission, review: Review) -> bool:
95 return review.is_submittable() and self.can_access_review(submission, review)
97 def can_access_version(self, version):
98 return version.reviews.all().filter(reviewer=self.user).exists()
100 def get_submission_status(self, submission: Submission) -> SubmissionStatusData:
101 """
102 For a reviewer, the submission status depends on the submission state and the
103 (optional) review for the submission's current version.
104 """
105 current_version = submission.get_current_version()
106 if not current_version:
107 return SubmissionStatusData(
108 submission=submission,
109 status=SubmissionStatus.ERROR,
110 description="Submission does not have a current version",
111 )
112 if not self.can_access_submission(submission):
113 return SubmissionStatusData(
114 submission=submission, status=SubmissionStatus.ERROR, description=""
115 )
116 if submission.state in [
117 SubmissionState.ACCEPTED.value,
118 SubmissionState.REJECTED.value,
119 ]:
120 return SubmissionStatusData(
121 submission=submission, status=SubmissionStatus.DONE, description=""
122 )
123 # Get the first review attached to the submission's current version.
124 # There is max. 1 review per user per version.
126 review = next((r for r in current_version.reviews_censored), None)
128 if not review or not current_version.review_open:
129 return SubmissionStatusData(
130 submission=submission,
131 status=SubmissionStatus.WAITING,
132 description=_("The round for your review is closed."),
133 )
134 elif review.submitted:
135 return SubmissionStatusData(
136 submission=submission,
137 status=SubmissionStatus.WAITING,
138 description=_("Review submitted"),
139 )
140 elif review.accepted is False:
141 return SubmissionStatusData(
142 submission=submission,
143 status=SubmissionStatus.DONE,
144 description=_("Review declined"),
145 )
146 elif review.accepted is None:
147 status_data = SubmissionStatusData(
148 submission=submission,
149 status=SubmissionStatus.TODO,
150 description=_("Accept or decline the review request.")
151 + " "
152 + _("Due date")
153 + f": {review.date_response_due.strftime('%Y-%m-%d')}",
154 # shortcut_actions=[
155 # Button(
156 # id=f"review-accept-{review.pk}",
157 # title=_("Accept"),
158 # icon_class="fa-check",
159 # attrs={
160 # "href": [
161 # add_query_parameters_to_url(
162 # reverse("mesh:review_accept", kwargs={"pk": review.pk}),
163 # {"accepted": ["true"]},
164 # )
165 # ],
166 # "class": ["as-button", "button-success"],
167 # },
168 # ),
169 # Button(
170 # id=f"review-accept-{review.pk}",
171 # title=_("Decline"),
172 # icon_class="fa-xmark",
173 # attrs={
174 # "href": [
175 # add_query_parameters_to_url(
176 # reverse("mesh:review_accept", kwargs={"pk": review.pk}),
177 # {"accepted": ["false"]},
178 # )
179 # ],
180 # "class": ["as-button", "button-error"],
181 # },
182 # ),
183 # ],
184 )
185 # status_data.shortcut_actions.append(status_data.default_action_button)
186 return status_data
187 # The review is accepted but not submitted yet;
188 else:
189 return SubmissionStatusData(
190 submission=submission,
191 status=SubmissionStatus.TODO,
192 description=_("Submit your review report.")
193 + " "
194 + _("Due date")
195 + f": {review.date_review_due.strftime('%Y-%m-%d')}",
196 # shortcut_actions=[
197 # Button(
198 # id=f"review-submit-{review.pk}",
199 # title=_("Submit review"),
200 # icon_class="fa-pen-to-square",
201 # attrs={
202 # "href": [reverse_lazy("mesh:review_update", kwargs={"pk": review.pk})],
203 # "class": ["as-button"],
204 # },
205 # )
206 # ],
207 )
209 def annotate_submission_status(self, qs):
210 current_version = SubmissionVersion.objects.filter(
211 submission=OuterRef(OuterRef("pk"))
212 ).values("pk")[:1]
214 my_review = Review.objects.filter(
215 reviewer=self.user,
216 version=Subquery(current_version),
217 )
219 qs = qs.annotate(
220 _review_exists=Exists(my_review),
221 _review_open=Subquery(my_review.values("version__review_open")[:1]),
222 _review_submitted=Subquery(my_review.values("submitted")[:1]),
223 _review_accepted=Subquery(my_review.values("accepted")[:1]),
224 )
225 return qs.annotate(
226 computed_status=Case(
227 When(
228 state__in=[
229 SubmissionState.ACCEPTED.value,
230 SubmissionState.REJECTED.value,
231 ],
232 then=Value(SubmissionStatus.DONE.value),
233 ),
234 When(
235 Q(_review_exists=False) | Q(_review_open=False),
236 then=Value(SubmissionStatus.WAITING.value),
237 ),
238 When(_review_submitted=True, then=Value(SubmissionStatus.WAITING.value)),
239 When(_review_accepted=False, then=Value(SubmissionStatus.DONE.value)),
240 default=Value(SubmissionStatus.TODO.value),
241 output_field=CharField(),
242 )
243 )