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

1685 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-27 13:01 +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.locks import ( 

63 is_tex_conversion_locked, 

64 release_tex_conversion_lock, 

65) 

66from ptf_back.tex import create_frontpage 

67from ptf_back.tex.tex_tasks import convert_article_tex 

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

69 

70# from ptf.views import ArticleEditFormWithVueAPIView 

71from ptf_external.views import ArticleEditFormWithVueAPIView 

72from pubmed.views import recordPubmed 

73from requests import Timeout 

74from requests.exceptions import HTTPError 

75from task.tasks.archiving_tasks import archive_resource 

76 

77from comments_moderation.utils import get_comments_for_home, is_comment_moderator 

78from history import models as history_models 

79from history import views as history_views 

80from history.utils import ( 

81 get_gap, 

82 get_history_last_event_by, 

83 get_last_unsolved_error, 

84) 

85from ptf_tools.doaj import doaj_pid_register 

86from ptf_tools.forms import ( 

87 CollectionForm, 

88 ContainerForm, 

89 DiffContainerForm, 

90 ExtIdForm, 

91 ExtLinkForm, 

92 FormSetHelper, 

93 ImportArticleForm, 

94 ImportContainerForm, 

95 ImportEditflowArticleForm, 

96 PtfFormHelper, 

97 PtfLargeModalFormHelper, 

98 PtfModalFormHelper, 

99 RegisterPubmedForm, 

100 ResourceIdForm, 

101 get_article_choices, 

102) 

103from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos 

104from ptf_tools.models import ResourceInNumdam 

105from ptf_tools.signals import update_user_from_invite 

106from ptf_tools.tasks import ( 

107 archive_numdam_collection, 

108 archive_numdam_collections, 

109) 

110from ptf_tools.templatetags.tools_helpers import get_authorized_collections 

111from ptf_tools.utils import is_authorized_editor 

112from ptf_tools.views.components import breadcrumb 

113 

114logger = logging.getLogger(__name__) 

115 

116 

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

118 """ 

119 Dummy view raising HTTP 404 exception. 

120 """ 

121 raise Http404 

122 

123 

124def check_collection(collection, server_url, server_type): 

125 """ 

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

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

128 """ 

129 

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

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

132 # First, upload the collection XML 

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

134 body = xml.encode("utf8") 

135 

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

137 if response.status_code == 200: 

138 # PUT http verb is used for update 

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

140 else: 

141 # POST http verb is used for creation 

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

143 

144 # Second, copy the collection images 

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

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

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

148 # /mersenne_prod_data during an upload to prod 

149 if server_type == "website": 

150 resolver.copy_binary_files( 

151 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

152 ) 

153 elif server_type == "numdam": 

154 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

155 if collection.pid in settings.NUMDAM_COLLECTIONS: 

156 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

157 

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

159 

160 

161def check_lock(): 

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

163 

164 

165def load_cedrics_article_choices(request): 

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

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

168 article_choices = get_article_choices(colid, issue) 

169 return render( 

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

171 ) 

172 

173 

174class ImportCedricsArticleFormView(FormView): 

175 template_name = "import_article.html" 

176 form_class = ImportArticleForm 

177 

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

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

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

181 

182 def get_success_url(self): 

183 if self.colid: 

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

185 return "/" 

186 

187 def get_context_data(self, **kwargs): 

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

189 context["colid"] = self.colid 

190 context["helper"] = PtfModalFormHelper 

191 return context 

192 

193 def get_form_kwargs(self): 

194 kwargs = super().get_form_kwargs() 

195 kwargs["colid"] = self.colid 

196 return kwargs 

197 

198 def form_valid(self, form): 

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

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

201 return super().form_valid(form) 

202 

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

204 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd( 

205 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid} 

206 ) 

207 cmd.do() 

208 

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

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

211 issue = request.POST["issue"] 

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

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

214 

215 import_args = [self] 

216 import_kwargs = {} 

217 

218 try: 

219 _, _status, _message = history_views.execute_and_record_func( 

220 "import", 

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

222 self.colid, 

223 self.import_cedrics_article, 

224 "", 

225 False, 

226 None, 

227 None, 

228 *import_args, 

229 **import_kwargs, 

230 ) 

231 

232 messages.success( 

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

234 ) 

235 

236 except Exception as exception: 

237 messages.error( 

238 self.request, 

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

240 ) 

241 

242 return redirect(self.get_success_url()) 

243 

244 

245class ImportCedricsIssueView(FormView): 

246 template_name = "import_container.html" 

247 form_class = ImportContainerForm 

248 

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

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

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

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

253 

254 def get_success_url(self): 

255 if self.filename: 

256 return reverse( 

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

258 ) 

259 return "/" 

260 

261 def get_context_data(self, **kwargs): 

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

263 context["colid"] = self.colid 

264 context["helper"] = PtfModalFormHelper 

265 return context 

266 

267 def get_form_kwargs(self): 

268 kwargs = super().get_form_kwargs() 

269 kwargs["colid"] = self.colid 

270 kwargs["to_appear"] = self.to_appear 

271 return kwargs 

272 

273 def form_valid(self, form): 

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

275 return super().form_valid(form) 

276 

277 

278class DiffCedricsIssueView(FormView): 

279 template_name = "diff_container_form.html" 

280 form_class = DiffContainerForm 

281 diffs = None 

282 xissue = None 

283 xissue_encoded = None 

284 

285 def get_success_url(self): 

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

287 

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

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

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

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

292 

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

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

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

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

297 self.remove_email = self.remove_mail == "on" 

298 self.remove_date_prod = self.remove_date_prod == "on" 

299 

300 try: 

301 result, _status, _message = history_views.execute_and_record_func( 

302 "import", 

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

304 self.colid, 

305 self.diff_cedrics_issue, 

306 "", 

307 True, 

308 ) 

309 except Exception as exception: 

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

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

312 return HttpResponseRedirect(self.get_success_url()) 

313 

314 result[0] 

315 self.diffs = result[1] 

316 self.xissue = result[2] 

317 

318 if True: 

319 # Proceed with the import 

320 self.form_valid(self.get_form()) 

321 return redirect(self.get_success_url()) 

322 else: 

323 # Display the diff template 

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

325 

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

327 

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

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

330 data = request.POST["xissue_encoded"] 

331 self.xissue = jsonpickle.decode(data) 

332 

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

334 

335 def get_context_data(self, **kwargs): 

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

337 context["colid"] = self.colid 

338 context["diff"] = self.diffs 

339 context["filename"] = self.filename 

340 context["xissue_encoded"] = self.xissue_encoded 

341 return context 

342 

343 def get_form_kwargs(self): 

344 kwargs = super().get_form_kwargs() 

345 kwargs["colid"] = self.colid 

346 return kwargs 

347 

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

349 params = { 

350 "colid": self.colid, 

351 "input_file": self.filename, 

352 "remove_email": self.remove_mail, 

353 "remove_date_prod": self.remove_date_prod, 

354 "diff_only": True, 

355 } 

356 

357 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

360 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

361 else: 

362 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

363 

364 result = cmd.do() 

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

366 messages.warning( 

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

368 ) 

369 

370 return result 

371 

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

373 # modify xissue with data_issue if params to override 

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

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

376 if issue: 

377 data_issue = model_data_converter.db_to_issue_data(issue) 

378 for xarticle in self.xissue.articles: 

379 filter_articles = [ 

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

381 ] 

382 if len(filter_articles) > 0: 

383 db_article = filter_articles[0] 

384 xarticle.coi_statement = db_article.coi_statement 

385 xarticle.kwds = db_article.kwds 

386 xarticle.contrib_groups = db_article.contrib_groups 

387 

388 params = { 

389 "colid": self.colid, 

390 "xissue": self.xissue, 

391 "input_file": self.filename, 

392 } 

393 

394 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

397 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

398 else: 

399 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

400 

401 cmd.do() 

402 

403 def form_valid(self, form): 

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

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

406 else: 

407 import_kwargs = {} 

408 import_args = [self] 

409 

410 try: 

411 _, _status, _message = history_views.execute_and_record_func( 

412 "import", 

413 self.xissue.pid, 

414 self.kwargs["colid"], 

415 self.import_cedrics_issue, 

416 "", 

417 False, 

418 None, 

419 None, 

420 *import_args, 

421 **import_kwargs, 

422 ) 

423 except Exception as exception: 

424 messages.error( 

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

426 ) 

427 return super().form_invalid(form) 

428 

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

430 return super().form_valid(form) 

431 

432 

433class ImportEditflowArticleFormView(FormView): 

434 template_name = "import_editflow_article.html" 

435 form_class = ImportEditflowArticleForm 

436 

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

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

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

440 

441 def get_success_url(self): 

442 if self.colid: 

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

444 return "/" 

445 

446 def get_context_data(self, **kwargs): 

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

448 context["colid"] = self.colid 

449 context["helper"] = PtfLargeModalFormHelper 

450 return context 

451 

452 def get_form_kwargs(self): 

453 kwargs = super().get_form_kwargs() 

454 kwargs["colid"] = self.colid 

455 return kwargs 

456 

457 def form_valid(self, form): 

458 try: 

459 if not self.colid: 

460 raise ValueError("Missing collection id") 

461 

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

463 if not issue_name: 

464 raise ValueError( 

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

466 ) 

467 

468 issue = model_helpers.get_container(issue_name) 

469 if not issue: 

470 raise ValueError("No issue found") 

471 

472 editflow_xml_file = form.cleaned_data["editflow_xml_file"] 

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

474 

475 cmd = xml_cmds.addArticleXmlCmd( 

476 { 

477 "body": body, 

478 "issue": issue, 

479 "assign_doi": True, 

480 "standalone": True, 

481 "from_folder": settings.RESOURCES_ROOT, 

482 } 

483 ) 

484 cmd.set_collection(issue.get_collection()) 

485 cmd.do() 

486 

487 messages.success( 

488 self.request, 

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

490 ) 

491 

492 except Exception as exception: 

493 messages.error( 

494 self.request, 

495 f"Import failed: {exception}", 

496 ) 

497 return super().form_invalid(form) 

498 

499 return super().form_valid(form) 

500 

501 

502class MatchingAPIView(View): 

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

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

505 

506 url = settings.MATCHING_URL 

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

508 

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

510 

511 if settings.DEBUG: 

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

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

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

515 f.close() 

516 

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

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

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

520 

521 if settings.DEBUG: 

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

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

524 text = body 

525 f.write(text) 

526 f.close() 

527 

528 resource = model_helpers.get_resource(pid) 

529 obj = resource.cast() 

530 colid = obj.get_collection().pid 

531 

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

533 

534 cmd = xml_cmds.addOrUpdateIssueXmlCmd( 

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

536 ) 

537 cmd.do() 

538 

539 print("Matching finished") 

540 return JsonResponse(data) 

541 

542 

543class ImportAllAPIView(View): 

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

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

546 

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

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

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

550 

551 resource = model_helpers.get_resource(pid) 

552 if not resource: 

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

554 body = utils.get_file_content_in_utf8(file) 

555 journals = xml_cmds.addCollectionsXmlCmd( 

556 { 

557 "body": body, 

558 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

559 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

560 } 

561 ).do() 

562 if not journals: 

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

564 resource = journals[0] 

565 # resolver.copy_binary_files( 

566 # resource, 

567 # settings.MATHDOC_ARCHIVE_FOLDER, 

568 # settings.MERSENNE_TEST_DATA_FOLDER) 

569 

570 obj = resource.cast() 

571 

572 if obj.classname != "Collection": 

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

574 

575 cmd = xml_cmds.collectEntireCollectionXmlCmd( 

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

577 ) 

578 pids = cmd.do() 

579 

580 return pids 

581 

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

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

584 

585 try: 

586 pids, status, message = history_views.execute_and_record_func( 

587 "import", pid, pid, self.internal_do 

588 ) 

589 except Timeout as exception: 

590 return HttpResponse(exception, status=408) 

591 except Exception as exception: 

592 return HttpResponseServerError(exception) 

593 

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

595 return JsonResponse(data) 

596 

597 

598class DeployAllAPIView(View): 

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

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

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

602 

603 pids = [] 

604 

605 collection = model_helpers.get_collection(pid) 

606 if not collection: 

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

608 

609 if site == "numdam": 

610 server_url = settings.NUMDAM_PRE_URL 

611 elif site != "ptf_tools": 

612 server_url = getattr(collection, site)() 

613 if not server_url: 

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

615 

616 if site != "ptf_tools": 

617 # check if the collection exists on the server 

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

619 # image...) 

620 check_collection(collection, server_url, site) 

621 

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

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

624 pids.append(issue.pid) 

625 

626 return pids 

627 

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

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

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

631 

632 try: 

633 pids, status, message = history_views.execute_and_record_func( 

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

635 ) 

636 except Timeout as exception: 

637 return HttpResponse(exception, status=408) 

638 except Exception as exception: 

639 return HttpResponseServerError(exception) 

640 

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

642 return JsonResponse(data) 

643 

644 

645class AddIssuePDFView(View): 

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

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

648 self.pid = None 

649 self.issue = None 

650 self.collection = None 

651 self.site = "test_website" 

652 

653 def post_to_site(self, url): 

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

655 status = response.status_code 

656 if not (199 < status < 205): 

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

658 if status == 503: 

659 raise ServerUnderMaintenance(response.text) 

660 else: 

661 raise RuntimeError(response.text) 

662 

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

664 """ 

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

666 """ 

667 

668 issue_pid = self.issue.pid 

669 colid = self.collection.pid 

670 

671 if self.site == "website": 

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

673 resolver.copy_binary_files( 

674 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

675 ) 

676 else: 

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

678 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid) 

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

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

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

682 

683 to_path = resolver.get_disk_location( 

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

685 ) 

686 resolver.copy_file(from_path, to_path) 

687 

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

689 

690 if self.site == "test_website": 

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

692 absolute_url = self.request.build_absolute_uri(url) 

693 self.post_to_site(absolute_url) 

694 

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

696 absolute_url = server_url + url 

697 # Post to the test or production website 

698 self.post_to_site(absolute_url) 

699 

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

701 """ 

702 Send an issue PDF to the test or production website 

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

704 :param args: 

705 :param kwargs: 

706 :return: 

707 """ 

708 if check_lock(): 

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

710 messages.error(self.request, m) 

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

712 

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

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

715 

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

717 if not self.issue: 

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

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

720 

721 try: 

722 _pids, status, message = history_views.execute_and_record_func( 

723 "deploy", 

724 self.pid, 

725 self.collection.pid, 

726 self.internal_do, 

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

728 ) 

729 

730 except Timeout as exception: 

731 return HttpResponse(exception, status=408) 

732 except Exception as exception: 

733 return HttpResponseServerError(exception) 

734 

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

736 return JsonResponse(data) 

737 

738 

739class ArchiveAllAPIView(View): 

740 """ 

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

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

743 @return array of issues pid 

744 """ 

745 

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

747 collection = kwargs["collection"] 

748 pids = [] 

749 colid = collection.pid 

750 

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

752 if os.path.isfile(logfile): 

753 os.remove(logfile) 

754 

755 ptf_cmds.exportPtfCmd( 

756 { 

757 "pid": colid, 

758 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

759 "with_binary_files": True, 

760 "for_archive": True, 

761 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

762 } 

763 ).do() 

764 

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

766 if os.path.isfile(cedramcls): 

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

768 resolver.create_folder(dest_folder) 

769 resolver.copy_file(cedramcls, dest_folder) 

770 

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

772 qs = issue.article_set.filter( 

773 date_online_first__isnull=True, date_published__isnull=True 

774 ) 

775 if qs.count() == 0: 

776 pids.append(issue.pid) 

777 

778 return pids 

779 

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

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

782 

783 collection = model_helpers.get_collection(pid) 

784 if not collection: 

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

786 

787 dict_ = {"collection": collection} 

788 args_ = [self] 

789 

790 try: 

791 pids, status, message = history_views.execute_and_record_func( 

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

793 ) 

794 except Timeout as exception: 

795 return HttpResponse(exception, status=408) 

796 except Exception as exception: 

797 return HttpResponseServerError(exception) 

798 

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

800 return JsonResponse(data) 

801 

802 

803class CreateAllDjvuAPIView(View): 

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

805 issue = kwargs["issue"] 

806 pids = [issue.pid] 

807 

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

809 pids.append(article.pid) 

810 

811 return pids 

812 

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

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

815 issue = model_helpers.get_container(pid) 

816 if not issue: 

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

818 

819 try: 

820 dict_ = {"issue": issue} 

821 args_ = [self] 

822 

823 pids, status, message = history_views.execute_and_record_func( 

824 "numdam", 

825 pid, 

826 issue.get_collection().pid, 

827 self.internal_do, 

828 "", 

829 False, 

830 None, 

831 None, 

832 *args_, 

833 **dict_, 

834 ) 

835 except Exception as exception: 

836 return HttpResponseServerError(exception) 

837 

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

839 return JsonResponse(data) 

840 

841 

842class ImportJatsContainerAPIView(View): 

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

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

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

846 

847 if pid and colid: 

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

849 

850 cmd = xml_cmds.addOrUpdateContainerXmlCmd( 

851 { 

852 "body": body, 

853 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

854 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

855 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

856 } 

857 ) 

858 container = cmd.do() 

859 if len(cmd.warnings) > 0: 

860 messages.warning( 

861 self.request, 

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

863 ) 

864 

865 if not container: 

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

867 

868 # resolver.copy_binary_files( 

869 # container, 

870 # settings.MATHDOC_ARCHIVE_FOLDER, 

871 # settings.MERSENNE_TEST_DATA_FOLDER) 

872 # 

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

874 # resolver.copy_binary_files( 

875 # article, 

876 # settings.MATHDOC_ARCHIVE_FOLDER, 

877 # settings.MERSENNE_TEST_DATA_FOLDER) 

878 else: 

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

880 

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

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

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

884 

885 try: 

886 _, status, message = history_views.execute_and_record_func( 

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

888 ) 

889 except Timeout as exception: 

890 return HttpResponse(exception, status=408) 

891 except Exception as exception: 

892 return HttpResponseServerError(exception) 

893 

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

895 return JsonResponse(data) 

896 

897 

898class DeployCollectionAPIView(View): 

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

900 

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

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

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

904 

905 collection = model_helpers.get_collection(colid) 

906 if not collection: 

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

908 

909 if site == "numdam": 

910 server_url = settings.NUMDAM_PRE_URL 

911 else: 

912 server_url = getattr(collection, site)() 

913 if not server_url: 

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

915 

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

917 check_collection(collection, server_url, site) 

918 

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

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

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

922 

923 try: 

924 _, status, message = history_views.execute_and_record_func( 

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

926 ) 

927 except Timeout as exception: 

928 return HttpResponse(exception, status=408) 

929 except Exception as exception: 

930 return HttpResponseServerError(exception) 

931 

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

933 return JsonResponse(data) 

934 

935 

936class DeployJatsResourceAPIView(View): 

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

938 

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

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

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

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

943 

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

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

946 if check_lock(): 

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

948 messages.error(self.request, msg) 

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

950 

951 resource = model_helpers.get_resource(pid) 

952 if not resource: 

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

954 

955 obj = resource.cast() 

956 article = None 

957 if obj.classname == "Article": 

958 article = obj 

959 container = article.my_container 

960 articles_to_deploy = [article] 

961 else: 

962 container = obj 

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

964 

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

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

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

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

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

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

971 

972 collection = container.get_top_collection() 

973 colid = collection.pid 

974 djvu_exception = None 

975 

976 if site == "numdam": 

977 server_url = settings.NUMDAM_PRE_URL 

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

979 

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

981 # Add Djvu (before exporting the XML) 

982 if False: 

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

984 try: 

985 cmd = ptf_cmds.addDjvuPtfCmd() 

986 cmd.set_resource(art) 

987 cmd.do() 

988 except Exception as e: 

989 # Djvu are optional. 

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

991 djvu_exception = e 

992 else: 

993 server_url = getattr(collection, site)() 

994 if not server_url: 

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

996 

997 # check if the collection exists on the server 

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

999 # image...) 

1000 if article is None: 

1001 check_collection(collection, server_url, site) 

1002 

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

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

1005 if site == "website": 

1006 file_.write( 

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

1008 ) 

1009 

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

1011 cmd = ptf_cmds.publishResourcePtfCmd() 

1012 cmd.set_resource(resource) 

1013 updated_articles = cmd.do() 

1014 

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

1016 

1017 mersenneSite = model_helpers.get_site_mersenne(colid) 

1018 # create or update deployed_date on container and articles 

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

1020 

1021 for art in articles_to_deploy: 

1022 if ( 

1023 art.doi 

1024 and (art.date_published or art.date_online_first) 

1025 and art.my_container.fyear is None 

1026 ): 

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

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

1029 

1030 file_.write( 

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

1032 ) 

1033 

1034 if article is None: 

1035 resolver.copy_binary_files( 

1036 container, 

1037 settings.MERSENNE_TEST_DATA_FOLDER, 

1038 settings.MERSENNE_PROD_DATA_FOLDER, 

1039 ) 

1040 

1041 for art in articles_to_deploy: 

1042 resolver.copy_binary_files( 

1043 art, 

1044 settings.MERSENNE_TEST_DATA_FOLDER, 

1045 settings.MERSENNE_PROD_DATA_FOLDER, 

1046 ) 

1047 

1048 elif site == "test_website": 

1049 # create date_pre_published on articles without date_pre_published 

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

1051 cmd.set_resource(resource) 

1052 updated_articles = cmd.do() 

1053 

1054 create_frontpage(colid, container, updated_articles) 

1055 

1056 export_to_website = site == "website" 

1057 

1058 if article is None: 

1059 with_djvu = site == "numdam" 

1060 xml = ptf_cmds.exportPtfCmd( 

1061 { 

1062 "pid": pid, 

1063 "with_djvu": with_djvu, 

1064 "export_to_website": export_to_website, 

1065 } 

1066 ).do() 

1067 body = xml.encode("utf8") 

1068 

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

1070 url = server_url + reverse("issue_upload") 

1071 else: 

1072 url = server_url + reverse("book_upload") 

1073 

1074 # verify=False: ignore TLS certificate 

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

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

1077 else: 

1078 xml = ptf_cmds.exportPtfCmd( 

1079 { 

1080 "pid": pid, 

1081 "with_djvu": False, 

1082 "article_standalone": True, 

1083 "collection_pid": collection.pid, 

1084 "export_to_website": export_to_website, 

1085 "export_folder": settings.LOG_DIR, 

1086 } 

1087 ).do() 

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

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

1090 xml_file = io.StringIO(xml) 

1091 files = {"xml": xml_file} 

1092 

1093 url = server_url + reverse( 

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

1095 ) 

1096 # verify=False: ignore TLS certificate 

1097 header = {} 

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

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

1100 

1101 status = response.status_code 

1102 

1103 if 199 < status < 205: 

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

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

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

1107 # /mersenne_prod_data during an upload to prod 

1108 if site == "website": 

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

1110 if container.doi: 

1111 recordDOI(container) 

1112 

1113 for art in articles_to_deploy: 

1114 # record DOI automatically when deploying in prod 

1115 

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

1117 recordDOI(art) 

1118 

1119 if colid == "CRBIOL": 

1120 recordPubmed( 

1121 art, force_update=False, updated_articles=updated_articles 

1122 ) 

1123 

1124 if colid == "PCJ": 

1125 self.update_pcj_editor(updated_articles) 

1126 

1127 # Archive the container or the article 

1128 if article is None: 

1129 archive_resource.delay( 

1130 pid, 

1131 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1132 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1133 ) 

1134 

1135 else: 

1136 archive_resource.delay( 

1137 pid, 

1138 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1139 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1140 article_doi=article.doi, 

1141 ) 

1142 # cmd = ptf_cmds.archiveIssuePtfCmd({ 

1143 # "pid": pid, 

1144 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1145 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER}) 

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

1147 # cmd.do() 

1148 

1149 elif site == "numdam": 

1150 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

1151 if colid in settings.NUMDAM_COLLECTIONS: 

1152 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

1153 

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

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

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

1157 

1158 elif status == 503: 

1159 raise ServerUnderMaintenance(response.text) 

1160 else: 

1161 raise RuntimeError(response.text) 

1162 

1163 if djvu_exception: 

1164 raise djvu_exception 

1165 

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

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

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

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

1170 

1171 try: 

1172 _, status, message = history_views.execute_and_record_func( 

1173 "deploy", 

1174 pid, 

1175 colid, 

1176 self.internal_do, 

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

1178 ) 

1179 except Timeout as exception: 

1180 return HttpResponse(exception, status=408) 

1181 except Exception as exception: 

1182 return HttpResponseServerError(exception) 

1183 

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

1185 return JsonResponse(data) 

1186 

1187 def update_pcj_editor(self, updated_articles): 

1188 for article in updated_articles: 

1189 data = { 

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

1191 "article_number": article.article_number, 

1192 } 

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

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

1195 

1196 

1197class DeployTranslatedArticleAPIView(CsrfExemptMixin, View): 

1198 article = None 

1199 

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

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

1202 

1203 translation = None 

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

1205 if trans_article.lang == lang: 

1206 translation = trans_article 

1207 

1208 if translation is None: 

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

1210 

1211 collection = self.article.get_top_collection() 

1212 colid = collection.pid 

1213 container = self.article.my_container 

1214 

1215 if translation.date_published is None: 

1216 # Add date posted 

1217 cmd = ptf_cmds.publishResourcePtfCmd() 

1218 cmd.set_resource(translation) 

1219 cmd.do() 

1220 # updated_articles = cmd.do() 

1221 

1222 # # Recompile PDF to add the date posted 

1223 # try: 

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

1225 # except Exception: 

1226 # raise PDFException( 

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

1228 # ) 

1229 

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

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

1232 resolver.copy_binary_files( 

1233 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

1234 ) 

1235 

1236 # Deploy in prod 

1237 xml = ptf_cmds.exportPtfCmd( 

1238 { 

1239 "pid": self.article.pid, 

1240 "with_djvu": False, 

1241 "article_standalone": True, 

1242 "collection_pid": colid, 

1243 "export_to_website": True, 

1244 "export_folder": settings.LOG_DIR, 

1245 } 

1246 ).do() 

1247 xml_file = io.StringIO(xml) 

1248 files = {"xml": xml_file} 

1249 

1250 server_url = collection.website() 

1251 if not server_url: 

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

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

1254 header = {} 

1255 

1256 try: 

1257 response = requests.post( 

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

1259 ) # verify: ignore TLS certificate 

1260 status = response.status_code 

1261 except requests.exceptions.ConnectionError: 

1262 raise ServerUnderMaintenance( 

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

1264 ) 

1265 

1266 # Register translation in Crossref 

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

1268 try: 

1269 recordDOI(translation) 

1270 except Exception: 

1271 raise DOIException( 

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

1273 ) 

1274 

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

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

1277 self.article = model_helpers.get_article_by_doi(doi) 

1278 if self.article is None: 

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

1280 

1281 try: 

1282 _, status, message = history_views.execute_and_record_func( 

1283 "deploy", 

1284 self.article.pid, 

1285 self.article.get_top_collection().pid, 

1286 self.internal_do, 

1287 "website", 

1288 ) 

1289 except Timeout as exception: 

1290 return HttpResponse(exception, status=408) 

1291 except Exception as exception: 

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

1293 return HttpResponseServerError(exception) 

1294 

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

1296 return JsonResponse(data) 

1297 

1298 

1299class DeleteJatsIssueAPIView(View): 

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

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

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

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

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

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

1306 status = 200 

1307 

1308 issue = model_helpers.get_container(pid) 

1309 if not issue: 

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

1311 try: 

1312 if check_lock(): 

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

1314 messages.error(self.request, message) 

1315 raise ServerUnderMaintenance(message) 

1316 

1317 mersenneSite = model_helpers.get_site_mersenne(colid) 

1318 

1319 if site == "ptf_tools": 

1320 if issue.is_deployed(mersenneSite): 

1321 issue.undeploy(mersenneSite) 

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

1323 article.undeploy(mersenneSite) 

1324 

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

1326 

1327 cmd = ptf_cmds.addContainerPtfCmd( 

1328 { 

1329 "pid": issue.pid, 

1330 "ctype": "issue", 

1331 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

1332 } 

1333 ) 

1334 cmd.set_provider(p) 

1335 cmd.add_collection(issue.get_collection()) 

1336 cmd.set_object_to_be_deleted(issue) 

1337 cmd.undo() 

1338 

1339 else: 

1340 if site == "numdam": 

1341 server_url = settings.NUMDAM_PRE_URL 

1342 else: 

1343 collection = issue.get_collection() 

1344 server_url = getattr(collection, site)() 

1345 

1346 if not server_url: 

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

1348 else: 

1349 # Delete on site 

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

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

1352 status = response.status_code 

1353 

1354 if status == 404: 

1355 raise Http404( 

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

1357 ) 

1358 elif status > 204: 

1359 raise HTTPError( 

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

1361 response=response, 

1362 ) 

1363 else: 

1364 status = 200 

1365 

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

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

1368 issue.undeploy(mersenneSite) 

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

1370 article.undeploy(mersenneSite) 

1371 # Delete article binary files 

1372 folder = article.get_relative_folder() 

1373 resolver.delete_object_folder( 

1374 folder, 

1375 to_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1376 ) 

1377 # Delete issue binary files 

1378 folder = issue.get_relative_folder() 

1379 resolver.delete_object_folder( 

1380 folder, to_folder=settings.MERSENNE_PROD_DATA_FOLDER 

1381 ) 

1382 

1383 except ValueError as exception: 

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

1385 raise 

1386 message, status = str(exception), 400 

1387 except Timeout: 

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

1389 status = 503 

1390 except Http404 as exception: 

1391 message, status = str(exception), 404 

1392 except HTTPError as exception: 

1393 message, status = str(exception), 502 

1394 except ServerUnderMaintenance as exception: 

1395 message, status = str(exception), 503 

1396 except requests.exceptions.ConnectionError: 

1397 message, status = ( 

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

1399 503, 

1400 ) 

1401 except Exception as exception: 

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

1403 message, status = str(exception), 500 

1404 

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

1406 

1407 

1408class ArchiveIssueAPIView(View): 

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

1410 try: 

1411 pid = kwargs["pid"] 

1412 colid = kwargs["colid"] 

1413 except IndexError: 

1414 raise Http404 

1415 

1416 try: 

1417 cmd = ptf_cmds.archiveIssuePtfCmd( 

1418 { 

1419 "pid": pid, 

1420 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1421 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

1422 "needs_publication_date": True, 

1423 } 

1424 ) 

1425 _result, _status, message = history_views.execute_and_record_func( 

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

1427 ) 

1428 except Exception as exception: 

1429 return HttpResponseServerError(exception) 

1430 

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

1432 return JsonResponse(data) 

1433 

1434 

1435class CreateDjvuAPIView(View): 

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

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

1438 

1439 resource = model_helpers.get_resource(pid) 

1440 cmd = ptf_cmds.addDjvuPtfCmd() 

1441 cmd.set_resource(resource) 

1442 cmd.do() 

1443 

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

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

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

1447 

1448 try: 

1449 _, status, message = history_views.execute_and_record_func( 

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

1451 ) 

1452 except Exception as exception: 

1453 return HttpResponseServerError(exception) 

1454 

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

1456 return JsonResponse(data) 

1457 

1458 

1459class PTFToolsHomeView(LoginRequiredMixin, View): 

1460 """ 

1461 Home Page. 

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

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

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

1465 - Comment moderator -> Comments dashboard 

1466 - Others -> 404 response 

1467 """ 

1468 

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

1470 # Staff or user with authorized collections 

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

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

1473 

1474 colids = get_authorized_collections(request.user) 

1475 is_mod = is_comment_moderator(request.user) 

1476 

1477 # The user has no rights 

1478 if not (colids or is_mod): 

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

1480 # Comment moderator only 

1481 elif not colids: 

1482 return HttpResponseRedirect(reverse("comment_list")) 

1483 

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

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

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

1487 

1488 # User with multiple authorized collections - Special home 

1489 context = {} 

1490 context["overview"] = True 

1491 

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

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

1494 

1495 # Comments summary 

1496 try: 

1497 error, comments_data = get_comments_for_home(request.user) 

1498 except AttributeError: 

1499 error, comments_data = True, {} 

1500 

1501 context["comment_server_ok"] = False 

1502 

1503 if not error: 

1504 context["comment_server_ok"] = True 

1505 if comments_data: 

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

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

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

1509 

1510 # TODO: Translations summary 

1511 context["translation_server_ok"] = False 

1512 

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

1514 context["collections"] = sorted( 

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

1516 ) 

1517 

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

1519 

1520 

1521class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin): 

1522 columns = 5 

1523 

1524 def get_common_context_data(self, **kwargs): 

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

1526 now = timezone.now() 

1527 curyear = now.year 

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

1529 

1530 context["collections"] = settings.MERSENNE_COLLECTIONS 

1531 context["containers_to_be_published"] = [] 

1532 context["last_col_events"] = [] 

1533 

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

1535 clockss_gap = get_gap(now, event) 

1536 

1537 context["years"] = years 

1538 context["clockss_gap"] = clockss_gap 

1539 

1540 return context 

1541 

1542 def calculate_articles_and_pages(self, pid, years): 

1543 data_by_year = [] 

1544 total_articles = [0] * len(years) 

1545 total_pages = [0] * len(years) 

1546 

1547 for year in years: 

1548 articles = self.get_articles_for_year(pid, year) 

1549 articles_count = articles.count() 

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

1551 

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

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

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

1555 

1556 return data_by_year, total_articles, total_pages 

1557 

1558 def get_articles_for_year(self, pid, year): 

1559 return Article.objects.filter( 

1560 Q(my_container__my_collection__pid=pid) 

1561 & ( 

1562 Q(date_published__year=year, date_online_first__isnull=True) 

1563 | Q(date_online_first__year=year) 

1564 ) 

1565 ).prefetch_related("resourcecount_set") 

1566 

1567 

1568class PublishedArticlesDashboardView(BaseMersenneDashboardView): 

1569 template_name = "dashboard/published_articles.html" 

1570 

1571 def get_context_data(self, **kwargs): 

1572 context = self.get_common_context_data(**kwargs) 

1573 years = context["years"] 

1574 

1575 published_articles = [] 

1576 total_published_articles = [ 

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

1578 ] 

1579 

1580 for pid in settings.MERSENNE_COLLECTIONS: 

1581 if pid != "MERSENNE": 

1582 articles_data, total_articles, total_pages = self.calculate_articles_and_pages( 

1583 pid, years 

1584 ) 

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

1586 

1587 for i, year in enumerate(years): 

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

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

1590 

1591 context["published_articles"] = published_articles 

1592 context["total_published_articles"] = total_published_articles 

1593 

1594 return context 

1595 

1596 

1597class CreatedVolumesDashboardView(BaseMersenneDashboardView): 

1598 template_name = "dashboard/created_volumes.html" 

1599 

1600 def get_context_data(self, **kwargs): 

1601 context = self.get_common_context_data(**kwargs) 

1602 years = context["years"] 

1603 

1604 created_volumes = [] 

1605 total_created_volumes = [ 

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

1607 ] 

1608 

1609 for pid in settings.MERSENNE_COLLECTIONS: 

1610 if pid != "MERSENNE": 

1611 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages( 

1612 pid, years 

1613 ) 

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

1615 

1616 for i, _ in enumerate(years): 

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

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

1619 

1620 context["created_volumes"] = created_volumes 

1621 context["total_created_volumes"] = total_created_volumes 

1622 

1623 return context 

1624 

1625 def calculate_volumes_and_pages(self, pid, years): 

1626 data_by_year = [] 

1627 total_articles = [0] * len(years) 

1628 total_pages = [0] * len(years) 

1629 

1630 for year in years: 

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

1632 articles_count = 0 

1633 page_count = 0 

1634 

1635 for issue in issues: 

1636 articles = issue.article_set.filter( 

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

1638 ).prefetch_related("resourcecount_set") 

1639 

1640 articles_count += articles.count() 

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

1642 

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

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

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

1646 

1647 return data_by_year, total_articles, total_pages 

1648 

1649 

1650class ReferencingChoice(View): 

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

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

1653 return redirect( 

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

1655 ) 

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

1657 comp = ReferencingCheckerWos() 

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

1659 if journal is None: 

1660 return render( 

1661 request, 

1662 "dashboard/referencing.html", 

1663 { 

1664 "error": "Collection not found", 

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

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

1667 }, 

1668 ) 

1669 return render( 

1670 request, 

1671 "dashboard/referencing.html", 

1672 { 

1673 "journal": journal, 

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

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

1676 }, 

1677 ) 

1678 

1679 

1680class ReferencingWosFileView(View): 

1681 template_name = "dashboard/referencing.html" 

1682 

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

1684 colid = request.POST["colid"] 

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

1686 message = "No file uploaded" 

1687 return render( 

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

1689 ) 

1690 uploaded_file = request.FILES["risfile"] 

1691 comp = ReferencingCheckerWos() 

1692 journal = comp.check_references(colid, uploaded_file) 

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

1694 

1695 

1696class ReferencingDashboardView(BaseMersenneDashboardView): 

1697 template_name = "dashboard/referencing.html" 

1698 

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

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

1701 comp = ReferencingCheckerAds() 

1702 journal = comp.check_references(colid) 

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

1704 

1705 

1706class BaseCollectionView(TemplateView): 

1707 def get_context_data(self, **kwargs): 

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

1709 aid = context.get("aid") 

1710 year = context.get("year") 

1711 

1712 if aid and year: 

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

1714 

1715 return context 

1716 

1717 def get_collection(self, aid, year): 

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

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

1720 

1721 

1722class ArticleListView(BaseCollectionView): 

1723 template_name = "collection-list.html" 

1724 

1725 def get_collection(self, aid, year): 

1726 return Article.objects.filter( 

1727 Q(my_container__my_collection__pid=aid) 

1728 & ( 

1729 Q(date_published__year=year, date_online_first__isnull=True) 

1730 | Q(date_online_first__year=year) 

1731 ) 

1732 ).prefetch_related("resourcecount_set") 

1733 

1734 

1735class VolumeListView(BaseCollectionView): 

1736 template_name = "collection-list.html" 

1737 

1738 def get_collection(self, aid, year): 

1739 return Article.objects.filter( 

1740 Q(my_container__my_collection__pid=aid, my_container__fyear=year) 

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

1742 ).prefetch_related("resourcecount_set") 

1743 

1744 

1745class DOAJResourceRegisterView(View): 

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

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

1748 resource = model_helpers.get_resource(pid) 

1749 if resource is None: 

1750 raise Http404 

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

1752 resource.colid, None 

1753 ): 

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

1755 

1756 try: 

1757 data = {} 

1758 doaj_meta, response = doaj_pid_register(pid) 

1759 if response is None: 

1760 return HttpResponse(status=204) 

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

1762 data.update(doaj_meta) 

1763 else: 

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

1765 except Timeout as exception: 

1766 return HttpResponse(exception, status=408) 

1767 except Exception as exception: 

1768 return HttpResponseServerError(exception) 

1769 return JsonResponse(data) 

1770 

1771 

1772class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

1773 """ 

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

1775 """ 

1776 

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

1778 pid = kwargs.get("pid") 

1779 if not pid: 

1780 raise Http404("Missing pid") 

1781 

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

1783 if not article: 

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

1785 

1786 colid = article.get_collection().pid 

1787 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS: 

1788 return JsonResponse( 

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

1790 ) 

1791 

1792 if is_tex_conversion_locked(pid): 

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

1794 return JsonResponse( 

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

1796 ) 

1797 

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

1799 

1800 try: 

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

1802 except Exception: 

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

1804 release_tex_conversion_lock(pid) 

1805 raise 

1806 

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

1808 

1809 

1810class CROSSREFResourceRegisterView(View): 

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

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

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

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

1815 if not request.user.is_superuser: 

1816 force = None 

1817 

1818 resource = model_helpers.get_resource(pid) 

1819 if resource is None: 

1820 raise Http404 

1821 

1822 resource = resource.cast() 

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

1824 try: 

1825 data = meth(resource, force) 

1826 except Timeout as exception: 

1827 return HttpResponse(exception, status=408) 

1828 except Exception as exception: 

1829 return HttpResponseServerError(exception) 

1830 return JsonResponse(data) 

1831 

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

1833 result = {"status": 404} 

1834 if ( 

1835 article.doi 

1836 and not article.do_not_publish 

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

1838 ): 

1839 if article.my_container.fyear == 0: 

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

1841 result = recordDOI(article) 

1842 return result 

1843 

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

1845 return recordDOI(collection) 

1846 

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

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

1849 

1850 if container.ctype == "issue": 

1851 if container.doi: 

1852 result = recordDOI(container) 

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

1854 return result 

1855 if force == "force": 

1856 articles = container.article_set.exclude( 

1857 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True 

1858 ) 

1859 else: 

1860 articles = container.article_set.exclude( 

1861 doi__isnull=True, 

1862 do_not_publish=True, 

1863 date_published__isnull=True, 

1864 date_online_first__isnull=True, 

1865 ) 

1866 

1867 for article in articles: 

1868 result = self.recordDOIArticle(article, force) 

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

1870 data = result 

1871 else: 

1872 return recordDOI(container) 

1873 return data 

1874 

1875 

1876class CROSSREFResourceCheckStatusView(View): 

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

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

1879 resource = model_helpers.get_resource(pid) 

1880 if resource is None: 

1881 raise Http404 

1882 resource = resource.cast() 

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

1884 try: 

1885 meth(resource) 

1886 except Timeout as exception: 

1887 return HttpResponse(exception, status=408) 

1888 except Exception as exception: 

1889 return HttpResponseServerError(exception) 

1890 

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

1892 return JsonResponse(data) 

1893 

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

1895 if article.my_container.fyear == 0: 

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

1897 checkDOI(article) 

1898 

1899 def checkDOICollection(self, collection): 

1900 checkDOI(collection) 

1901 

1902 def checkDOIContainer(self, container): 

1903 if container.doi is not None: 

1904 checkDOI(container) 

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

1906 self.checkDOIArticle(article) 

1907 

1908 

1909class CROSSREFResourcePendingPublicationRegisterView(View): 

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

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

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

1913 

1914 resource = model_helpers.get_resource(pid) 

1915 if resource is None: 

1916 raise Http404 

1917 

1918 resource = resource.cast() 

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

1920 try: 

1921 data = meth(resource) 

1922 except Timeout as exception: 

1923 return HttpResponse(exception, status=408) 

1924 except Exception as exception: 

1925 return HttpResponseServerError(exception) 

1926 return JsonResponse(data) 

1927 

1928 def recordPendingPublicationArticle(self, article): 

1929 result = {"status": 404} 

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

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

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

1933 result = recordPendingPublication(article) 

1934 return result 

1935 

1936 

1937class RegisterPubmedFormView(FormView): 

1938 template_name = "record_pubmed_dialog.html" 

1939 form_class = RegisterPubmedForm 

1940 

1941 def get_context_data(self, **kwargs): 

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

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

1944 context["helper"] = PtfLargeModalFormHelper 

1945 return context 

1946 

1947 

1948class RegisterPubmedView(View): 

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

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

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

1952 

1953 article = model_helpers.get_article(pid) 

1954 if article is None: 

1955 raise Http404 

1956 try: 

1957 recordPubmed(article, update_article) 

1958 except Exception as exception: 

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

1960 return HttpResponseServerError(exception) 

1961 

1962 return HttpResponseRedirect( 

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

1964 ) 

1965 

1966 

1967class PTFToolsArticleView(ArticleView): 

1968 def get_context_data(self, **kwargs): 

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

1970 

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

1972 

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

1974 if qs: 

1975 test_website = qs.first().location 

1976 context["test_website"] = test_website 

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

1978 if qs: 

1979 prod_location = qs.first().location 

1980 context["prod_website"] = prod_location 

1981 

1982 return context 

1983 

1984 

1985ItemViewClassFactory.views["article"] = PTFToolsArticleView 

1986 

1987 

1988class PTFToolsContainerView(TemplateView): 

1989 template_name = "" 

1990 

1991 def get_context_data(self, **kwargs): 

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

1993 

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

1995 if container is None: 

1996 raise Http404 

1997 citing_articles = container.citations() 

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

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

2000 book_parts = ( 

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

2002 ) 

2003 references = False 

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

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

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

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

2008 references = True 

2009 context.update( 

2010 { 

2011 "book": container, 

2012 "book_parts": list(book_parts), 

2013 "source": source, 

2014 "citing_articles": citing_articles, 

2015 "references": references, 

2016 "test_website": container.get_top_collection() 

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

2018 .location, 

2019 "prod_website": container.get_top_collection() 

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

2021 .location, 

2022 } 

2023 ) 

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

2025 else: 

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

2027 for article in articles: 

2028 try: 

2029 last_match = ( 

2030 history_models.HistoryEvent.objects.filter( 

2031 pid=article.pid, 

2032 type="matching", 

2033 ) 

2034 .only("created_on") 

2035 .latest("created_on") 

2036 ) 

2037 except history_models.HistoryEvent.DoesNotExist as _: 

2038 article.last_match = None 

2039 else: 

2040 article.last_match = last_match.created_on 

2041 

2042 # article1 = articles.first() 

2043 # date = article1.deployed_date() 

2044 # TODO next_issue, previous_issue 

2045 

2046 # check DOI est maintenant une commande à part 

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

2048 # articlesWithStatus = [] 

2049 # for article in articles: 

2050 # checkDOIExistence(article) 

2051 # articlesWithStatus.append(article) 

2052 

2053 test_location = prod_location = "" 

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

2055 if qs: 

2056 test_location = qs.first().location 

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

2058 if qs: 

2059 prod_location = qs.first().location 

2060 context.update( 

2061 { 

2062 "issue": container, 

2063 "articles": articles, 

2064 "source": source, 

2065 "citing_articles": citing_articles, 

2066 "test_website": test_location, 

2067 "prod_website": prod_location, 

2068 } 

2069 ) 

2070 

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

2072 context["is_issue_pending_publication"] = True 

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

2074 context["is_excluded_from_tex_conversion"] = True 

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

2076 

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

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

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

2080 return context 

2081 

2082 

2083class ExtLinkInline(InlineFormSetFactory): 

2084 model = ExtLink 

2085 form_class = ExtLinkForm 

2086 factory_kwargs = {"extra": 0} 

2087 

2088 

2089class ResourceIdInline(InlineFormSetFactory): 

2090 model = ResourceId 

2091 form_class = ResourceIdForm 

2092 factory_kwargs = {"extra": 0} 

2093 

2094 

2095class IssueDetailAPIView(View): 

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

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

2098 deployed_date = issue.deployed_date() 

2099 result = { 

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

2101 if deployed_date 

2102 else None, 

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

2104 "all_doi_are_registered": issue.all_doi_are_registered(), 

2105 "registered_in_doaj": issue.registered_in_doaj(), 

2106 "doi": issue.my_collection.doi, 

2107 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(), 

2108 } 

2109 try: 

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

2111 except history_models.HistoryEvent.DoesNotExist as _: 

2112 pass 

2113 else: 

2114 result["latest"] = latest.message 

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

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

2117 ) 

2118 

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

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

2121 try: 

2122 result[event_type] = timezone.localtime( 

2123 history_models.HistoryEvent.objects.filter( 

2124 type=event_type, 

2125 status="OK", 

2126 pid__startswith=issue.pid, 

2127 ) 

2128 .latest("created_on") 

2129 .created_on 

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

2131 except history_models.HistoryEvent.DoesNotExist as _: 

2132 result[event_type] = "" 

2133 return JsonResponse(result) 

2134 

2135 

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

2137 model = Collection 

2138 form_class = CollectionForm 

2139 inlines = [ResourceIdInline, ExtLinkInline] 

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

2141 

2142 def get_context_data(self, **kwargs): 

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

2144 context["helper"] = PtfFormHelper 

2145 context["formset_helper"] = FormSetHelper 

2146 return context 

2147 

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

2149 if description: 

2150 la = Abstract( 

2151 resource=collection, 

2152 tag="description", 

2153 lang=lang, 

2154 seq=seq, 

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

2156 value_html=description, 

2157 value_tex=description, 

2158 ) 

2159 la.save() 

2160 

2161 def form_valid(self, form): 

2162 if form.instance.abbrev: 

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

2164 else: 

2165 form.instance.title_xml = ( 

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

2167 ) 

2168 

2169 form.instance.title_html = form.instance.title_tex 

2170 form.instance.title_sort = form.instance.title_tex 

2171 result = super().form_valid(form) 

2172 

2173 collection = self.object 

2174 collection.abstract_set.all().delete() 

2175 

2176 seq = 1 

2177 description = form.cleaned_data["description_en"] 

2178 if description: 

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

2180 seq += 1 

2181 description = form.cleaned_data["description_fr"] 

2182 if description: 

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

2184 

2185 return result 

2186 

2187 def get_success_url(self): 

2188 messages.success( 

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

2190 ) 

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

2192 

2193 

2194class CollectionCreate(CollectionFormView, CreateWithInlinesView): 

2195 """ 

2196 Warning : Not yet finished 

2197 Automatic site membership creation is still missing 

2198 """ 

2199 

2200 

2201class CollectionUpdate(CollectionFormView, UpdateWithInlinesView): 

2202 slug_field = "pid" 

2203 slug_url_kwarg = "pid" 

2204 

2205 

2206def suggest_load_journal_dois(colid): 

2207 articles = ( 

2208 Article.objects.filter(my_container__my_collection__pid=colid) 

2209 .filter(doi__isnull=False) 

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

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

2212 ) 

2213 

2214 try: 

2215 articles = sorted( 

2216 articles, 

2217 key=lambda d: ( 

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

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

2220 ), 

2221 ) 

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

2223 pass 

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

2225 

2226 

2227def get_context_with_volumes(journal): 

2228 result = model_helpers.get_volumes_in_collection(journal) 

2229 volume_count = result["volume_count"] 

2230 collections = [] 

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

2232 item = model_helpers.get_volumes_in_collection(ancestor) 

2233 volume_count = max(0, volume_count) 

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

2235 collections.append(item) 

2236 

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

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

2239 collections.append(result) 

2240 

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

2242 collections.sort( 

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

2244 reverse=True, 

2245 ) 

2246 

2247 context = { 

2248 "journal": journal, 

2249 "sorted_issues": result["sorted_issues"], 

2250 "volume_count": volume_count, 

2251 "max_width": result["max_width"], 

2252 "collections": collections, 

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

2254 } 

2255 return context 

2256 

2257 

2258class CollectionDetail( 

2259 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin 

2260): 

2261 model = Collection 

2262 slug_field = "pid" 

2263 slug_url_kwarg = "pid" 

2264 template_name = "ptf/collection_detail.html" 

2265 

2266 def test_func(self): 

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

2268 

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

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

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

2272 

2273 def get_context_data(self, **kwargs): 

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

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

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

2277 ) 

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

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

2280 

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

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

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

2284 pid=context["issue_to_appear_pid"] 

2285 ).exists() 

2286 try: 

2287 latest_error = history_models.HistoryEvent.objects.filter( 

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

2289 ).latest("created_on") 

2290 except history_models.HistoryEvent.DoesNotExist as _: 

2291 pass 

2292 else: 

2293 message = latest_error.message 

2294 if message: 

2295 i = message.find(" - ") 

2296 latest_exception = message[:i] 

2297 latest_error_message = message[i + 3 :] 

2298 context["latest_exception"] = latest_exception 

2299 context["latest_exception_date"] = latest_error.created_on 

2300 context["latest_exception_type"] = latest_error.type 

2301 context["latest_error_message"] = latest_error_message 

2302 

2303 archive_in_error = history_models.HistoryEvent.objects.filter( 

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

2305 ).exists() 

2306 

2307 context["archive_in_error"] = archive_in_error 

2308 

2309 return context 

2310 

2311 def get_queryset(self): 

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

2313 

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

2315 query |= ancestor.content.all() 

2316 

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

2318 

2319 

2320class ContainerEditView(FormView): 

2321 template_name = "container_form.html" 

2322 form_class = ContainerForm 

2323 

2324 def get_success_url(self): 

2325 if self.kwargs["pid"]: 

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

2327 return reverse("mersenne_dashboard/published_articles") 

2328 

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

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

2331 

2332 def get_form_kwargs(self): 

2333 kwargs = super().get_form_kwargs() 

2334 if "pid" not in self.kwargs: 

2335 self.kwargs["pid"] = None 

2336 if "colid" not in self.kwargs: 

2337 self.kwargs["colid"] = None 

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

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

2340 # It is used when you submit a new container 

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

2342 

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

2344 self.kwargs["pid"] 

2345 ) 

2346 return kwargs 

2347 

2348 def get_context_data(self, **kwargs): 

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

2350 

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

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

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

2354 

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

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

2357 

2358 return context 

2359 

2360 def form_valid(self, form): 

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

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

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

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

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

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

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

2368 

2369 collection = None 

2370 issue = self.kwargs["container"] 

2371 if issue is not None: 

2372 collection = issue.my_collection 

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

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

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

2376 else: 

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

2378 

2379 if collection is None: 

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

2381 

2382 # Icon 

2383 new_icon_location = "" 

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

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

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

2387 

2388 icon_filename = resolver.get_disk_location( 

2389 settings.MERSENNE_TEST_DATA_FOLDER, 

2390 collection.pid, 

2391 file_extension, 

2392 new_pid, 

2393 None, 

2394 True, 

2395 ) 

2396 

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

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

2399 

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

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

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

2403 if name == "special_issue_create": 

2404 self.kwargs["name"] = name 

2405 if self.kwargs["container"]: 

2406 # Edit Issue 

2407 issue = self.kwargs["container"] 

2408 if issue is None: 

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

2410 

2411 issue.pid = new_pid 

2412 issue.title_tex = issue.title_html = new_title 

2413 issue.title_xml = build_title_xml( 

2414 title=new_title, 

2415 lang=issue.lang, 

2416 title_type="issue-title", 

2417 ) 

2418 

2419 trans_lang = "" 

2420 if new_trans_title != "": 

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

2422 

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

2424 title_xml = build_title_xml( 

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

2426 ) 

2427 

2428 issue.title_set.update_or_create( 

2429 lang=trans_lang, 

2430 type="main", 

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

2432 ) 

2433 

2434 issue.fyear = new_year 

2435 issue.volume = new_volume 

2436 issue.volume_int = make_int(new_volume) 

2437 issue.number = new_number 

2438 issue.number_int = make_int(new_number) 

2439 issue.save() 

2440 else: 

2441 xissue = create_issuedata() 

2442 

2443 xissue.ctype = "issue" 

2444 xissue.pid = new_pid 

2445 xissue.lang = "en" 

2446 xissue.title_tex = new_title 

2447 xissue.title_html = new_title 

2448 xissue.title_xml = build_title_xml( 

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

2450 ) 

2451 

2452 if new_trans_title != "": 

2453 trans_lang = "fr" 

2454 title_xml = build_title_xml( 

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

2456 ) 

2457 title = create_titledata( 

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

2459 ) 

2460 issue.titles = [title] 

2461 

2462 xissue.fyear = new_year 

2463 xissue.volume = new_volume 

2464 xissue.number = new_number 

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

2466 

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

2468 cmd.add_collection(collection) 

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

2470 issue = cmd.do() 

2471 

2472 self.kwargs["pid"] = new_pid 

2473 

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

2475 params = { 

2476 "icon_location": new_icon_location, 

2477 } 

2478 cmd = ptf_cmds.updateContainerPtfCmd(params) 

2479 cmd.set_resource(issue) 

2480 cmd.do() 

2481 

2482 publisher = model_helpers.get_publisher(new_publisher) 

2483 if not publisher: 

2484 xpub = create_publisherdata() 

2485 xpub.name = new_publisher 

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

2487 issue.my_publisher = publisher 

2488 issue.save() 

2489 

2490 self.set_success_message() 

2491 

2492 return super().form_valid(form) 

2493 

2494 

2495# class ArticleEditView(FormView): 

2496# template_name = 'article_form.html' 

2497# form_class = ArticleForm 

2498# 

2499# def get_success_url(self): 

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

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

2502# return reverse('mersenne_dashboard/published_articles') 

2503# 

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

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

2506# 

2507# def get_form_kwargs(self): 

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

2509# 

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

2511# # Article creation: pid is None 

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

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

2514# # Article edit: issue_id is not passed 

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

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

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

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

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

2520# 

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

2522# return kwargs 

2523# 

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

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

2526# 

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

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

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

2530# 

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

2532# 

2533# article = context['article'] 

2534# if article: 

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

2536# context['kwds_fr'] = None 

2537# context['kwds_en'] = None 

2538# kwd_gps = article.get_non_msc_kwds() 

2539# for kwd_gp in kwd_gps: 

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

2541# if kwd_gp.value_xml: 

2542# kwd_ = types.SimpleNamespace() 

2543# kwd_.value = kwd_gp.value_tex 

2544# context['kwd_unstructured_fr'] = kwd_ 

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

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

2547# if kwd_gp.value_xml: 

2548# kwd_ = types.SimpleNamespace() 

2549# kwd_.value = kwd_gp.value_tex 

2550# context['kwd_unstructured_en'] = kwd_ 

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

2552# 

2553# # Article creation: init pid 

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

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

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

2557# 

2558# return context 

2559# 

2560# def form_valid(self, form): 

2561# 

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

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

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

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

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

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

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

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

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

2571# 

2572# # TODO support MathML 

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

2574# # We need to pass trans_title to get_title_xml 

2575# # Meanwhile, ignore new_title_xml 

2576# new_title_xml = jats_parser.get_title_xml(new_title) 

2577# new_title_html = new_title 

2578# 

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

2580# i = 1 

2581# new_authors = [] 

2582# old_author_contributions = [] 

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

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

2585# 

2586# while authors_count > 0: 

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

2588# 

2589# if prefix is not None: 

2590# addresses = [] 

2591# if len(old_author_contributions) >= i: 

2592# old_author_contribution = old_author_contributions[i - 1] 

2593# addresses = [contrib_address.address for contrib_address in 

2594# old_author_contribution.get_addresses()] 

2595# 

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

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

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

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

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

2601# deceased_before_publication = deceased == 'on' 

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

2603# equal_contrib = equal_contrib == 'on' 

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

2605# corresponding = corresponding == 'on' 

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

2607# 

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

2609# params['deceased_before_publication'] = deceased_before_publication 

2610# params['equal_contrib'] = equal_contrib 

2611# params['corresponding'] = corresponding 

2612# params['addresses'] = addresses 

2613# params['email'] = email 

2614# 

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

2616# 

2617# new_authors.append(params) 

2618# 

2619# authors_count -= 1 

2620# i += 1 

2621# 

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

2623# i = 1 

2624# new_kwds_fr = [] 

2625# while kwds_fr_count > 0: 

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

2627# new_kwds_fr.append(value) 

2628# kwds_fr_count -= 1 

2629# i += 1 

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

2631# 

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

2633# i = 1 

2634# new_kwds_en = [] 

2635# while kwds_en_count > 0: 

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

2637# new_kwds_en.append(value) 

2638# kwds_en_count -= 1 

2639# i += 1 

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

2641# 

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

2643# # Edit article 

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

2645# else: 

2646# # New article 

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

2648# 

2649# if container is None: 

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

2651# 

2652# collection = container.my_collection 

2653# 

2654# # Copy PDF file & extract full text 

2655# body = '' 

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

2657# collection.pid, 

2658# "pdf", 

2659# container.pid, 

2660# new_pid, 

2661# True) 

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

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

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

2665# destination.write(chunk) 

2666# 

2667# # Extract full text from the PDF 

2668# body = utils.pdf_to_text(pdf_filename) 

2669# 

2670# # Icon 

2671# new_icon_location = '' 

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

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

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

2675# 

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

2677# collection.pid, 

2678# file_extension, 

2679# container.pid, 

2680# new_pid, 

2681# True) 

2682# 

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

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

2685# destination.write(chunk) 

2686# 

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

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

2689# 

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

2691# # Edit article 

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

2693# article.fpage = new_fpage 

2694# article.lpage = new_lpage 

2695# article.page_range = new_page_range 

2696# article.coi_statement = new_coi_statement 

2697# article.show_body = new_show_body 

2698# article.do_not_publish = new_do_not_publish 

2699# article.save() 

2700# 

2701# else: 

2702# # New article 

2703# params = { 

2704# 'pid': new_pid, 

2705# 'title_xml': new_title_xml, 

2706# 'title_html': new_title_html, 

2707# 'title_tex': new_title, 

2708# 'fpage': new_fpage, 

2709# 'lpage': new_lpage, 

2710# 'page_range': new_page_range, 

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

2712# 'body': body, 

2713# 'coi_statement': new_coi_statement, 

2714# 'show_body': new_show_body, 

2715# 'do_not_publish': new_do_not_publish 

2716# } 

2717# 

2718# xarticle = create_articledata() 

2719# xarticle.pid = new_pid 

2720# xarticle.title_xml = new_title_xml 

2721# xarticle.title_html = new_title_html 

2722# xarticle.title_tex = new_title 

2723# xarticle.fpage = new_fpage 

2724# xarticle.lpage = new_lpage 

2725# xarticle.page_range = new_page_range 

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

2727# xarticle.body = body 

2728# xarticle.coi_statement = new_coi_statement 

2729# params['xobj'] = xarticle 

2730# 

2731# cmd = ptf_cmds.addArticlePtfCmd(params) 

2732# cmd.set_container(container) 

2733# cmd.add_collection(container.my_collection) 

2734# article = cmd.do() 

2735# 

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

2737# 

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

2739# params = { 

2740# # 'title_xml': new_title_xml, 

2741# # 'title_html': new_title_html, 

2742# # 'title_tex': new_title, 

2743# 'authors': new_authors, 

2744# 'page_count': new_page_count, 

2745# 'icon_location': new_icon_location, 

2746# 'body': body, 

2747# 'use_kwds': True, 

2748# 'kwds_fr': new_kwds_fr, 

2749# 'kwds_en': new_kwds_en, 

2750# 'kwd_uns_fr': new_kwd_uns_fr, 

2751# 'kwd_uns_en': new_kwd_uns_en 

2752# } 

2753# cmd = ptf_cmds.updateArticlePtfCmd(params) 

2754# cmd.set_article(article) 

2755# cmd.do() 

2756# 

2757# self.set_success_message() 

2758# 

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

2760 

2761 

2762@require_http_methods(["POST"]) 

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

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

2765 

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

2767 

2768 article = model_helpers.get_article(pid) 

2769 if article: 

2770 article.do_not_publish = not article.do_not_publish 

2771 article.save() 

2772 else: 

2773 raise Http404 

2774 

2775 return HttpResponseRedirect(next) 

2776 

2777 

2778@require_http_methods(["POST"]) 

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

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

2781 

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

2783 

2784 article = model_helpers.get_article(pid) 

2785 if article: 

2786 article.show_body = not article.show_body 

2787 article.save() 

2788 else: 

2789 raise Http404 

2790 

2791 return HttpResponseRedirect(next) 

2792 

2793 

2794class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView): 

2795 """ 

2796 API to get/post article metadata 

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

2798 """ 

2799 

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

2801 """ 

2802 we define here what fields we want in the form 

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

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

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

2806 self.fields_to_update = [ 

2807 "lang", 

2808 "atype", 

2809 "contributors", 

2810 "abstracts", 

2811 "kwds", 

2812 "titles", 

2813 "title_html", 

2814 "title_xml", 

2815 "title_tex", 

2816 "streams", 

2817 "ext_links", 

2818 "date_accepted", 

2819 "history_dates", 

2820 "subjs", 

2821 "bibitems", 

2822 "references", 

2823 ] 

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

2825 self.additional_fields = [ 

2826 "doi", 

2827 "pid", 

2828 "container_pid", 

2829 "pdf", 

2830 "illustration", 

2831 "dates", 

2832 "msc_keywords", 

2833 ] 

2834 self.editorial_tools = [ 

2835 "translation", 

2836 "sidebar", 

2837 "lang_selection", 

2838 "back_to_article_option", 

2839 "msc_keywords", 

2840 ] 

2841 self.validation = { 

2842 "required_checks": [], 

2843 "blocking_checks": [], 

2844 } 

2845 self.article_container_pid = "" 

2846 self.back_url = "trammel" 

2847 

2848 def save_data(self, data_article): 

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

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

2851 params = { 

2852 "pid": data_article.pid, 

2853 "export_folder": settings.MERSENNE_TMP_FOLDER, 

2854 "export_all": True, 

2855 "with_binary_files": False, 

2856 } 

2857 ptf_cmds.exportExtraDataPtfCmd(params).do() 

2858 

2859 def restore_data(self, article): 

2860 ptf_cmds.importExtraDataPtfCmd( 

2861 { 

2862 "pid": article.pid, 

2863 "import_folder": settings.MERSENNE_TMP_FOLDER, 

2864 "import_bibitemid": False, 

2865 } 

2866 ).do() 

2867 

2868 @method_decorator(csrf_exempt) 

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

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

2871 if user_role: 

2872 new_fields_to_update = [] 

2873 new_additional_fields = [] 

2874 new_editorial_tools = [] 

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

2876 for role in user_role: 

2877 role_fields_to_updates, role_additional_fields = role.get_real_fields_to_update() 

2878 new_fields_to_update += role_fields_to_updates 

2879 new_additional_fields += role_additional_fields 

2880 new_editorial_tools += role.editorial_tools 

2881 # in case two roles have some fields in common 

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

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

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

2885 

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

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

2888 

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

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

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

2892 return redirect( 

2893 "api-edit-article", 

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

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

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

2897 ) 

2898 else: 

2899 raise Http404 

2900 

2901 

2902class ArticleEditWithVueView(LoginRequiredMixin, TemplateView): 

2903 template_name = "article_form.html" 

2904 

2905 def get_success_url(self): 

2906 if self.kwargs["doi"]: 

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

2908 return reverse("mersenne_dashboard/published_articles") 

2909 

2910 def get_context_data(self, **kwargs): 

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

2912 if "doi" in self.kwargs: 

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

2914 context["article"] = article 

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

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

2917 

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

2919 return context 

2920 

2921 

2922class ArticleDeleteView(View): 

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

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

2925 article = get_object_or_404(Article, pid=pid) 

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

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

2928 status = 200 

2929 

2930 try: 

2931 if check_lock(): 

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

2933 messages.error(self.request, message) 

2934 raise ServerUnderMaintenance(message) 

2935 

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

2937 

2938 if site == "trammel": 

2939 if article.is_deployed(mersenneSite): 

2940 article.undeploy(mersenneSite) 

2941 

2942 cmd = ptf_cmds.addArticlePtfCmd( 

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

2944 ) 

2945 cmd.set_container(article.my_container) 

2946 cmd.set_object_to_be_deleted(article) 

2947 cmd.undo() 

2948 

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

2950 collection = article.get_collection() 

2951 server_url = getattr(collection, site)() 

2952 if not server_url: 

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

2954 

2955 # Delete on site 

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

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

2958 status = response.status_code 

2959 

2960 if status == 404: 

2961 raise Http404( 

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

2963 ) 

2964 elif status > 204: 

2965 exception = HTTPError( 

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

2967 response=response, 

2968 ) 

2969 raise exception 

2970 else: 

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

2972 article.undeploy(mersenneSite) 

2973 # Delete article binary files 

2974 folder = article.get_relative_folder() 

2975 resolver.delete_object_folder( 

2976 folder, 

2977 to_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

2978 ) 

2979 else: 

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

2981 

2982 except ValueError as exception: 

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

2984 raise 

2985 message, status = str(exception), 400 

2986 except Http404 as exception: 

2987 message, status = str(exception), 404 

2988 except HTTPError as exception: 

2989 message, status = str(exception), 502 

2990 except ServerUnderMaintenance as exception: 

2991 message, status = str(exception), 503 

2992 except requests.exceptions.ConnectionError: 

2993 message, status = ( 

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

2995 503, 

2996 ) 

2997 except Exception as exception: 

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

2999 message, status = str(exception), 500 

3000 

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

3002 

3003 

3004def get_messages_in_queue(): 

3005 app = Celery("ptf-tools") 

3006 # tasks = list(current_app.tasks) 

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

3008 print(tasks) 

3009 # i = app.control.inspect() 

3010 

3011 with app.connection_or_acquire() as conn: 

3012 remaining = conn.default_channel.queue_declare( 

3013 queue="coordinator", passive=True 

3014 ).message_count 

3015 return remaining 

3016 

3017 

3018class NumdamView(TemplateView, history_views.HistoryContextMixin): 

3019 template_name = "numdam.html" 

3020 

3021 def get_context_data(self, **kwargs): 

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

3023 

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

3025 

3026 pre_issues = [] 

3027 prod_issues = [] 

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

3029 try: 

3030 response = requests.get(url) 

3031 if response.status_code == 200: 

3032 data = response.json() 

3033 if "issues" in data: 

3034 pre_issues = data["issues"] 

3035 except Exception: 

3036 pass 

3037 

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

3039 response = requests.get(url) 

3040 if response.status_code == 200: 

3041 data = response.json() 

3042 if "issues" in data: 

3043 prod_issues = data["issues"] 

3044 

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

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

3047 grouped = [ 

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

3049 ] 

3050 grouped_removed = [ 

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

3052 ] 

3053 context["added_issues"] = grouped 

3054 context["removed_issues"] = grouped_removed 

3055 

3056 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS 

3057 return context 

3058 

3059 

3060class NumdamArchiveView(RedirectView): 

3061 @staticmethod 

3062 def reset_task_results(): 

3063 TaskResult.objects.all().delete() 

3064 

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

3066 self.colid = kwargs["colid"] 

3067 

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

3069 return Http404 

3070 

3071 # we make sure archiving is not already running 

3072 # if not get_messages_in_queue(): 

3073 # self.reset_task_results() 

3074 

3075 if self.colid == "ALL": 

3076 archive_numdam_collections.delay() 

3077 else: 

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

3079 

3080 return reverse("numdam") 

3081 

3082 

3083class DeployAllNumdamAPIView(View): 

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

3085 pids = [] 

3086 

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

3088 pids.append(obj.pid) 

3089 

3090 return pids 

3091 

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

3093 try: 

3094 pids, status, message = history_views.execute_and_record_func( 

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

3096 ) 

3097 except Exception as exception: 

3098 return HttpResponseServerError(exception) 

3099 

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

3101 return JsonResponse(data) 

3102 

3103 

3104class NumdamDeleteAPIView(View): 

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

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

3107 

3108 try: 

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

3110 obj.delete() 

3111 except Exception as exception: 

3112 return HttpResponseServerError(exception) 

3113 

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

3115 return JsonResponse(data) 

3116 

3117 

3118class ExtIdApiDetail(View): 

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

3120 extid = get_object_or_404( 

3121 ExtId, 

3122 resource__pid=kwargs["pid"], 

3123 id_type=kwargs["what"], 

3124 ) 

3125 return JsonResponse( 

3126 { 

3127 "pk": extid.pk, 

3128 "href": extid.get_href(), 

3129 "fetch": reverse( 

3130 "api-fetch-id", 

3131 args=( 

3132 extid.resource.pk, 

3133 extid.id_value, 

3134 extid.id_type, 

3135 "extid", 

3136 ), 

3137 ), 

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

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

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

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

3142 "is_valid": extid.checked, 

3143 } 

3144 ) 

3145 

3146 

3147class ExtIdFormTemplate(TemplateView): 

3148 template_name = "common/externalid_form.html" 

3149 

3150 def get_context_data(self, **kwargs): 

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

3152 context["sequence"] = kwargs["sequence"] 

3153 return context 

3154 

3155 

3156class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

3157 def get_context_data(self, **kwargs): 

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

3159 context["helper"] = PtfFormHelper 

3160 return context 

3161 

3162 def get_success_url(self): 

3163 self.post_process() 

3164 return self.object.resource.get_absolute_url() 

3165 

3166 def post_process(self): 

3167 model_helpers.post_resource_updated(self.object.resource) 

3168 

3169 

3170class ExtIdCreate(ExtIdFormView, CreateView): 

3171 model = ExtId 

3172 form_class = ExtIdForm 

3173 

3174 def get_context_data(self, **kwargs): 

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

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

3177 return context 

3178 

3179 def get_initial(self): 

3180 initial = super().get_initial() 

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

3182 return initial 

3183 

3184 def form_valid(self, form): 

3185 form.instance.checked = False 

3186 return super().form_valid(form) 

3187 

3188 

3189class ExtIdUpdate(ExtIdFormView, UpdateView): 

3190 model = ExtId 

3191 form_class = ExtIdForm 

3192 

3193 def get_context_data(self, **kwargs): 

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

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

3196 return context 

3197 

3198 

3199class UpdateTexmfZipAPIView(View): 

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

3201 def copy_zip_files(src_folder, dest_folder): 

3202 os.makedirs(dest_folder, exist_ok=True) 

3203 

3204 zip_files = [ 

3205 os.path.join(src_folder, f) 

3206 for f in os.listdir(src_folder) 

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

3208 ] 

3209 for zip_file in zip_files: 

3210 resolver.copy_file(zip_file, dest_folder) 

3211 

3212 # Exceptions: specific zip/gz files 

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

3214 resolver.copy_file(zip_file, dest_folder) 

3215 

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

3217 resolver.copy_file(zip_file, dest_folder) 

3218 

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

3220 resolver.copy_file(gz_file, dest_folder) 

3221 

3222 src_folder = settings.CEDRAM_DISTRIB_FOLDER 

3223 

3224 dest_folder = os.path.join( 

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

3226 ) 

3227 

3228 try: 

3229 copy_zip_files(src_folder, dest_folder) 

3230 except Exception as exception: 

3231 return HttpResponseServerError(exception) 

3232 

3233 try: 

3234 dest_folder = os.path.join( 

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

3236 ) 

3237 copy_zip_files(src_folder, dest_folder) 

3238 except Exception as exception: 

3239 return HttpResponseServerError(exception) 

3240 

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

3242 return JsonResponse(data) 

3243 

3244 

3245class TrammelTasksProgressView(View): 

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

3247 """ 

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

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

3250 """ 

3251 task_name = task 

3252 

3253 def get_event_data(): 

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

3255 

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

3257 remaining_messages = get_messages_in_queue() 

3258 

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

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

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

3262 

3263 all_tasks_count = all_tasks.count() 

3264 success_count = successed_tasks.count() 

3265 fail_count = failed_tasks.count() 

3266 

3267 all_count = all_tasks_count + remaining_messages 

3268 remaining_count = all_count - success_count - fail_count 

3269 

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

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

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

3273 

3274 last_task = successed_tasks.first() 

3275 last_task = ( 

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

3277 if last_task 

3278 else "" 

3279 ) 

3280 

3281 # SSE event format 

3282 event_data = { 

3283 "status": status, 

3284 "success_rate": success_rate, 

3285 "error_rate": error_rate, 

3286 "all_count": all_count, 

3287 "remaining_count": remaining_count, 

3288 "success_count": success_count, 

3289 "fail_count": fail_count, 

3290 "last_task": last_task, 

3291 } 

3292 

3293 return event_data 

3294 

3295 def stream_response(data): 

3296 # Send initial response headers 

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

3298 

3299 data = get_event_data() 

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

3301 if format == "json": 

3302 response = JsonResponse(data) 

3303 else: 

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

3305 return response 

3306 

3307 

3308user_signed_up.connect(update_user_from_invite)