Coverage for src/mesh/views/views_submission.py: 49%

166 statements  

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

1from collections.abc import Iterable 

2from typing import TYPE_CHECKING, Any 

3 

4from django.http import HttpRequest, JsonResponse 

5from django.shortcuts import get_object_or_404, render 

6from django.urls import reverse, reverse_lazy 

7from django.utils.decorators import method_decorator 

8from django.utils.translation import gettext_lazy as _ 

9from django.views.decorators.csrf import csrf_exempt 

10from django.views.generic import TemplateView, View 

11from ptf.url_utils import format_url_with_params 

12 

13from ..app_settings import app_settings 

14from ..model.filters import FieldGetter, Filter, FilterSet 

15from ..model.roles.editor import Editor 

16from ..model.roles.journal_manager import JournalManager 

17from ..model.roles.role_handler import RoleHandler 

18from ..models.journal_models import JournalSection 

19from ..models.submission_models import Submission, SubmissionLog, SubmissionState 

20from ..views.mixins import RoleMixin 

21from .components.breadcrumb import get_base_breadcrumb, get_submission_breadcrumb 

22from .components.ckeditor_config import sanitize_html_input 

23from .components.review_summary import CountWithTotal 

24from .components.submission_list import ( 

25 SubmissionListEnum, 

26 get_all_submission_list_config, 

27 get_done_submission_list_config, 

28 get_submission_by_state_config, 

29 get_submission_list_config, 

30) 

31from .model_proxy import BuildSubmissionProxyVisitor 

32from .utils import group_by 

33 

34if TYPE_CHECKING: 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true

35 from mesh.views.components.submission_list import SubmissionListConfig 

36 

37 

38def get_submission_message_if_no_actions(submission, role_handler): 

39 """ 

40 An author can not do anything if the submission is under review. 

41 Instead, we display a "thank you" message 

42 """ 

43 

44 html_message = "" 

45 if not role_handler.check_rights("can_edit_submission", submission): 

46 contact_email = app_settings.JOURNAL_EMAIL_CONTACT 

47 html_message = f""" 

48<p>Thank you for submitting your article.</p> 

49We will review your submission and get back to you. You can contact us at {contact_email}""" 

50 

51 return html_message 

52 

53 

54class SubmissionDetailsView(RoleMixin, TemplateView): 

55 """ 

56 View for a single submission. 

57 

58 The context data is populated with all the available actions to the user 

59 regarding the submission. 

60 """ 

61 

62 template_name = "mesh/submission/submission_details.html" 

63 submission: Submission 

64 

65 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs): 

66 # TODO: Should we prefetch all data for an unique instance ? 

67 # It would result in the same amount of queries but, with prefetch, they would 

68 # all be made at the same time. 

69 self.submission = get_object_or_404(Submission, pk=kwargs["pk"]) 

70 return not self.role_handler.check_rights("can_access_submission", self.submission) 

71 

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

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

74 

75 builder = BuildSubmissionProxyVisitor(self.role_handler) 

76 

77 context["submission_proxy"] = self.role_handler.current_role.accept( 

78 builder, 

79 self.submission, 

80 display_as_btn=False, 

81 reload_page=True, 

82 shortlist=False, 

83 ) 

84 

85 # editorial_decisions = ( 

86 # self.submission.editorial_decisions.all().order_by( # type:ignore 

87 # "-date_created" 

88 # ) 

89 # ) 

90 # 

91 # context["editorial_decisions"] = editorial_decisions 

92 

93 # review_form = StartReviewProcessForm(initial={"process": True}) 

94 # context["action_lists"] = build_submission_actions( 

95 # self.submission, self.role_handler, review_form 

96 # ) 

97 context["message_if_no_actions"] = get_submission_message_if_no_actions( 

98 self.submission, self.role_handler 

99 ) 

100 

101 # # Generate breadcrumb data 

102 # context["breadcrumb"] = get_submission_breadcrumb(self.submission) 

103 return context 

104 

105 

106def submission_list_filters() -> FilterSet: 

107 """ 

108 Instantiate a fresh FilterSet for the submission list view. 

109 """ 

110 return FilterSet( 

111 id="submission-filters", 

112 name="Filters", 

113 filters=[ 

114 Filter(id="created_by", name=_("Author"), type="model"), 

115 Filter( 

116 id="state", 

117 name=_("State"), 

118 type="string", 

119 name_getter=FieldGetter(attr="get_state_display", callable=True), 

120 ), 

121 Filter(id="journal_section", name=_("Section"), type="model"), 

122 Filter(id="all_assigned_editors", name=_("Assigned editors"), type="model"), 

123 ], 

124 ) 

125 

126 

127def group_submissions_per_status( 

128 submissions: Iterable[Submission], role_handler: RoleHandler 

129) -> dict[Any, list[Submission]]: 

130 return group_by( 

131 submissions, 

132 lambda s: role_handler.get_from_rights("get_submission_status", s).status, 

133 ) 

134 

135 

136def group_submissions_per_state( 

137 submissions: Iterable[Submission], role_handler: RoleHandler 

138) -> dict[Any, list[Submission]]: 

139 return group_by( 

140 submissions, 

141 lambda s: SubmissionState(s.state), 

142 ) 

143 

144 

145def one_group_submissions(submissions, role_handler): 

146 return {SubmissionListEnum.ALL: submissions} 

147 

148 

149def prepare_submissions_lists( 

150 role_handler: RoleHandler, 

151 request: HttpRequest, 

152 list_config_ftor=get_submission_list_config, 

153 group_submissions_ftor=group_submissions_per_status, 

154) -> dict[str, Any]: 

155 """ 

156 Retrieve, group and filter the user's submissions to show according to: 

157 - the user's role submission list config 

158 - the applied filters (contained in the HTTP request) 

159 

160 Params: 

161 - `role_handler` The role handler 

162 - `request` The underlying HTTP request 

163 - `config_key` The right function to be called to get the SubmissionListConfig(s) 

164 to be used. This config defines which submissions to display along 

165 with filtering behavior, etc. 

166 """ 

167 context: dict[str, Any] = {} 

168 

169 # The obtained submissions should have prefetched their related data. 

170 # It might be used extensively in the filtering and rendering processes. 

171 all_submissions = role_handler.get_attribute("submissions") 

172 # The submissions are displayed in different tables grouped by status. Precise 

173 # behavior is configured per role with SubmissionListConfig objects. 

174 grouped_submissions = group_submissions_ftor(all_submissions, role_handler) 

175 

176 submission_list_configs: list[SubmissionListConfig] = list_config_ftor() 

177 for config in submission_list_configs: 

178 config.all_submissions = grouped_submissions.get(config.key, []) 

179 

180 builder = BuildSubmissionProxyVisitor(role_handler) 

181 

182 # Handle the filters per submission list 

183 if role_handler.check_rights("can_filter_submissions"): 183 ↛ 209line 183 didn't jump to line 209 because the condition on line 183 was always true

184 # Init filters 

185 filters = submission_list_filters() 

186 filters.init_filters( 

187 request.GET, 

188 *(config.all_submissions for config in submission_list_configs if config.in_filters), 

189 ) 

190 context["filters"] = filters 

191 # Filter all submission lists 

192 for config in submission_list_configs: 

193 if not config.display: 

194 continue 

195 if not config.in_filters: 

196 config.submissions = [ 

197 role_handler.current_role.accept( 

198 builder, s, row_id=f"{config.id}-row-{counter}" 

199 ) 

200 for counter, s in enumerate(config.all_submissions) 

201 ] 

202 continue 

203 config.submissions = [ 

204 role_handler.current_role.accept(builder, s, row_id=f"{config.id}-row-{counter}") 

205 for counter, s in enumerate(filters.filter(config.all_submissions)) 

206 ] 

207 

208 else: 

209 for config in submission_list_configs: 

210 if not config.display: 

211 continue 

212 config.submissions = [ 

213 role_handler.current_role.accept(builder, s, row_id=f"{config.id}-row-{counter}") 

214 for counter, s in enumerate(config.all_submissions) 

215 ] 

216 

217 context["submissions_config"] = submission_list_configs 

218 context["all_submissions_count"] = sum( 

219 len(config.all_submissions) for config in submission_list_configs 

220 ) 

221 context["submission_count"] = CountWithTotal( 

222 value=sum( 

223 len(config.submissions) 

224 for config in submission_list_configs 

225 if config.display and config.in_filters 

226 ), 

227 total=sum( 

228 len(config.all_submissions) 

229 for config in submission_list_configs 

230 if config.display and config.in_filters 

231 ), 

232 ) 

233 

234 return context 

235 

236 

237def all_role_submissions_count(role_handler: RoleHandler) -> list[dict]: 

238 """ 

239 Returns the total number of submissions per active role. 

240 """ 

241 all_role_submissions = [] 

242 

243 for role in role_handler.role_data.get_active_roles(): 

244 role_submissions = { 

245 "role": role.summary().serialize(), 

246 "submissions": len(role.rights.submissions), 

247 } 

248 if role == role_handler.current_role: 

249 role_submissions["active"] = True 

250 all_role_submissions.append(role_submissions) 

251 

252 return all_role_submissions 

253 

254 

255class SubmissionListView(RoleMixin, TemplateView): 

256 """ 

257 View for the list of all submissions currently accessible to the user role, grouped by Status 

258 """ 

259 

260 template_name = "mesh/submission/submission_list_page.html" 

261 

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

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

264 

265 context.update(prepare_submissions_lists(self.role_handler, self.request)) 

266 all_role_submissions = all_role_submissions_count(self.role_handler) 

267 if len(all_role_submissions) > 1: 

268 context["all_role_submissions"] = all_role_submissions 

269 

270 context["with_journal_sections"] = JournalSection.objects.all_journal_sections().exists() 

271 

272 context["active_tab"] = "status" 

273 

274 # # Generate breadcrumb data 

275 # breadcrumb = get_base_breadcrumb() 

276 # breadcrumb.add_item(title=_("My submissions"), url=reverse_lazy("mesh:submission_list")) 

277 # context["breadcrumb"] = breadcrumb 

278 return context 

279 

280 

281class AllSubmissionsListView(RoleMixin, TemplateView): 

282 """ 

283 View for the list of all submissions currently accessible to the user role, in 1 flat list 

284 """ 

285 

286 template_name = "mesh/submission/submission_list_page.html" 

287 

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

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

290 

291 context.update( 

292 prepare_submissions_lists( 

293 self.role_handler, 

294 self.request, 

295 list_config_ftor=get_all_submission_list_config, 

296 group_submissions_ftor=one_group_submissions, 

297 ) 

298 ) 

299 

300 context["with_journal_sections"] = JournalSection.objects.all_journal_sections().exists() 

301 

302 context["active_tab"] = "all" 

303 

304 # # Generate breadcrumb data 

305 # breadcrumb = get_base_breadcrumb() 

306 # breadcrumb.add_item(title=_("My submissions"), url=reverse_lazy("mesh:submission_list")) 

307 # context["breadcrumb"] = breadcrumb 

308 return context 

309 

310 

311class SubmissionsByStateListView(RoleMixin, TemplateView): 

312 """ 

313 View for the list of all submissions currently accessible to the user role, in 1 flat list 

314 """ 

315 

316 template_name = "mesh/submission/submission_list_page.html" 

317 

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

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

320 

321 context.update( 

322 prepare_submissions_lists( 

323 self.role_handler, 

324 self.request, 

325 list_config_ftor=get_submission_by_state_config, 

326 group_submissions_ftor=group_submissions_per_state, 

327 ) 

328 ) 

329 

330 context["with_journal_sections"] = JournalSection.objects.all_journal_sections().exists() 

331 

332 context["active_tab"] = "state" 

333 

334 # # Generate breadcrumb data 

335 # breadcrumb = get_base_breadcrumb() 

336 # breadcrumb.add_item(title=_("My submissions"), url=reverse_lazy("mesh:submission_list")) 

337 # context["breadcrumb"] = breadcrumb 

338 return context 

339 

340 

341class DoneSubmissionListView(RoleMixin, TemplateView): 

342 restricted_roles = [JournalManager, Editor] 

343 template_name = "mesh/submission/submission_list_page.html" 

344 

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

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

347 context.update( 

348 prepare_submissions_lists( 

349 self.role_handler, 

350 self.request, 

351 list_config_ftor=get_done_submission_list_config, 

352 ) 

353 ) 

354 context["done_page"] = True 

355 

356 # Generate breadcrumb data 

357 breadcrumb = get_base_breadcrumb() 

358 breadcrumb.add_item( 

359 title=_("Done submissions"), 

360 url=reverse_lazy("mesh:submission_list_done"), 

361 ) 

362 context["breadcrumb"] = breadcrumb 

363 return context 

364 

365 

366class SubmissionLogView(RoleMixin, TemplateView): 

367 """ 

368 View to display the `SubmissionLog` of a submission. 

369 """ 

370 

371 template_name = "mesh/log.html" 

372 submission: Submission 

373 

374 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs): 

375 pk = kwargs["pk"] 

376 self.submission = get_object_or_404(Submission, pk=pk) 

377 

378 return not self.role_handler.check_rights("can_access_submission_log", self.submission) 

379 

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

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

382 

383 logs = SubmissionLog.objects.filter(attached_to=self.submission) 

384 order = self.request.GET.get("order") 

385 context["order"] = order 

386 if order == "asc": 

387 logs = logs.order_by("date_created") 

388 else: 

389 logs = logs.order_by("-date_created") 

390 context["logs"] = logs 

391 

392 query_params = {"order": "desc" if order == "asc" else "asc"} 

393 sort_url = reverse("mesh:submission_log", kwargs={"pk": self.submission.pk}) 

394 context["sort_url"] = format_url_with_params(sort_url, query_params) 

395 

396 context["page_title"] = _("Submission history") 

397 

398 # Generate breadcrumb data 

399 breadcrumb = get_submission_breadcrumb(self.submission) 

400 breadcrumb.add_item( 

401 title=_("Submission history"), 

402 url=reverse_lazy("mesh:submission_log", kwargs={"pk": self.submission.pk}), 

403 ) 

404 context["breadcrumb"] = breadcrumb 

405 return context 

406 

407 

408class SubmissionInListAPIView(RoleMixin, View): 

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

410 submission = get_object_or_404(Submission, pk=kwargs["pk"]) 

411 row_id = kwargs["row_id"] 

412 

413 builder = BuildSubmissionProxyVisitor(self.role_handler) 

414 submission_proxy = self.role_handler.current_role.accept( 

415 builder, submission, row_id=row_id 

416 ) 

417 

418 context = { 

419 "submission_proxy": submission_proxy, 

420 "role_handler": self.role_handler, 

421 "with_journal_sections": JournalSection.objects.all_journal_sections().exists(), 

422 } 

423 

424 return render( 

425 request, 

426 "mesh/submission/submission_table_row.html", 

427 context, 

428 ) 

429 

430 

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

432class SubmissionNotesAPIView(RoleMixin, View): 

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

434 submission = get_object_or_404(Submission, pk=kwargs["pk"]) 

435 

436 notes = request.POST.get("editabledata", "") 

437 sanitized_notes = sanitize_html_input(notes) 

438 

439 submission.notes = sanitized_notes 

440 submission.save() 

441 

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