Coverage for src/ptf_tools/views/base_views.py: 18%

1686 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-10 14:28 +0000

1import io 

2import json 

3import logging 

4import os 

5import re 

6from datetime import datetime 

7from itertools import groupby 

8 

9import jsonpickle 

10import requests 

11from allauth.account.signals import user_signed_up 

12from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin 

13from celery import Celery, current_app 

14from django.conf import settings 

15from django.contrib import messages 

16from django.contrib.auth.mixins import UserPassesTestMixin 

17from django.db.models import Q 

18from django.http import ( 

19 Http404, 

20 HttpRequest, 

21 HttpResponse, 

22 HttpResponseRedirect, 

23 HttpResponseServerError, 

24 JsonResponse, 

25) 

26from django.shortcuts import get_object_or_404, redirect, render 

27from django.urls import resolve, reverse 

28from django.utils import timezone 

29from django.utils.decorators import method_decorator 

30from django.views.decorators.csrf import csrf_exempt 

31from django.views.decorators.http import require_http_methods 

32from django.views.generic import ListView, TemplateView, View 

33from django.views.generic.base import RedirectView 

34from django.views.generic.detail import SingleObjectMixin 

35from django.views.generic.edit import CreateView, FormView, UpdateView 

36from django_celery_results.models import TaskResult 

37from extra_views import ( 

38 CreateWithInlinesView, 

39 InlineFormSetFactory, 

40 NamedFormsetsMixin, 

41 UpdateWithInlinesView, 

42) 

43from ptf import model_data_converter, model_helpers, utils 

44from ptf.cmds import ptf_cmds, xml_cmds 

45from ptf.cmds.base_cmds import make_int 

46from ptf.cmds.xml.jats.builder.issue import build_title_xml 

47from ptf.cmds.xml.xml_utils import replace_html_entities 

48from ptf.display import resolver 

49from ptf.exceptions import DOIException, ServerUnderMaintenance 

50from ptf.model_data import create_issuedata, create_publisherdata, create_titledata 

51from ptf.models import ( 

52 Abstract, 

53 Article, 

54 Collection, 

55 Container, 

56 ExtId, 

57 ExtLink, 

58 Resource, 

59 ResourceId, 

60) 

61from ptf.views import ArticleView, ItemViewClassFactory 

62from ptf_back.cmds.xml_cmds import addorUpdateCedricsArticleWithUnicodeXmlCmd 

63from ptf_back.locks import ( 

64 is_tex_conversion_locked, 

65 release_tex_conversion_lock, 

66) 

67from ptf_back.tex import create_frontpage 

68from ptf_back.tex.tex_tasks import convert_article_tex 

69from ptf_external.back.crossref.doi import checkDOI, recordDOI, recordPendingPublication 

70 

71# from ptf.views import ArticleEditFormWithVueAPIView 

72from ptf_external.views import ArticleEditFormWithVueAPIView 

73from pubmed.views import recordPubmed 

74from requests import Timeout 

75from requests.exceptions import HTTPError 

76from task.tasks.archiving_tasks import archive_resource 

77 

78from comments_moderation.utils import get_comments_for_home, is_comment_moderator 

79from history import models as history_models 

80from history import views as history_views 

81from history.utils import ( 

82 get_gap, 

83 get_history_last_event_by, 

84 get_last_unsolved_error, 

85) 

86from ptf_tools.doaj import doaj_pid_register 

87from ptf_tools.forms import ( 

88 CollectionForm, 

89 ContainerForm, 

90 DiffContainerForm, 

91 ExtIdForm, 

92 ExtLinkForm, 

93 FormSetHelper, 

94 ImportArticleForm, 

95 ImportContainerForm, 

96 ImportEditflowArticleForm, 

97 PtfFormHelper, 

98 PtfLargeModalFormHelper, 

99 PtfModalFormHelper, 

100 RegisterPubmedForm, 

101 ResourceIdForm, 

102 get_article_choices, 

103) 

104from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos 

105from ptf_tools.models import ResourceInNumdam 

106from ptf_tools.signals import update_user_from_invite 

107from ptf_tools.tasks import ( 

108 archive_numdam_collection, 

109 archive_numdam_collections, 

110) 

111from ptf_tools.templatetags.tools_helpers import get_authorized_collections 

112from ptf_tools.utils import is_authorized_editor 

113from ptf_tools.views.components import breadcrumb 

114 

115logger = logging.getLogger(__name__) 

116 

117 

118def view_404(request: HttpRequest, *args, **kwargs): 

119 """ 

120 Dummy view raising HTTP 404 exception. 

121 """ 

122 raise Http404 

123 

124 

125def check_collection(collection, server_url, server_type): 

126 """ 

127 Check if a collection exists on a serveur (test/prod) 

128 and upload the collection (XML, image) if necessary 

129 """ 

130 

131 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid}) 

132 response = requests.get(url, verify=False) 

133 # First, upload the collection XML 

134 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do() 

135 body = xml.encode("utf8") 

136 

137 url = server_url + reverse("upload-serials") 

138 if response.status_code == 200: 

139 # PUT http verb is used for update 

140 response = requests.put(url, data=body, verify=False) 

141 else: 

142 # POST http verb is used for creation 

143 response = requests.post(url, data=body, verify=False) 

144 

145 # Second, copy the collection images 

146 # There is no need to copy files for the test server 

147 # Files were already copied in /mersenne_test_data during the ptf_tools import 

148 # We only need to copy files from /mersenne_test_data to 

149 # /mersenne_prod_data during an upload to prod 

150 if server_type == "website": 

151 resolver.copy_binary_files( 

152 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

153 ) 

154 elif server_type == "numdam": 

155 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

156 if collection.pid in settings.NUMDAM_COLLECTIONS: 

157 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

158 

159 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT) 

160 

161 

162def check_lock(): 

163 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE) 

164 

165 

166def load_cedrics_article_choices(request): 

167 colid = request.GET.get("colid") 

168 issue = request.GET.get("issue") 

169 article_choices = get_article_choices(colid, issue) 

170 return render( 

171 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices} 

172 ) 

173 

174 

175class ImportCedricsArticleFormView(FormView): 

176 template_name = "import_article.html" 

177 form_class = ImportArticleForm 

178 

179 def dispatch(self, request, *args, **kwargs): 

180 self.colid = self.kwargs["colid"] 

181 return super().dispatch(request, *args, **kwargs) 

182 

183 def get_success_url(self): 

184 if self.colid: 

185 return reverse("collection-detail", kwargs={"pid": self.colid}) 

186 return "/" 

187 

188 def get_context_data(self, **kwargs): 

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

190 context["colid"] = self.colid 

191 context["helper"] = PtfModalFormHelper 

192 return context 

193 

194 def get_form_kwargs(self): 

195 kwargs = super().get_form_kwargs() 

196 kwargs["colid"] = self.colid 

197 return kwargs 

198 

199 def form_valid(self, form): 

200 self.issue = form.cleaned_data["issue"] 

201 self.article = form.cleaned_data["article"] 

202 return super().form_valid(form) 

203 

204 def import_cedrics_article(self, *args, **kwargs): 

205 cmd = addorUpdateCedricsArticleWithUnicodeXmlCmd( 

206 { 

207 "container_pid": self.issue_pid, 

208 "article_folder_name": self.article_pid, 

209 "convert_tex_to_unicode": self.convert_tex_to_unicode, 

210 } 

211 ) 

212 cmd.do() 

213 

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

215 self.colid = self.kwargs.get("colid", None) 

216 issue = request.POST["issue"] 

217 self.article_pid = request.POST["article"] 

218 self.issue_pid = os.path.basename(os.path.dirname(issue)) 

219 # Only present in the POST data when the checkbox is checked 

220 self.convert_tex_to_unicode = request.POST.get("convert_tex_to_unicode", False) 

221 

222 import_args = [self] 

223 import_kwargs = {} 

224 

225 try: 

226 _, _status, _message = history_views.execute_and_record_func( 

227 "import", 

228 f"{self.issue_pid} / {self.article_pid}", 

229 self.colid, 

230 self.import_cedrics_article, 

231 "", 

232 False, 

233 None, 

234 None, 

235 *import_args, 

236 **import_kwargs, 

237 ) 

238 

239 messages.success( 

240 self.request, f"L'article {self.article_pid} a été importé avec succès" 

241 ) 

242 

243 except Exception as exception: 

244 messages.error( 

245 self.request, 

246 f"Echec de l'import de l'article {self.article_pid} : {exception!s}", 

247 ) 

248 

249 return redirect(self.get_success_url()) 

250 

251 

252class ImportCedricsIssueView(FormView): 

253 template_name = "import_container.html" 

254 form_class = ImportContainerForm 

255 

256 def dispatch(self, request, *args, **kwargs): 

257 self.colid = self.kwargs["colid"] 

258 self.to_appear = self.request.GET.get("to_appear", False) 

259 return super().dispatch(request, *args, **kwargs) 

260 

261 def get_success_url(self): 

262 if self.filename: 

263 return reverse( 

264 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename} 

265 ) 

266 return "/" 

267 

268 def get_context_data(self, **kwargs): 

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

270 context["colid"] = self.colid 

271 context["helper"] = PtfModalFormHelper 

272 return context 

273 

274 def get_form_kwargs(self): 

275 kwargs = super().get_form_kwargs() 

276 kwargs["colid"] = self.colid 

277 kwargs["to_appear"] = self.to_appear 

278 return kwargs 

279 

280 def form_valid(self, form): 

281 self.filename = form.cleaned_data["filename"].split("/")[-1] 

282 return super().form_valid(form) 

283 

284 

285class DiffCedricsIssueView(FormView): 

286 template_name = "diff_container_form.html" 

287 form_class = DiffContainerForm 

288 diffs = None 

289 xissue = None 

290 xissue_encoded = None 

291 

292 def get_success_url(self): 

293 return reverse("collection-detail", kwargs={"pid": self.colid}) 

294 

295 def dispatch(self, request, *args, **kwargs): 

296 self.colid = self.kwargs["colid"] 

297 # self.filename = self.kwargs['filename'] 

298 return super().dispatch(request, *args, **kwargs) 

299 

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

301 self.filename = request.GET["filename"] 

302 self.remove_mail = request.GET.get("remove_email", "off") 

303 self.remove_date_prod = request.GET.get("remove_date_prod", "off") 

304 self.remove_email = self.remove_mail == "on" 

305 self.remove_date_prod = self.remove_date_prod == "on" 

306 

307 try: 

308 result, _status, _message = history_views.execute_and_record_func( 

309 "import", 

310 os.path.basename(self.filename), 

311 self.colid, 

312 self.diff_cedrics_issue, 

313 "", 

314 True, 

315 ) 

316 except Exception as exception: 

317 pid = self.filename.split("/")[-1] 

318 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}") 

319 return HttpResponseRedirect(self.get_success_url()) 

320 

321 result[0] 

322 self.diffs = result[1] 

323 self.xissue = result[2] 

324 

325 if True: 

326 # Proceed with the import 

327 self.form_valid(self.get_form()) 

328 return redirect(self.get_success_url()) 

329 else: 

330 # Display the diff template 

331 self.xissue_encoded = jsonpickle.encode(self.xissue) 

332 

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

334 

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

336 self.filename = request.POST["filename"] 

337 data = request.POST["xissue_encoded"] 

338 self.xissue = jsonpickle.decode(data) 

339 

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

341 

342 def get_context_data(self, **kwargs): 

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

344 context["colid"] = self.colid 

345 context["diff"] = self.diffs 

346 context["filename"] = self.filename 

347 context["xissue_encoded"] = self.xissue_encoded 

348 return context 

349 

350 def get_form_kwargs(self): 

351 kwargs = super().get_form_kwargs() 

352 kwargs["colid"] = self.colid 

353 return kwargs 

354 

355 def diff_cedrics_issue(self, *args, **kwargs): 

356 params = { 

357 "colid": self.colid, 

358 "input_file": self.filename, 

359 "remove_email": self.remove_mail, 

360 "remove_date_prod": self.remove_date_prod, 

361 "diff_only": True, 

362 } 

363 

364 if settings.IMPORT_CEDRICS_DIRECTLY: 

365 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS 

366 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS 

367 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

368 else: 

369 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

370 

371 result = cmd.do() 

372 if len(cmd.warnings) > 0 and self.request.user.is_superuser: 

373 messages.warning( 

374 self.request, message=f"Balises non parsées lors de l'import : {cmd.warnings}" 

375 ) 

376 

377 return result 

378 

379 def import_cedrics_issue(self, *args, **kwargs): 

380 # modify xissue with data_issue if params to override 

381 if "import_choice" in kwargs and kwargs["import_choice"] == "1": 

382 issue = model_helpers.get_container(self.xissue.pid) 

383 if issue: 

384 data_issue = model_data_converter.db_to_issue_data(issue) 

385 for xarticle in self.xissue.articles: 

386 filter_articles = [ 

387 article for article in data_issue.articles if article.doi == xarticle.doi 

388 ] 

389 if len(filter_articles) > 0: 

390 db_article = filter_articles[0] 

391 xarticle.coi_statement = db_article.coi_statement 

392 xarticle.kwds = db_article.kwds 

393 xarticle.contrib_groups = db_article.contrib_groups 

394 

395 params = { 

396 "colid": self.colid, 

397 "xissue": self.xissue, 

398 "input_file": self.filename, 

399 } 

400 

401 if settings.IMPORT_CEDRICS_DIRECTLY: 

402 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS 

403 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS 

404 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

405 else: 

406 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

407 

408 cmd.do() 

409 

410 def form_valid(self, form): 

411 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1": 

412 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]} 

413 else: 

414 import_kwargs = {} 

415 import_args = [self] 

416 

417 try: 

418 _, _status, _message = history_views.execute_and_record_func( 

419 "import", 

420 self.xissue.pid, 

421 self.kwargs["colid"], 

422 self.import_cedrics_issue, 

423 "", 

424 False, 

425 None, 

426 None, 

427 *import_args, 

428 **import_kwargs, 

429 ) 

430 except Exception as exception: 

431 messages.error( 

432 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception) 

433 ) 

434 return super().form_invalid(form) 

435 

436 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès") 

437 return super().form_valid(form) 

438 

439 

440class ImportEditflowArticleFormView(FormView): 

441 template_name = "import_editflow_article.html" 

442 form_class = ImportEditflowArticleForm 

443 

444 def dispatch(self, request, *args, **kwargs): 

445 self.colid = self.kwargs["colid"] 

446 return super().dispatch(request, *args, **kwargs) 

447 

448 def get_success_url(self): 

449 if self.colid: 

450 return reverse("collection-detail", kwargs={"pid": self.colid}) 

451 return "/" 

452 

453 def get_context_data(self, **kwargs): 

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

455 context["colid"] = self.colid 

456 context["helper"] = PtfLargeModalFormHelper 

457 return context 

458 

459 def get_form_kwargs(self): 

460 kwargs = super().get_form_kwargs() 

461 kwargs["colid"] = self.colid 

462 return kwargs 

463 

464 def form_valid(self, form): 

465 try: 

466 if not self.colid: 

467 raise ValueError("Missing collection id") 

468 

469 issue_name = settings.ISSUE_PENDING_PUBLICATION_PIDS.get(self.colid) 

470 if not issue_name: 

471 raise ValueError( 

472 "Issue not found in Pending Publications PIDs. Did you forget to add it?" 

473 ) 

474 

475 issue = model_helpers.get_container(issue_name) 

476 if not issue: 

477 raise ValueError("No issue found") 

478 

479 editflow_xml_file = form.cleaned_data["editflow_xml_file"] 

480 body = editflow_xml_file.read().decode("utf-8") 

481 

482 cmd = xml_cmds.addArticleXmlCmd( 

483 { 

484 "body": body, 

485 "issue": issue, 

486 "assign_doi": True, 

487 "standalone": True, 

488 "from_folder": settings.RESOURCES_ROOT, 

489 } 

490 ) 

491 cmd.set_collection(issue.get_collection()) 

492 cmd.do() 

493 

494 messages.success( 

495 self.request, 

496 f'Editflow article successfully imported into issue "{issue_name}"', 

497 ) 

498 

499 except Exception as exception: 

500 messages.error( 

501 self.request, 

502 f"Import failed: {exception}", 

503 ) 

504 return super().form_invalid(form) 

505 

506 return super().form_valid(form) 

507 

508 

509class MatchingAPIView(View): 

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

511 pid = self.kwargs.get("pid", None) 

512 

513 url = settings.MATCHING_URL 

514 headers = {"Content-Type": "application/xml"} 

515 

516 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do() 

517 

518 if settings.DEBUG: 

519 print("Issue exported to /tmp/issue.xml") 

520 f = open("/tmp/issue.xml", "w") 

521 f.write(body.encode("utf8")) 

522 f.close() 

523 

524 r = requests.post(url, data=body.encode("utf8"), headers=headers) 

525 body = r.text.encode("utf8") 

526 data = {"status": r.status_code, "message": body[:1000]} 

527 

528 if settings.DEBUG: 

529 print("Matching received, new issue exported to /tmp/issue1.xml") 

530 f = open("/tmp/issue1.xml", "w") 

531 text = body 

532 f.write(text) 

533 f.close() 

534 

535 resource = model_helpers.get_resource(pid) 

536 obj = resource.cast() 

537 colid = obj.get_collection().pid 

538 

539 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/" 

540 

541 cmd = xml_cmds.addOrUpdateIssueXmlCmd( 

542 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder} 

543 ) 

544 cmd.do() 

545 

546 print("Matching finished") 

547 return JsonResponse(data) 

548 

549 

550class ImportAllAPIView(View): 

551 def internal_do(self, *args, **kwargs): 

552 pid = self.kwargs.get("pid", None) 

553 

554 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid) 

555 if not os.path.isdir(root_folder): 

556 raise ValueError(root_folder + " does not exist") 

557 

558 resource = model_helpers.get_resource(pid) 

559 if not resource: 

560 file = os.path.join(root_folder, pid + ".xml") 

561 body = utils.get_file_content_in_utf8(file) 

562 journals = xml_cmds.addCollectionsXmlCmd( 

563 { 

564 "body": body, 

565 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

566 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

567 } 

568 ).do() 

569 if not journals: 

570 raise ValueError(file + " does not contain a collection") 

571 resource = journals[0] 

572 # resolver.copy_binary_files( 

573 # resource, 

574 # settings.MATHDOC_ARCHIVE_FOLDER, 

575 # settings.MERSENNE_TEST_DATA_FOLDER) 

576 

577 obj = resource.cast() 

578 

579 if obj.classname != "Collection": 

580 raise ValueError(pid + " does not contain a collection") 

581 

582 cmd = xml_cmds.collectEntireCollectionXmlCmd( 

583 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER} 

584 ) 

585 pids = cmd.do() 

586 

587 return pids 

588 

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

590 pid = self.kwargs.get("pid", None) 

591 

592 try: 

593 pids, status, message = history_views.execute_and_record_func( 

594 "import", pid, pid, self.internal_do 

595 ) 

596 except Timeout as exception: 

597 return HttpResponse(exception, status=408) 

598 except Exception as exception: 

599 return HttpResponseServerError(exception) 

600 

601 data = {"message": message, "ids": pids, "status": status} 

602 return JsonResponse(data) 

603 

604 

605class DeployAllAPIView(View): 

606 def internal_do(self, *args, **kwargs): 

607 pid = self.kwargs.get("pid", None) 

608 site = self.kwargs.get("site", None) 

609 

610 pids = [] 

611 

612 collection = model_helpers.get_collection(pid) 

613 if not collection: 

614 raise RuntimeError(pid + " does not exist") 

615 

616 if site == "numdam": 

617 server_url = settings.NUMDAM_PRE_URL 

618 elif site != "ptf_tools": 

619 server_url = getattr(collection, site)() 

620 if not server_url: 

621 raise RuntimeError("The collection has no " + site) 

622 

623 if site != "ptf_tools": 

624 # check if the collection exists on the server 

625 # if not, check_collection will upload the collection (XML, 

626 # image...) 

627 check_collection(collection, server_url, site) 

628 

629 for issue in collection.content.all(): 

630 if site != "website" or (site == "website" and issue.are_all_articles_published()): 

631 pids.append(issue.pid) 

632 

633 return pids 

634 

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

636 pid = self.kwargs.get("pid", None) 

637 site = self.kwargs.get("site", None) 

638 

639 try: 

640 pids, status, message = history_views.execute_and_record_func( 

641 "deploy", pid, pid, self.internal_do, site 

642 ) 

643 except Timeout as exception: 

644 return HttpResponse(exception, status=408) 

645 except Exception as exception: 

646 return HttpResponseServerError(exception) 

647 

648 data = {"message": message, "ids": pids, "status": status} 

649 return JsonResponse(data) 

650 

651 

652class AddIssuePDFView(View): 

653 def __init(self, *args, **kwargs): 

654 super().__init__(*args, **kwargs) 

655 self.pid = None 

656 self.issue = None 

657 self.collection = None 

658 self.site = "test_website" 

659 

660 def post_to_site(self, url): 

661 response = requests.post(url, verify=False) 

662 status = response.status_code 

663 if not (199 < status < 205): 

664 messages.error(self.request, response.text) 

665 if status == 503: 

666 raise ServerUnderMaintenance(response.text) 

667 else: 

668 raise RuntimeError(response.text) 

669 

670 def internal_do(self, *args, **kwargs): 

671 """ 

672 Called by history_views.execute_and_record_func to do the actual job. 

673 """ 

674 

675 issue_pid = self.issue.pid 

676 colid = self.collection.pid 

677 

678 if self.site == "website": 

679 # Copy the PDF from the test to the production folder 

680 resolver.copy_binary_files( 

681 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

682 ) 

683 else: 

684 # Copy the PDF from the cedram to the test folder 

685 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid) 

686 from_path = os.path.join(from_folder, issue_pid + ".pdf") 

687 if not os.path.isfile(from_path): 

688 raise Http404(f"{from_path} does not exist") 

689 

690 to_path = resolver.get_disk_location( 

691 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid 

692 ) 

693 resolver.copy_file(from_path, to_path) 

694 

695 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid}) 

696 

697 if self.site == "test_website": 

698 # Post to ptf-tools: it will add a Datastream to the issue 

699 absolute_url = self.request.build_absolute_uri(url) 

700 self.post_to_site(absolute_url) 

701 

702 server_url = getattr(self.collection, self.site)() 

703 absolute_url = server_url + url 

704 # Post to the test or production website 

705 self.post_to_site(absolute_url) 

706 

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

708 """ 

709 Send an issue PDF to the test or production website 

710 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website' 

711 :param args: 

712 :param kwargs: 

713 :return: 

714 """ 

715 if check_lock(): 

716 m = "Trammel is under maintenance. Please try again later." 

717 messages.error(self.request, m) 

718 return JsonResponse({"message": m, "status": 503}) 

719 

720 self.pid = self.kwargs.get("pid", None) 

721 self.site = self.kwargs.get("site", "test_website") 

722 

723 self.issue = model_helpers.get_container(self.pid) 

724 if not self.issue: 

725 raise Http404(f"{self.pid} does not exist") 

726 self.collection = self.issue.get_top_collection() 

727 

728 try: 

729 _pids, status, message = history_views.execute_and_record_func( 

730 "deploy", 

731 self.pid, 

732 self.collection.pid, 

733 self.internal_do, 

734 f"add issue PDF to {self.site}", 

735 ) 

736 

737 except Timeout as exception: 

738 return HttpResponse(exception, status=408) 

739 except Exception as exception: 

740 return HttpResponseServerError(exception) 

741 

742 data = {"message": message, "status": status} 

743 return JsonResponse(data) 

744 

745 

746class ArchiveAllAPIView(View): 

747 """ 

748 - archive le xml de la collection ainsi que les binaires liés 

749 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS 

750 @return array of issues pid 

751 """ 

752 

753 def internal_do(self, *args, **kwargs): 

754 collection = kwargs["collection"] 

755 pids = [] 

756 colid = collection.pid 

757 

758 logfile = os.path.join(settings.LOG_DIR, "archive.log") 

759 if os.path.isfile(logfile): 

760 os.remove(logfile) 

761 

762 ptf_cmds.exportPtfCmd( 

763 { 

764 "pid": colid, 

765 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

766 "with_binary_files": True, 

767 "for_archive": True, 

768 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

769 } 

770 ).do() 

771 

772 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls") 

773 if os.path.isfile(cedramcls): 

774 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex") 

775 resolver.create_folder(dest_folder) 

776 resolver.copy_file(cedramcls, dest_folder) 

777 

778 for issue in collection.content.all(): 

779 qs = issue.article_set.filter( 

780 date_online_first__isnull=True, date_published__isnull=True 

781 ) 

782 if qs.count() == 0: 

783 pids.append(issue.pid) 

784 

785 return pids 

786 

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

788 pid = self.kwargs.get("pid", None) 

789 

790 collection = model_helpers.get_collection(pid) 

791 if not collection: 

792 return HttpResponse(f"{pid} does not exist", status=400) 

793 

794 dict_ = {"collection": collection} 

795 args_ = [self] 

796 

797 try: 

798 pids, status, message = history_views.execute_and_record_func( 

799 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_ 

800 ) 

801 except Timeout as exception: 

802 return HttpResponse(exception, status=408) 

803 except Exception as exception: 

804 return HttpResponseServerError(exception) 

805 

806 data = {"message": message, "ids": pids, "status": status} 

807 return JsonResponse(data) 

808 

809 

810class CreateAllDjvuAPIView(View): 

811 def internal_do(self, *args, **kwargs): 

812 issue = kwargs["issue"] 

813 pids = [issue.pid] 

814 

815 for article in issue.article_set.all(): 

816 pids.append(article.pid) 

817 

818 return pids 

819 

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

821 pid = self.kwargs.get("pid", None) 

822 issue = model_helpers.get_container(pid) 

823 if not issue: 

824 raise Http404(f"{pid} does not exist") 

825 

826 try: 

827 dict_ = {"issue": issue} 

828 args_ = [self] 

829 

830 pids, status, message = history_views.execute_and_record_func( 

831 "numdam", 

832 pid, 

833 issue.get_collection().pid, 

834 self.internal_do, 

835 "", 

836 False, 

837 None, 

838 None, 

839 *args_, 

840 **dict_, 

841 ) 

842 except Exception as exception: 

843 return HttpResponseServerError(exception) 

844 

845 data = {"message": message, "ids": pids, "status": status} 

846 return JsonResponse(data) 

847 

848 

849class ImportJatsContainerAPIView(View): 

850 def internal_do(self, *args, **kwargs): 

851 pid = self.kwargs.get("pid", None) 

852 colid = self.kwargs.get("colid", None) 

853 

854 if pid and colid: 

855 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid) 

856 

857 cmd = xml_cmds.addOrUpdateContainerXmlCmd( 

858 { 

859 "body": body, 

860 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

861 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

862 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

863 } 

864 ) 

865 container = cmd.do() 

866 if len(cmd.warnings) > 0: 

867 messages.warning( 

868 self.request, 

869 message=f"Balises non parsées lors de l'import : {cmd.warnings}", 

870 ) 

871 

872 if not container: 

873 raise RuntimeError("Error: the container " + pid + " was not imported") 

874 

875 # resolver.copy_binary_files( 

876 # container, 

877 # settings.MATHDOC_ARCHIVE_FOLDER, 

878 # settings.MERSENNE_TEST_DATA_FOLDER) 

879 # 

880 # for article in container.article_set.all(): 

881 # resolver.copy_binary_files( 

882 # article, 

883 # settings.MATHDOC_ARCHIVE_FOLDER, 

884 # settings.MERSENNE_TEST_DATA_FOLDER) 

885 else: 

886 raise RuntimeError("colid or pid are not defined") 

887 

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

889 pid = self.kwargs.get("pid", None) 

890 colid = self.kwargs.get("colid", None) 

891 

892 try: 

893 _, status, message = history_views.execute_and_record_func( 

894 "import", pid, colid, self.internal_do 

895 ) 

896 except Timeout as exception: 

897 return HttpResponse(exception, status=408) 

898 except Exception as exception: 

899 return HttpResponseServerError(exception) 

900 

901 data = {"message": message, "status": status} 

902 return JsonResponse(data) 

903 

904 

905class DeployCollectionAPIView(View): 

906 # Update collection.xml on a site (with its images) 

907 

908 def internal_do(self, *args, **kwargs): 

909 colid = self.kwargs.get("colid", None) 

910 site = self.kwargs.get("site", None) 

911 

912 collection = model_helpers.get_collection(colid) 

913 if not collection: 

914 raise RuntimeError(f"{colid} does not exist") 

915 

916 if site == "numdam": 

917 server_url = settings.NUMDAM_PRE_URL 

918 else: 

919 server_url = getattr(collection, site)() 

920 if not server_url: 

921 raise RuntimeError(f"The collection has no {site}") 

922 

923 # check_collection creates or updates the collection (XML, image...) 

924 check_collection(collection, server_url, site) 

925 

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

927 colid = self.kwargs.get("colid", None) 

928 site = self.kwargs.get("site", None) 

929 

930 try: 

931 _, status, message = history_views.execute_and_record_func( 

932 "deploy", colid, colid, self.internal_do, site 

933 ) 

934 except Timeout as exception: 

935 return HttpResponse(exception, status=408) 

936 except Exception as exception: 

937 return HttpResponseServerError(exception) 

938 

939 data = {"message": message, "status": status} 

940 return JsonResponse(data) 

941 

942 

943class DeployJatsResourceAPIView(View): 

944 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel) 

945 

946 def internal_do(self, *args, **kwargs): 

947 pid = self.kwargs.get("pid", None) 

948 colid = self.kwargs.get("colid", None) 

949 site = self.kwargs.get("site", None) 

950 

951 if site in ["ptf_tools", "trammel"]: 

952 raise RuntimeError("You can't deploy on Trammel itself.") 

953 if check_lock(): 

954 msg = "Trammel is under maintenance. Please try again later." 

955 messages.error(self.request, msg) 

956 return JsonResponse({"messages": msg, "status": 503}) 

957 

958 resource = model_helpers.get_resource(pid) 

959 if not resource: 

960 raise RuntimeError(f"{pid} does not exist") 

961 

962 obj = resource.cast() 

963 article = None 

964 if obj.classname == "Article": 

965 article = obj 

966 container = article.my_container 

967 articles_to_deploy = [article] 

968 else: 

969 container = obj 

970 articles_to_deploy = container.article_set.exclude(do_not_publish=True) 

971 

972 if container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(colid, None): 

973 raise RuntimeError("Pending publications should not be deployed") 

974 if site == "website" and article is not None and article.do_not_publish: 

975 raise RuntimeError(f"{pid} is marked as Do not publish. You can't deploy it.") 

976 if site == "numdam" and article is not None: 

977 raise RuntimeError("You can only deploy issues to Numdam") 

978 

979 collection = container.get_top_collection() 

980 colid = collection.pid 

981 djvu_exception = None 

982 

983 if site == "numdam": 

984 server_url = settings.NUMDAM_PRE_URL 

985 ResourceInNumdam.objects.get_or_create(pid=container.pid) 

986 

987 # 06/12/2022: DjVu are no longer added with Mersenne articles 

988 # Add Djvu (before exporting the XML) 

989 if False: 

990 for art in container.article_set.all(): 

991 try: 

992 cmd = ptf_cmds.addDjvuPtfCmd() 

993 cmd.set_resource(art) 

994 cmd.do() 

995 except Exception as e: 

996 # Djvu are optional. 

997 # Allow the deployment, but record the exception in the history 

998 djvu_exception = e 

999 else: 

1000 server_url = getattr(collection, site)() 

1001 if not server_url: 

1002 raise RuntimeError(f"The collection has no {site}") 

1003 

1004 # check if the collection exists on the server 

1005 # if not, check_collection will upload the collection (XML, 

1006 # image...) 

1007 if article is None: 

1008 check_collection(collection, server_url, site) 

1009 

1010 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_: 

1011 # Create/update deployed date and published date on all container articles 

1012 if site == "website": 

1013 file_.write( 

1014 f"Create/Update deployed_date and date_published on all articles for {pid}\n" 

1015 ) 

1016 

1017 # create date_published on articles without date_published (ou date_online_first pour le volume 0) 

1018 cmd = ptf_cmds.publishResourcePtfCmd() 

1019 cmd.set_resource(resource) 

1020 updated_articles = cmd.do() 

1021 

1022 create_frontpage(colid, container, updated_articles, test=False) 

1023 

1024 mersenneSite = model_helpers.get_site_mersenne(colid) 

1025 # create or update deployed_date on container and articles 

1026 model_helpers.update_deployed_date(obj, mersenneSite, None, file_) 

1027 

1028 for art in articles_to_deploy: 

1029 if ( 

1030 art.doi 

1031 and (art.date_published or art.date_online_first) 

1032 and art.my_container.fyear is None 

1033 ): 

1034 art.my_container.fyear = datetime.now().year 

1035 # BUG ? update the container but no save() ? 

1036 

1037 file_.write( 

1038 f"Publication date of {art.pid} : Online First: {art.date_online_first}, Published: {art.date_published}\n" 

1039 ) 

1040 

1041 if article is None: 

1042 resolver.copy_binary_files( 

1043 container, 

1044 settings.MERSENNE_TEST_DATA_FOLDER, 

1045 settings.MERSENNE_PROD_DATA_FOLDER, 

1046 ) 

1047 

1048 for art in articles_to_deploy: 

1049 resolver.copy_binary_files( 

1050 art, 

1051 settings.MERSENNE_TEST_DATA_FOLDER, 

1052 settings.MERSENNE_PROD_DATA_FOLDER, 

1053 ) 

1054 

1055 elif site == "test_website": 

1056 # create date_pre_published on articles without date_pre_published 

1057 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True}) 

1058 cmd.set_resource(resource) 

1059 updated_articles = cmd.do() 

1060 

1061 create_frontpage(colid, container, updated_articles) 

1062 

1063 export_to_website = site == "website" 

1064 

1065 if article is None: 

1066 with_djvu = site == "numdam" 

1067 xml = ptf_cmds.exportPtfCmd( 

1068 { 

1069 "pid": pid, 

1070 "with_djvu": with_djvu, 

1071 "export_to_website": export_to_website, 

1072 } 

1073 ).do() 

1074 body = xml.encode("utf8") 

1075 

1076 if container.ctype == "issue" or container.ctype.startswith("issue_special"): 

1077 url = server_url + reverse("issue_upload") 

1078 else: 

1079 url = server_url + reverse("book_upload") 

1080 

1081 # verify=False: ignore TLS certificate 

1082 response = requests.post(url, data=body, verify=False) 

1083 # response = requests.post(url, files=files, verify=False) 

1084 else: 

1085 xml = ptf_cmds.exportPtfCmd( 

1086 { 

1087 "pid": pid, 

1088 "with_djvu": False, 

1089 "article_standalone": True, 

1090 "collection_pid": collection.pid, 

1091 "export_to_website": export_to_website, 

1092 "export_folder": settings.LOG_DIR, 

1093 } 

1094 ).do() 

1095 # Unlike containers that send their XML as the body of the POST request, 

1096 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img) 

1097 xml_file = io.StringIO(xml) 

1098 files = {"xml": xml_file} 

1099 

1100 url = server_url + reverse( 

1101 "article_in_issue_upload", kwargs={"pid": container.pid} 

1102 ) 

1103 # verify=False: ignore TLS certificate 

1104 header = {} 

1105 logger.debug(f"Sending POST request to {url}") 

1106 response = requests.post(url, headers=header, files=files, verify=False) 

1107 

1108 status = response.status_code 

1109 

1110 if 199 < status < 205: 

1111 # There is no need to copy files for the test server 

1112 # Files were already copied in /mersenne_test_data during the ptf_tools import 

1113 # We only need to copy files from /mersenne_test_data to 

1114 # /mersenne_prod_data during an upload to prod 

1115 if site == "website": 

1116 # TODO mettre ici le record doi pour un issue publié 

1117 if container.doi: 

1118 recordDOI(container) 

1119 

1120 for art in articles_to_deploy: 

1121 # record DOI automatically when deploying in prod 

1122 

1123 if art.doi and art.allow_crossref(): 

1124 recordDOI(art) 

1125 

1126 if colid == "CRBIOL": 

1127 recordPubmed( 

1128 art, force_update=False, updated_articles=updated_articles 

1129 ) 

1130 

1131 if colid == "PCJ": 

1132 self.update_pcj_editor(updated_articles) 

1133 

1134 # Archive the container or the article 

1135 if article is None: 

1136 archive_resource.delay( 

1137 pid, 

1138 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1139 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1140 ) 

1141 

1142 else: 

1143 archive_resource.delay( 

1144 pid, 

1145 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1146 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1147 article_doi=article.doi, 

1148 ) 

1149 # cmd = ptf_cmds.archiveIssuePtfCmd({ 

1150 # "pid": pid, 

1151 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1152 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER}) 

1153 # cmd.set_article(article) # set_article allows archiving only the article 

1154 # cmd.do() 

1155 

1156 elif site == "numdam": 

1157 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

1158 if colid in settings.NUMDAM_COLLECTIONS: 

1159 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

1160 

1161 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT) 

1162 for article in container.article_set.all(): 

1163 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT) 

1164 

1165 elif status == 503: 

1166 raise ServerUnderMaintenance(response.text) 

1167 else: 

1168 raise RuntimeError(response.text) 

1169 

1170 if djvu_exception: 

1171 raise djvu_exception 

1172 

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

1174 pid = self.kwargs.get("pid", None) 

1175 colid = self.kwargs.get("colid", None) 

1176 site = self.kwargs.get("site", None) 

1177 

1178 try: 

1179 _, status, message = history_views.execute_and_record_func( 

1180 "deploy", 

1181 pid, 

1182 colid, 

1183 self.internal_do, 

1184 f"Article successfully transferred to {site}", 

1185 ) 

1186 except Timeout as exception: 

1187 return HttpResponse(exception, status=408) 

1188 except Exception as exception: 

1189 return HttpResponseServerError(exception) 

1190 

1191 data = {"message": message, "status": status} 

1192 return JsonResponse(data) 

1193 

1194 def update_pcj_editor(self, updated_articles): 

1195 for article in updated_articles: 

1196 data = { 

1197 "date_published": article.date_published.strftime("%Y-%m-%d"), 

1198 "article_number": article.article_number, 

1199 } 

1200 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/" 

1201 requests.post(url, json=data, verify=False) 

1202 

1203 

1204class DeployTranslatedArticleAPIView(CsrfExemptMixin, View): 

1205 article = None 

1206 

1207 def internal_do(self, *args, **kwargs): 

1208 lang = self.kwargs.get("lang", None) 

1209 

1210 translation = None 

1211 for trans_article in self.article.translations.all(): 

1212 if trans_article.lang == lang: 

1213 translation = trans_article 

1214 

1215 if translation is None: 

1216 raise RuntimeError(f"{self.article.doi} does not exist in {lang}") 

1217 

1218 collection = self.article.get_top_collection() 

1219 colid = collection.pid 

1220 container = self.article.my_container 

1221 

1222 if translation.date_published is None: 

1223 # Add date posted 

1224 cmd = ptf_cmds.publishResourcePtfCmd() 

1225 cmd.set_resource(translation) 

1226 cmd.do() 

1227 # updated_articles = cmd.do() 

1228 

1229 # # Recompile PDF to add the date posted 

1230 # try: 

1231 # create_frontpage(colid, container, updated_articles, test=False, lang=lang) 

1232 # except Exception: 

1233 # raise PDFException( 

1234 # "Unable to compile the article PDF. Please contact the centre Mersenne" 

1235 # ) 

1236 

1237 # Unlike regular articles, binary files of translations need to be copied before uploading the XML. 

1238 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk 

1239 resolver.copy_binary_files( 

1240 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

1241 ) 

1242 

1243 # Deploy in prod 

1244 xml = ptf_cmds.exportPtfCmd( 

1245 { 

1246 "pid": self.article.pid, 

1247 "with_djvu": False, 

1248 "article_standalone": True, 

1249 "collection_pid": colid, 

1250 "export_to_website": True, 

1251 "export_folder": settings.LOG_DIR, 

1252 } 

1253 ).do() 

1254 xml_file = io.StringIO(xml) 

1255 files = {"xml": xml_file} 

1256 

1257 server_url = collection.website() 

1258 if not server_url: 

1259 raise RuntimeError("The collection has no website") 

1260 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid}) 

1261 header = {} 

1262 

1263 try: 

1264 response = requests.post( 

1265 url, headers=header, files=files, verify=False 

1266 ) # verify: ignore TLS certificate 

1267 status = response.status_code 

1268 except requests.exceptions.ConnectionError: 

1269 raise ServerUnderMaintenance( 

1270 "The journal is under maintenance. Please try again later." 

1271 ) 

1272 

1273 # Register translation in Crossref 

1274 if 199 < status < 205 and self.article.allow_crossref(): 

1275 try: 

1276 recordDOI(translation) 

1277 except Exception: 

1278 raise DOIException( 

1279 "Error while recording the DOI. Please contact the centre Mersenne" 

1280 ) 

1281 

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

1283 doi = kwargs.get("doi", None) 

1284 self.article = model_helpers.get_article_by_doi(doi) 

1285 if self.article is None: 

1286 raise Http404(f"{doi} does not exist") 

1287 

1288 try: 

1289 _, status, message = history_views.execute_and_record_func( 

1290 "deploy", 

1291 self.article.pid, 

1292 self.article.get_top_collection().pid, 

1293 self.internal_do, 

1294 "website", 

1295 ) 

1296 except Timeout as exception: 

1297 return HttpResponse(exception, status=408) 

1298 except Exception as exception: 

1299 logger.exception(f"Failed to post translation for {self.article.pid}") 

1300 return HttpResponseServerError(exception) 

1301 

1302 data = {"message": message, "status": status} 

1303 return JsonResponse(data) 

1304 

1305 

1306class DeleteJatsIssueAPIView(View): 

1307 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book* 

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

1309 pid = self.kwargs.get("pid", None) 

1310 colid = self.kwargs.get("colid", None) 

1311 site = self.kwargs.get("site", None) 

1312 message = f"The issue has correctly been removed from {site}" 

1313 status = 200 

1314 

1315 issue = model_helpers.get_container(pid) 

1316 if not issue: 

1317 raise Http404(f"{pid} does not exist") 

1318 try: 

1319 if check_lock(): 

1320 message = "Trammel is under maintenance. Please try again later." 

1321 messages.error(self.request, message) 

1322 raise ServerUnderMaintenance(message) 

1323 

1324 mersenneSite = model_helpers.get_site_mersenne(colid) 

1325 

1326 if site == "ptf_tools": 

1327 if issue.is_deployed(mersenneSite): 

1328 issue.undeploy(mersenneSite) 

1329 for article in issue.article_set.all(): 

1330 article.undeploy(mersenneSite) 

1331 

1332 p = model_helpers.get_provider("mathdoc-id") 

1333 

1334 cmd = ptf_cmds.addContainerPtfCmd( 

1335 { 

1336 "pid": issue.pid, 

1337 "ctype": "issue", 

1338 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

1339 } 

1340 ) 

1341 cmd.set_provider(p) 

1342 cmd.add_collection(issue.get_collection()) 

1343 cmd.set_object_to_be_deleted(issue) 

1344 cmd.undo() 

1345 

1346 else: 

1347 if site == "numdam": 

1348 server_url = settings.NUMDAM_PRE_URL 

1349 else: 

1350 collection = issue.get_collection() 

1351 server_url = getattr(collection, site)() 

1352 

1353 if not server_url: 

1354 raise Http404(f"The collection has no {site}") 

1355 else: 

1356 # Delete on site 

1357 url = server_url + reverse("issue_delete", kwargs={"pid": pid}) 

1358 response = requests.delete(url, verify=False) 

1359 status = response.status_code 

1360 

1361 if status == 404: 

1362 raise Http404( 

1363 "Issue not found on {site}. Please make sure the article exists." 

1364 ) 

1365 elif status > 204: 

1366 raise HTTPError( 

1367 response.text.encode("utf8")[:1000], 

1368 response=response, 

1369 ) 

1370 else: 

1371 status = 200 

1372 

1373 # Unpublish issue in collection site (site_register.json) 

1374 if site == "website" and issue.is_deployed(mersenneSite): 

1375 issue.undeploy(mersenneSite) 

1376 for article in issue.article_set.all(): 

1377 article.undeploy(mersenneSite) 

1378 # Delete article binary files 

1379 folder = article.get_relative_folder() 

1380 resolver.delete_object_folder( 

1381 folder, 

1382 to_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1383 ) 

1384 # Delete issue binary files 

1385 folder = issue.get_relative_folder() 

1386 resolver.delete_object_folder( 

1387 folder, to_folder=settings.MERSENNE_PROD_DATA_FOLDER 

1388 ) 

1389 

1390 except ValueError as exception: 

1391 if site in {"ptf_tools", "numdam", "website", "test_website"}: 

1392 raise 

1393 message, status = str(exception), 400 

1394 except Timeout: 

1395 message = f"{site} is under maintenance. Please try again later." 

1396 status = 503 

1397 except Http404 as exception: 

1398 message, status = str(exception), 404 

1399 except HTTPError as exception: 

1400 message, status = str(exception), 502 

1401 except ServerUnderMaintenance as exception: 

1402 message, status = str(exception), 503 

1403 except requests.exceptions.ConnectionError: 

1404 message, status = ( 

1405 f"{site} is under maintenance. Please try again later.", 

1406 503, 

1407 ) 

1408 except Exception as exception: 

1409 logger.exception("Unexpected error when deleting article %s from %s", pid, site) 

1410 message, status = str(exception), 500 

1411 

1412 return JsonResponse({"message": message, "status": status}, status=status) 

1413 

1414 

1415class ArchiveIssueAPIView(View): 

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

1417 try: 

1418 pid = kwargs["pid"] 

1419 colid = kwargs["colid"] 

1420 except IndexError: 

1421 raise Http404 

1422 

1423 try: 

1424 cmd = ptf_cmds.archiveIssuePtfCmd( 

1425 { 

1426 "pid": pid, 

1427 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1428 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

1429 "needs_publication_date": True, 

1430 } 

1431 ) 

1432 _result, _status, message = history_views.execute_and_record_func( 

1433 "archive", pid, colid, cmd.do 

1434 ) 

1435 except Exception as exception: 

1436 return HttpResponseServerError(exception) 

1437 

1438 data = {"message": message, "status": 200} 

1439 return JsonResponse(data) 

1440 

1441 

1442class CreateDjvuAPIView(View): 

1443 def internal_do(self, *args, **kwargs): 

1444 pid = self.kwargs.get("pid", None) 

1445 

1446 resource = model_helpers.get_resource(pid) 

1447 cmd = ptf_cmds.addDjvuPtfCmd() 

1448 cmd.set_resource(resource) 

1449 cmd.do() 

1450 

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

1452 pid = self.kwargs.get("pid", None) 

1453 colid = pid.split("_")[0] 

1454 

1455 try: 

1456 _, status, message = history_views.execute_and_record_func( 

1457 "numdam", pid, colid, self.internal_do 

1458 ) 

1459 except Exception as exception: 

1460 return HttpResponseServerError(exception) 

1461 

1462 data = {"message": message, "status": status} 

1463 return JsonResponse(data) 

1464 

1465 

1466class PTFToolsHomeView(LoginRequiredMixin, View): 

1467 """ 

1468 Home Page. 

1469 - Admin & staff -> Render blank home.html 

1470 - User with unique authorized collection -> Redirect to collection details page 

1471 - User with multiple authorized collections -> Render home.html with data 

1472 - Comment moderator -> Comments dashboard 

1473 - Others -> 404 response 

1474 """ 

1475 

1476 def get(self, request, *args, **kwargs) -> HttpResponse: 

1477 # Staff or user with authorized collections 

1478 if request.user.is_staff or request.user.is_superuser: 

1479 return render(request, "home.html") 

1480 

1481 colids = get_authorized_collections(request.user) 

1482 is_mod = is_comment_moderator(request.user) 

1483 

1484 # The user has no rights 

1485 if not (colids or is_mod): 

1486 raise Http404("No collections associated with your account.") 

1487 # Comment moderator only 

1488 elif not colids: 

1489 return HttpResponseRedirect(reverse("comment_list")) 

1490 

1491 # User with unique collection -> Redirect to collection detail page 

1492 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False): 

1493 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]})) 

1494 

1495 # User with multiple authorized collections - Special home 

1496 context = {} 

1497 context["overview"] = True 

1498 

1499 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html") 

1500 all_collections = {c["pid"]: c for c in all_collections} 

1501 

1502 # Comments summary 

1503 try: 

1504 error, comments_data = get_comments_for_home(request.user) 

1505 except AttributeError: 

1506 error, comments_data = True, {} 

1507 

1508 context["comment_server_ok"] = False 

1509 

1510 if not error: 

1511 context["comment_server_ok"] = True 

1512 if comments_data: 

1513 for col_id, comment_nb in comments_data.items(): 

1514 if col_id.upper() in all_collections: 1514 ↛ 1513line 1514 didn't jump to line 1513 because the condition on line 1514 was always true

1515 all_collections[col_id.upper()]["pending_comments"] = comment_nb 

1516 

1517 # TODO: Translations summary 

1518 context["translation_server_ok"] = False 

1519 

1520 # Sort the collections according to the number of pending comments 

1521 context["collections"] = sorted( 

1522 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True 

1523 ) 

1524 

1525 return render(request, "home.html", context) 

1526 

1527 

1528class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin): 

1529 columns = 5 

1530 

1531 def get_common_context_data(self, **kwargs): 

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

1533 now = timezone.now() 

1534 curyear = now.year 

1535 years = range(curyear - self.columns + 1, curyear + 1) 

1536 

1537 context["collections"] = settings.MERSENNE_COLLECTIONS 

1538 context["containers_to_be_published"] = [] 

1539 context["last_col_events"] = [] 

1540 

1541 event = get_history_last_event_by("clockss", "ALL") 

1542 clockss_gap = get_gap(now, event) 

1543 

1544 context["years"] = years 

1545 context["clockss_gap"] = clockss_gap 

1546 

1547 return context 

1548 

1549 def calculate_articles_and_pages(self, pid, years): 

1550 data_by_year = [] 

1551 total_articles = [0] * len(years) 

1552 total_pages = [0] * len(years) 

1553 

1554 for year in years: 

1555 articles = self.get_articles_for_year(pid, year) 

1556 articles_count = articles.count() 

1557 page_count = sum(article.get_article_page_count() for article in articles) 

1558 

1559 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count}) 

1560 total_articles[year - years[0]] += articles_count 

1561 total_pages[year - years[0]] += page_count 

1562 

1563 return data_by_year, total_articles, total_pages 

1564 

1565 def get_articles_for_year(self, pid, year): 

1566 return Article.objects.filter( 

1567 Q(my_container__my_collection__pid=pid) 

1568 & ( 

1569 Q(date_published__year=year, date_online_first__isnull=True) 

1570 | Q(date_online_first__year=year) 

1571 ) 

1572 ).prefetch_related("resourcecount_set") 

1573 

1574 

1575class PublishedArticlesDashboardView(BaseMersenneDashboardView): 

1576 template_name = "dashboard/published_articles.html" 

1577 

1578 def get_context_data(self, **kwargs): 

1579 context = self.get_common_context_data(**kwargs) 

1580 years = context["years"] 

1581 

1582 published_articles = [] 

1583 total_published_articles = [ 

1584 {"year": year, "total_articles": 0, "total_pages": 0} for year in years 

1585 ] 

1586 

1587 for pid in settings.MERSENNE_COLLECTIONS: 

1588 if pid != "MERSENNE": 

1589 articles_data, total_articles, total_pages = self.calculate_articles_and_pages( 

1590 pid, years 

1591 ) 

1592 published_articles.append({"pid": pid, "years": articles_data}) 

1593 

1594 for i, year in enumerate(years): 

1595 total_published_articles[i]["total_articles"] += total_articles[i] 

1596 total_published_articles[i]["total_pages"] += total_pages[i] 

1597 

1598 context["published_articles"] = published_articles 

1599 context["total_published_articles"] = total_published_articles 

1600 

1601 return context 

1602 

1603 

1604class CreatedVolumesDashboardView(BaseMersenneDashboardView): 

1605 template_name = "dashboard/created_volumes.html" 

1606 

1607 def get_context_data(self, **kwargs): 

1608 context = self.get_common_context_data(**kwargs) 

1609 years = context["years"] 

1610 

1611 created_volumes = [] 

1612 total_created_volumes = [ 

1613 {"year": year, "total_articles": 0, "total_pages": 0} for year in years 

1614 ] 

1615 

1616 for pid in settings.MERSENNE_COLLECTIONS: 

1617 if pid != "MERSENNE": 

1618 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages( 

1619 pid, years 

1620 ) 

1621 created_volumes.append({"pid": pid, "years": volumes_data}) 

1622 

1623 for i, _ in enumerate(years): 

1624 total_created_volumes[i]["total_articles"] += total_articles[i] 

1625 total_created_volumes[i]["total_pages"] += total_pages[i] 

1626 

1627 context["created_volumes"] = created_volumes 

1628 context["total_created_volumes"] = total_created_volumes 

1629 

1630 return context 

1631 

1632 def calculate_volumes_and_pages(self, pid, years): 

1633 data_by_year = [] 

1634 total_articles = [0] * len(years) 

1635 total_pages = [0] * len(years) 

1636 

1637 for year in years: 

1638 issues = Container.objects.filter(my_collection__pid=pid, fyear=year) 

1639 articles_count = 0 

1640 page_count = 0 

1641 

1642 for issue in issues: 

1643 articles = issue.article_set.filter( 

1644 Q(date_published__isnull=False) | Q(date_online_first__isnull=False) 

1645 ).prefetch_related("resourcecount_set") 

1646 

1647 articles_count += articles.count() 

1648 page_count += sum(article.get_article_page_count() for article in articles) 

1649 

1650 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count}) 

1651 total_articles[year - years[0]] += articles_count 

1652 total_pages[year - years[0]] += page_count 

1653 

1654 return data_by_year, total_articles, total_pages 

1655 

1656 

1657class ReferencingChoice(View): 

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

1659 if request.POST.get("optSite") == "ads": 

1660 return redirect( 

1661 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")}) 

1662 ) 

1663 elif request.POST.get("optSite") == "wos": 

1664 comp = ReferencingCheckerWos() 

1665 journal = comp.make_journal(request.POST.get("selectCol")) 

1666 if journal is None: 

1667 return render( 

1668 request, 

1669 "dashboard/referencing.html", 

1670 { 

1671 "error": "Collection not found", 

1672 "colid": request.POST.get("selectCol"), 

1673 "optSite": request.POST.get("optSite"), 

1674 }, 

1675 ) 

1676 return render( 

1677 request, 

1678 "dashboard/referencing.html", 

1679 { 

1680 "journal": journal, 

1681 "colid": request.POST.get("selectCol"), 

1682 "optSite": request.POST.get("optSite"), 

1683 }, 

1684 ) 

1685 

1686 

1687class ReferencingWosFileView(View): 

1688 template_name = "dashboard/referencing.html" 

1689 

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

1691 colid = request.POST["colid"] 

1692 if request.FILES.get("risfile") is None: 

1693 message = "No file uploaded" 

1694 return render( 

1695 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"} 

1696 ) 

1697 uploaded_file = request.FILES["risfile"] 

1698 comp = ReferencingCheckerWos() 

1699 journal = comp.check_references(colid, uploaded_file) 

1700 return render(request, self.template_name, {"journal": journal}) 

1701 

1702 

1703class ReferencingDashboardView(BaseMersenneDashboardView): 

1704 template_name = "dashboard/referencing.html" 

1705 

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

1707 colid = self.kwargs.get("colid", None) 

1708 comp = ReferencingCheckerAds() 

1709 journal = comp.check_references(colid) 

1710 return render(request, self.template_name, {"journal": journal}) 

1711 

1712 

1713class BaseCollectionView(TemplateView): 

1714 def get_context_data(self, **kwargs): 

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

1716 aid = context.get("aid") 

1717 year = context.get("year") 

1718 

1719 if aid and year: 

1720 context["collection"] = self.get_collection(aid, year) 

1721 

1722 return context 

1723 

1724 def get_collection(self, aid, year): 

1725 """Method to be overridden by subclasses to fetch the appropriate collection""" 

1726 raise NotImplementedError("Subclasses must implement get_collection method") 

1727 

1728 

1729class ArticleListView(BaseCollectionView): 

1730 template_name = "collection-list.html" 

1731 

1732 def get_collection(self, aid, year): 

1733 return Article.objects.filter( 

1734 Q(my_container__my_collection__pid=aid) 

1735 & ( 

1736 Q(date_published__year=year, date_online_first__isnull=True) 

1737 | Q(date_online_first__year=year) 

1738 ) 

1739 ).prefetch_related("resourcecount_set") 

1740 

1741 

1742class VolumeListView(BaseCollectionView): 

1743 template_name = "collection-list.html" 

1744 

1745 def get_collection(self, aid, year): 

1746 return Article.objects.filter( 

1747 Q(my_container__my_collection__pid=aid, my_container__fyear=year) 

1748 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False)) 

1749 ).prefetch_related("resourcecount_set") 

1750 

1751 

1752class DOAJResourceRegisterView(View): 

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

1754 pid = kwargs.get("pid", None) 

1755 resource = model_helpers.get_resource(pid) 

1756 if resource is None: 

1757 raise Http404 

1758 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get( 

1759 resource.colid, None 

1760 ): 

1761 raise RuntimeError("Pending publications should not be deployed") 

1762 

1763 try: 

1764 data = {} 

1765 doaj_meta, response = doaj_pid_register(pid) 

1766 if response is None: 

1767 return HttpResponse(status=204) 

1768 elif doaj_meta and 200 <= response.status_code <= 299: 

1769 data.update(doaj_meta) 

1770 else: 

1771 return HttpResponse(status=response.status_code, reason=response.text) 

1772 except Timeout as exception: 

1773 return HttpResponse(exception, status=408) 

1774 except Exception as exception: 

1775 return HttpResponseServerError(exception) 

1776 return JsonResponse(data) 

1777 

1778 

1779class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

1780 """ 

1781 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml 

1782 """ 

1783 

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

1785 pid = kwargs.get("pid") 

1786 if not pid: 

1787 raise Http404("Missing pid") 

1788 

1789 article = Article.objects.filter(pid=pid).first() 

1790 if not article: 

1791 raise Http404(f"Article not found: {pid}") 

1792 

1793 colid = article.get_collection().pid 

1794 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS: 

1795 return JsonResponse( 

1796 {"status": 403, "message": f"Tex conversions are disabled in {colid}"} 

1797 ) 

1798 

1799 if is_tex_conversion_locked(pid): 

1800 logger.warning("Conversion rejected (lock exists) for %s", pid) 

1801 return JsonResponse( 

1802 {"status": 409, "message": f"A conversion is already running for {pid}"} 

1803 ) 

1804 

1805 logger.info("No lock → scheduling conversion for %s", pid) 

1806 

1807 try: 

1808 convert_article_tex.delay(pid=pid, user_pk=request.user.pk) 

1809 except Exception: 

1810 logger.exception("Failed to enqueue task for %s", pid) 

1811 release_tex_conversion_lock(pid) 

1812 raise 

1813 

1814 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"}) 

1815 

1816 

1817class CROSSREFResourceRegisterView(View): 

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

1819 pid = kwargs.get("pid", None) 

1820 # option force for registering doi of articles without date_published (ex; TSG from Numdam) 

1821 force = kwargs.get("force", None) 

1822 if not request.user.is_superuser: 

1823 force = None 

1824 

1825 resource = model_helpers.get_resource(pid) 

1826 if resource is None: 

1827 raise Http404 

1828 

1829 resource = resource.cast() 

1830 meth = getattr(self, "recordDOI" + resource.classname) 

1831 try: 

1832 data = meth(resource, force) 

1833 except Timeout as exception: 

1834 return HttpResponse(exception, status=408) 

1835 except Exception as exception: 

1836 return HttpResponseServerError(exception) 

1837 return JsonResponse(data) 

1838 

1839 def recordDOIArticle(self, article: "Article", force=None): 

1840 result = {"status": 404} 

1841 if ( 

1842 article.doi 

1843 and not article.do_not_publish 

1844 and (article.date_published or article.date_online_first or force == "force") 

1845 ): 

1846 if article.my_container.fyear == 0: 

1847 article.my_container.fyear = datetime.now().year 

1848 result = recordDOI(article) 

1849 return result 

1850 

1851 def recordDOICollection(self, collection, force=None): 

1852 return recordDOI(collection) 

1853 

1854 def recordDOIContainer(self, container, force=None): 

1855 data = {"status": 200, "message": "All DOI successfully checked"} 

1856 

1857 if container.ctype == "issue": 

1858 if container.doi: 

1859 result = recordDOI(container) 

1860 if result["status"] != 200: 

1861 return result 

1862 if force == "force": 

1863 articles = container.article_set.exclude( 

1864 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True 

1865 ) 

1866 else: 

1867 articles = container.article_set.exclude( 

1868 doi__isnull=True, 

1869 do_not_publish=True, 

1870 date_published__isnull=True, 

1871 date_online_first__isnull=True, 

1872 ) 

1873 

1874 for article in articles: 

1875 result = self.recordDOIArticle(article, force) 

1876 if result["status"] != 200: 

1877 data = result 

1878 else: 

1879 return recordDOI(container) 

1880 return data 

1881 

1882 

1883class CROSSREFResourceCheckStatusView(View): 

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

1885 pid = kwargs.get("pid", None) 

1886 resource = model_helpers.get_resource(pid) 

1887 if resource is None: 

1888 raise Http404 

1889 resource = resource.cast() 

1890 meth = getattr(self, "checkDOI" + resource.classname) 

1891 try: 

1892 meth(resource) 

1893 except Timeout as exception: 

1894 return HttpResponse(exception, status=408) 

1895 except Exception as exception: 

1896 return HttpResponseServerError(exception) 

1897 

1898 data = {"status": 200, "message": "DOI successfully checked"} 

1899 return JsonResponse(data) 

1900 

1901 def checkDOIArticle(self, article: "Article"): 

1902 if article.my_container.fyear == 0: 

1903 article.my_container.fyear = datetime.now().year 

1904 checkDOI(article) 

1905 

1906 def checkDOICollection(self, collection): 

1907 checkDOI(collection) 

1908 

1909 def checkDOIContainer(self, container): 

1910 if container.doi is not None: 

1911 checkDOI(container) 

1912 for article in container.article_set.all(): 

1913 self.checkDOIArticle(article) 

1914 

1915 

1916class CROSSREFResourcePendingPublicationRegisterView(View): 

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

1918 pid = kwargs.get("pid", None) 

1919 # option force for registering doi of articles without date_published (ex; TSG from Numdam) 

1920 

1921 resource = model_helpers.get_resource(pid) 

1922 if resource is None: 

1923 raise Http404 

1924 

1925 resource = resource.cast() 

1926 meth = getattr(self, "recordPendingPublication" + resource.classname) 

1927 try: 

1928 data = meth(resource) 

1929 except Timeout as exception: 

1930 return HttpResponse(exception, status=408) 

1931 except Exception as exception: 

1932 return HttpResponseServerError(exception) 

1933 return JsonResponse(data) 

1934 

1935 def recordPendingPublicationArticle(self, article): 

1936 result = {"status": 404} 

1937 if article.doi and not article.date_published and not article.date_online_first: 

1938 if article.my_container.fyear is None or article.my_container.fyear == "0": 

1939 article.my_container.fyear = datetime.now().year 

1940 result = recordPendingPublication(article) 

1941 return result 

1942 

1943 

1944class RegisterPubmedFormView(FormView): 

1945 template_name = "record_pubmed_dialog.html" 

1946 form_class = RegisterPubmedForm 

1947 

1948 def get_context_data(self, **kwargs): 

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

1950 context["pid"] = self.kwargs["pid"] 

1951 context["helper"] = PtfLargeModalFormHelper 

1952 return context 

1953 

1954 

1955class RegisterPubmedView(View): 

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

1957 pid = kwargs.get("pid", None) 

1958 update_article = self.request.GET.get("update_article", "on") == "on" 

1959 

1960 article = model_helpers.get_article(pid) 

1961 if article is None: 

1962 raise Http404 

1963 try: 

1964 recordPubmed(article, update_article) 

1965 except Exception as exception: 

1966 messages.error("Unable to register the article in PubMed") 

1967 return HttpResponseServerError(exception) 

1968 

1969 return HttpResponseRedirect( 

1970 reverse("issue-items", kwargs={"pid": article.my_container.pid}) 

1971 ) 

1972 

1973 

1974class PTFToolsArticleView(ArticleView): 

1975 def get_context_data(self, **kwargs): 

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

1977 

1978 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(self.obj) 

1979 

1980 qs = self.obj.get_top_collection().extlink_set.filter(rel="test_website") 

1981 if qs: 

1982 test_website = qs.first().location 

1983 context["test_website"] = test_website 

1984 qs = self.obj.get_top_collection().extlink_set.filter(rel="website") 

1985 if qs: 

1986 prod_location = qs.first().location 

1987 context["prod_website"] = prod_location 

1988 

1989 return context 

1990 

1991 

1992ItemViewClassFactory.views["article"] = PTFToolsArticleView 

1993 

1994 

1995class PTFToolsContainerView(TemplateView): 

1996 template_name = "" 

1997 

1998 def get_context_data(self, **kwargs): 

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

2000 

2001 container = model_helpers.get_container(self.kwargs.get("pid")) 

2002 if container is None: 

2003 raise Http404 

2004 citing_articles = container.citations() 

2005 source = self.request.GET.get("source", None) 

2006 if container.ctype.startswith("book"): 

2007 book_parts = ( 

2008 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq") 

2009 ) 

2010 references = False 

2011 if container.ctype == "book-monograph": 

2012 # on regarde si il y a au moins une bibliographie 

2013 for art in container.article_set.all(): 

2014 if art.bibitem_set.count() > 0: 

2015 references = True 

2016 context.update( 

2017 { 

2018 "book": container, 

2019 "book_parts": list(book_parts), 

2020 "source": source, 

2021 "citing_articles": citing_articles, 

2022 "references": references, 

2023 "test_website": container.get_top_collection() 

2024 .extlink_set.get(rel="test_website") 

2025 .location, 

2026 "prod_website": container.get_top_collection() 

2027 .extlink_set.get(rel="website") 

2028 .location, 

2029 } 

2030 ) 

2031 self.template_name = "book-toc.html" 

2032 else: 

2033 articles = container.article_set.all().order_by("seq") 

2034 for article in articles: 

2035 try: 

2036 last_match = ( 

2037 history_models.HistoryEvent.objects.filter( 

2038 pid=article.pid, 

2039 type="matching", 

2040 ) 

2041 .only("created_on") 

2042 .latest("created_on") 

2043 ) 

2044 except history_models.HistoryEvent.DoesNotExist as _: 

2045 article.last_match = None 

2046 else: 

2047 article.last_match = last_match.created_on 

2048 

2049 # article1 = articles.first() 

2050 # date = article1.deployed_date() 

2051 # TODO next_issue, previous_issue 

2052 

2053 # check DOI est maintenant une commande à part 

2054 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI 

2055 # articlesWithStatus = [] 

2056 # for article in articles: 

2057 # checkDOIExistence(article) 

2058 # articlesWithStatus.append(article) 

2059 

2060 test_location = prod_location = "" 

2061 qs = container.get_top_collection().extlink_set.filter(rel="test_website") 

2062 if qs: 

2063 test_location = qs.first().location 

2064 qs = container.get_top_collection().extlink_set.filter(rel="website") 

2065 if qs: 

2066 prod_location = qs.first().location 

2067 context.update( 

2068 { 

2069 "issue": container, 

2070 "articles": articles, 

2071 "source": source, 

2072 "citing_articles": citing_articles, 

2073 "test_website": test_location, 

2074 "prod_website": prod_location, 

2075 } 

2076 ) 

2077 

2078 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values(): 

2079 context["is_issue_pending_publication"] = True 

2080 if container.get_top_collection().pid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS: 

2081 context["is_excluded_from_tex_conversion"] = True 

2082 self.template_name = "issue-items.html" 

2083 

2084 context["allow_crossref"] = container.allow_crossref() 

2085 context["coltype"] = container.my_collection.coltype 

2086 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(container) 

2087 return context 

2088 

2089 

2090class ExtLinkInline(InlineFormSetFactory): 

2091 model = ExtLink 

2092 form_class = ExtLinkForm 

2093 factory_kwargs = {"extra": 0} 

2094 

2095 

2096class ResourceIdInline(InlineFormSetFactory): 

2097 model = ResourceId 

2098 form_class = ResourceIdForm 

2099 factory_kwargs = {"extra": 0} 

2100 

2101 

2102class IssueDetailAPIView(View): 

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

2104 issue = get_object_or_404(Container, pid=kwargs["pid"]) 

2105 deployed_date = issue.deployed_date() 

2106 result = { 

2107 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M") 

2108 if deployed_date 

2109 else None, 

2110 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"), 

2111 "all_doi_are_registered": issue.all_doi_are_registered(), 

2112 "registered_in_doaj": issue.registered_in_doaj(), 

2113 "doi": issue.my_collection.doi, 

2114 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(), 

2115 } 

2116 try: 

2117 latest = get_last_unsolved_error(pid=issue.pid, strict=False) 

2118 except history_models.HistoryEvent.DoesNotExist as _: 

2119 pass 

2120 else: 

2121 result["latest"] = latest.message 

2122 result["latest_date"] = timezone.localtime(latest.created_on).strftime( 

2123 "%Y-%m-%d %H:%M" 

2124 ) 

2125 

2126 result["latest_type"] = latest.type.capitalize() 

2127 for event_type in ["matching", "edit", "deploy", "archive", "import"]: 

2128 try: 

2129 result[event_type] = timezone.localtime( 

2130 history_models.HistoryEvent.objects.filter( 

2131 type=event_type, 

2132 status="OK", 

2133 pid__startswith=issue.pid, 

2134 ) 

2135 .latest("created_on") 

2136 .created_on 

2137 ).strftime("%Y-%m-%d %H:%M") 

2138 except history_models.HistoryEvent.DoesNotExist as _: 

2139 result[event_type] = "" 

2140 return JsonResponse(result) 

2141 

2142 

2143class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View): 

2144 model = Collection 

2145 form_class = CollectionForm 

2146 inlines = [ResourceIdInline, ExtLinkInline] 

2147 inlines_names = ["resource_ids_form", "ext_links_form"] 

2148 

2149 def get_context_data(self, **kwargs): 

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

2151 context["helper"] = PtfFormHelper 

2152 context["formset_helper"] = FormSetHelper 

2153 return context 

2154 

2155 def add_description(self, collection, description, lang, seq): 

2156 if description: 

2157 la = Abstract( 

2158 resource=collection, 

2159 tag="description", 

2160 lang=lang, 

2161 seq=seq, 

2162 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>', 

2163 value_html=description, 

2164 value_tex=description, 

2165 ) 

2166 la.save() 

2167 

2168 def form_valid(self, form): 

2169 if form.instance.abbrev: 

2170 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>" 

2171 else: 

2172 form.instance.title_xml = ( 

2173 f"<title-group><title>{form.instance.title_tex}</title></title-group>" 

2174 ) 

2175 

2176 form.instance.title_html = form.instance.title_tex 

2177 form.instance.title_sort = form.instance.title_tex 

2178 result = super().form_valid(form) 

2179 

2180 collection = self.object 

2181 collection.abstract_set.all().delete() 

2182 

2183 seq = 1 

2184 description = form.cleaned_data["description_en"] 

2185 if description: 

2186 self.add_description(collection, description, "en", seq) 

2187 seq += 1 

2188 description = form.cleaned_data["description_fr"] 

2189 if description: 

2190 self.add_description(collection, description, "fr", seq) 

2191 

2192 return result 

2193 

2194 def get_success_url(self): 

2195 messages.success( 

2196 self.request, f'The collection "{self.object.pid}" has been successfully updated' 

2197 ) 

2198 return reverse("collection-detail", kwargs={"pid": self.object.pid}) 

2199 

2200 

2201class CollectionCreate(CollectionFormView, CreateWithInlinesView): 

2202 """ 

2203 Warning : Not yet finished 

2204 Automatic site membership creation is still missing 

2205 """ 

2206 

2207 

2208class CollectionUpdate(CollectionFormView, UpdateWithInlinesView): 

2209 slug_field = "pid" 

2210 slug_url_kwarg = "pid" 

2211 

2212 

2213def suggest_load_journal_dois(colid): 

2214 articles = ( 

2215 Article.objects.filter(my_container__my_collection__pid=colid) 

2216 .filter(doi__isnull=False) 

2217 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False)) 

2218 .values_list("doi", flat=True) 

2219 ) 

2220 

2221 try: 

2222 articles = sorted( 

2223 articles, 

2224 key=lambda d: ( 

2225 re.search(r"([a-zA-Z]+).\d+$", d).group(1), 

2226 int(re.search(r".(\d+)$", d).group(1)), 

2227 ), 

2228 ) 

2229 except: # noqa: E722 (we'll look later) 

2230 pass 

2231 return [f'<option value="{doi}">' for doi in articles] 

2232 

2233 

2234def get_context_with_volumes(journal): 

2235 result = model_helpers.get_volumes_in_collection(journal) 

2236 volume_count = result["volume_count"] 

2237 collections = [] 

2238 for ancestor in journal.ancestors.all(): 

2239 item = model_helpers.get_volumes_in_collection(ancestor) 

2240 volume_count = max(0, volume_count) 

2241 item.update({"journal": ancestor}) 

2242 collections.append(item) 

2243 

2244 # add the parent collection to its children list and sort it by date 

2245 result.update({"journal": journal}) 

2246 collections.append(result) 

2247 

2248 collections = [c for c in collections if c["sorted_issues"]] 

2249 collections.sort( 

2250 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"], 

2251 reverse=True, 

2252 ) 

2253 

2254 context = { 

2255 "journal": journal, 

2256 "sorted_issues": result["sorted_issues"], 

2257 "volume_count": volume_count, 

2258 "max_width": result["max_width"], 

2259 "collections": collections, 

2260 "choices": "\n".join(suggest_load_journal_dois(journal.pid)), 

2261 } 

2262 return context 

2263 

2264 

2265class CollectionDetail( 

2266 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin 

2267): 

2268 model = Collection 

2269 slug_field = "pid" 

2270 slug_url_kwarg = "pid" 

2271 template_name = "ptf/collection_detail.html" 

2272 

2273 def test_func(self): 

2274 return is_authorized_editor(self.request.user, self.kwargs.get("pid")) 

2275 

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

2277 self.object = self.get_object(queryset=Collection.objects.all()) 

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

2279 

2280 def get_context_data(self, **kwargs): 

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

2282 context["object_list"] = context["object_list"].filter( 

2283 Q(ctype="issue") | Q(ctype="book-lecture-notes") | Q(ctype="book-monograph") 

2284 ) 

2285 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS 

2286 context.update(get_context_with_volumes(self.object)) 

2287 

2288 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS: 

2289 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid] 

2290 context["issue_to_appear"] = Container.objects.filter( 

2291 pid=context["issue_to_appear_pid"] 

2292 ).exists() 

2293 try: 

2294 latest_error = history_models.HistoryEvent.objects.filter( 

2295 status="ERROR", col=self.object 

2296 ).latest("created_on") 

2297 except history_models.HistoryEvent.DoesNotExist as _: 

2298 pass 

2299 else: 

2300 message = latest_error.message 

2301 if message: 

2302 i = message.find(" - ") 

2303 latest_exception = message[:i] 

2304 latest_error_message = message[i + 3 :] 

2305 context["latest_exception"] = latest_exception 

2306 context["latest_exception_date"] = latest_error.created_on 

2307 context["latest_exception_type"] = latest_error.type 

2308 context["latest_error_message"] = latest_error_message 

2309 

2310 archive_in_error = history_models.HistoryEvent.objects.filter( 

2311 status="ERROR", col=self.object, type="archive" 

2312 ).exists() 

2313 

2314 context["archive_in_error"] = archive_in_error 

2315 

2316 return context 

2317 

2318 def get_queryset(self): 

2319 query = self.object.content.all() 

2320 

2321 for ancestor in self.object.ancestors.all(): 

2322 query |= ancestor.content.all() 

2323 

2324 return query.order_by("-fyear", "-vseries", "-volume", "-volume_int", "-number_int") 

2325 

2326 

2327class ContainerEditView(FormView): 

2328 template_name = "container_form.html" 

2329 form_class = ContainerForm 

2330 

2331 def get_success_url(self): 

2332 if self.kwargs["pid"]: 

2333 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]}) 

2334 return reverse("mersenne_dashboard/published_articles") 

2335 

2336 def set_success_message(self): # pylint: disable=no-self-use 

2337 messages.success(self.request, "Booklet updated") 

2338 

2339 def get_form_kwargs(self): 

2340 kwargs = super().get_form_kwargs() 

2341 if "pid" not in self.kwargs: 

2342 self.kwargs["pid"] = None 

2343 if "colid" not in self.kwargs: 

2344 self.kwargs["colid"] = None 

2345 if "data" in kwargs and "colid" in kwargs["data"]: 

2346 # colid is passed as a hidden param in the form. 

2347 # It is used when you submit a new container 

2348 self.kwargs["colid"] = kwargs["data"]["colid"] 

2349 

2350 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container( 

2351 self.kwargs["pid"] 

2352 ) 

2353 return kwargs 

2354 

2355 def get_context_data(self, **kwargs): 

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

2357 

2358 context["pid"] = self.kwargs["pid"] 

2359 context["colid"] = self.kwargs["colid"] 

2360 context["container"] = self.kwargs["container"] 

2361 

2362 context["edit_container"] = context["pid"] is not None 

2363 context["name"] = resolve(self.request.path_info).url_name 

2364 

2365 return context 

2366 

2367 def form_valid(self, form): 

2368 new_pid = form.cleaned_data.get("pid") 

2369 new_title = form.cleaned_data.get("title") 

2370 new_trans_title = form.cleaned_data.get("trans_title") 

2371 new_publisher = form.cleaned_data.get("publisher") 

2372 new_year = form.cleaned_data.get("year") 

2373 new_volume = form.cleaned_data.get("volume") 

2374 new_number = form.cleaned_data.get("number") 

2375 

2376 collection = None 

2377 issue = self.kwargs["container"] 

2378 if issue is not None: 

2379 collection = issue.my_collection 

2380 elif self.kwargs["colid"] is not None: 

2381 if "CR" in self.kwargs["colid"]: 

2382 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False) 

2383 else: 

2384 collection = model_helpers.get_collection(self.kwargs["colid"]) 

2385 

2386 if collection is None: 

2387 raise ValueError("Collection for " + new_pid + " does not exist") 

2388 

2389 # Icon 

2390 new_icon_location = "" 

2391 if "icon" in self.request.FILES: 

2392 filename = os.path.basename(self.request.FILES["icon"].name) 

2393 file_extension = filename.split(".")[1] 

2394 

2395 icon_filename = resolver.get_disk_location( 

2396 settings.MERSENNE_TEST_DATA_FOLDER, 

2397 collection.pid, 

2398 file_extension, 

2399 new_pid, 

2400 None, 

2401 True, 

2402 ) 

2403 

2404 with open(icon_filename, "wb+") as destination: 

2405 destination.writelines(self.request.FILES["icon"].chunks()) 

2406 

2407 folder = resolver.get_relative_folder(collection.pid, new_pid) 

2408 new_icon_location = os.path.join(folder, new_pid + "." + file_extension) 

2409 name = resolve(self.request.path_info).url_name 

2410 if name == "special_issue_create": 

2411 self.kwargs["name"] = name 

2412 if self.kwargs["container"]: 

2413 # Edit Issue 

2414 issue = self.kwargs["container"] 

2415 if issue is None: 

2416 raise ValueError(self.kwargs["pid"] + " does not exist") 

2417 

2418 issue.pid = new_pid 

2419 issue.title_tex = issue.title_html = new_title 

2420 issue.title_xml = build_title_xml( 

2421 title=new_title, 

2422 lang=issue.lang, 

2423 title_type="issue-title", 

2424 ) 

2425 

2426 trans_lang = "" 

2427 if new_trans_title != "": 

2428 trans_lang = "fr" if issue.lang == "en" else "en" 

2429 

2430 if trans_lang != "" and new_trans_title != "": 

2431 title_xml = build_title_xml( 

2432 title=new_trans_title, lang=trans_lang, title_type="issue-title" 

2433 ) 

2434 

2435 issue.title_set.update_or_create( 

2436 lang=trans_lang, 

2437 type="main", 

2438 defaults={"title_html": new_trans_title, "title_xml": title_xml}, 

2439 ) 

2440 

2441 issue.fyear = new_year 

2442 issue.volume = new_volume 

2443 issue.volume_int = make_int(new_volume) 

2444 issue.number = new_number 

2445 issue.number_int = make_int(new_number) 

2446 issue.save() 

2447 else: 

2448 xissue = create_issuedata() 

2449 

2450 xissue.ctype = "issue" 

2451 xissue.pid = new_pid 

2452 xissue.lang = "en" 

2453 xissue.title_tex = new_title 

2454 xissue.title_html = new_title 

2455 xissue.title_xml = build_title_xml( 

2456 title=new_title, lang=xissue.lang, title_type="issue-title" 

2457 ) 

2458 

2459 if new_trans_title != "": 

2460 trans_lang = "fr" 

2461 title_xml = build_title_xml( 

2462 title=new_trans_title, lang=trans_lang, title_type="trans-title" 

2463 ) 

2464 title = create_titledata( 

2465 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml 

2466 ) 

2467 issue.titles = [title] 

2468 

2469 xissue.fyear = new_year 

2470 xissue.volume = new_volume 

2471 xissue.number = new_number 

2472 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 

2473 

2474 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue}) 

2475 cmd.add_collection(collection) 

2476 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc")) 

2477 issue = cmd.do() 

2478 

2479 self.kwargs["pid"] = new_pid 

2480 

2481 # Add objects related to the article: contribs, datastream, counts... 

2482 params = { 

2483 "icon_location": new_icon_location, 

2484 } 

2485 cmd = ptf_cmds.updateContainerPtfCmd(params) 

2486 cmd.set_resource(issue) 

2487 cmd.do() 

2488 

2489 publisher = model_helpers.get_publisher(new_publisher) 

2490 if not publisher: 

2491 xpub = create_publisherdata() 

2492 xpub.name = new_publisher 

2493 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do() 

2494 issue.my_publisher = publisher 

2495 issue.save() 

2496 

2497 self.set_success_message() 

2498 

2499 return super().form_valid(form) 

2500 

2501 

2502# class ArticleEditView(FormView): 

2503# template_name = 'article_form.html' 

2504# form_class = ArticleForm 

2505# 

2506# def get_success_url(self): 

2507# if self.kwargs['pid']: 

2508# return reverse('article', kwargs={'aid': self.kwargs['pid']}) 

2509# return reverse('mersenne_dashboard/published_articles') 

2510# 

2511# def set_success_message(self): # pylint: disable=no-self-use 

2512# messages.success(self.request, "L'article a été modifié") 

2513# 

2514# def get_form_kwargs(self): 

2515# kwargs = super(ArticleEditView, self).get_form_kwargs() 

2516# 

2517# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None': 

2518# # Article creation: pid is None 

2519# self.kwargs['pid'] = None 

2520# if 'issue_id' not in self.kwargs: 

2521# # Article edit: issue_id is not passed 

2522# self.kwargs['issue_id'] = None 

2523# if 'data' in kwargs and 'issue_id' in kwargs['data']: 

2524# # colid is passed as a hidden param in the form. 

2525# # It is used when you submit a new container 

2526# self.kwargs['issue_id'] = kwargs['data']['issue_id'] 

2527# 

2528# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid']) 

2529# return kwargs 

2530# 

2531# def get_context_data(self, **kwargs): 

2532# context = super(ArticleEditView, self).get_context_data(**kwargs) 

2533# 

2534# context['pid'] = self.kwargs['pid'] 

2535# context['issue_id'] = self.kwargs['issue_id'] 

2536# context['article'] = self.kwargs['article'] 

2537# 

2538# context['edit_article'] = context['pid'] is not None 

2539# 

2540# article = context['article'] 

2541# if article: 

2542# context['author_contributions'] = article.get_author_contributions() 

2543# context['kwds_fr'] = None 

2544# context['kwds_en'] = None 

2545# kwd_gps = article.get_non_msc_kwds() 

2546# for kwd_gp in kwd_gps: 

2547# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'): 

2548# if kwd_gp.value_xml: 

2549# kwd_ = types.SimpleNamespace() 

2550# kwd_.value = kwd_gp.value_tex 

2551# context['kwd_unstructured_fr'] = kwd_ 

2552# context['kwds_fr'] = kwd_gp.kwd_set.all() 

2553# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'): 

2554# if kwd_gp.value_xml: 

2555# kwd_ = types.SimpleNamespace() 

2556# kwd_.value = kwd_gp.value_tex 

2557# context['kwd_unstructured_en'] = kwd_ 

2558# context['kwds_en'] = kwd_gp.kwd_set.all() 

2559# 

2560# # Article creation: init pid 

2561# if context['issue_id'] and context['pid'] is None: 

2562# issue = model_helpers.get_container(context['issue_id']) 

2563# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0' 

2564# 

2565# return context 

2566# 

2567# def form_valid(self, form): 

2568# 

2569# new_pid = form.cleaned_data.get('pid') 

2570# new_title = form.cleaned_data.get('title') 

2571# new_fpage = form.cleaned_data.get('fpage') 

2572# new_lpage = form.cleaned_data.get('lpage') 

2573# new_page_range = form.cleaned_data.get('page_range') 

2574# new_page_count = form.cleaned_data.get('page_count') 

2575# new_coi_statement = form.cleaned_data.get('coi_statement') 

2576# new_show_body = form.cleaned_data.get('show_body') 

2577# new_do_not_publish = form.cleaned_data.get('do_not_publish') 

2578# 

2579# # TODO support MathML 

2580# # 27/10/2020: title_xml embeds the trans_title_group in JATS. 

2581# # We need to pass trans_title to get_title_xml 

2582# # Meanwhile, ignore new_title_xml 

2583# new_title_xml = jats_parser.get_title_xml(new_title) 

2584# new_title_html = new_title 

2585# 

2586# authors_count = int(self.request.POST.get('authors_count', "0")) 

2587# i = 1 

2588# new_authors = [] 

2589# old_author_contributions = [] 

2590# if self.kwargs['article']: 

2591# old_author_contributions = self.kwargs['article'].get_author_contributions() 

2592# 

2593# while authors_count > 0: 

2594# prefix = self.request.POST.get('contrib-p-' + str(i), None) 

2595# 

2596# if prefix is not None: 

2597# addresses = [] 

2598# if len(old_author_contributions) >= i: 

2599# old_author_contribution = old_author_contributions[i - 1] 

2600# addresses = [contrib_address.address for contrib_address in 

2601# old_author_contribution.get_addresses()] 

2602# 

2603# first_name = self.request.POST.get('contrib-f-' + str(i), None) 

2604# last_name = self.request.POST.get('contrib-l-' + str(i), None) 

2605# suffix = self.request.POST.get('contrib-s-' + str(i), None) 

2606# orcid = self.request.POST.get('contrib-o-' + str(i), None) 

2607# deceased = self.request.POST.get('contrib-d-' + str(i), None) 

2608# deceased_before_publication = deceased == 'on' 

2609# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None) 

2610# equal_contrib = equal_contrib == 'on' 

2611# corresponding = self.request.POST.get('corresponding-' + str(i), None) 

2612# corresponding = corresponding == 'on' 

2613# email = self.request.POST.get('email-' + str(i), None) 

2614# 

2615# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid) 

2616# params['deceased_before_publication'] = deceased_before_publication 

2617# params['equal_contrib'] = equal_contrib 

2618# params['corresponding'] = corresponding 

2619# params['addresses'] = addresses 

2620# params['email'] = email 

2621# 

2622# params['contrib_xml'] = xml_utils.get_contrib_xml(params) 

2623# 

2624# new_authors.append(params) 

2625# 

2626# authors_count -= 1 

2627# i += 1 

2628# 

2629# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0")) 

2630# i = 1 

2631# new_kwds_fr = [] 

2632# while kwds_fr_count > 0: 

2633# value = self.request.POST.get('kwd-fr-' + str(i), None) 

2634# new_kwds_fr.append(value) 

2635# kwds_fr_count -= 1 

2636# i += 1 

2637# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None) 

2638# 

2639# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0")) 

2640# i = 1 

2641# new_kwds_en = [] 

2642# while kwds_en_count > 0: 

2643# value = self.request.POST.get('kwd-en-' + str(i), None) 

2644# new_kwds_en.append(value) 

2645# kwds_en_count -= 1 

2646# i += 1 

2647# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None) 

2648# 

2649# if self.kwargs['article']: 

2650# # Edit article 

2651# container = self.kwargs['article'].my_container 

2652# else: 

2653# # New article 

2654# container = model_helpers.get_container(self.kwargs['issue_id']) 

2655# 

2656# if container is None: 

2657# raise ValueError(self.kwargs['issue_id'] + " does not exist") 

2658# 

2659# collection = container.my_collection 

2660# 

2661# # Copy PDF file & extract full text 

2662# body = '' 

2663# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER, 

2664# collection.pid, 

2665# "pdf", 

2666# container.pid, 

2667# new_pid, 

2668# True) 

2669# if 'pdf' in self.request.FILES: 

2670# with open(pdf_filename, 'wb+') as destination: 

2671# for chunk in self.request.FILES['pdf'].chunks(): 

2672# destination.write(chunk) 

2673# 

2674# # Extract full text from the PDF 

2675# body = utils.pdf_to_text(pdf_filename) 

2676# 

2677# # Icon 

2678# new_icon_location = '' 

2679# if 'icon' in self.request.FILES: 

2680# filename = os.path.basename(self.request.FILES['icon'].name) 

2681# file_extension = filename.split('.')[1] 

2682# 

2683# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER, 

2684# collection.pid, 

2685# file_extension, 

2686# container.pid, 

2687# new_pid, 

2688# True) 

2689# 

2690# with open(icon_filename, 'wb+') as destination: 

2691# for chunk in self.request.FILES['icon'].chunks(): 

2692# destination.write(chunk) 

2693# 

2694# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid) 

2695# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension) 

2696# 

2697# if self.kwargs['article']: 

2698# # Edit article 

2699# article = self.kwargs['article'] 

2700# article.fpage = new_fpage 

2701# article.lpage = new_lpage 

2702# article.page_range = new_page_range 

2703# article.coi_statement = new_coi_statement 

2704# article.show_body = new_show_body 

2705# article.do_not_publish = new_do_not_publish 

2706# article.save() 

2707# 

2708# else: 

2709# # New article 

2710# params = { 

2711# 'pid': new_pid, 

2712# 'title_xml': new_title_xml, 

2713# 'title_html': new_title_html, 

2714# 'title_tex': new_title, 

2715# 'fpage': new_fpage, 

2716# 'lpage': new_lpage, 

2717# 'page_range': new_page_range, 

2718# 'seq': container.article_set.count() + 1, 

2719# 'body': body, 

2720# 'coi_statement': new_coi_statement, 

2721# 'show_body': new_show_body, 

2722# 'do_not_publish': new_do_not_publish 

2723# } 

2724# 

2725# xarticle = create_articledata() 

2726# xarticle.pid = new_pid 

2727# xarticle.title_xml = new_title_xml 

2728# xarticle.title_html = new_title_html 

2729# xarticle.title_tex = new_title 

2730# xarticle.fpage = new_fpage 

2731# xarticle.lpage = new_lpage 

2732# xarticle.page_range = new_page_range 

2733# xarticle.seq = container.article_set.count() + 1 

2734# xarticle.body = body 

2735# xarticle.coi_statement = new_coi_statement 

2736# params['xobj'] = xarticle 

2737# 

2738# cmd = ptf_cmds.addArticlePtfCmd(params) 

2739# cmd.set_container(container) 

2740# cmd.add_collection(container.my_collection) 

2741# article = cmd.do() 

2742# 

2743# self.kwargs['pid'] = new_pid 

2744# 

2745# # Add objects related to the article: contribs, datastream, counts... 

2746# params = { 

2747# # 'title_xml': new_title_xml, 

2748# # 'title_html': new_title_html, 

2749# # 'title_tex': new_title, 

2750# 'authors': new_authors, 

2751# 'page_count': new_page_count, 

2752# 'icon_location': new_icon_location, 

2753# 'body': body, 

2754# 'use_kwds': True, 

2755# 'kwds_fr': new_kwds_fr, 

2756# 'kwds_en': new_kwds_en, 

2757# 'kwd_uns_fr': new_kwd_uns_fr, 

2758# 'kwd_uns_en': new_kwd_uns_en 

2759# } 

2760# cmd = ptf_cmds.updateArticlePtfCmd(params) 

2761# cmd.set_article(article) 

2762# cmd.do() 

2763# 

2764# self.set_success_message() 

2765# 

2766# return super(ArticleEditView, self).form_valid(form) 

2767 

2768 

2769@require_http_methods(["POST"]) 

2770def do_not_publish_article(request, *args, **kwargs): 

2771 next = request.headers.get("referer") 

2772 

2773 pid = kwargs.get("pid", "") 

2774 

2775 article = model_helpers.get_article(pid) 

2776 if article: 

2777 article.do_not_publish = not article.do_not_publish 

2778 article.save() 

2779 else: 

2780 raise Http404 

2781 

2782 return HttpResponseRedirect(next) 

2783 

2784 

2785@require_http_methods(["POST"]) 

2786def show_article_body(request, *args, **kwargs): 

2787 next = request.headers.get("referer") 

2788 

2789 pid = kwargs.get("pid", "") 

2790 

2791 article = model_helpers.get_article(pid) 

2792 if article: 

2793 article.show_body = not article.show_body 

2794 article.save() 

2795 else: 

2796 raise Http404 

2797 

2798 return HttpResponseRedirect(next) 

2799 

2800 

2801class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView): 

2802 """ 

2803 API to get/post article metadata 

2804 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views) 

2805 """ 

2806 

2807 def __init__(self, *args, **kwargs): 

2808 """ 

2809 we define here what fields we want in the form 

2810 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title 

2811 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update""" 

2812 super().__init__(*args, **kwargs) 

2813 self.fields_to_update = [ 

2814 "lang", 

2815 "atype", 

2816 "contributors", 

2817 "abstracts", 

2818 "kwds", 

2819 "titles", 

2820 "title_html", 

2821 "title_xml", 

2822 "title_tex", 

2823 "streams", 

2824 "ext_links", 

2825 "date_accepted", 

2826 "history_dates", 

2827 "subjs", 

2828 "bibitems", 

2829 "references", 

2830 ] 

2831 # order between doi and pid is important as for pending article we need doi to create a temporary pid 

2832 self.additional_fields = [ 

2833 "doi", 

2834 "pid", 

2835 "container_pid", 

2836 "pdf", 

2837 "illustration", 

2838 "dates", 

2839 "msc_keywords", 

2840 ] 

2841 self.editorial_tools = [ 

2842 "translation", 

2843 "sidebar", 

2844 "lang_selection", 

2845 "back_to_article_option", 

2846 "msc_keywords", 

2847 ] 

2848 self.article_container_pid = "" 

2849 self.back_url = "trammel" 

2850 

2851 def save_data(self, data_article): 

2852 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json 

2853 # The icons are not preserved since we can add/edit/delete them in VueJs 

2854 params = { 

2855 "pid": data_article.pid, 

2856 "export_folder": settings.MERSENNE_TMP_FOLDER, 

2857 "export_all": True, 

2858 "with_binary_files": False, 

2859 } 

2860 ptf_cmds.exportExtraDataPtfCmd(params).do() 

2861 

2862 def restore_data(self, article): 

2863 ptf_cmds.importExtraDataPtfCmd( 

2864 { 

2865 "pid": article.pid, 

2866 "import_folder": settings.MERSENNE_TMP_FOLDER, 

2867 "import_bibitemid": False, 

2868 } 

2869 ).do() 

2870 

2871 @method_decorator(csrf_exempt) 

2872 def dispatch(self, request, *args, **kwargs): 

2873 user_role = request.user.roles.all() 

2874 if user_role: 

2875 new_fields_to_update = [] 

2876 new_additional_fields = [] 

2877 new_editorial_tools = [] 

2878 # if a user have > 1 role, fields accessible shall be cumulative 

2879 for role in user_role: 

2880 role_fields_to_updates, role_additional_fields = role.get_real_fields_to_update() 

2881 new_fields_to_update += role_fields_to_updates 

2882 new_additional_fields += role_additional_fields 

2883 new_editorial_tools += role.editorial_tools 

2884 # in case two roles have some fields in common 

2885 self.fields_to_update = list(set(new_fields_to_update)) 

2886 self.additional_fields = list(set(new_additional_fields)) 

2887 self.editorial_tools = list(set(new_editorial_tools)) 

2888 

2889 self.additional_fields += ["doi", "pid", "container_pid"] 

2890 return super().dispatch(request, *args, **kwargs) 

2891 

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

2893 response = super().post(request, *args, **kwargs) 

2894 if response.status_code in [200, 302]: 2894 ↛ 2902line 2894 didn't jump to line 2902 because the condition on line 2894 was always true

2895 return redirect( 

2896 "api-edit-article", 

2897 colid=kwargs.get("colid", ""), 

2898 containerPid=kwargs.get("containerPid"), 

2899 doi=kwargs.get("doi", ""), 

2900 ) 

2901 else: 

2902 raise Http404 

2903 

2904 

2905class ArticleEditWithVueView(LoginRequiredMixin, TemplateView): 

2906 template_name = "article_form.html" 

2907 

2908 def get_success_url(self): 

2909 if self.kwargs["doi"]: 

2910 return reverse("article", kwargs={"aid": self.kwargs["doi"]}) 

2911 return reverse("mersenne_dashboard/published_articles") 

2912 

2913 def get_context_data(self, **kwargs): 

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

2915 if "doi" in self.kwargs: 

2916 article = model_helpers.get_article_by_doi(self.kwargs["doi"]) 

2917 context["article"] = article 

2918 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(article) 

2919 context["pid"] = context["article"].pid 

2920 

2921 context["container_pid"] = kwargs.get("container_pid", "") 

2922 return context 

2923 

2924 

2925class ArticleDeleteView(View): 

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

2927 pid = self.kwargs.get("pid", None) 

2928 article = get_object_or_404(Article, pid=pid) 

2929 site = self.kwargs.get("site", None) 

2930 message = f"Article successfully removed from {site}" 

2931 status = 200 

2932 

2933 try: 

2934 if check_lock(): 

2935 message = "Trammel is under maintenance. Please try again later." 

2936 messages.error(self.request, message) 

2937 raise ServerUnderMaintenance(message) 

2938 

2939 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid) 

2940 

2941 if site == "trammel": 

2942 if article.is_deployed(mersenneSite): 

2943 article.undeploy(mersenneSite) 

2944 

2945 cmd = ptf_cmds.addArticlePtfCmd( 

2946 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER} 

2947 ) 

2948 cmd.set_container(article.my_container) 

2949 cmd.set_object_to_be_deleted(article) 

2950 cmd.undo() 

2951 

2952 elif site in ["website", "test_website"]: 

2953 collection = article.get_collection() 

2954 server_url = getattr(collection, site)() 

2955 if not server_url: 

2956 raise Http404(f"The collection has no {site}") 

2957 

2958 # Delete on site 

2959 url = server_url + reverse("article_delete", kwargs={"pid": pid}) 

2960 response = requests.delete(url, verify=False) 

2961 status = response.status_code 

2962 

2963 if status == 404: 

2964 raise Http404( 

2965 f"Article not found on {site}. Please make sure the article exists." 

2966 ) 

2967 elif status > 204: 

2968 exception = HTTPError( 

2969 response.text.encode("utf8")[:1000], 

2970 response=response, 

2971 ) 

2972 raise exception 

2973 else: 

2974 if site == "website" and article.is_deployed(mersenneSite): 

2975 article.undeploy(mersenneSite) 

2976 # Delete article binary files 

2977 folder = article.get_relative_folder() 

2978 resolver.delete_object_folder( 

2979 folder, 

2980 to_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

2981 ) 

2982 else: 

2983 raise ValueError(f"{site} is not a supported site") 

2984 

2985 except ValueError as exception: 

2986 if site in {"trammel", "website", "test_website"}: 

2987 raise 

2988 message, status = str(exception), 400 

2989 except Http404 as exception: 

2990 message, status = str(exception), 404 

2991 except HTTPError as exception: 

2992 message, status = str(exception), 502 

2993 except ServerUnderMaintenance as exception: 

2994 message, status = str(exception), 503 

2995 except requests.exceptions.ConnectionError: 

2996 message, status = ( 

2997 f"{site} is under maintenance. Please try again later.", 

2998 503, 

2999 ) 

3000 except Exception as exception: 

3001 logger.exception("Unexpected error when deleting article %s from %s", pid, site) 

3002 message, status = str(exception), 500 

3003 

3004 return JsonResponse({"message": message, "status": status}, status=status) 

3005 

3006 

3007def get_messages_in_queue(): 

3008 app = Celery("ptf-tools") 

3009 # tasks = list(current_app.tasks) 

3010 tasks = sorted(name for name in current_app.tasks if name.startswith("celery")) 

3011 print(tasks) 

3012 # i = app.control.inspect() 

3013 

3014 with app.connection_or_acquire() as conn: 

3015 remaining = conn.default_channel.queue_declare( 

3016 queue="coordinator", passive=True 

3017 ).message_count 

3018 return remaining 

3019 

3020 

3021class NumdamView(TemplateView, history_views.HistoryContextMixin): 

3022 template_name = "numdam.html" 

3023 

3024 def get_context_data(self, **kwargs): 

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

3026 

3027 context["objs"] = ResourceInNumdam.objects.all() 

3028 

3029 pre_issues = [] 

3030 prod_issues = [] 

3031 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/" 

3032 try: 

3033 response = requests.get(url) 

3034 if response.status_code == 200: 

3035 data = response.json() 

3036 if "issues" in data: 

3037 pre_issues = data["issues"] 

3038 except Exception: 

3039 pass 

3040 

3041 url = f"{settings.NUMDAM_URL}/api-all-issues/" 

3042 response = requests.get(url) 

3043 if response.status_code == 200: 

3044 data = response.json() 

3045 if "issues" in data: 

3046 prod_issues = data["issues"] 

3047 

3048 new = sorted(set(pre_issues).difference(prod_issues)) 

3049 removed = sorted(set(prod_issues).difference(pre_issues)) 

3050 grouped = [ 

3051 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0]) 

3052 ] 

3053 grouped_removed = [ 

3054 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0]) 

3055 ] 

3056 context["added_issues"] = grouped 

3057 context["removed_issues"] = grouped_removed 

3058 

3059 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS 

3060 return context 

3061 

3062 

3063class NumdamArchiveView(RedirectView): 

3064 @staticmethod 

3065 def reset_task_results(): 

3066 TaskResult.objects.all().delete() 

3067 

3068 def get_redirect_url(self, *args, **kwargs): 

3069 self.colid = kwargs["colid"] 

3070 

3071 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS: 

3072 return Http404 

3073 

3074 # we make sure archiving is not already running 

3075 # if not get_messages_in_queue(): 

3076 # self.reset_task_results() 

3077 

3078 if self.colid == "ALL": 

3079 archive_numdam_collections.delay() 

3080 else: 

3081 archive_numdam_collection.s(self.colid).delay() 

3082 

3083 return reverse("numdam") 

3084 

3085 

3086class DeployAllNumdamAPIView(View): 

3087 def internal_do(self, *args, **kwargs): 

3088 pids = [] 

3089 

3090 for obj in ResourceInNumdam.objects.all(): 

3091 pids.append(obj.pid) 

3092 

3093 return pids 

3094 

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

3096 try: 

3097 pids, status, message = history_views.execute_and_record_func( 

3098 "deploy", "numdam", "ALL", self.internal_do, "numdam" 

3099 ) 

3100 except Exception as exception: 

3101 return HttpResponseServerError(exception) 

3102 

3103 data = {"message": message, "ids": pids, "status": status} 

3104 return JsonResponse(data) 

3105 

3106 

3107class NumdamDeleteAPIView(View): 

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

3109 pid = self.kwargs.get("pid", None) 

3110 

3111 try: 

3112 obj = ResourceInNumdam.objects.get(pid=pid) 

3113 obj.delete() 

3114 except Exception as exception: 

3115 return HttpResponseServerError(exception) 

3116 

3117 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200} 

3118 return JsonResponse(data) 

3119 

3120 

3121class ExtIdApiDetail(View): 

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

3123 extid = get_object_or_404( 

3124 ExtId, 

3125 resource__pid=kwargs["pid"], 

3126 id_type=kwargs["what"], 

3127 ) 

3128 return JsonResponse( 

3129 { 

3130 "pk": extid.pk, 

3131 "href": extid.get_href(), 

3132 "fetch": reverse( 

3133 "api-fetch-id", 

3134 args=( 

3135 extid.resource.pk, 

3136 extid.id_value, 

3137 extid.id_type, 

3138 "extid", 

3139 ), 

3140 ), 

3141 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")), 

3142 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")), 

3143 "update": reverse("extid-update", kwargs={"pk": extid.pk}), 

3144 "delete": reverse("update-extid", args=(extid.pk, "delete")), 

3145 "is_valid": extid.checked, 

3146 } 

3147 ) 

3148 

3149 

3150class ExtIdFormTemplate(TemplateView): 

3151 template_name = "common/externalid_form.html" 

3152 

3153 def get_context_data(self, **kwargs): 

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

3155 context["sequence"] = kwargs["sequence"] 

3156 return context 

3157 

3158 

3159class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

3160 def get_context_data(self, **kwargs): 

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

3162 context["helper"] = PtfFormHelper 

3163 return context 

3164 

3165 def get_success_url(self): 

3166 self.post_process() 

3167 return self.object.resource.get_absolute_url() 

3168 

3169 def post_process(self): 

3170 model_helpers.post_resource_updated(self.object.resource) 

3171 

3172 

3173class ExtIdCreate(ExtIdFormView, CreateView): 

3174 model = ExtId 

3175 form_class = ExtIdForm 

3176 

3177 def get_context_data(self, **kwargs): 

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

3179 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"]) 

3180 return context 

3181 

3182 def get_initial(self): 

3183 initial = super().get_initial() 

3184 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"]) 

3185 return initial 

3186 

3187 def form_valid(self, form): 

3188 form.instance.checked = False 

3189 return super().form_valid(form) 

3190 

3191 

3192class ExtIdUpdate(ExtIdFormView, UpdateView): 

3193 model = ExtId 

3194 form_class = ExtIdForm 

3195 

3196 def get_context_data(self, **kwargs): 

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

3198 context["resource"] = self.object.resource 

3199 return context 

3200 

3201 

3202class UpdateTexmfZipAPIView(View): 

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

3204 def copy_zip_files(src_folder, dest_folder): 

3205 os.makedirs(dest_folder, exist_ok=True) 

3206 

3207 zip_files = [ 

3208 os.path.join(src_folder, f) 

3209 for f in os.listdir(src_folder) 

3210 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip") 

3211 ] 

3212 for zip_file in zip_files: 

3213 resolver.copy_file(zip_file, dest_folder) 

3214 

3215 # Exceptions: specific zip/gz files 

3216 zip_file = os.path.join(src_folder, "texmf-bsmf.zip") 

3217 resolver.copy_file(zip_file, dest_folder) 

3218 

3219 zip_file = os.path.join(src_folder, "texmf-cg.zip") 

3220 resolver.copy_file(zip_file, dest_folder) 

3221 

3222 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz") 

3223 resolver.copy_file(gz_file, dest_folder) 

3224 

3225 src_folder = settings.CEDRAM_DISTRIB_FOLDER 

3226 

3227 dest_folder = os.path.join( 

3228 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf" 

3229 ) 

3230 

3231 try: 

3232 copy_zip_files(src_folder, dest_folder) 

3233 except Exception as exception: 

3234 return HttpResponseServerError(exception) 

3235 

3236 try: 

3237 dest_folder = os.path.join( 

3238 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf" 

3239 ) 

3240 copy_zip_files(src_folder, dest_folder) 

3241 except Exception as exception: 

3242 return HttpResponseServerError(exception) 

3243 

3244 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200} 

3245 return JsonResponse(data) 

3246 

3247 

3248class TrammelTasksProgressView(View): 

3249 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs): 

3250 """ 

3251 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement 

3252 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events) 

3253 """ 

3254 task_name = task 

3255 

3256 def get_event_data(): 

3257 # Tasks are typically in the CREATED then SUCCESS or FAILURE state 

3258 

3259 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet 

3260 remaining_messages = get_messages_in_queue() 

3261 

3262 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}") 

3263 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done") 

3264 failed_tasks = all_tasks.filter(status="FAILURE") 

3265 

3266 all_tasks_count = all_tasks.count() 

3267 success_count = successed_tasks.count() 

3268 fail_count = failed_tasks.count() 

3269 

3270 all_count = all_tasks_count + remaining_messages 

3271 remaining_count = all_count - success_count - fail_count 

3272 

3273 success_rate = int(success_count * 100 / all_count) if all_count else 0 

3274 error_rate = int(fail_count * 100 / all_count) if all_count else 0 

3275 status = "consuming_queue" if remaining_count != 0 else "polling" 

3276 

3277 last_task = successed_tasks.first() 

3278 last_task = ( 

3279 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args]) 

3280 if last_task 

3281 else "" 

3282 ) 

3283 

3284 # SSE event format 

3285 event_data = { 

3286 "status": status, 

3287 "success_rate": success_rate, 

3288 "error_rate": error_rate, 

3289 "all_count": all_count, 

3290 "remaining_count": remaining_count, 

3291 "success_count": success_count, 

3292 "fail_count": fail_count, 

3293 "last_task": last_task, 

3294 } 

3295 

3296 return event_data 

3297 

3298 def stream_response(data): 

3299 # Send initial response headers 

3300 yield f"data: {json.dumps(data)}\n\n" 

3301 

3302 data = get_event_data() 

3303 format = request.GET.get("format", "stream") 

3304 if format == "json": 

3305 response = JsonResponse(data) 

3306 else: 

3307 response = HttpResponse(stream_response(data), content_type="text/event-stream") 

3308 return response 

3309 

3310 

3311user_signed_up.connect(update_user_from_invite)