Coverage for src / mesh / views / views_editorial.py: 61%

181 statements  

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

1import json 

2from typing import Any 

3 

4from django.contrib import messages 

5from django.contrib.auth.mixins import LoginRequiredMixin 

6from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse 

7from django.shortcuts import render 

8from django.urls import reverse 

9from django.utils.translation import gettext_lazy as _ 

10from django.views.generic import CreateView, FormView, TemplateView, UpdateView, View 

11 

12from mesh.models.file_helpers import post_delete_model_file 

13from mesh.models.orm.editorial_models import EditorialDecision, EditorSubmissionRight 

14from mesh.models.orm.submission_models import Submission, SubmissionLog 

15from mesh.models.orm.user_models import User 

16from mesh.models.roles.editor import get_section_editors 

17from mesh.views.components.breadcrumb import get_submission_breadcrumb 

18from mesh.views.components.button import Button 

19from mesh.views.forms.editorial_forms import ( 

20 EditorialDecisionCreateForm, 

21 EditorialDecisionUpdateForm, 

22 StartReviewProcessForm, 

23) 

24from mesh.views.forms.user_forms import UserForm 

25from mesh.views.utils import assign_editor, remove_editor 

26from mesh.views.views_base import MeshObjectMixin 

27 

28 

29class SendToReviewView(LoginRequiredMixin, MeshObjectMixin, FormView): 

30 """ 

31 View to send a submission with its curent version to the Review process. 

32 

33 It sets the version's boolean `review_open = True` and switch the submission 

34 state to `on_review`. 

35 """ 

36 

37 submission: Submission 

38 # restricted_roles = [Editor, JournalManager] 

39 form_class = StartReviewProcessForm 

40 

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

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

43 self.submission = self.get_submission() 

44 if not self.request.current_role.can_start_review_process(self.submission): 

45 raise PermissionError 

46 

47 def get_success_url(self) -> str: 

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

49 

50 def form_valid(self, form: Any) -> HttpResponse: 

51 self.submission.start_review_process(self.request.user) 

52 messages.success(self.request, _("The submission has been sent to review.")) 

53 return super().form_valid(form) 

54 

55 

56class EditorialDecisionCreateView(LoginRequiredMixin, MeshObjectMixin, CreateView): 

57 """ 

58 View for creating an editorial decision. 

59 """ 

60 

61 model = EditorialDecision 

62 form_class = EditorialDecisionCreateForm 

63 template_name = "mesh/forms/form_full_page.html" 

64 # restricted_roles = [JournalManager, Editor] 

65 submission: Submission 

66 

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

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

69 self.submission = self.get_submission() 

70 if not self.request.current_role.can_create_editorial_decision(self.submission): 

71 raise PermissionError 

72 

73 def get_success_url(self) -> str: 

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

75 

76 def form_valid(self, form) -> HttpResponse: 

77 form.instance._user = self.request.user 

78 form.instance.version = self.submission.get_current_version() 

79 

80 response = super().form_valid(form) 

81 

82 self.submission.apply_editorial_decision(self.object, self.request.user) 

83 

84 messages.success(self.request, _("Editorial decision successfully submitted")) 

85 

86 return response 

87 

88 def get_context_data(self, *args, **kwargs): 

89 context = super().get_context_data(*args, **kwargs) 

90 

91 context["page_title"] = _("New editorial decision") 

92 

93 submission_url = reverse( 

94 "mesh:submission_details", kwargs={"submission_pk": self.submission.pk} 

95 ) 

96 description = "Please fill the form below to submit your editorial decision " 

97 description += ( 

98 f"for the submission <a href='{submission_url}'><i>{self.submission.name}</i></a>." 

99 ) 

100 context["form_description"] = [description] 

101 

102 # Breadcrumnb 

103 breadcrumb = get_submission_breadcrumb(self.submission) 

104 breadcrumb.add_item( 

105 title=_("New editorial decision"), 

106 url=reverse( 

107 "mesh:editorial_decision_create", 

108 kwargs={"submission_pk": self.submission.pk}, 

109 ), 

110 ) 

111 context["breadcrumb"] = breadcrumb 

112 

113 return context 

114 

115 

116class EditorialDecisionUpdateView(LoginRequiredMixin, MeshObjectMixin, UpdateView): 

117 """ 

118 View for updating an existing editorial decision. 

119 """ 

120 

121 model = EditorialDecision 

122 form_class = EditorialDecisionUpdateForm 

123 template_name = "mesh/forms/form_full_page.html" 

124 # restricted_roled = [JournalManager, Editor] 

125 decision: EditorialDecision 

126 

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

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

129 self.decision = self.get_decision() 

130 if not self.request.current_role.can_edit_editorial_decision(self.decision): 

131 raise PermissionError 

132 

133 def get_object(self, *args, **kwargs) -> EditorialDecision: 

134 return self.decision 

135 

136 def get_success_url(self) -> str: 

137 return reverse( 

138 "mesh:submission_details", 

139 kwargs={"submission_pk": self.decision.version.submission.pk}, 

140 ) 

141 

142 def form_valid(self, form: EditorialDecisionUpdateForm) -> HttpResponse: 

143 form.instance._user = self.request.user 

144 if not form.changed_data: 

145 form.instance.do_not_update = True 

146 

147 response = super().form_valid(form) 

148 

149 decision_str = self.decision.get_value_display() # type: ignore 

150 

151 if form.has_changed(): 

152 SubmissionLog.add_message( 

153 self.decision.version.submission, 

154 _("Editorial decision updated") + f": {decision_str}", 

155 f"Editorial decision updated: {decision_str}", 

156 user=self.request.user, 

157 ) 

158 messages.success(self.request, _("Editorial decision successfully updated")) 

159 

160 return response 

161 

162 def get_context_data(self, *args, **kwargs): 

163 context = super().get_context_data(*args, **kwargs) 

164 

165 context["page_title"] = _("Edit editorial decision") 

166 

167 submission_url = reverse( 

168 "mesh:submission_details", 

169 kwargs={"submission_pk": self.decision.version.submission.pk}, 

170 ) 

171 description = "Please fill the form below to edit your editorial decision " 

172 description += f"for the submission <a href='{submission_url}'><i>{self.decision.version.submission.name}</i></a>." 

173 context["form_description"] = [description] 

174 

175 # Breadcrumnb 

176 breadcrumb = get_submission_breadcrumb(self.decision.version.submission) 

177 breadcrumb.add_item( 

178 title=_("New editorial decision"), 

179 url=reverse( 

180 "mesh:editorial_decision_create", 

181 kwargs={"submission_pk": self.decision.version.submission.pk}, 

182 ), 

183 ) 

184 context["breadcrumb"] = breadcrumb 

185 

186 return context 

187 

188 def post(self, request: HttpRequest, *args: str, **kwargs: Any) -> HttpResponse: 

189 """ 

190 View for updating the editorial decision or deleting one of 

191 the associated files. 

192 """ 

193 deletion_requested, _ = post_delete_model_file( 

194 self.decision, request, self.request.current_role 

195 ) 

196 

197 if deletion_requested: 

198 # Update the submission object. 

199 self.decision = self.get_decision() 

200 return self.get(request, *args, **kwargs) 

201 

202 return super().post(request, *args, **kwargs) 

203 

204 

205class AssignEditorView(LoginRequiredMixin, MeshObjectMixin, TemplateView): 

206 """ 

207 View to add/remove editors assigned to a given submission. 

208 """ 

209 

210 submission: Submission 

211 template_name = "mesh/forms/assign_editor.html" 

212 

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

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

215 self.submission = self.get_submission() 

216 if not self.request.current_role.can_assign_editor(self.submission): 

217 raise PermissionError 

218 

219 def get_context_data(self, *args, **kwargs): 

220 """ 

221 2 models are used to compute editor rights over a submission. 

222 Section rights & Submission rights. 

223 This view will display all editors having right over the given submission. 

224 """ 

225 context = super().get_context_data(*args, **kwargs) 

226 context["submission"] = self.submission 

227 

228 direct_editors = User.objects.filter(editor_submissions__submission=self.submission) 

229 context["assigned_editors"] = [ 

230 { 

231 "user": editor, 

232 "buttons": [ 

233 Button( 

234 id=f"remove-editor-{editor.pk}", 

235 title=_("Remove"), 

236 icon_class="fa-trash", 

237 form=UserForm( 

238 _users=direct_editors, 

239 _hide_user=True, 

240 initial={"user": editor}, 

241 ), 

242 attrs={ 

243 "href": [ 

244 reverse( 

245 "mesh:submission_editors", 

246 kwargs={"pk": self.submission.pk}, 

247 ) 

248 ], 

249 "class": ["button-error"], 

250 "type": ["submit"], 

251 "name": ["_action_remove"], 

252 }, 

253 ) 

254 ], 

255 } 

256 for editor in direct_editors 

257 ] 

258 

259 excluded_user_pks = [e.pk for e in direct_editors] 

260 context["form"] = UserForm( 

261 _users=User.objects.exclude(pk__in=excluded_user_pks), _user_label=_("User") 

262 ) 

263 

264 section_editors = [] 

265 for editor in get_section_editors(self.submission): 

266 editor_data: dict[str, Any] = {"user": editor, "buttons": []} 

267 if editor not in direct_editors: 

268 editor_data["buttons"].append( 

269 Button( 

270 id=f"assign-editor-{editor.pk}", 

271 title=_("Assign"), 

272 icon_class="fa-plus", 

273 form=UserForm( 

274 _users=direct_editors, 

275 _hide_user=True, 

276 initial={"user": editor}, 

277 ), 

278 attrs={ 

279 "href": [ 

280 reverse( 

281 "mesh:submission_editors", 

282 kwargs={"pk": self.submission.pk}, 

283 ) 

284 ], 

285 "type": ["submit"], 

286 }, 

287 ) 

288 ) 

289 else: 

290 editor_data["buttons"].append( 

291 Button( 

292 id=f"assigned-editor-{editor.pk}", 

293 title=_("Assigned"), 

294 icon_class="fa-check", 

295 attrs={"class": ["inactive"]}, 

296 ) 

297 ) 

298 section_editors.append(editor_data) 

299 context["section_editors"] = section_editors 

300 

301 context["form_save_title"] = _("Add editor") 

302 context["form_save_icon"] = "fa-plus" 

303 context["page_title"] = _("Assigned editors") 

304 

305 # Breadcrumnb 

306 breadcrumb = get_submission_breadcrumb(self.submission) 

307 breadcrumb.add_item( 

308 title=_("Assigned editors"), 

309 url=reverse("mesh:submission_editors", kwargs={"submission_pk": self.submission.pk}), 

310 ) 

311 context["breadcrumb"] = breadcrumb 

312 

313 return context 

314 

315 def post(self, request: HttpRequest, *args, **kwargs): 

316 remove_form = "_action_remove" in request.POST 

317 if remove_form: 

318 users = User.objects.filter(editor_submissions__submission=self.submission) 

319 else: 

320 users = User.objects.exclude(editor_submissions__submission=self.submission) 

321 

322 response = HttpResponseRedirect( 

323 reverse("mesh:submission_details", kwargs={"submission_pk": self.submission.pk}) 

324 ) 

325 form = UserForm(request.POST, _users=users) 

326 if not form.is_valid(): 

327 messages.error(request, "ERROR") 

328 return response 

329 

330 user = form.cleaned_data["user"] 

331 

332 if remove_form: 

333 EditorSubmissionRight.objects.get(submission=self.submission, user=user).delete() 

334 messages.success( 

335 request, 

336 _(f"{user} was successfully removed from the assigned editors of the submission."), 

337 ) 

338 SubmissionLog.add_message( 

339 self.submission, 

340 content=_(f"{user} removed from the assigned editors."), 

341 content_en=f"{user} removed from the assigned editors.", 

342 user=self.request.user, 

343 significant=True, 

344 ) 

345 else: 

346 EditorSubmissionRight.objects.create(submission=self.submission, user=user) 

347 messages.success( 

348 request, 

349 _(f"{user} was successfully assigned as an editor for the submission."), 

350 ) 

351 SubmissionLog.add_message( 

352 self.submission, 

353 content=_(f"{user} assigned as an editor."), 

354 content_en=f"{user} assigned as an editor.", 

355 user=self.request.user, 

356 significant=True, 

357 ) 

358 

359 return response 

360 

361 

362class AssignEditorAPIView(LoginRequiredMixin, MeshObjectMixin, View): 

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

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

365 self.submission = self.get_submission() 

366 if not self.request.current_role.can_assign_editor(self.submission): 

367 raise PermissionError 

368 

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

370 users = User.objects.all() 

371 editors = User.objects.filter(editor_submissions__submission=self.submission) 

372 for user in users: 

373 if user in editors: 

374 user.selected = True 

375 

376 return render( 

377 request, 

378 "mesh/forms/assign_editor_modal.html", 

379 {"users": users, "submission_proxy_pk": kwargs["submission_pk"]}, 

380 ) 

381 

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

383 existing_pks = User.objects.filter( 

384 editor_submissions__submission=self.submission 

385 ).values_list("pk", flat=True) 

386 

387 editors_data = request.POST.get("select-editors", "") 

388 editors = json.loads(editors_data) 

389 new_pks = [int(pk) for pk in editors] 

390 

391 for pk in existing_pks: 

392 if pk not in new_pks: 

393 user = User.objects.get(pk=pk) 

394 remove_editor(self.submission, user, self.request.user) 

395 

396 for pk in new_pks: 

397 if pk not in existing_pks: 

398 user = User.objects.get(pk=pk) 

399 assign_editor(self.submission, user, self.request.user) 

400 

401 return JsonResponse({"message": "OK"})