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

1613 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-08-14 10:08 +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.views.decorators.http import require_http_methods 

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

31from django.views.generic.base import RedirectView 

32from django.views.generic.detail import SingleObjectMixin 

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

34from django_celery_results.models import TaskResult 

35from external.back.crossref.doi import checkDOI, recordDOI, recordPendingPublication 

36from extra_views import ( 

37 CreateWithInlinesView, 

38 InlineFormSetFactory, 

39 NamedFormsetsMixin, 

40 UpdateWithInlinesView, 

41) 

42 

43# from ptf.views import ArticleEditFormWithVueAPIView 

44from matching_back.views import ArticleEditFormWithVueAPIView 

45from ptf import model_data_converter, model_helpers, utils 

46from ptf.cmds import ptf_cmds, xml_cmds 

47from ptf.cmds.base_cmds import make_int 

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

49from ptf.cmds.xml.xml_utils import replace_html_entities 

50from ptf.display import resolver 

51from ptf.exceptions import DOIException, ServerUnderMaintenance 

52from ptf.model_data import create_issuedata, create_publisherdata, create_titledata 

53from ptf.models import ( 

54 Abstract, 

55 Article, 

56 Collection, 

57 Container, 

58 ExtId, 

59 ExtLink, 

60 Resource, 

61 ResourceId, 

62) 

63from ptf_back.locks import ( 

64 is_tex_conversion_locked, 

65 release_tex_conversion_lock, 

66) 

67from ptf_back.tex import create_frontpage 

68from ptf_back.tex.tex_tasks import convert_article_tex 

69from pubmed.views import recordPubmed 

70from requests import Timeout 

71from task.tasks.archiving_tasks import archive_resource 

72 

73from comments_moderation.utils import get_comments_for_home, is_comment_moderator 

74from history import models as history_models 

75from history import views as history_views 

76from history.utils import ( 

77 get_gap, 

78 get_history_last_event_by, 

79 get_last_unsolved_error, 

80) 

81from ptf_tools.doaj import doaj_pid_register 

82from ptf_tools.forms import ( 

83 CollectionForm, 

84 ContainerForm, 

85 DiffContainerForm, 

86 ExtIdForm, 

87 ExtLinkForm, 

88 FormSetHelper, 

89 ImportArticleForm, 

90 ImportContainerForm, 

91 ImportEditflowArticleForm, 

92 PtfFormHelper, 

93 PtfLargeModalFormHelper, 

94 PtfModalFormHelper, 

95 RegisterPubmedForm, 

96 ResourceIdForm, 

97 get_article_choices, 

98) 

99from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos 

100from ptf_tools.models import ResourceInNumdam 

101from ptf_tools.signals import update_user_from_invite 

102from ptf_tools.tasks import ( 

103 archive_numdam_collection, 

104 archive_numdam_collections, 

105) 

106from ptf_tools.templatetags.tools_helpers import get_authorized_collections 

107from ptf_tools.utils import is_authorized_editor 

108from ptf_tools.views.components import breadcrumb 

109 

110logger = logging.getLogger(__name__) 

111 

112 

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

114 """ 

115 Dummy view raising HTTP 404 exception. 

116 """ 

117 raise Http404 

118 

119 

120def check_collection(collection, server_url, server_type): 

121 """ 

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

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

124 """ 

125 

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

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

128 # First, upload the collection XML 

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

130 body = xml.encode("utf8") 

131 

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

133 if response.status_code == 200: 

134 # PUT http verb is used for update 

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

136 else: 

137 # POST http verb is used for creation 

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

139 

140 # Second, copy the collection images 

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

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

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

144 # /mersenne_prod_data during an upload to prod 

145 if server_type == "website": 

146 resolver.copy_binary_files( 

147 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

148 ) 

149 elif server_type == "numdam": 

150 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

151 if collection.pid in settings.NUMDAM_COLLECTIONS: 

152 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

153 

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

155 

156 

157def check_lock(): 

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

159 

160 

161def load_cedrics_article_choices(request): 

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

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

164 article_choices = get_article_choices(colid, issue) 

165 return render( 

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

167 ) 

168 

169 

170class ImportCedricsArticleFormView(FormView): 

171 template_name = "import_article.html" 

172 form_class = ImportArticleForm 

173 

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

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

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

177 

178 def get_success_url(self): 

179 if self.colid: 

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

181 return "/" 

182 

183 def get_context_data(self, **kwargs): 

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

185 context["colid"] = self.colid 

186 context["helper"] = PtfModalFormHelper 

187 return context 

188 

189 def get_form_kwargs(self): 

190 kwargs = super().get_form_kwargs() 

191 kwargs["colid"] = self.colid 

192 return kwargs 

193 

194 def form_valid(self, form): 

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

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

197 return super().form_valid(form) 

198 

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

200 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd( 

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

202 ) 

203 cmd.do() 

204 

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

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

207 issue = request.POST["issue"] 

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

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

210 

211 import_args = [self] 

212 import_kwargs = {} 

213 

214 try: 

215 _, status, message = history_views.execute_and_record_func( 

216 "import", 

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

218 self.colid, 

219 self.import_cedrics_article, 

220 "", 

221 False, 

222 None, 

223 None, 

224 *import_args, 

225 **import_kwargs, 

226 ) 

227 

228 messages.success( 

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

230 ) 

231 

232 except Exception as exception: 

233 messages.error( 

234 self.request, 

235 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}", 

236 ) 

237 

238 return redirect(self.get_success_url()) 

239 

240 

241class ImportCedricsIssueView(FormView): 

242 template_name = "import_container.html" 

243 form_class = ImportContainerForm 

244 

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

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

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

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

249 

250 def get_success_url(self): 

251 if self.filename: 

252 return reverse( 

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

254 ) 

255 return "/" 

256 

257 def get_context_data(self, **kwargs): 

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

259 context["colid"] = self.colid 

260 context["helper"] = PtfModalFormHelper 

261 return context 

262 

263 def get_form_kwargs(self): 

264 kwargs = super().get_form_kwargs() 

265 kwargs["colid"] = self.colid 

266 kwargs["to_appear"] = self.to_appear 

267 return kwargs 

268 

269 def form_valid(self, form): 

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

271 return super().form_valid(form) 

272 

273 

274class DiffCedricsIssueView(FormView): 

275 template_name = "diff_container_form.html" 

276 form_class = DiffContainerForm 

277 diffs = None 

278 xissue = None 

279 xissue_encoded = None 

280 

281 def get_success_url(self): 

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

283 

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

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

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

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

288 

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

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

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

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

293 self.remove_email = self.remove_mail == "on" 

294 self.remove_date_prod = self.remove_date_prod == "on" 

295 

296 try: 

297 result, status, message = history_views.execute_and_record_func( 

298 "import", 

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

300 self.colid, 

301 self.diff_cedrics_issue, 

302 "", 

303 True, 

304 ) 

305 except Exception as exception: 

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

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

308 return HttpResponseRedirect(self.get_success_url()) 

309 

310 no_conflict = result[0] 

311 self.diffs = result[1] 

312 self.xissue = result[2] 

313 

314 if True or no_conflict: 

315 # Proceed with the import 

316 self.form_valid(self.get_form()) 

317 return redirect(self.get_success_url()) 

318 else: 

319 # Display the diff template 

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

321 

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

323 

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

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

326 data = request.POST["xissue_encoded"] 

327 self.xissue = jsonpickle.decode(data) 

328 

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

330 

331 def get_context_data(self, **kwargs): 

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

333 context["colid"] = self.colid 

334 context["diff"] = self.diffs 

335 context["filename"] = self.filename 

336 context["xissue_encoded"] = self.xissue_encoded 

337 return context 

338 

339 def get_form_kwargs(self): 

340 kwargs = super().get_form_kwargs() 

341 kwargs["colid"] = self.colid 

342 return kwargs 

343 

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

345 params = { 

346 "colid": self.colid, 

347 "input_file": self.filename, 

348 "remove_email": self.remove_mail, 

349 "remove_date_prod": self.remove_date_prod, 

350 "diff_only": True, 

351 } 

352 

353 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

356 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

357 else: 

358 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

359 

360 result = cmd.do() 

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

362 messages.warning( 

363 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings 

364 ) 

365 

366 return result 

367 

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

369 # modify xissue with data_issue if params to override 

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

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

372 if issue: 

373 data_issue = model_data_converter.db_to_issue_data(issue) 

374 for xarticle in self.xissue.articles: 

375 filter_articles = [ 

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

377 ] 

378 if len(filter_articles) > 0: 

379 db_article = filter_articles[0] 

380 xarticle.coi_statement = db_article.coi_statement 

381 xarticle.kwds = db_article.kwds 

382 xarticle.contrib_groups = db_article.contrib_groups 

383 

384 params = { 

385 "colid": self.colid, 

386 "xissue": self.xissue, 

387 "input_file": self.filename, 

388 } 

389 

390 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

393 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

394 else: 

395 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

396 

397 cmd.do() 

398 

399 def form_valid(self, form): 

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

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

402 else: 

403 import_kwargs = {} 

404 import_args = [self] 

405 

406 try: 

407 _, status, message = history_views.execute_and_record_func( 

408 "import", 

409 self.xissue.pid, 

410 self.kwargs["colid"], 

411 self.import_cedrics_issue, 

412 "", 

413 False, 

414 None, 

415 None, 

416 *import_args, 

417 **import_kwargs, 

418 ) 

419 except Exception as exception: 

420 messages.error( 

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

422 ) 

423 return super().form_invalid(form) 

424 

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

426 return super().form_valid(form) 

427 

428 

429class ImportEditflowArticleFormView(FormView): 

430 template_name = "import_editflow_article.html" 

431 form_class = ImportEditflowArticleForm 

432 

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

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

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

436 

437 def get_context_data(self, **kwargs): 

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

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

440 context["helper"] = PtfLargeModalFormHelper 

441 return context 

442 

443 def get_success_url(self): 

444 if self.colid: 

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

446 return "/" 

447 

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

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

450 try: 

451 if not self.colid: 

452 raise ValueError("Missing collection id") 

453 

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

455 if not issue_name: 

456 raise ValueError( 

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

458 ) 

459 

460 issue = model_helpers.get_container(issue_name) 

461 if not issue: 

462 raise ValueError("No issue found") 

463 

464 editflow_xml_file = request.FILES.get("editflow_xml_file") 

465 if not editflow_xml_file: 

466 raise ValueError("The file you specified couldn't be found") 

467 

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

469 

470 cmd = xml_cmds.addArticleXmlCmd( 

471 { 

472 "body": body, 

473 "issue": issue, 

474 "assign_doi": True, 

475 "standalone": True, 

476 "from_folder": settings.RESOURCES_ROOT, 

477 } 

478 ) 

479 cmd.set_collection(issue.get_collection()) 

480 cmd.do() 

481 

482 messages.success( 

483 request, 

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

485 ) 

486 

487 except Exception as exception: 

488 messages.error( 

489 request, 

490 f"Import failed: {str(exception)}", 

491 ) 

492 

493 return redirect(self.get_success_url()) 

494 

495 

496class MatchingAPIView(View): 

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

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

499 

500 url = settings.MATCHING_URL 

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

502 

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

504 

505 if settings.DEBUG: 

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

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

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

509 f.close() 

510 

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

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

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

514 

515 if settings.DEBUG: 

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

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

518 text = body 

519 f.write(text) 

520 f.close() 

521 

522 resource = model_helpers.get_resource(pid) 

523 obj = resource.cast() 

524 colid = obj.get_collection().pid 

525 

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

527 

528 cmd = xml_cmds.addOrUpdateIssueXmlCmd( 

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

530 ) 

531 cmd.do() 

532 

533 print("Matching finished") 

534 return JsonResponse(data) 

535 

536 

537class ImportAllAPIView(View): 

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

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

540 

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

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

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

544 

545 resource = model_helpers.get_resource(pid) 

546 if not resource: 

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

548 body = utils.get_file_content_in_utf8(file) 

549 journals = xml_cmds.addCollectionsXmlCmd( 

550 { 

551 "body": body, 

552 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

553 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

554 } 

555 ).do() 

556 if not journals: 

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

558 resource = journals[0] 

559 # resolver.copy_binary_files( 

560 # resource, 

561 # settings.MATHDOC_ARCHIVE_FOLDER, 

562 # settings.MERSENNE_TEST_DATA_FOLDER) 

563 

564 obj = resource.cast() 

565 

566 if obj.classname != "Collection": 

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

568 

569 cmd = xml_cmds.collectEntireCollectionXmlCmd( 

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

571 ) 

572 pids = cmd.do() 

573 

574 return pids 

575 

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

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

578 

579 try: 

580 pids, status, message = history_views.execute_and_record_func( 

581 "import", pid, pid, self.internal_do 

582 ) 

583 except Timeout as exception: 

584 return HttpResponse(exception, status=408) 

585 except Exception as exception: 

586 return HttpResponseServerError(exception) 

587 

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

589 return JsonResponse(data) 

590 

591 

592class DeployAllAPIView(View): 

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

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

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

596 

597 pids = [] 

598 

599 collection = model_helpers.get_collection(pid) 

600 if not collection: 

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

602 

603 if site == "numdam": 

604 server_url = settings.NUMDAM_PRE_URL 

605 elif site != "ptf_tools": 

606 server_url = getattr(collection, site)() 

607 if not server_url: 

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

609 

610 if site != "ptf_tools": 

611 # check if the collection exists on the server 

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

613 # image...) 

614 check_collection(collection, server_url, site) 

615 

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

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

618 pids.append(issue.pid) 

619 

620 return pids 

621 

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

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

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

625 

626 try: 

627 pids, status, message = history_views.execute_and_record_func( 

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

629 ) 

630 except Timeout as exception: 

631 return HttpResponse(exception, status=408) 

632 except Exception as exception: 

633 return HttpResponseServerError(exception) 

634 

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

636 return JsonResponse(data) 

637 

638 

639class AddIssuePDFView(View): 

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

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

642 self.pid = None 

643 self.issue = None 

644 self.collection = None 

645 self.site = "test_website" 

646 

647 def post_to_site(self, url): 

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

649 status = response.status_code 

650 if not (199 < status < 205): 

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

652 if status == 503: 

653 raise ServerUnderMaintenance(response.text) 

654 else: 

655 raise RuntimeError(response.text) 

656 

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

658 """ 

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

660 """ 

661 

662 issue_pid = self.issue.pid 

663 colid = self.collection.pid 

664 

665 if self.site == "website": 

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

667 resolver.copy_binary_files( 

668 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

669 ) 

670 else: 

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

672 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid) 

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

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

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

676 

677 to_path = resolver.get_disk_location( 

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

679 ) 

680 resolver.copy_file(from_path, to_path) 

681 

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

683 

684 if self.site == "test_website": 

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

686 absolute_url = self.request.build_absolute_uri(url) 

687 self.post_to_site(absolute_url) 

688 

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

690 absolute_url = server_url + url 

691 # Post to the test or production website 

692 self.post_to_site(absolute_url) 

693 

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

695 """ 

696 Send an issue PDF to the test or production website 

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

698 :param args: 

699 :param kwargs: 

700 :return: 

701 """ 

702 if check_lock(): 

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

704 messages.error(self.request, m) 

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

706 

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

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

709 

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

711 if not self.issue: 

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

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

714 

715 try: 

716 pids, status, message = history_views.execute_and_record_func( 

717 "deploy", 

718 self.pid, 

719 self.collection.pid, 

720 self.internal_do, 

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

722 ) 

723 

724 except Timeout as exception: 

725 return HttpResponse(exception, status=408) 

726 except Exception as exception: 

727 return HttpResponseServerError(exception) 

728 

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

730 return JsonResponse(data) 

731 

732 

733class ArchiveAllAPIView(View): 

734 """ 

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

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

737 @return array of issues pid 

738 """ 

739 

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

741 collection = kwargs["collection"] 

742 pids = [] 

743 colid = collection.pid 

744 

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

746 if os.path.isfile(logfile): 

747 os.remove(logfile) 

748 

749 ptf_cmds.exportPtfCmd( 

750 { 

751 "pid": colid, 

752 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

753 "with_binary_files": True, 

754 "for_archive": True, 

755 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

756 } 

757 ).do() 

758 

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

760 if os.path.isfile(cedramcls): 

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

762 resolver.create_folder(dest_folder) 

763 resolver.copy_file(cedramcls, dest_folder) 

764 

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

766 qs = issue.article_set.filter( 

767 date_online_first__isnull=True, date_published__isnull=True 

768 ) 

769 if qs.count() == 0: 

770 pids.append(issue.pid) 

771 

772 return pids 

773 

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

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

776 

777 collection = model_helpers.get_collection(pid) 

778 if not collection: 

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

780 

781 dict_ = {"collection": collection} 

782 args_ = [self] 

783 

784 try: 

785 pids, status, message = history_views.execute_and_record_func( 

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

787 ) 

788 except Timeout as exception: 

789 return HttpResponse(exception, status=408) 

790 except Exception as exception: 

791 return HttpResponseServerError(exception) 

792 

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

794 return JsonResponse(data) 

795 

796 

797class CreateAllDjvuAPIView(View): 

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

799 issue = kwargs["issue"] 

800 pids = [issue.pid] 

801 

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

803 pids.append(article.pid) 

804 

805 return pids 

806 

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

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

809 issue = model_helpers.get_container(pid) 

810 if not issue: 

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

812 

813 try: 

814 dict_ = {"issue": issue} 

815 args_ = [self] 

816 

817 pids, status, message = history_views.execute_and_record_func( 

818 "numdam", 

819 pid, 

820 issue.get_collection().pid, 

821 self.internal_do, 

822 "", 

823 False, 

824 None, 

825 None, 

826 *args_, 

827 **dict_, 

828 ) 

829 except Exception as exception: 

830 return HttpResponseServerError(exception) 

831 

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

833 return JsonResponse(data) 

834 

835 

836class ImportJatsContainerAPIView(View): 

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

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

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

840 

841 if pid and colid: 

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

843 

844 cmd = xml_cmds.addOrUpdateContainerXmlCmd( 

845 { 

846 "body": body, 

847 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

848 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

849 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

850 } 

851 ) 

852 container = cmd.do() 

853 if len(cmd.warnings) > 0: 

854 messages.warning( 

855 self.request, 

856 message="Balises non parsées lors de l'import : %s" % cmd.warnings, 

857 ) 

858 

859 if not container: 

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

861 

862 # resolver.copy_binary_files( 

863 # container, 

864 # settings.MATHDOC_ARCHIVE_FOLDER, 

865 # settings.MERSENNE_TEST_DATA_FOLDER) 

866 # 

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

868 # resolver.copy_binary_files( 

869 # article, 

870 # settings.MATHDOC_ARCHIVE_FOLDER, 

871 # settings.MERSENNE_TEST_DATA_FOLDER) 

872 else: 

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

874 

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

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

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

878 

879 try: 

880 _, status, message = history_views.execute_and_record_func( 

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

882 ) 

883 except Timeout as exception: 

884 return HttpResponse(exception, status=408) 

885 except Exception as exception: 

886 return HttpResponseServerError(exception) 

887 

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

889 return JsonResponse(data) 

890 

891 

892class DeployCollectionAPIView(View): 

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

894 

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

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

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

898 

899 collection = model_helpers.get_collection(colid) 

900 if not collection: 

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

902 

903 if site == "numdam": 

904 server_url = settings.NUMDAM_PRE_URL 

905 else: 

906 server_url = getattr(collection, site)() 

907 if not server_url: 

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

909 

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

911 check_collection(collection, server_url, site) 

912 

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

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

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

916 

917 try: 

918 _, status, message = history_views.execute_and_record_func( 

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

920 ) 

921 except Timeout as exception: 

922 return HttpResponse(exception, status=408) 

923 except Exception as exception: 

924 return HttpResponseServerError(exception) 

925 

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

927 return JsonResponse(data) 

928 

929 

930class DeployJatsResourceAPIView(View): 

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

932 

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

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

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

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

937 

938 if site == "ptf_tools": 

939 raise RuntimeError("Do not choose to deploy on PTF Tools") 

940 if check_lock(): 

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

942 messages.error(self.request, msg) 

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

944 

945 resource = model_helpers.get_resource(pid) 

946 if not resource: 

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

948 

949 obj = resource.cast() 

950 article = None 

951 if obj.classname == "Article": 

952 article = obj 

953 container = article.my_container 

954 articles_to_deploy = [article] 

955 else: 

956 container = obj 

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

958 

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

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

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

962 raise RuntimeError(f"{pid} is marked as Do not publish") 

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

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

965 

966 collection = container.get_top_collection() 

967 colid = collection.pid 

968 djvu_exception = None 

969 

970 if site == "numdam": 

971 server_url = settings.NUMDAM_PRE_URL 

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

973 

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

975 # Add Djvu (before exporting the XML) 

976 if False and int(container.fyear) < 2020: 

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

978 try: 

979 cmd = ptf_cmds.addDjvuPtfCmd() 

980 cmd.set_resource(art) 

981 cmd.do() 

982 except Exception as e: 

983 # Djvu are optional. 

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

985 djvu_exception = e 

986 else: 

987 server_url = getattr(collection, site)() 

988 if not server_url: 

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

990 

991 # check if the collection exists on the server 

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

993 # image...) 

994 if article is None: 

995 check_collection(collection, server_url, site) 

996 

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

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

999 if site == "website": 

1000 file_.write( 

1001 "Create/Update deployed_date and date_published on all articles for {}\n".format( 

1002 pid 

1003 ) 

1004 ) 

1005 

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

1007 cmd = ptf_cmds.publishResourcePtfCmd() 

1008 cmd.set_resource(resource) 

1009 updated_articles = cmd.do() 

1010 

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

1012 

1013 mersenneSite = model_helpers.get_site_mersenne(colid) 

1014 # create or update deployed_date on container and articles 

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

1016 

1017 for art in articles_to_deploy: 

1018 if art.doi and (art.date_published or art.date_online_first): 

1019 if art.my_container.fyear is None: 

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

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

1022 

1023 file_.write( 

1024 "Publication date of {} : Online First: {}, Published: {}\n".format( 

1025 art.pid, art.date_online_first, art.date_published 

1026 ) 

1027 ) 

1028 

1029 if article is None: 

1030 resolver.copy_binary_files( 

1031 container, 

1032 settings.MERSENNE_TEST_DATA_FOLDER, 

1033 settings.MERSENNE_PROD_DATA_FOLDER, 

1034 ) 

1035 

1036 for art in articles_to_deploy: 

1037 resolver.copy_binary_files( 

1038 art, 

1039 settings.MERSENNE_TEST_DATA_FOLDER, 

1040 settings.MERSENNE_PROD_DATA_FOLDER, 

1041 ) 

1042 

1043 elif site == "test_website": 

1044 # create date_pre_published on articles without date_pre_published 

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

1046 cmd.set_resource(resource) 

1047 updated_articles = cmd.do() 

1048 

1049 create_frontpage(colid, container, updated_articles) 

1050 

1051 export_to_website = site == "website" 

1052 

1053 if article is None: 

1054 with_djvu = site == "numdam" 

1055 xml = ptf_cmds.exportPtfCmd( 

1056 { 

1057 "pid": pid, 

1058 "with_djvu": with_djvu, 

1059 "export_to_website": export_to_website, 

1060 } 

1061 ).do() 

1062 body = xml.encode("utf8") 

1063 

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

1065 url = server_url + reverse("issue_upload") 

1066 else: 

1067 url = server_url + reverse("book_upload") 

1068 

1069 # verify=False: ignore TLS certificate 

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

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

1072 else: 

1073 xml = ptf_cmds.exportPtfCmd( 

1074 { 

1075 "pid": pid, 

1076 "with_djvu": False, 

1077 "article_standalone": True, 

1078 "collection_pid": collection.pid, 

1079 "export_to_website": export_to_website, 

1080 "export_folder": settings.LOG_DIR, 

1081 } 

1082 ).do() 

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

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

1085 xml_file = io.StringIO(xml) 

1086 files = {"xml": xml_file} 

1087 

1088 url = server_url + reverse( 

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

1090 ) 

1091 # verify=False: ignore TLS certificate 

1092 header = {} 

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

1094 

1095 status = response.status_code 

1096 

1097 if 199 < status < 205: 

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

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

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

1101 # /mersenne_prod_data during an upload to prod 

1102 if site == "website": 

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

1104 if container.doi: 

1105 recordDOI(container) 

1106 

1107 for art in articles_to_deploy: 

1108 # record DOI automatically when deploying in prod 

1109 

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

1111 recordDOI(art) 

1112 

1113 if colid == "CRBIOL": 

1114 recordPubmed( 

1115 art, force_update=False, updated_articles=updated_articles 

1116 ) 

1117 

1118 if colid == "PCJ": 

1119 self.update_pcj_editor(updated_articles) 

1120 

1121 # Archive the container or the article 

1122 if article is None: 

1123 archive_resource.delay( 

1124 pid, 

1125 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1126 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1127 ) 

1128 

1129 else: 

1130 archive_resource.delay( 

1131 pid, 

1132 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1133 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1134 article_doi=article.doi, 

1135 ) 

1136 # cmd = ptf_cmds.archiveIssuePtfCmd({ 

1137 # "pid": pid, 

1138 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1139 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER}) 

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

1141 # cmd.do() 

1142 

1143 elif site == "numdam": 

1144 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

1145 if colid in settings.NUMDAM_COLLECTIONS: 

1146 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

1147 

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

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

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

1151 

1152 elif status == 503: 

1153 raise ServerUnderMaintenance(response.text) 

1154 else: 

1155 raise RuntimeError(response.text) 

1156 

1157 if djvu_exception: 

1158 raise djvu_exception 

1159 

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

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

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

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

1164 

1165 try: 

1166 _, status, message = history_views.execute_and_record_func( 

1167 "deploy", pid, colid, self.internal_do, site 

1168 ) 

1169 except Timeout as exception: 

1170 return HttpResponse(exception, status=408) 

1171 except Exception as exception: 

1172 return HttpResponseServerError(exception) 

1173 

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

1175 return JsonResponse(data) 

1176 

1177 def update_pcj_editor(self, updated_articles): 

1178 for article in updated_articles: 

1179 data = { 

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

1181 "article_number": article.article_number, 

1182 } 

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

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

1185 

1186 

1187class DeployTranslatedArticleAPIView(CsrfExemptMixin, View): 

1188 article = None 

1189 

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

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

1192 

1193 translation = None 

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

1195 if trans_article.lang == lang: 

1196 translation = trans_article 

1197 

1198 if translation is None: 

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

1200 

1201 collection = self.article.get_top_collection() 

1202 colid = collection.pid 

1203 container = self.article.my_container 

1204 

1205 if translation.date_published is None: 

1206 # Add date posted 

1207 cmd = ptf_cmds.publishResourcePtfCmd() 

1208 cmd.set_resource(translation) 

1209 cmd.do() 

1210 # updated_articles = cmd.do() 

1211 

1212 # # Recompile PDF to add the date posted 

1213 # try: 

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

1215 # except Exception: 

1216 # raise PDFException( 

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

1218 # ) 

1219 

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

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

1222 resolver.copy_binary_files( 

1223 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

1224 ) 

1225 

1226 # Deploy in prod 

1227 xml = ptf_cmds.exportPtfCmd( 

1228 { 

1229 "pid": self.article.pid, 

1230 "with_djvu": False, 

1231 "article_standalone": True, 

1232 "collection_pid": colid, 

1233 "export_to_website": True, 

1234 "export_folder": settings.LOG_DIR, 

1235 } 

1236 ).do() 

1237 xml_file = io.StringIO(xml) 

1238 files = {"xml": xml_file} 

1239 

1240 server_url = getattr(collection, "website")() 

1241 if not server_url: 

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

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

1244 header = {} 

1245 

1246 try: 

1247 response = requests.post( 

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

1249 ) # verify: ignore TLS certificate 

1250 status = response.status_code 

1251 except requests.exceptions.ConnectionError: 

1252 raise ServerUnderMaintenance( 

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

1254 ) 

1255 

1256 # Register translation in Crossref 

1257 if 199 < status < 205: 

1258 if self.article.allow_crossref(): 

1259 try: 

1260 recordDOI(translation) 

1261 except Exception: 

1262 raise DOIException( 

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

1264 ) 

1265 

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

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

1268 self.article = model_helpers.get_article_by_doi(doi) 

1269 if self.article is None: 

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

1271 

1272 try: 

1273 _, status, message = history_views.execute_and_record_func( 

1274 "deploy", 

1275 self.article.pid, 

1276 self.article.get_top_collection().pid, 

1277 self.internal_do, 

1278 "website", 

1279 ) 

1280 except Timeout as exception: 

1281 return HttpResponse(exception, status=408) 

1282 except Exception as exception: 

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

1284 return HttpResponseServerError(exception) 

1285 

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

1287 return JsonResponse(data) 

1288 

1289 

1290class DeleteJatsIssueAPIView(View): 

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

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

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

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

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

1296 message = "Le volume a bien été supprimé" 

1297 status = 200 

1298 

1299 issue = model_helpers.get_container(pid) 

1300 if not issue: 

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

1302 try: 

1303 mersenneSite = model_helpers.get_site_mersenne(colid) 

1304 

1305 if site == "ptf_tools": 

1306 if issue.is_deployed(mersenneSite): 

1307 issue.undeploy(mersenneSite) 

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

1309 article.undeploy(mersenneSite) 

1310 

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

1312 

1313 cmd = ptf_cmds.addContainerPtfCmd( 

1314 { 

1315 "pid": issue.pid, 

1316 "ctype": "issue", 

1317 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

1318 } 

1319 ) 

1320 cmd.set_provider(p) 

1321 cmd.add_collection(issue.get_collection()) 

1322 cmd.set_object_to_be_deleted(issue) 

1323 cmd.undo() 

1324 

1325 else: 

1326 if site == "numdam": 

1327 server_url = settings.NUMDAM_PRE_URL 

1328 else: 

1329 collection = issue.get_collection() 

1330 server_url = getattr(collection, site)() 

1331 

1332 if not server_url: 

1333 message = "The collection has no " + site 

1334 status = 500 

1335 else: 

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

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

1338 status = response.status_code 

1339 

1340 if status == 404: 

1341 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur" 

1342 elif status > 204: 

1343 body = response.text.encode("utf8") 

1344 message = body[:1000] 

1345 else: 

1346 status = 200 

1347 # unpublish issue in collection site (site_register.json) 

1348 if site == "website": 

1349 if issue.is_deployed(mersenneSite): 

1350 issue.undeploy(mersenneSite) 

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

1352 article.undeploy(mersenneSite) 

1353 # delete article binary files 

1354 folder = article.get_relative_folder() 

1355 resolver.delete_object_folder( 

1356 folder, 

1357 to_folder=settings.MERSENNE_PROD_DATA_FORLDER, 

1358 ) 

1359 # delete issue binary files 

1360 folder = issue.get_relative_folder() 

1361 resolver.delete_object_folder( 

1362 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER 

1363 ) 

1364 

1365 except Timeout as exception: 

1366 return HttpResponse(exception, status=408) 

1367 except Exception as exception: 

1368 return HttpResponseServerError(exception) 

1369 

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

1371 return JsonResponse(data) 

1372 

1373 

1374class ArchiveIssueAPIView(View): 

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

1376 try: 

1377 pid = kwargs["pid"] 

1378 colid = kwargs["colid"] 

1379 except IndexError: 

1380 raise Http404 

1381 

1382 try: 

1383 cmd = ptf_cmds.archiveIssuePtfCmd( 

1384 { 

1385 "pid": pid, 

1386 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1387 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

1388 "needs_publication_date": True, 

1389 } 

1390 ) 

1391 result_, status, message = history_views.execute_and_record_func( 

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

1393 ) 

1394 except Exception as exception: 

1395 return HttpResponseServerError(exception) 

1396 

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

1398 return JsonResponse(data) 

1399 

1400 

1401class CreateDjvuAPIView(View): 

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

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

1404 

1405 resource = model_helpers.get_resource(pid) 

1406 cmd = ptf_cmds.addDjvuPtfCmd() 

1407 cmd.set_resource(resource) 

1408 cmd.do() 

1409 

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

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

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

1413 

1414 try: 

1415 _, status, message = history_views.execute_and_record_func( 

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

1417 ) 

1418 except Exception as exception: 

1419 return HttpResponseServerError(exception) 

1420 

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

1422 return JsonResponse(data) 

1423 

1424 

1425class PTFToolsHomeView(LoginRequiredMixin, View): 

1426 """ 

1427 Home Page. 

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

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

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

1431 - Comment moderator -> Comments dashboard 

1432 - Others -> 404 response 

1433 """ 

1434 

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

1436 # Staff or user with authorized collections 

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

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

1439 

1440 colids = get_authorized_collections(request.user) 

1441 is_mod = is_comment_moderator(request.user) 

1442 

1443 # The user has no rights 

1444 if not (colids or is_mod): 

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

1446 # Comment moderator only 

1447 elif not colids: 

1448 return HttpResponseRedirect(reverse("comment_list")) 

1449 

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

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

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

1453 

1454 # User with multiple authorized collections - Special home 

1455 context = {} 

1456 context["overview"] = True 

1457 

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

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

1460 

1461 # Comments summary 

1462 try: 

1463 error, comments_data = get_comments_for_home(request.user) 

1464 except AttributeError: 

1465 error, comments_data = True, {} 

1466 

1467 context["comment_server_ok"] = False 

1468 

1469 if not error: 

1470 context["comment_server_ok"] = True 

1471 if comments_data: 

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

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

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

1475 

1476 # TODO: Translations summary 

1477 context["translation_server_ok"] = False 

1478 

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

1480 context["collections"] = sorted( 

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

1482 ) 

1483 

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

1485 

1486 

1487class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin): 

1488 columns = 5 

1489 

1490 def get_common_context_data(self, **kwargs): 

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

1492 now = timezone.now() 

1493 curyear = now.year 

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

1495 

1496 context["collections"] = settings.MERSENNE_COLLECTIONS 

1497 context["containers_to_be_published"] = [] 

1498 context["last_col_events"] = [] 

1499 

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

1501 clockss_gap = get_gap(now, event) 

1502 

1503 context["years"] = years 

1504 context["clockss_gap"] = clockss_gap 

1505 

1506 return context 

1507 

1508 def calculate_articles_and_pages(self, pid, years): 

1509 data_by_year = [] 

1510 total_articles = [0] * len(years) 

1511 total_pages = [0] * len(years) 

1512 

1513 for year in years: 

1514 articles = self.get_articles_for_year(pid, year) 

1515 articles_count = articles.count() 

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

1517 

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

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

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

1521 

1522 return data_by_year, total_articles, total_pages 

1523 

1524 def get_articles_for_year(self, pid, year): 

1525 return Article.objects.filter( 

1526 Q(my_container__my_collection__pid=pid) 

1527 & ( 

1528 Q(date_published__year=year, date_online_first__isnull=True) 

1529 | Q(date_online_first__year=year) 

1530 ) 

1531 ).prefetch_related("resourcecount_set") 

1532 

1533 

1534class PublishedArticlesDashboardView(BaseMersenneDashboardView): 

1535 template_name = "dashboard/published_articles.html" 

1536 

1537 def get_context_data(self, **kwargs): 

1538 context = self.get_common_context_data(**kwargs) 

1539 years = context["years"] 

1540 

1541 published_articles = [] 

1542 total_published_articles = [ 

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

1544 ] 

1545 

1546 for pid in settings.MERSENNE_COLLECTIONS: 

1547 if pid != "MERSENNE": 

1548 articles_data, total_articles, total_pages = self.calculate_articles_and_pages( 

1549 pid, years 

1550 ) 

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

1552 

1553 for i, year in enumerate(years): 

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

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

1556 

1557 context["published_articles"] = published_articles 

1558 context["total_published_articles"] = total_published_articles 

1559 

1560 return context 

1561 

1562 

1563class CreatedVolumesDashboardView(BaseMersenneDashboardView): 

1564 template_name = "dashboard/created_volumes.html" 

1565 

1566 def get_context_data(self, **kwargs): 

1567 context = self.get_common_context_data(**kwargs) 

1568 years = context["years"] 

1569 

1570 created_volumes = [] 

1571 total_created_volumes = [ 

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

1573 ] 

1574 

1575 for pid in settings.MERSENNE_COLLECTIONS: 

1576 if pid != "MERSENNE": 

1577 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages( 

1578 pid, years 

1579 ) 

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

1581 

1582 for i, _ in enumerate(years): 

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

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

1585 

1586 context["created_volumes"] = created_volumes 

1587 context["total_created_volumes"] = total_created_volumes 

1588 

1589 return context 

1590 

1591 def calculate_volumes_and_pages(self, pid, years): 

1592 data_by_year = [] 

1593 total_articles = [0] * len(years) 

1594 total_pages = [0] * len(years) 

1595 

1596 for year in years: 

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

1598 articles_count = 0 

1599 page_count = 0 

1600 

1601 for issue in issues: 

1602 articles = issue.article_set.filter( 

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

1604 ).prefetch_related("resourcecount_set") 

1605 

1606 articles_count += articles.count() 

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

1608 

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

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

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

1612 

1613 return data_by_year, total_articles, total_pages 

1614 

1615 

1616class ReferencingChoice(View): 

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

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

1619 return redirect( 

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

1621 ) 

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

1623 comp = ReferencingCheckerWos() 

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

1625 if journal is None: 

1626 return render( 

1627 request, 

1628 "dashboard/referencing.html", 

1629 { 

1630 "error": "Collection not found", 

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

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

1633 }, 

1634 ) 

1635 return render( 

1636 request, 

1637 "dashboard/referencing.html", 

1638 { 

1639 "journal": journal, 

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

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

1642 }, 

1643 ) 

1644 

1645 

1646class ReferencingWosFileView(View): 

1647 template_name = "dashboard/referencing.html" 

1648 

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

1650 colid = request.POST["colid"] 

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

1652 message = "No file uploaded" 

1653 return render( 

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

1655 ) 

1656 uploaded_file = request.FILES["risfile"] 

1657 comp = ReferencingCheckerWos() 

1658 journal = comp.check_references(colid, uploaded_file) 

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

1660 

1661 

1662class ReferencingDashboardView(BaseMersenneDashboardView): 

1663 template_name = "dashboard/referencing.html" 

1664 

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

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

1667 comp = ReferencingCheckerAds() 

1668 journal = comp.check_references(colid) 

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

1670 

1671 

1672class BaseCollectionView(TemplateView): 

1673 def get_context_data(self, **kwargs): 

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

1675 aid = context.get("aid") 

1676 year = context.get("year") 

1677 

1678 if aid and year: 

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

1680 

1681 return context 

1682 

1683 def get_collection(self, aid, year): 

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

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

1686 

1687 

1688class ArticleListView(BaseCollectionView): 

1689 template_name = "collection-list.html" 

1690 

1691 def get_collection(self, aid, year): 

1692 return Article.objects.filter( 

1693 Q(my_container__my_collection__pid=aid) 

1694 & ( 

1695 Q(date_published__year=year, date_online_first__isnull=True) 

1696 | Q(date_online_first__year=year) 

1697 ) 

1698 ).prefetch_related("resourcecount_set") 

1699 

1700 

1701class VolumeListView(BaseCollectionView): 

1702 template_name = "collection-list.html" 

1703 

1704 def get_collection(self, aid, year): 

1705 return Article.objects.filter( 

1706 Q(my_container__my_collection__pid=aid, my_container__fyear=year) 

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

1708 ).prefetch_related("resourcecount_set") 

1709 

1710 

1711class DOAJResourceRegisterView(View): 

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

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

1714 resource = model_helpers.get_resource(pid) 

1715 if resource is None: 

1716 raise Http404 

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

1718 resource.colid, None 

1719 ): 

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

1721 

1722 try: 

1723 data = {} 

1724 doaj_meta, response = doaj_pid_register(pid) 

1725 if response is None: 

1726 return HttpResponse(status=204) 

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

1728 data.update(doaj_meta) 

1729 else: 

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

1731 except Timeout as exception: 

1732 return HttpResponse(exception, status=408) 

1733 except Exception as exception: 

1734 return HttpResponseServerError(exception) 

1735 return JsonResponse(data) 

1736 

1737 

1738class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

1739 """ 

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

1741 """ 

1742 

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

1744 pid = kwargs.get("pid") 

1745 if not pid: 

1746 raise Http404("Missing pid") 

1747 

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

1749 if not article: 

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

1751 

1752 colid = article.get_collection().pid 

1753 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS: 

1754 return JsonResponse( 

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

1756 ) 

1757 

1758 if is_tex_conversion_locked(pid): 

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

1760 return JsonResponse( 

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

1762 ) 

1763 

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

1765 

1766 try: 

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

1768 except Exception: 

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

1770 release_tex_conversion_lock(pid) 

1771 raise 

1772 

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

1774 

1775 

1776class CROSSREFResourceRegisterView(View): 

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

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

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

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

1781 if not request.user.is_superuser: 

1782 force = None 

1783 

1784 resource = model_helpers.get_resource(pid) 

1785 if resource is None: 

1786 raise Http404 

1787 

1788 resource = resource.cast() 

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

1790 try: 

1791 data = meth(resource, force) 

1792 except Timeout as exception: 

1793 return HttpResponse(exception, status=408) 

1794 except Exception as exception: 

1795 return HttpResponseServerError(exception) 

1796 return JsonResponse(data) 

1797 

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

1799 result = {"status": 404} 

1800 if ( 

1801 article.doi 

1802 and not article.do_not_publish 

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

1804 ): 

1805 if article.my_container.fyear == 0: 

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

1807 result = recordDOI(article) 

1808 return result 

1809 

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

1811 return recordDOI(collection) 

1812 

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

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

1815 

1816 if container.ctype == "issue": 

1817 if container.doi: 

1818 result = recordDOI(container) 

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

1820 return result 

1821 if force == "force": 

1822 articles = container.article_set.exclude( 

1823 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True 

1824 ) 

1825 else: 

1826 articles = container.article_set.exclude( 

1827 doi__isnull=True, 

1828 do_not_publish=True, 

1829 date_published__isnull=True, 

1830 date_online_first__isnull=True, 

1831 ) 

1832 

1833 for article in articles: 

1834 result = self.recordDOIArticle(article, force) 

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

1836 data = result 

1837 else: 

1838 return recordDOI(container) 

1839 return data 

1840 

1841 

1842class CROSSREFResourceCheckStatusView(View): 

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

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

1845 resource = model_helpers.get_resource(pid) 

1846 if resource is None: 

1847 raise Http404 

1848 resource = resource.cast() 

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

1850 try: 

1851 meth(resource) 

1852 except Timeout as exception: 

1853 return HttpResponse(exception, status=408) 

1854 except Exception as exception: 

1855 return HttpResponseServerError(exception) 

1856 

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

1858 return JsonResponse(data) 

1859 

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

1861 if article.my_container.fyear == 0: 

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

1863 checkDOI(article) 

1864 

1865 def checkDOICollection(self, collection): 

1866 checkDOI(collection) 

1867 

1868 def checkDOIContainer(self, container): 

1869 if container.doi is not None: 

1870 checkDOI(container) 

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

1872 self.checkDOIArticle(article) 

1873 

1874 

1875class CROSSREFResourcePendingPublicationRegisterView(View): 

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

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

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

1879 

1880 resource = model_helpers.get_resource(pid) 

1881 if resource is None: 

1882 raise Http404 

1883 

1884 resource = resource.cast() 

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

1886 try: 

1887 data = meth(resource) 

1888 except Timeout as exception: 

1889 return HttpResponse(exception, status=408) 

1890 except Exception as exception: 

1891 return HttpResponseServerError(exception) 

1892 return JsonResponse(data) 

1893 

1894 def recordPendingPublicationArticle(self, article): 

1895 result = {"status": 404} 

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

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

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

1899 result = recordPendingPublication(article) 

1900 return result 

1901 

1902 

1903class RegisterPubmedFormView(FormView): 

1904 template_name = "record_pubmed_dialog.html" 

1905 form_class = RegisterPubmedForm 

1906 

1907 def get_context_data(self, **kwargs): 

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

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

1910 context["helper"] = PtfLargeModalFormHelper 

1911 return context 

1912 

1913 

1914class RegisterPubmedView(View): 

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

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

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

1918 

1919 article = model_helpers.get_article(pid) 

1920 if article is None: 

1921 raise Http404 

1922 try: 

1923 recordPubmed(article, update_article) 

1924 except Exception as exception: 

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

1926 return HttpResponseServerError(exception) 

1927 

1928 return HttpResponseRedirect( 

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

1930 ) 

1931 

1932 

1933class PTFToolsContainerView(TemplateView): 

1934 template_name = "" 

1935 

1936 def get_context_data(self, **kwargs): 

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

1938 

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

1940 if container is None: 

1941 raise Http404 

1942 citing_articles = container.citations() 

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

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

1945 book_parts = ( 

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

1947 ) 

1948 references = False 

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

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

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

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

1953 references = True 

1954 context.update( 

1955 { 

1956 "book": container, 

1957 "book_parts": list(book_parts), 

1958 "source": source, 

1959 "citing_articles": citing_articles, 

1960 "references": references, 

1961 "test_website": container.get_top_collection() 

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

1963 .location, 

1964 "prod_website": container.get_top_collection() 

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

1966 .location, 

1967 } 

1968 ) 

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

1970 else: 

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

1972 for article in articles: 

1973 try: 

1974 last_match = ( 

1975 history_models.HistoryEvent.objects.filter( 

1976 pid=article.pid, 

1977 type="matching", 

1978 ) 

1979 .only("created_on") 

1980 .latest("created_on") 

1981 ) 

1982 except history_models.HistoryEvent.DoesNotExist as _: 

1983 article.last_match = None 

1984 else: 

1985 article.last_match = last_match.created_on 

1986 

1987 # article1 = articles.first() 

1988 # date = article1.deployed_date() 

1989 # TODO next_issue, previous_issue 

1990 

1991 # check DOI est maintenant une commande à part 

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

1993 # articlesWithStatus = [] 

1994 # for article in articles: 

1995 # checkDOIExistence(article) 

1996 # articlesWithStatus.append(article) 

1997 

1998 test_location = prod_location = "" 

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

2000 if qs: 

2001 test_location = qs.first().location 

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

2003 if qs: 

2004 prod_location = qs.first().location 

2005 context.update( 

2006 { 

2007 "issue": container, 

2008 "articles": articles, 

2009 "source": source, 

2010 "citing_articles": citing_articles, 

2011 "test_website": test_location, 

2012 "prod_website": prod_location, 

2013 } 

2014 ) 

2015 

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

2017 context["is_issue_pending_publication"] = True 

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

2019 context["is_excluded_from_tex_conversion"] = True 

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

2021 

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

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

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

2025 return context 

2026 

2027 

2028class ExtLinkInline(InlineFormSetFactory): 

2029 model = ExtLink 

2030 form_class = ExtLinkForm 

2031 factory_kwargs = {"extra": 0} 

2032 

2033 

2034class ResourceIdInline(InlineFormSetFactory): 

2035 model = ResourceId 

2036 form_class = ResourceIdForm 

2037 factory_kwargs = {"extra": 0} 

2038 

2039 

2040class IssueDetailAPIView(View): 

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

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

2043 deployed_date = issue.deployed_date() 

2044 result = { 

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

2046 if deployed_date 

2047 else None, 

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

2049 "all_doi_are_registered": issue.all_doi_are_registered(), 

2050 "registered_in_doaj": issue.registered_in_doaj(), 

2051 "doi": issue.my_collection.doi, 

2052 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(), 

2053 } 

2054 try: 

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

2056 except history_models.HistoryEvent.DoesNotExist as _: 

2057 pass 

2058 else: 

2059 result["latest"] = latest.message 

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

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

2062 ) 

2063 

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

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

2066 try: 

2067 result[event_type] = timezone.localtime( 

2068 history_models.HistoryEvent.objects.filter( 

2069 type=event_type, 

2070 status="OK", 

2071 pid__startswith=issue.pid, 

2072 ) 

2073 .latest("created_on") 

2074 .created_on 

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

2076 except history_models.HistoryEvent.DoesNotExist as _: 

2077 result[event_type] = "" 

2078 return JsonResponse(result) 

2079 

2080 

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

2082 model = Collection 

2083 form_class = CollectionForm 

2084 inlines = [ResourceIdInline, ExtLinkInline] 

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

2086 

2087 def get_context_data(self, **kwargs): 

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

2089 context["helper"] = PtfFormHelper 

2090 context["formset_helper"] = FormSetHelper 

2091 return context 

2092 

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

2094 if description: 

2095 la = Abstract( 

2096 resource=collection, 

2097 tag="description", 

2098 lang=lang, 

2099 seq=seq, 

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

2101 value_html=description, 

2102 value_tex=description, 

2103 ) 

2104 la.save() 

2105 

2106 def form_valid(self, form): 

2107 if form.instance.abbrev: 

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

2109 else: 

2110 form.instance.title_xml = ( 

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

2112 ) 

2113 

2114 form.instance.title_html = form.instance.title_tex 

2115 form.instance.title_sort = form.instance.title_tex 

2116 result = super().form_valid(form) 

2117 

2118 collection = self.object 

2119 collection.abstract_set.all().delete() 

2120 

2121 seq = 1 

2122 description = form.cleaned_data["description_en"] 

2123 if description: 

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

2125 seq += 1 

2126 description = form.cleaned_data["description_fr"] 

2127 if description: 

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

2129 

2130 return result 

2131 

2132 def get_success_url(self): 

2133 messages.success( 

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

2135 ) 

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

2137 

2138 

2139class CollectionCreate(CollectionFormView, CreateWithInlinesView): 

2140 """ 

2141 Warning : Not yet finished 

2142 Automatic site membership creation is still missing 

2143 """ 

2144 

2145 

2146class CollectionUpdate(CollectionFormView, UpdateWithInlinesView): 

2147 slug_field = "pid" 

2148 slug_url_kwarg = "pid" 

2149 

2150 

2151def suggest_load_journal_dois(colid): 

2152 articles = ( 

2153 Article.objects.filter(my_container__my_collection__pid=colid) 

2154 .filter(doi__isnull=False) 

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

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

2157 ) 

2158 

2159 try: 

2160 articles = sorted( 

2161 articles, 

2162 key=lambda d: ( 

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

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

2165 ), 

2166 ) 

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

2168 pass 

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

2170 

2171 

2172def get_context_with_volumes(journal): 

2173 result = model_helpers.get_volumes_in_collection(journal) 

2174 volume_count = result["volume_count"] 

2175 collections = [] 

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

2177 item = model_helpers.get_volumes_in_collection(ancestor) 

2178 volume_count = max(0, volume_count) 

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

2180 collections.append(item) 

2181 

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

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

2184 collections.append(result) 

2185 

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

2187 collections.sort( 

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

2189 reverse=True, 

2190 ) 

2191 

2192 context = { 

2193 "journal": journal, 

2194 "sorted_issues": result["sorted_issues"], 

2195 "volume_count": volume_count, 

2196 "max_width": result["max_width"], 

2197 "collections": collections, 

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

2199 } 

2200 return context 

2201 

2202 

2203class CollectionDetail( 

2204 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin 

2205): 

2206 model = Collection 

2207 slug_field = "pid" 

2208 slug_url_kwarg = "pid" 

2209 template_name = "ptf/collection_detail.html" 

2210 

2211 def test_func(self): 

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

2213 

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

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

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

2217 

2218 def get_context_data(self, **kwargs): 

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

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

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

2222 ) 

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

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

2225 

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

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

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

2229 pid=context["issue_to_appear_pid"] 

2230 ).exists() 

2231 try: 

2232 latest_error = history_models.HistoryEvent.objects.filter( 

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

2234 ).latest("created_on") 

2235 except history_models.HistoryEvent.DoesNotExist as _: 

2236 pass 

2237 else: 

2238 message = latest_error.message 

2239 if message: 

2240 i = message.find(" - ") 

2241 latest_exception = message[:i] 

2242 latest_error_message = message[i + 3 :] 

2243 context["latest_exception"] = latest_exception 

2244 context["latest_exception_date"] = latest_error.created_on 

2245 context["latest_exception_type"] = latest_error.type 

2246 context["latest_error_message"] = latest_error_message 

2247 

2248 archive_in_error = history_models.HistoryEvent.objects.filter( 

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

2250 ).exists() 

2251 

2252 context["archive_in_error"] = archive_in_error 

2253 

2254 return context 

2255 

2256 def get_queryset(self): 

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

2258 

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

2260 query |= ancestor.content.all() 

2261 

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

2263 

2264 

2265class ContainerEditView(FormView): 

2266 template_name = "container_form.html" 

2267 form_class = ContainerForm 

2268 

2269 def get_success_url(self): 

2270 if self.kwargs["pid"]: 

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

2272 return reverse("mersenne_dashboard/published_articles") 

2273 

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

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

2276 

2277 def get_form_kwargs(self): 

2278 kwargs = super().get_form_kwargs() 

2279 if "pid" not in self.kwargs: 

2280 self.kwargs["pid"] = None 

2281 if "colid" not in self.kwargs: 

2282 self.kwargs["colid"] = None 

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

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

2285 # It is used when you submit a new container 

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

2287 

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

2289 self.kwargs["pid"] 

2290 ) 

2291 return kwargs 

2292 

2293 def get_context_data(self, **kwargs): 

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

2295 

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

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

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

2299 

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

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

2302 

2303 return context 

2304 

2305 def form_valid(self, form): 

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

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

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

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

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

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

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

2313 

2314 collection = None 

2315 issue = self.kwargs["container"] 

2316 if issue is not None: 

2317 collection = issue.my_collection 

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

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

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

2321 else: 

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

2323 

2324 if collection is None: 

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

2326 

2327 # Icon 

2328 new_icon_location = "" 

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

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

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

2332 

2333 icon_filename = resolver.get_disk_location( 

2334 settings.MERSENNE_TEST_DATA_FOLDER, 

2335 collection.pid, 

2336 file_extension, 

2337 new_pid, 

2338 None, 

2339 True, 

2340 ) 

2341 

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

2343 for chunk in self.request.FILES["icon"].chunks(): 

2344 destination.write(chunk) 

2345 

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

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

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

2349 if name == "special_issue_create": 

2350 self.kwargs["name"] = name 

2351 if self.kwargs["container"]: 

2352 # Edit Issue 

2353 issue = self.kwargs["container"] 

2354 if issue is None: 

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

2356 

2357 issue.pid = new_pid 

2358 issue.title_tex = issue.title_html = new_title 

2359 issue.title_xml = build_title_xml( 

2360 title=new_title, 

2361 lang=issue.lang, 

2362 title_type="issue-title", 

2363 ) 

2364 

2365 trans_lang = "" 

2366 if new_trans_title != "": 

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

2368 

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

2370 title_xml = build_title_xml( 

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

2372 ) 

2373 

2374 issue.title_set.update_or_create( 

2375 lang=trans_lang, 

2376 type="main", 

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

2378 ) 

2379 

2380 issue.fyear = new_year 

2381 issue.volume = new_volume 

2382 issue.volume_int = make_int(new_volume) 

2383 issue.number = new_number 

2384 issue.number_int = make_int(new_number) 

2385 issue.save() 

2386 else: 

2387 xissue = create_issuedata() 

2388 

2389 xissue.ctype = "issue" 

2390 xissue.pid = new_pid 

2391 xissue.lang = "en" 

2392 xissue.title_tex = new_title 

2393 xissue.title_html = new_title 

2394 xissue.title_xml = build_title_xml( 

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

2396 ) 

2397 

2398 if new_trans_title != "": 

2399 trans_lang = "fr" 

2400 title_xml = build_title_xml( 

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

2402 ) 

2403 title = create_titledata( 

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

2405 ) 

2406 issue.titles = [title] 

2407 

2408 xissue.fyear = new_year 

2409 xissue.volume = new_volume 

2410 xissue.number = new_number 

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

2412 

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

2414 cmd.add_collection(collection) 

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

2416 issue = cmd.do() 

2417 

2418 self.kwargs["pid"] = new_pid 

2419 

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

2421 params = { 

2422 "icon_location": new_icon_location, 

2423 } 

2424 cmd = ptf_cmds.updateContainerPtfCmd(params) 

2425 cmd.set_resource(issue) 

2426 cmd.do() 

2427 

2428 publisher = model_helpers.get_publisher(new_publisher) 

2429 if not publisher: 

2430 xpub = create_publisherdata() 

2431 xpub.name = new_publisher 

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

2433 issue.my_publisher = publisher 

2434 issue.save() 

2435 

2436 self.set_success_message() 

2437 

2438 return super().form_valid(form) 

2439 

2440 

2441# class ArticleEditView(FormView): 

2442# template_name = 'article_form.html' 

2443# form_class = ArticleForm 

2444# 

2445# def get_success_url(self): 

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

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

2448# return reverse('mersenne_dashboard/published_articles') 

2449# 

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

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

2452# 

2453# def get_form_kwargs(self): 

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

2455# 

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

2457# # Article creation: pid is None 

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

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

2460# # Article edit: issue_id is not passed 

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

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

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

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

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

2466# 

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

2468# return kwargs 

2469# 

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

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

2472# 

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

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

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

2476# 

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

2478# 

2479# article = context['article'] 

2480# if article: 

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

2482# context['kwds_fr'] = None 

2483# context['kwds_en'] = None 

2484# kwd_gps = article.get_non_msc_kwds() 

2485# for kwd_gp in kwd_gps: 

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

2487# if kwd_gp.value_xml: 

2488# kwd_ = types.SimpleNamespace() 

2489# kwd_.value = kwd_gp.value_tex 

2490# context['kwd_unstructured_fr'] = kwd_ 

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

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

2493# if kwd_gp.value_xml: 

2494# kwd_ = types.SimpleNamespace() 

2495# kwd_.value = kwd_gp.value_tex 

2496# context['kwd_unstructured_en'] = kwd_ 

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

2498# 

2499# # Article creation: init pid 

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

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

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

2503# 

2504# return context 

2505# 

2506# def form_valid(self, form): 

2507# 

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

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

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

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

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

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

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

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

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

2517# 

2518# # TODO support MathML 

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

2520# # We need to pass trans_title to get_title_xml 

2521# # Meanwhile, ignore new_title_xml 

2522# new_title_xml = jats_parser.get_title_xml(new_title) 

2523# new_title_html = new_title 

2524# 

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

2526# i = 1 

2527# new_authors = [] 

2528# old_author_contributions = [] 

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

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

2531# 

2532# while authors_count > 0: 

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

2534# 

2535# if prefix is not None: 

2536# addresses = [] 

2537# if len(old_author_contributions) >= i: 

2538# old_author_contribution = old_author_contributions[i - 1] 

2539# addresses = [contrib_address.address for contrib_address in 

2540# old_author_contribution.get_addresses()] 

2541# 

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

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

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

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

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

2547# deceased_before_publication = deceased == 'on' 

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

2549# equal_contrib = equal_contrib == 'on' 

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

2551# corresponding = corresponding == 'on' 

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

2553# 

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

2555# params['deceased_before_publication'] = deceased_before_publication 

2556# params['equal_contrib'] = equal_contrib 

2557# params['corresponding'] = corresponding 

2558# params['addresses'] = addresses 

2559# params['email'] = email 

2560# 

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

2562# 

2563# new_authors.append(params) 

2564# 

2565# authors_count -= 1 

2566# i += 1 

2567# 

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

2569# i = 1 

2570# new_kwds_fr = [] 

2571# while kwds_fr_count > 0: 

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

2573# new_kwds_fr.append(value) 

2574# kwds_fr_count -= 1 

2575# i += 1 

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

2577# 

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

2579# i = 1 

2580# new_kwds_en = [] 

2581# while kwds_en_count > 0: 

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

2583# new_kwds_en.append(value) 

2584# kwds_en_count -= 1 

2585# i += 1 

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

2587# 

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

2589# # Edit article 

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

2591# else: 

2592# # New article 

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

2594# 

2595# if container is None: 

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

2597# 

2598# collection = container.my_collection 

2599# 

2600# # Copy PDF file & extract full text 

2601# body = '' 

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

2603# collection.pid, 

2604# "pdf", 

2605# container.pid, 

2606# new_pid, 

2607# True) 

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

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

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

2611# destination.write(chunk) 

2612# 

2613# # Extract full text from the PDF 

2614# body = utils.pdf_to_text(pdf_filename) 

2615# 

2616# # Icon 

2617# new_icon_location = '' 

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

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

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

2621# 

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

2623# collection.pid, 

2624# file_extension, 

2625# container.pid, 

2626# new_pid, 

2627# True) 

2628# 

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

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

2631# destination.write(chunk) 

2632# 

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

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

2635# 

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

2637# # Edit article 

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

2639# article.fpage = new_fpage 

2640# article.lpage = new_lpage 

2641# article.page_range = new_page_range 

2642# article.coi_statement = new_coi_statement 

2643# article.show_body = new_show_body 

2644# article.do_not_publish = new_do_not_publish 

2645# article.save() 

2646# 

2647# else: 

2648# # New article 

2649# params = { 

2650# 'pid': new_pid, 

2651# 'title_xml': new_title_xml, 

2652# 'title_html': new_title_html, 

2653# 'title_tex': new_title, 

2654# 'fpage': new_fpage, 

2655# 'lpage': new_lpage, 

2656# 'page_range': new_page_range, 

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

2658# 'body': body, 

2659# 'coi_statement': new_coi_statement, 

2660# 'show_body': new_show_body, 

2661# 'do_not_publish': new_do_not_publish 

2662# } 

2663# 

2664# xarticle = create_articledata() 

2665# xarticle.pid = new_pid 

2666# xarticle.title_xml = new_title_xml 

2667# xarticle.title_html = new_title_html 

2668# xarticle.title_tex = new_title 

2669# xarticle.fpage = new_fpage 

2670# xarticle.lpage = new_lpage 

2671# xarticle.page_range = new_page_range 

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

2673# xarticle.body = body 

2674# xarticle.coi_statement = new_coi_statement 

2675# params['xobj'] = xarticle 

2676# 

2677# cmd = ptf_cmds.addArticlePtfCmd(params) 

2678# cmd.set_container(container) 

2679# cmd.add_collection(container.my_collection) 

2680# article = cmd.do() 

2681# 

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

2683# 

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

2685# params = { 

2686# # 'title_xml': new_title_xml, 

2687# # 'title_html': new_title_html, 

2688# # 'title_tex': new_title, 

2689# 'authors': new_authors, 

2690# 'page_count': new_page_count, 

2691# 'icon_location': new_icon_location, 

2692# 'body': body, 

2693# 'use_kwds': True, 

2694# 'kwds_fr': new_kwds_fr, 

2695# 'kwds_en': new_kwds_en, 

2696# 'kwd_uns_fr': new_kwd_uns_fr, 

2697# 'kwd_uns_en': new_kwd_uns_en 

2698# } 

2699# cmd = ptf_cmds.updateArticlePtfCmd(params) 

2700# cmd.set_article(article) 

2701# cmd.do() 

2702# 

2703# self.set_success_message() 

2704# 

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

2706 

2707 

2708@require_http_methods(["POST"]) 

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

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

2711 

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

2713 

2714 article = model_helpers.get_article(pid) 

2715 if article: 

2716 article.do_not_publish = not article.do_not_publish 

2717 article.save() 

2718 else: 

2719 raise Http404 

2720 

2721 return HttpResponseRedirect(next) 

2722 

2723 

2724@require_http_methods(["POST"]) 

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

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

2727 

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

2729 

2730 article = model_helpers.get_article(pid) 

2731 if article: 

2732 article.show_body = not article.show_body 

2733 article.save() 

2734 else: 

2735 raise Http404 

2736 

2737 return HttpResponseRedirect(next) 

2738 

2739 

2740class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView): 

2741 """ 

2742 API to get/post article metadata 

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

2744 """ 

2745 

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

2747 """ 

2748 we define here what fields we want in the form 

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

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

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

2752 self.fields_to_update = [ 

2753 "lang", 

2754 "atype", 

2755 "contributors", 

2756 "abstracts", 

2757 "kwds", 

2758 "titles", 

2759 "title_html", 

2760 "title_xml", 

2761 "title_tex", 

2762 "streams", 

2763 "ext_links", 

2764 "date_accepted", 

2765 "history_dates", 

2766 "subjs", 

2767 "bibitems", 

2768 "references", 

2769 ] 

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

2771 self.additional_fields = [ 

2772 "doi", 

2773 "pid", 

2774 "container_pid", 

2775 "pdf", 

2776 "illustration", 

2777 "dates", 

2778 "msc_keywords", 

2779 ] 

2780 self.editorial_tools = [ 

2781 "translation", 

2782 "sidebar", 

2783 "lang_selection", 

2784 "back_to_article_option", 

2785 "msc_keywords", 

2786 ] 

2787 self.article_container_pid = "" 

2788 self.back_url = "trammel" 

2789 

2790 def save_data(self, data_article): 

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

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

2793 params = { 

2794 "pid": data_article.pid, 

2795 "export_folder": settings.MERSENNE_TMP_FOLDER, 

2796 "export_all": True, 

2797 "with_binary_files": False, 

2798 } 

2799 ptf_cmds.exportExtraDataPtfCmd(params).do() 

2800 

2801 def restore_data(self, article): 

2802 ptf_cmds.importExtraDataPtfCmd( 

2803 { 

2804 "pid": article.pid, 

2805 "import_folder": settings.MERSENNE_TMP_FOLDER, 

2806 "import_bibitemid": False, 

2807 } 

2808 ).do() 

2809 

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

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

2812 if user_role: 

2813 new_fields_to_update = [] 

2814 new_additional_fields = [] 

2815 new_editorial_tools = [] 

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

2817 for role in user_role: 

2818 role_fields_to_updates, role_additional_fields = role.get_real_fields_to_update() 

2819 new_fields_to_update += role_fields_to_updates 

2820 new_additional_fields += role_additional_fields 

2821 new_editorial_tools += role.editorial_tools 

2822 # in case two roles have some fields in common 

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

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

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

2826 

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

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

2829 return response 

2830 

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

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

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

2834 return redirect( 

2835 "api-edit-article", 

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

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

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

2839 ) 

2840 else: 

2841 raise Http404 

2842 

2843 

2844class ArticleEditWithVueView(LoginRequiredMixin, TemplateView): 

2845 template_name = "article_form.html" 

2846 

2847 def get_success_url(self): 

2848 if self.kwargs["doi"]: 

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

2850 return reverse("mersenne_dashboard/published_articles") 

2851 

2852 def get_context_data(self, **kwargs): 

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

2854 if "doi" in self.kwargs: 

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

2856 context["article"] = article 

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

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

2859 

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

2861 return context 

2862 

2863 

2864class ArticleDeleteView(View): 

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

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

2867 article = get_object_or_404(Article, pid=pid) 

2868 

2869 try: 

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

2871 article.undeploy(mersenneSite) 

2872 

2873 cmd = ptf_cmds.addArticlePtfCmd( 

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

2875 ) 

2876 cmd.set_container(article.my_container) 

2877 cmd.set_object_to_be_deleted(article) 

2878 cmd.undo() 

2879 except Exception as exception: 

2880 return HttpResponseServerError(exception) 

2881 

2882 data = {"message": "Article successfully removed from Trammel", "status": 200} 

2883 return JsonResponse(data) 

2884 

2885 

2886def get_messages_in_queue(): 

2887 app = Celery("ptf-tools") 

2888 # tasks = list(current_app.tasks) 

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

2890 print(tasks) 

2891 # i = app.control.inspect() 

2892 

2893 with app.connection_or_acquire() as conn: 

2894 remaining = conn.default_channel.queue_declare( 

2895 queue="coordinator", passive=True 

2896 ).message_count 

2897 return remaining 

2898 

2899 

2900class NumdamView(TemplateView, history_views.HistoryContextMixin): 

2901 template_name = "numdam.html" 

2902 

2903 def get_context_data(self, **kwargs): 

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

2905 

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

2907 

2908 pre_issues = [] 

2909 prod_issues = [] 

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

2911 try: 

2912 response = requests.get(url) 

2913 if response.status_code == 200: 

2914 data = response.json() 

2915 if "issues" in data: 

2916 pre_issues = data["issues"] 

2917 except Exception: 

2918 pass 

2919 

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

2921 response = requests.get(url) 

2922 if response.status_code == 200: 

2923 data = response.json() 

2924 if "issues" in data: 

2925 prod_issues = data["issues"] 

2926 

2927 new = sorted(list(set(pre_issues).difference(prod_issues))) 

2928 removed = sorted(list(set(prod_issues).difference(pre_issues))) 

2929 grouped = [ 

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

2931 ] 

2932 grouped_removed = [ 

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

2934 ] 

2935 context["added_issues"] = grouped 

2936 context["removed_issues"] = grouped_removed 

2937 

2938 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS 

2939 return context 

2940 

2941 

2942class NumdamArchiveView(RedirectView): 

2943 @staticmethod 

2944 def reset_task_results(): 

2945 TaskResult.objects.all().delete() 

2946 

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

2948 self.colid = kwargs["colid"] 

2949 

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

2951 return Http404 

2952 

2953 # we make sure archiving is not already running 

2954 # if not get_messages_in_queue(): 

2955 # self.reset_task_results() 

2956 

2957 if self.colid == "ALL": 

2958 archive_numdam_collections.delay() 

2959 else: 

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

2961 

2962 return reverse("numdam") 

2963 

2964 

2965class DeployAllNumdamAPIView(View): 

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

2967 pids = [] 

2968 

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

2970 pids.append(obj.pid) 

2971 

2972 return pids 

2973 

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

2975 try: 

2976 pids, status, message = history_views.execute_and_record_func( 

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

2978 ) 

2979 except Exception as exception: 

2980 return HttpResponseServerError(exception) 

2981 

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

2983 return JsonResponse(data) 

2984 

2985 

2986class NumdamDeleteAPIView(View): 

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

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

2989 

2990 try: 

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

2992 obj.delete() 

2993 except Exception as exception: 

2994 return HttpResponseServerError(exception) 

2995 

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

2997 return JsonResponse(data) 

2998 

2999 

3000class ExtIdApiDetail(View): 

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

3002 extid = get_object_or_404( 

3003 ExtId, 

3004 resource__pid=kwargs["pid"], 

3005 id_type=kwargs["what"], 

3006 ) 

3007 return JsonResponse( 

3008 { 

3009 "pk": extid.pk, 

3010 "href": extid.get_href(), 

3011 "fetch": reverse( 

3012 "api-fetch-id", 

3013 args=( 

3014 extid.resource.pk, 

3015 extid.id_value, 

3016 extid.id_type, 

3017 "extid", 

3018 ), 

3019 ), 

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

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

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

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

3024 "is_valid": extid.checked, 

3025 } 

3026 ) 

3027 

3028 

3029class ExtIdFormTemplate(TemplateView): 

3030 template_name = "common/externalid_form.html" 

3031 

3032 def get_context_data(self, **kwargs): 

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

3034 context["sequence"] = kwargs["sequence"] 

3035 return context 

3036 

3037 

3038class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

3039 def get_context_data(self, **kwargs): 

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

3041 context["helper"] = PtfFormHelper 

3042 return context 

3043 

3044 def get_success_url(self): 

3045 self.post_process() 

3046 return self.object.resource.get_absolute_url() 

3047 

3048 def post_process(self): 

3049 model_helpers.post_resource_updated(self.object.resource) 

3050 

3051 

3052class ExtIdCreate(ExtIdFormView, CreateView): 

3053 model = ExtId 

3054 form_class = ExtIdForm 

3055 

3056 def get_context_data(self, **kwargs): 

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

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

3059 return context 

3060 

3061 def get_initial(self): 

3062 initial = super().get_initial() 

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

3064 return initial 

3065 

3066 def form_valid(self, form): 

3067 form.instance.checked = False 

3068 return super().form_valid(form) 

3069 

3070 

3071class ExtIdUpdate(ExtIdFormView, UpdateView): 

3072 model = ExtId 

3073 form_class = ExtIdForm 

3074 

3075 def get_context_data(self, **kwargs): 

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

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

3078 return context 

3079 

3080 

3081class UpdateTexmfZipAPIView(View): 

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

3083 def copy_zip_files(src_folder, dest_folder): 

3084 os.makedirs(dest_folder, exist_ok=True) 

3085 

3086 zip_files = [ 

3087 os.path.join(src_folder, f) 

3088 for f in os.listdir(src_folder) 

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

3090 ] 

3091 for zip_file in zip_files: 

3092 resolver.copy_file(zip_file, dest_folder) 

3093 

3094 # Exceptions: specific zip/gz files 

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

3096 resolver.copy_file(zip_file, dest_folder) 

3097 

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

3099 resolver.copy_file(zip_file, dest_folder) 

3100 

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

3102 resolver.copy_file(gz_file, dest_folder) 

3103 

3104 src_folder = settings.CEDRAM_DISTRIB_FOLDER 

3105 

3106 dest_folder = os.path.join( 

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

3108 ) 

3109 

3110 try: 

3111 copy_zip_files(src_folder, dest_folder) 

3112 except Exception as exception: 

3113 return HttpResponseServerError(exception) 

3114 

3115 try: 

3116 dest_folder = os.path.join( 

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

3118 ) 

3119 copy_zip_files(src_folder, dest_folder) 

3120 except Exception as exception: 

3121 return HttpResponseServerError(exception) 

3122 

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

3124 return JsonResponse(data) 

3125 

3126 

3127class TrammelTasksProgressView(View): 

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

3129 """ 

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

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

3132 """ 

3133 task_name = task 

3134 

3135 def get_event_data(): 

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

3137 

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

3139 remaining_messages = get_messages_in_queue() 

3140 

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

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

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

3144 

3145 all_tasks_count = all_tasks.count() 

3146 success_count = successed_tasks.count() 

3147 fail_count = failed_tasks.count() 

3148 

3149 all_count = all_tasks_count + remaining_messages 

3150 remaining_count = all_count - success_count - fail_count 

3151 

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

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

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

3155 

3156 last_task = successed_tasks.first() 

3157 last_task = ( 

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

3159 if last_task 

3160 else "" 

3161 ) 

3162 

3163 # SSE event format 

3164 event_data = { 

3165 "status": status, 

3166 "success_rate": success_rate, 

3167 "error_rate": error_rate, 

3168 "all_count": all_count, 

3169 "remaining_count": remaining_count, 

3170 "success_count": success_count, 

3171 "fail_count": fail_count, 

3172 "last_task": last_task, 

3173 } 

3174 

3175 return event_data 

3176 

3177 def stream_response(data): 

3178 # Send initial response headers 

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

3180 

3181 data = get_event_data() 

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

3183 if format == "json": 

3184 response = JsonResponse(data) 

3185 else: 

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

3187 return response 

3188 

3189 

3190user_signed_up.connect(update_user_from_invite)