Coverage for src/mesh/model/roles/reviewer.py: 92%

73 statements  

« prev     ^ index     » next       coverage.py v7.7.0, created at 2025-04-28 07:45 +0000

1from functools import cached_property 

2from typing import ClassVar 

3 

4from django.db.models import QuerySet 

5 

6# from django.urls import reverse 

7# from django.urls import reverse_lazy 

8from django.utils.translation import gettext as _ 

9 

10from mesh.app_settings import BlindMode, app_settings 

11from mesh.models.review_models import Review, ReviewAdditionalFile 

12from mesh.models.submission_models import Submission, SubmissionState, SubmissionVersion 

13from mesh.models.user_models import User 

14 

15from ..submission_status import SubmissionStatus, SubmissionStatusData 

16 

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

18from .base_role import Role, RoleRights 

19 

20# from ptf.url_utils import add_query_parameters_to_url 

21 

22 

23class ReviewerRights(RoleRights): 

24 """ 

25 Rights implementation for reviewer role. 

26 """ 

27 

28 @cached_property 

29 def reviews(self) -> QuerySet[Review]: 

30 return Review.objects.filter(reviewer=self.user) 

31 

32 @cached_property 

33 def submissions(self) -> QuerySet[Submission]: 

34 return ( 

35 Submission.objects.get_submissions() 

36 .filter(versions__reviews__in=self.reviews) 

37 .distinct() 

38 ) 

39 

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

41 return submission in self.submissions 

42 

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

44 return self.can_access_version(version) 

45 

46 def can_access_review(self, review: Review) -> bool: 

47 return review in self.reviews 

48 

49 def can_access_review_author(self, review: Review) -> bool: 

50 return self.can_access_review(review) 

51 

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

53 return self.can_access_review(file.attached_to) 

54 

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

56 return self.can_access_review(review) 

57 

58 def get_current_open_review(self, version: SubmissionVersion | None) -> Review | None: 

59 if not version: 59 ↛ 60line 59 didn't jump to line 60 because the condition on line 59 was never true

60 return None 

61 

62 current_reviews = [ 

63 r 

64 for r in self.reviews 

65 if r.version == version and r.version.review_open and not r.submitted 

66 ] 

67 if current_reviews: 

68 return current_reviews[0] 

69 return None 

70 

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

72 return review.is_editable and self.can_access_review(review) 

73 

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

75 return review.is_submittable and self.can_access_review(review) 

76 

77 def can_access_submission_author(self, submission: Submission) -> bool: 

78 return ( 

79 self.can_access_submission(submission) 

80 and app_settings.BLIND_MODE != BlindMode.DOUBLE_BLIND 

81 ) 

82 

83 def can_access_version(self, version: SubmissionVersion) -> bool: 

84 return version in [r.version for r in self.reviews] 

85 

86 def get_submission_status(self, submission: Submission) -> SubmissionStatusData: 

87 """ 

88 For a reviewer, the submission status depends on the submission state and the 

89 (optional) review for the submission's current version. 

90 """ 

91 if not self.can_access_submission(submission): 

92 return SubmissionStatusData( 

93 submission=submission, status=SubmissionStatus.ERROR, description="" 

94 ) 

95 if submission.state in [ 

96 SubmissionState.ACCEPTED.value, 

97 SubmissionState.REJECTED.value, 

98 ]: 

99 return SubmissionStatusData( 

100 submission=submission, status=SubmissionStatus.DONE, description="" 

101 ) 

102 # Get the first review attached to the submission's current version. 

103 # There is max. 1 review per user per version. 

104 review = next((r for r in self.reviews if r.version == submission.current_version), None) 

105 if not review or not submission.current_version.review_open: # type:ignore 

106 return SubmissionStatusData( 

107 submission=submission, 

108 status=SubmissionStatus.WAITING, 

109 description=_("The round for your review is closed."), 

110 ) 

111 elif review.submitted: 

112 return SubmissionStatusData( 

113 submission=submission, 

114 status=SubmissionStatus.WAITING, 

115 description=_("Review submitted"), 

116 ) 

117 elif review.accepted is False: 

118 return SubmissionStatusData( 

119 submission=submission, 

120 status=SubmissionStatus.DONE, 

121 description=_("Review declined"), 

122 ) 

123 elif review.accepted is None: 

124 status_data = SubmissionStatusData( 

125 submission=submission, 

126 status=SubmissionStatus.TODO, 

127 description=_("Accept or decline the review request.") 

128 + " " 

129 + _("Due date") 

130 + f": {review.date_response_due.strftime('%Y-%m-%d')}", 

131 # shortcut_actions=[ 

132 # Button( 

133 # id=f"review-accept-{review.pk}", 

134 # title=_("Accept"), 

135 # icon_class="fa-check", 

136 # attrs={ 

137 # "href": [ 

138 # add_query_parameters_to_url( 

139 # reverse("mesh:review_accept", kwargs={"pk": review.pk}), 

140 # {"accepted": ["true"]}, 

141 # ) 

142 # ], 

143 # "class": ["as-button", "button-success"], 

144 # }, 

145 # ), 

146 # Button( 

147 # id=f"review-accept-{review.pk}", 

148 # title=_("Decline"), 

149 # icon_class="fa-xmark", 

150 # attrs={ 

151 # "href": [ 

152 # add_query_parameters_to_url( 

153 # reverse("mesh:review_accept", kwargs={"pk": review.pk}), 

154 # {"accepted": ["false"]}, 

155 # ) 

156 # ], 

157 # "class": ["as-button", "button-error"], 

158 # }, 

159 # ), 

160 # ], 

161 ) 

162 # status_data.shortcut_actions.append(status_data.default_action_button) 

163 return status_data 

164 # The review is accepted but not submitted yet; 

165 else: 

166 return SubmissionStatusData( 

167 submission=submission, 

168 status=SubmissionStatus.TODO, 

169 description=_("Submit your review report.") 

170 + " " 

171 + _("Due date") 

172 + f": {review.date_review_due.strftime('%Y-%m-%d')}", 

173 # shortcut_actions=[ 

174 # Button( 

175 # id=f"review-submit-{review.pk}", 

176 # title=_("Submit review"), 

177 # icon_class="fa-pen-to-square", 

178 # attrs={ 

179 # "href": [reverse_lazy("mesh:review_update", kwargs={"pk": review.pk})], 

180 # "class": ["as-button"], 

181 # }, 

182 # ) 

183 # ], 

184 ) 

185 

186 

187class Reviewer(Role): 

188 """ 

189 Reviewer role. 

190 """ 

191 

192 _CODE: ClassVar[str] = "reviewer" 

193 _NAME: ClassVar[str] = _("Reviewer") 

194 _ICON_CLASS: ClassVar[str] = "fa-user-group" 

195 _SUBMISSION_LIST_TITLE: ClassVar[str] = _("My assignments") 

196 rights: ReviewerRights 

197 

198 def __init__(self, user: User) -> None: 

199 super().__init__(user) 

200 

201 @property 

202 def active(self) -> bool: 

203 return len(self.rights.reviews) > 0 

204 

205 def get_rights(self) -> ReviewerRights: 

206 return ReviewerRights(self.user)