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

1from typing import ClassVar 

2 

3from django.db.models import Case, CharField, Exists, OuterRef, Q, Subquery, Value, When 

4 

5# from django.urls import reverse 

6# from django.urls import reverse_lazy 

7from django.utils.translation import gettext as _ 

8from opentelemetry import trace 

9 

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) 

18 

19# from ..views.components.button import Button 

20from mesh.models.roles.base_role import Role 

21from mesh.models.submission_status import SubmissionStatus, SubmissionStatusData 

22 

23# from ptf.url_utils import add_query_parameters_to_url 

24 

25tracer = trace.get_tracer(__name__) 

26 

27 

28class ReviewerRights: 

29 """ 

30 Rights implementation for reviewer role. 

31 """ 

32 

33 

34class Reviewer(Role): 

35 """ 

36 Reviewer role. 

37 """ 

38 

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 

44 

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 ) 

57 

58 def _get_is_active(self): 

59 return Review.objects.filter(reviewer=self.user).exists() 

60 

61 def get_submissions(self): 

62 return self._annotate_submission_query(self.submissions_queryset).annotate( 

63 user_is_editor=Value(False) 

64 ) 

65 

66 def can_access_submission(self, submission: Submission) -> bool: 

67 return submission.versions.filter(reviews__reviewer=self.user).exists() 

68 

69 def can_access_reviews(self, version: SubmissionVersion) -> bool: 

70 # Reviews should be censored anyways 

71 return True 

72 

73 def can_access_review(self, submission, review) -> bool: 

74 return review.reviewer == self.user 

75 

76 def can_access_review_file(self, submission, file: ReviewAdditionalFile) -> bool: 

77 return self.can_access_review(submission, file.attached_to) 

78 

79 def can_access_review_details(self, submission, review: Review) -> bool: 

80 return self.can_access_review(submission, review) 

81 

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 ) 

87 

88 def can_edit_submission(self, submission: Submission) -> bool: 

89 return False 

90 

91 def can_edit_review(self, submission, review: Review) -> bool: 

92 return review.is_editable() and self.can_access_review(submission, review) 

93 

94 def can_submit_review(self, submission, review: Review) -> bool: 

95 return review.is_submittable() and self.can_access_review(submission, review) 

96 

97 def can_access_version(self, version): 

98 return version.reviews.all().filter(reviewer=self.user).exists() 

99 

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. 

125 

126 review = next((r for r in current_version.reviews_censored), None) 

127 

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 ) 

208 

209 def annotate_submission_status(self, qs): 

210 current_version = SubmissionVersion.objects.filter( 

211 submission=OuterRef(OuterRef("pk")) 

212 ).values("pk")[:1] 

213 

214 my_review = Review.objects.filter( 

215 reviewer=self.user, 

216 version=Subquery(current_version), 

217 ) 

218 

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 )