Coverage for src / mesh / views / views_reviewer.py: 59%

182 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-05-04 12:41 +0000

1from typing import Any 

2 

3from django.contrib import messages 

4from django.contrib.auth.mixins import LoginRequiredMixin 

5from django.db.models import Exists, OuterRef 

6from django.http import JsonResponse 

7from django.shortcuts import get_object_or_404 

8from django.template.loader import render_to_string 

9from django.urls import reverse, reverse_lazy 

10from django.utils.decorators import method_decorator 

11from django.views.decorators.csrf import csrf_exempt 

12from django.views.generic.base import TemplateView 

13from django.views.generic.edit import CreateView, DeleteView, UpdateView 

14 

15from mesh.models.orm.review_models import Review 

16from mesh.models.orm.submission_models import Submission 

17from mesh.models.orm.suggestion_model import Suggestion 

18from mesh.models.orm.user_models import SuggestedReviewer, User 

19from mesh.views.views_base import MeshObjectMixin 

20 

21from .forms.reviewer_forms import SimpleReviewerForm, SuggestedReviewerForm 

22from .utils import get_suggestion 

23 

24 

25def busy(reviewer): 

26 return reviewer.pending > 0 

27 

28 

29class ReviewerListView(LoginRequiredMixin, CreateView): 

30 """ 

31 View for the list of reviewers and suggested reviewers. 

32 A reviewer is a user with an account (which might have been created by an editor with Assign Reviewer), 

33 or a SuggestedReviewer (a person which does not have an account yet) 

34 This view can be called for all the reviewers in the system, or to create a shortlist for a submission 

35 """ 

36 

37 model = SuggestedReviewer 

38 template_name = "mesh/reviewer/reviewer_list_page.html" 

39 form_class = SuggestedReviewerForm 

40 

41 def setup(self, request, *args, **kwargs): 

42 super().setup(request, *args, **kwargs) 

43 if not self.request.current_role.can_access_journal_sections(): 

44 raise PermissionError 

45 

46 def get_success_url(self): 

47 return reverse("mesh:reviewer_list") 

48 

49 def set_success_message(self): 

50 # messages.success(self.request, "Le fascicule a été modifié") 

51 pass 

52 

53 def get_form_kwargs(self): 

54 kwargs = super().get_form_kwargs() 

55 # Update self.kwargs if needed 

56 return kwargs 

57 

58 def get_context_data(self, **kwargs) -> dict[str, Any]: 

59 context = super().get_context_data(**kwargs) 

60 

61 # User.objects.filter(review__isnull=False) is not equivalent to User.objects.exclude(review__isnull=True) 

62 # filter(review__isnull=False) does a simple INNER JOIN and you will get duplicates 

63 # adding a .distinct() would work but is less efficient 

64 subquery = Review.objects.filter(reviewer=OuterRef("pk")).order_by().values("reviewer") 

65 user_reviewers = User.objects.filter(Exists(subquery)) 

66 

67 reviewers = [] 

68 for reviewer in user_reviewers: 

69 reviewer.type = "user" 

70 reviewer.total = reviewer.review_set.count() 

71 reviewer.pending = reviewer.review_set.exclude( 

72 recommendation__isnull=False, accepted__isnull=False 

73 ).count() 

74 reviewer.accepted = reviewer.review_set.filter(accepted=True).count() 

75 reviewer.declined = reviewer.review_set.filter(accepted=False).count() 

76 reviewers.append(reviewer) 

77 

78 for reviewer in SuggestedReviewer.objects.all(): 

79 reviewer.type = "reviewer" 

80 reviewers.append(reviewer) 

81 context["reviewers"] = reviewers 

82 

83 return context 

84 

85 def form_valid(self, form): 

86 # Create/Update a SuggestedReviewer 

87 self.set_success_message() 

88 result = super().form_valid(form) 

89 

90 return result 

91 

92 def form_invalid(self, form): 

93 for key, value in form.errors.items(): 

94 messages.error(self.request, str(value[0])) 

95 return super().form_invalid(form) 

96 

97 

98class ReviewerDeleteView(LoginRequiredMixin, DeleteView): 

99 model = SuggestedReviewer 

100 success_url = reverse_lazy("mesh:reviewer_list") 

101 

102 def setup(self, request, *args, **kwargs): 

103 super().setup(request, *args, **kwargs) 

104 if not self.request.current_role.can_access_journal_sections(): 

105 raise PermissionError 

106 

107 

108class ReviewerEditAPIView(LoginRequiredMixin, UpdateView): 

109 """ 

110 Modal to add a new (suggested) reviewer, used in the ReviewerListView 

111 """ 

112 

113 model = SuggestedReviewer 

114 template_name = "mesh/reviewer/suggested_reviewer_modal.html" 

115 form_class = SuggestedReviewerForm 

116 success_url = reverse_lazy("mesh:home") 

117 

118 def setup(self, request, *args, **kwargs): 

119 super().setup(request, *args, **kwargs) 

120 if not self.request.current_role.can_access_journal_sections(): 

121 raise PermissionError 

122 

123 def get_context_data(self, **kwargs) -> dict[str, Any]: 

124 context = super().get_context_data(**kwargs) 

125 context["row_number"] = self.kwargs.get("row_number", -1) 

126 context["user_type"] = self.kwargs.get("user_type", "reviewer") 

127 return context 

128 

129 def get(self, request, *args, **kwargs): 

130 user_type = self.kwargs.get("user_type", "reviewer") 

131 if user_type == "user": 

132 self.form_class = SimpleReviewerForm 

133 self.model = User 

134 

135 response = super().get(request, *args, **kwargs) 

136 return response 

137 

138 def post(self, request, *args, **kwargs): 

139 user_type = self.kwargs.get("user_type", "reviewer") 

140 if user_type == "user": 

141 self.form_class = SimpleReviewerForm 

142 self.model = User 

143 

144 super().post(request, *args, **kwargs) 

145 if user_type == "user": 

146 reviewer = get_object_or_404(User, pk=kwargs["pk"]) 

147 else: 

148 reviewer = get_object_or_404(SuggestedReviewer, pk=kwargs["pk"]) 

149 row_number = self.kwargs.get("row_number", -1) 

150 

151 the_messages = messages.get_messages(request) 

152 message_html = ( 

153 render_to_string( 

154 "mesh/messages.html", {"messages": messages.get_messages(request)}, request=request 

155 ) 

156 if len(the_messages) > 0 

157 else "" 

158 ) 

159 

160 return JsonResponse( 

161 { 

162 "row_data": [ 

163 reviewer.first_name, 

164 reviewer.last_name, 

165 reviewer.email, 

166 reviewer.keywords, 

167 ], 

168 "row_number": row_number, 

169 "messages": message_html, 

170 } 

171 ) 

172 

173 def form_valid(self, form): 

174 return super().form_valid(form) 

175 

176 def form_invalid(self, form): 

177 for key, value in form.errors.items(): 

178 messages.error(self.request, str(value[0])) 

179 return super().form_invalid(form) 

180 

181 

182def add_suggestion(submission, suggested_user, suggested_reviewer, suggest_to_avoid=False): 

183 qs = submission.suggestions_for_reviewer.order_by("-seq") 

184 seq = qs.first().seq + 1 if qs.exists() else 0 

185 

186 suggestion = Suggestion( 

187 submission=submission, 

188 suggested_user=suggested_user, 

189 suggested_reviewer=suggested_reviewer, 

190 suggest_to_avoid=suggest_to_avoid, 

191 seq=seq, 

192 ) 

193 suggestion.save() 

194 

195 return suggestion 

196 

197 

198def add_suggestion_from_person(submission, person_info, suggest_to_avoid): 

199 if submission is not None and person_info is not None and person_info["email"] != "": 

200 suggested_user = suggested_reviewer = None 

201 qs = User.objects.filter(email=person_info["email"]) 

202 if qs.exists(): 

203 suggested_user = qs.first() 

204 else: 

205 qs = SuggestedReviewer.objects.filter(email=person_info["email"]) 

206 if qs.exists(): 

207 suggested_reviewer = qs.first() 

208 else: 

209 suggested_reviewer = SuggestedReviewer( 

210 first_name=person_info["first_name"], 

211 last_name=person_info["last_name"], 

212 email=person_info["email"], 

213 ) 

214 suggested_reviewer.save() 

215 

216 add_suggestion( 

217 submission=submission, 

218 suggested_user=suggested_user, 

219 suggested_reviewer=suggested_reviewer, 

220 suggest_to_avoid=suggest_to_avoid, 

221 ) 

222 

223 

224@method_decorator([csrf_exempt], name="dispatch") 

225class SuggestionView(MeshObjectMixin, ReviewerListView): 

226 """ 

227 Page to add/delete reviewers AND to add/remove reviewers to a shortlist (= Suggestion) 

228 """ 

229 

230 template_name = "mesh/reviewer/suggestion_page.html" 

231 submission: Submission 

232 

233 def setup(self, request, *args, **kwargs): 

234 super().setup(request, *args, **kwargs) 

235 if not self.request.current_role.can_access_journal_sections(): 

236 raise PermissionError 

237 self.submission = self.get_submission() 

238 

239 def get_success_url(self): 

240 return reverse("mesh:submission_shortlist", kwargs={"submission_pk": self.submission.pk}) 

241 

242 def get_context_data(self, **kwargs): 

243 context = super().get_context_data(**kwargs) 

244 # if "submission" in request.POST: 

245 # self.submission = Submission.objects.get(pk=int(kwargs.get"submission"])) 

246 

247 context["submission"] = self.submission 

248 context["selected_reviewers"] = get_suggestion(self.submission) 

249 

250 return context 

251 

252 def form_valid(self, form): 

253 result = super().form_valid(form) 

254 

255 # Adding a suggested reviewer to a shortlist 

256 # (the Add/Edit shortlist button was pressed in the Assign Reviewer page) 

257 # The submission object was passed as a hidden value in the form 

258 # submission = Submission.objects.get(pk=int(form.data["submission"])) 

259 add_suggestion( 

260 submission=self.submission, suggested_user=None, suggested_reviewer=form.instance 

261 ) 

262 

263 return result 

264 

265 

266@method_decorator([csrf_exempt], name="dispatch") 

267class SuggestionAPIView(MeshObjectMixin, TemplateView): 

268 """ 

269 API to add/remove users to a shortlist. It ends up adding or removing a Suggestion. 

270 """ 

271 

272 template_name = "mesh/reviewer/shortlist_content.html" 

273 

274 def setup(self, request, *args, **kwargs): 

275 super().setup(request, *args, **kwargs) 

276 if not self.request.current_role.can_access_journal_sections(): 

277 raise PermissionError 

278 self.submission = self.get_submission() 

279 self.user_type = self.kwargs.get("user_type", "reviewer") 

280 

281 def add_suggestion(self): 

282 if not self.qs.exists(): 

283 suggested_user = suggested_reviewer = None 

284 

285 if self.user_type == "user": 

286 suggested_user = self.reviewer 

287 else: 

288 suggested_reviewer = self.reviewer 

289 

290 add_suggestion( 

291 submission=self.submission, 

292 suggested_user=suggested_user, 

293 suggested_reviewer=suggested_reviewer, 

294 ) 

295 

296 def remove_suggestion(self): 

297 suggestion = self.qs.first() 

298 if suggestion: 

299 suggestion.delete() 

300 

301 def post(self, request, *args, **kwargs): 

302 if self.user_type == "user": 

303 self.reviewer = get_object_or_404(User, pk=kwargs["user_pk"]) 

304 self.qs = Suggestion.objects.filter( 

305 submission=self.submission, suggested_user=self.reviewer 

306 ) 

307 else: 

308 self.reviewer = get_object_or_404(SuggestedReviewer, pk=kwargs["user_pk"]) 

309 self.qs = Suggestion.objects.filter( 

310 submission=self.submission, suggested_reviewer=self.reviewer 

311 ) 

312 

313 if self.kwargs.get("action", "add") == "add": 

314 self.add_suggestion() 

315 else: 

316 self.remove_suggestion() 

317 

318 return super().get(request, *args, **kwargs) 

319 

320 def get_context_data(self, **kwargs): 

321 context = super().get_context_data(**kwargs) 

322 context["submission"] = self.submission 

323 context["reviewer"] = self.reviewer 

324 context["selected_reviewers"] = get_suggestion(self.submission) 

325 

326 return context