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

1624 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-23 12:08 +0000

1import io 

2import json 

3import os 

4import re 

5from datetime import datetime 

6from itertools import groupby 

7 

8import jsonpickle 

9import requests 

10from allauth.account.signals import user_signed_up 

11from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin 

12from celery import Celery, current_app 

13from django.conf import settings 

14from django.contrib import messages 

15from django.contrib.auth.mixins import UserPassesTestMixin 

16from django.db.models import Q 

17from django.http import ( 

18 Http404, 

19 HttpRequest, 

20 HttpResponse, 

21 HttpResponseRedirect, 

22 HttpResponseServerError, 

23 JsonResponse, 

24) 

25from django.shortcuts import get_object_or_404, redirect, render 

26from django.urls import resolve, reverse, reverse_lazy 

27from django.utils import timezone 

28from django.views.decorators.http import require_http_methods 

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

30from django.views.generic.base import RedirectView 

31from django.views.generic.detail import SingleObjectMixin 

32from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView 

33from django_celery_results.models import TaskResult 

34from extra_views import ( 

35 CreateWithInlinesView, 

36 InlineFormSetFactory, 

37 NamedFormsetsMixin, 

38 UpdateWithInlinesView, 

39) 

40from ptf import model_data_converter, model_helpers, tex, utils 

41from ptf.cmds import ptf_cmds, xml_cmds 

42from ptf.cmds.base_cmds import make_int 

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

44from ptf.cmds.xml.xml_utils import replace_html_entities 

45from ptf.display import resolver 

46from ptf.exceptions import DOIException, PDFException, ServerUnderMaintenance 

47from ptf.model_data import create_issuedata, create_publisherdata, create_titledata 

48from ptf.models import ( 

49 Abstract, 

50 Article, 

51 BibItem, 

52 BibItemId, 

53 Collection, 

54 Container, 

55 ExtId, 

56 ExtLink, 

57 Resource, 

58 ResourceId, 

59 Title, 

60) 

61from ptf.views import ArticleEditFormWithVueAPIView 

62from pubmed.views import recordPubmed 

63from requests import Timeout 

64from task.runner import run_task 

65from task.tasks.archiving_tasks import ArchiveResourceTask 

66 

67from comments_moderation.utils import get_comments_for_home, is_comment_moderator 

68from history import models as history_models 

69from history import views as history_views 

70from history.utils import get_gap, get_history_last_event_by, get_last_unsolved_error 

71from ptf_tools.doaj import doaj_pid_register 

72from ptf_tools.doi import checkDOI, recordDOI 

73from ptf_tools.forms import ( 

74 BibItemIdForm, 

75 CollectionForm, 

76 ContainerForm, 

77 DiffContainerForm, 

78 ExtIdForm, 

79 ExtLinkForm, 

80 FormSetHelper, 

81 ImportArticleForm, 

82 ImportContainerForm, 

83 PtfFormHelper, 

84 PtfLargeModalFormHelper, 

85 PtfModalFormHelper, 

86 RegisterPubmedForm, 

87 ResourceIdForm, 

88 get_article_choices, 

89) 

90from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos 

91from ptf_tools.models import ResourceInNumdam 

92from ptf_tools.signals import update_user_from_invite 

93from ptf_tools.tasks import ( 

94 ArchiveNumdamCollectionTask, 

95 ArchiveNumdamIssueTask, 

96) 

97from ptf_tools.templatetags.tools_helpers import get_authorized_collections 

98from ptf_tools.utils import is_authorized_editor 

99 

100 

101def view_404(request: HttpRequest): 

102 """ 

103 Dummy view raising HTTP 404 exception. 

104 """ 

105 raise Http404 

106 

107 

108def check_collection(collection, server_url, server_type): 

109 """ 

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

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

112 """ 

113 

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

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

116 # First, upload the collection XML 

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

118 body = xml.encode("utf8") 

119 

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

121 if response.status_code == 200: 

122 # PUT http verb is used for update 

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

124 else: 

125 # POST http verb is used for creation 

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

127 

128 # Second, copy the collection images 

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

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

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

132 # /mersenne_prod_data during an upload to prod 

133 if server_type == "website": 

134 resolver.copy_binary_files( 

135 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

136 ) 

137 elif server_type == "numdam": 

138 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

139 if collection.pid in settings.NUMDAM_COLLECTIONS: 

140 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

141 

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

143 

144 

145def check_lock(): 

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

147 

148 

149def load_cedrics_article_choices(request): 

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

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

152 article_choices = get_article_choices(colid, issue) 

153 return render( 

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

155 ) 

156 

157 

158class ImportCedricsArticleFormView(FormView): 

159 template_name = "import_article.html" 

160 form_class = ImportArticleForm 

161 

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

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

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

165 

166 def get_success_url(self): 

167 if self.colid: 

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

169 return "/" 

170 

171 def get_context_data(self, **kwargs): 

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

173 context["colid"] = self.colid 

174 context["helper"] = PtfModalFormHelper 

175 return context 

176 

177 def get_form_kwargs(self): 

178 kwargs = super().get_form_kwargs() 

179 kwargs["colid"] = self.colid 

180 return kwargs 

181 

182 def form_valid(self, form): 

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

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

185 return super().form_valid(form) 

186 

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

188 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd( 

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

190 ) 

191 cmd.do() 

192 

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

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

195 issue = request.POST["issue"] 

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

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

198 

199 import_args = [self] 

200 import_kwargs = {} 

201 

202 try: 

203 _, status, message = history_views.execute_and_record_func( 

204 "import", 

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

206 self.colid, 

207 self.import_cedrics_article, 

208 "", 

209 False, 

210 None, 

211 None, 

212 *import_args, 

213 **import_kwargs, 

214 ) 

215 

216 messages.success( 

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

218 ) 

219 

220 except Exception as exception: 

221 messages.error( 

222 self.request, 

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

224 ) 

225 

226 return redirect(self.get_success_url()) 

227 

228 

229class ImportCedricsIssueView(FormView): 

230 template_name = "import_container.html" 

231 form_class = ImportContainerForm 

232 

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

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

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

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

237 

238 def get_success_url(self): 

239 if self.filename: 

240 return reverse( 

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

242 ) 

243 return "/" 

244 

245 def get_context_data(self, **kwargs): 

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

247 context["colid"] = self.colid 

248 context["helper"] = PtfModalFormHelper 

249 return context 

250 

251 def get_form_kwargs(self): 

252 kwargs = super().get_form_kwargs() 

253 kwargs["colid"] = self.colid 

254 kwargs["to_appear"] = self.to_appear 

255 return kwargs 

256 

257 def form_valid(self, form): 

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

259 return super().form_valid(form) 

260 

261 

262class DiffCedricsIssueView(FormView): 

263 template_name = "diff_container_form.html" 

264 form_class = DiffContainerForm 

265 diffs = None 

266 xissue = None 

267 xissue_encoded = None 

268 

269 def get_success_url(self): 

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

271 

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

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

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

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

276 

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

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

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

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

281 self.remove_email = self.remove_mail == "on" 

282 self.remove_date_prod = self.remove_date_prod == "on" 

283 

284 try: 

285 result, status, message = history_views.execute_and_record_func( 

286 "import", 

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

288 self.colid, 

289 self.diff_cedrics_issue, 

290 "", 

291 True, 

292 ) 

293 except Exception as exception: 

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

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

296 return HttpResponseRedirect(self.get_success_url()) 

297 

298 no_conflict = result[0] 

299 self.diffs = result[1] 

300 self.xissue = result[2] 

301 

302 if no_conflict: 

303 # Proceed with the import 

304 self.form_valid(self.get_form()) 

305 return redirect(self.get_success_url()) 

306 else: 

307 # Display the diff template 

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

309 

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

311 

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

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

314 data = request.POST["xissue_encoded"] 

315 self.xissue = jsonpickle.decode(data) 

316 

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

318 

319 def get_context_data(self, **kwargs): 

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

321 context["colid"] = self.colid 

322 context["diff"] = self.diffs 

323 context["filename"] = self.filename 

324 context["xissue_encoded"] = self.xissue_encoded 

325 return context 

326 

327 def get_form_kwargs(self): 

328 kwargs = super().get_form_kwargs() 

329 kwargs["colid"] = self.colid 

330 return kwargs 

331 

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

333 params = { 

334 "colid": self.colid, 

335 "input_file": self.filename, 

336 "remove_email": self.remove_mail, 

337 "remove_date_prod": self.remove_date_prod, 

338 "diff_only": True, 

339 } 

340 

341 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

344 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

345 else: 

346 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

347 

348 result = cmd.do() 

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

350 messages.warning( 

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

352 ) 

353 

354 return result 

355 

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

357 # modify xissue with data_issue if params to override 

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

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

360 if issue: 

361 data_issue = model_data_converter.db_to_issue_data(issue) 

362 for xarticle in self.xissue.articles: 

363 filter_articles = [ 

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

365 ] 

366 if len(filter_articles) > 0: 

367 db_article = filter_articles[0] 

368 xarticle.coi_statement = db_article.coi_statement 

369 xarticle.kwds = db_article.kwds 

370 xarticle.contrib_groups = db_article.contrib_groups 

371 

372 params = { 

373 "colid": self.colid, 

374 "xissue": self.xissue, 

375 "input_file": self.filename, 

376 } 

377 

378 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

381 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

382 else: 

383 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

384 

385 cmd.do() 

386 

387 def form_valid(self, form): 

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

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

390 else: 

391 import_kwargs = {} 

392 import_args = [self] 

393 

394 try: 

395 _, status, message = history_views.execute_and_record_func( 

396 "import", 

397 self.xissue.pid, 

398 self.kwargs["colid"], 

399 self.import_cedrics_issue, 

400 "", 

401 False, 

402 None, 

403 None, 

404 *import_args, 

405 **import_kwargs, 

406 ) 

407 except Exception as exception: 

408 messages.error( 

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

410 ) 

411 return super().form_invalid(form) 

412 

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

414 return super().form_valid(form) 

415 

416 

417class BibtexAPIView(View): 

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

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

420 all_bibtex = "" 

421 if pid: 

422 article = model_helpers.get_article(pid) 

423 if article: 

424 for bibitem in article.bibitem_set.all(): 

425 bibtex_array = bibitem.get_bibtex() 

426 last = len(bibtex_array) 

427 i = 1 

428 for bibtex in bibtex_array: 

429 if i > 1 and i < last: 

430 all_bibtex += " " 

431 all_bibtex += bibtex + "\n" 

432 i += 1 

433 

434 data = {"bibtex": all_bibtex} 

435 return JsonResponse(data) 

436 

437 

438class MatchingAPIView(View): 

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

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

441 

442 url = settings.MATCHING_URL 

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

444 

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

446 

447 if settings.DEBUG: 

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

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

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

451 f.close() 

452 

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

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

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

456 

457 if settings.DEBUG: 

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

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

460 text = body 

461 f.write(text) 

462 f.close() 

463 

464 resource = model_helpers.get_resource(pid) 

465 obj = resource.cast() 

466 colid = obj.get_collection().pid 

467 

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

469 

470 cmd = xml_cmds.addOrUpdateIssueXmlCmd( 

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

472 ) 

473 cmd.do() 

474 

475 print("Matching finished") 

476 return JsonResponse(data) 

477 

478 

479class ImportAllAPIView(View): 

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

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

482 

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

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

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

486 

487 resource = model_helpers.get_resource(pid) 

488 if not resource: 

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

490 body = utils.get_file_content_in_utf8(file) 

491 journals = xml_cmds.addCollectionsXmlCmd( 

492 { 

493 "body": body, 

494 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

495 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

496 } 

497 ).do() 

498 if not journals: 

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

500 resource = journals[0] 

501 # resolver.copy_binary_files( 

502 # resource, 

503 # settings.MATHDOC_ARCHIVE_FOLDER, 

504 # settings.MERSENNE_TEST_DATA_FOLDER) 

505 

506 obj = resource.cast() 

507 

508 if obj.classname != "Collection": 

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

510 

511 cmd = xml_cmds.collectEntireCollectionXmlCmd( 

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

513 ) 

514 pids = cmd.do() 

515 

516 return pids 

517 

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

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

520 

521 try: 

522 pids, status, message = history_views.execute_and_record_func( 

523 "import", pid, pid, self.internal_do 

524 ) 

525 except Timeout as exception: 

526 return HttpResponse(exception, status=408) 

527 except Exception as exception: 

528 return HttpResponseServerError(exception) 

529 

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

531 return JsonResponse(data) 

532 

533 

534class DeployAllAPIView(View): 

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

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

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

538 

539 pids = [] 

540 

541 collection = model_helpers.get_collection(pid) 

542 if not collection: 

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

544 

545 if site == "numdam": 

546 server_url = settings.NUMDAM_PRE_URL 

547 elif site != "ptf_tools": 

548 server_url = getattr(collection, site)() 

549 if not server_url: 

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

551 

552 if site != "ptf_tools": 

553 # check if the collection exists on the server 

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

555 # image...) 

556 check_collection(collection, server_url, site) 

557 

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

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

560 pids.append(issue.pid) 

561 

562 return pids 

563 

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

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

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

567 

568 try: 

569 pids, status, message = history_views.execute_and_record_func( 

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

571 ) 

572 except Timeout as exception: 

573 return HttpResponse(exception, status=408) 

574 except Exception as exception: 

575 return HttpResponseServerError(exception) 

576 

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

578 return JsonResponse(data) 

579 

580 

581class AddIssuePDFView(View): 

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

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

584 self.pid = None 

585 self.issue = None 

586 self.collection = None 

587 self.site = "test_website" 

588 

589 def post_to_site(self, url): 

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

591 status = response.status_code 

592 if not (199 < status < 205): 

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

594 if status == 503: 

595 raise ServerUnderMaintenance(response.text) 

596 else: 

597 raise RuntimeError(response.text) 

598 

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

600 """ 

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

602 """ 

603 

604 issue_pid = self.issue.pid 

605 colid = self.collection.pid 

606 

607 if self.site == "website": 

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

609 resolver.copy_binary_files( 

610 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

611 ) 

612 else: 

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

614 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid) 

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

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

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

618 

619 to_path = resolver.get_disk_location( 

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

621 ) 

622 resolver.copy_file(from_path, to_path) 

623 

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

625 

626 if self.site == "test_website": 

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

628 absolute_url = self.request.build_absolute_uri(url) 

629 self.post_to_site(absolute_url) 

630 

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

632 absolute_url = server_url + url 

633 # Post to the test or production website 

634 self.post_to_site(absolute_url) 

635 

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

637 """ 

638 Send an issue PDF to the test or production website 

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

640 :param args: 

641 :param kwargs: 

642 :return: 

643 """ 

644 if check_lock(): 

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

646 messages.error(self.request, m) 

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

648 

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

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

651 

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

653 if not self.issue: 

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

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

656 

657 try: 

658 pids, status, message = history_views.execute_and_record_func( 

659 "deploy", 

660 self.pid, 

661 self.collection.pid, 

662 self.internal_do, 

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

664 ) 

665 

666 except Timeout as exception: 

667 return HttpResponse(exception, status=408) 

668 except Exception as exception: 

669 return HttpResponseServerError(exception) 

670 

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

672 return JsonResponse(data) 

673 

674 

675class ArchiveAllAPIView(View): 

676 """ 

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

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

679 @return array of issues pid 

680 """ 

681 

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

683 collection = kwargs["collection"] 

684 pids = [] 

685 colid = collection.pid 

686 

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

688 if os.path.isfile(logfile): 

689 os.remove(logfile) 

690 

691 ptf_cmds.exportPtfCmd( 

692 { 

693 "pid": colid, 

694 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

695 "with_binary_files": True, 

696 "for_archive": True, 

697 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

698 } 

699 ).do() 

700 

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

702 if os.path.isfile(cedramcls): 

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

704 resolver.create_folder(dest_folder) 

705 resolver.copy_file(cedramcls, dest_folder) 

706 

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

708 qs = issue.article_set.filter( 

709 date_online_first__isnull=True, date_published__isnull=True 

710 ) 

711 if qs.count() == 0: 

712 pids.append(issue.pid) 

713 

714 return pids 

715 

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

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

718 

719 collection = model_helpers.get_collection(pid) 

720 if not collection: 

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

722 

723 dict_ = {"collection": collection} 

724 args_ = [self] 

725 

726 try: 

727 pids, status, message = history_views.execute_and_record_func( 

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

729 ) 

730 except Timeout as exception: 

731 return HttpResponse(exception, status=408) 

732 except Exception as exception: 

733 return HttpResponseServerError(exception) 

734 

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

736 return JsonResponse(data) 

737 

738 

739class CreateAllDjvuAPIView(View): 

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

741 issue = kwargs["issue"] 

742 pids = [issue.pid] 

743 

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

745 pids.append(article.pid) 

746 

747 return pids 

748 

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

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

751 issue = model_helpers.get_container(pid) 

752 if not issue: 

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

754 

755 try: 

756 dict_ = {"issue": issue} 

757 args_ = [self] 

758 

759 pids, status, message = history_views.execute_and_record_func( 

760 "numdam", 

761 pid, 

762 issue.get_collection().pid, 

763 self.internal_do, 

764 "", 

765 False, 

766 None, 

767 None, 

768 *args_, 

769 **dict_, 

770 ) 

771 except Exception as exception: 

772 return HttpResponseServerError(exception) 

773 

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

775 return JsonResponse(data) 

776 

777 

778class ImportJatsContainerAPIView(View): 

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

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

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

782 

783 if pid and colid: 

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

785 

786 cmd = xml_cmds.addOrUpdateContainerXmlCmd( 

787 { 

788 "body": body, 

789 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

790 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

791 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

792 } 

793 ) 

794 container = cmd.do() 

795 if len(cmd.warnings) > 0: 

796 messages.warning( 

797 self.request, 

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

799 ) 

800 

801 if not container: 

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

803 

804 # resolver.copy_binary_files( 

805 # container, 

806 # settings.MATHDOC_ARCHIVE_FOLDER, 

807 # settings.MERSENNE_TEST_DATA_FOLDER) 

808 # 

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

810 # resolver.copy_binary_files( 

811 # article, 

812 # settings.MATHDOC_ARCHIVE_FOLDER, 

813 # settings.MERSENNE_TEST_DATA_FOLDER) 

814 else: 

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

816 

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

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

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

820 

821 try: 

822 _, status, message = history_views.execute_and_record_func( 

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

824 ) 

825 except Timeout as exception: 

826 return HttpResponse(exception, status=408) 

827 except Exception as exception: 

828 return HttpResponseServerError(exception) 

829 

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

831 return JsonResponse(data) 

832 

833 

834class DeployCollectionAPIView(View): 

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

836 

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

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

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

840 

841 collection = model_helpers.get_collection(colid) 

842 if not collection: 

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

844 

845 if site == "numdam": 

846 server_url = settings.NUMDAM_PRE_URL 

847 else: 

848 server_url = getattr(collection, site)() 

849 if not server_url: 

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

851 

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

853 check_collection(collection, server_url, site) 

854 

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

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

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

858 

859 try: 

860 _, status, message = history_views.execute_and_record_func( 

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

862 ) 

863 except Timeout as exception: 

864 return HttpResponse(exception, status=408) 

865 except Exception as exception: 

866 return HttpResponseServerError(exception) 

867 

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

869 return JsonResponse(data) 

870 

871 

872class DeployJatsResourceAPIView(View): 

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

874 

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

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

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

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

879 

880 if site == "ptf_tools": 

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

882 if check_lock(): 

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

884 messages.error(self.request, msg) 

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

886 

887 resource = model_helpers.get_resource(pid) 

888 if not resource: 

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

890 

891 obj = resource.cast() 

892 article = None 

893 if obj.classname == "Article": 

894 article = obj 

895 container = article.my_container 

896 articles_to_deploy = [article] 

897 else: 

898 container = obj 

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

900 

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

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

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

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

905 

906 collection = container.get_top_collection() 

907 colid = collection.pid 

908 djvu_exception = None 

909 

910 if site == "numdam": 

911 server_url = settings.NUMDAM_PRE_URL 

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

913 

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

915 # Add Djvu (before exporting the XML) 

916 if False and int(container.year) < 2020: 

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

918 try: 

919 cmd = ptf_cmds.addDjvuPtfCmd() 

920 cmd.set_resource(art) 

921 cmd.do() 

922 except Exception as e: 

923 # Djvu are optional. 

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

925 djvu_exception = e 

926 else: 

927 server_url = getattr(collection, site)() 

928 if not server_url: 

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

930 

931 # check if the collection exists on the server 

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

933 # image...) 

934 if article is None: 

935 check_collection(collection, server_url, site) 

936 

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

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

939 if site == "website": 

940 file_.write( 

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

942 pid 

943 ) 

944 ) 

945 

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

947 cmd = ptf_cmds.publishResourcePtfCmd() 

948 cmd.set_resource(resource) 

949 updated_articles = cmd.do() 

950 

951 tex.create_frontpage(colid, container, updated_articles, test=False) 

952 

953 mersenneSite = model_helpers.get_site_mersenne(colid) 

954 # create or update deployed_date on container and articles 

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

956 

957 for art in articles_to_deploy: 

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

959 if art.my_container.year is None: 

960 art.my_container.year = datetime.now().strftime("%Y") 

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

962 

963 file_.write( 

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

965 art.pid, art.date_online_first, art.date_published 

966 ) 

967 ) 

968 

969 if article is None: 

970 resolver.copy_binary_files( 

971 container, 

972 settings.MERSENNE_TEST_DATA_FOLDER, 

973 settings.MERSENNE_PROD_DATA_FOLDER, 

974 ) 

975 

976 for art in articles_to_deploy: 

977 resolver.copy_binary_files( 

978 art, 

979 settings.MERSENNE_TEST_DATA_FOLDER, 

980 settings.MERSENNE_PROD_DATA_FOLDER, 

981 ) 

982 

983 elif site == "test_website": 

984 # create date_pre_published on articles without date_pre_published 

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

986 cmd.set_resource(resource) 

987 updated_articles = cmd.do() 

988 

989 tex.create_frontpage(colid, container, updated_articles) 

990 

991 export_to_website = site == "website" 

992 

993 if article is None: 

994 with_djvu = site == "numdam" 

995 xml = ptf_cmds.exportPtfCmd( 

996 { 

997 "pid": pid, 

998 "with_djvu": with_djvu, 

999 "export_to_website": export_to_website, 

1000 } 

1001 ).do() 

1002 body = xml.encode("utf8") 

1003 

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

1005 url = server_url + reverse("issue_upload") 

1006 else: 

1007 url = server_url + reverse("book_upload") 

1008 

1009 # verify=False: ignore TLS certificate 

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

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

1012 else: 

1013 xml = ptf_cmds.exportPtfCmd( 

1014 { 

1015 "pid": pid, 

1016 "with_djvu": False, 

1017 "article_standalone": True, 

1018 "collection_pid": collection.pid, 

1019 "export_to_website": export_to_website, 

1020 "export_folder": settings.LOG_DIR, 

1021 } 

1022 ).do() 

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

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

1025 xml_file = io.StringIO(xml) 

1026 files = {"xml": xml_file} 

1027 

1028 url = server_url + reverse( 

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

1030 ) 

1031 # verify=False: ignore TLS certificate 

1032 header = {} 

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

1034 

1035 status = response.status_code 

1036 

1037 if 199 < status < 205: 

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

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

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

1041 # /mersenne_prod_data during an upload to prod 

1042 if site == "website": 

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

1044 if container.doi: 

1045 recordDOI(container) 

1046 

1047 for art in articles_to_deploy: 

1048 # record DOI automatically when deploying in prod 

1049 

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

1051 recordDOI(art) 

1052 

1053 if colid == "CRBIOL": 

1054 recordPubmed( 

1055 art, force_update=False, updated_articles=updated_articles 

1056 ) 

1057 

1058 if colid == "PCJ": 

1059 self.update_pcj_editor(updated_articles) 

1060 

1061 # Archive the container or the article 

1062 if article is None: 

1063 run_task( 

1064 ArchiveResourceTask, 

1065 colid=colid, 

1066 pid=pid, 

1067 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1068 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1069 ) 

1070 

1071 else: 

1072 run_task( 

1073 ArchiveResourceTask, 

1074 colid=colid, 

1075 pid=pid, 

1076 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1077 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1078 article_doi=article.doi, 

1079 ) 

1080 # cmd = ptf_cmds.archiveIssuePtfCmd({ 

1081 # "pid": pid, 

1082 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1083 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER}) 

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

1085 # cmd.do() 

1086 

1087 elif site == "numdam": 

1088 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

1089 if colid in settings.NUMDAM_COLLECTIONS: 

1090 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

1091 

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

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

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

1095 

1096 elif status == 503: 

1097 raise ServerUnderMaintenance(response.text) 

1098 else: 

1099 raise RuntimeError(response.text) 

1100 

1101 if djvu_exception: 

1102 raise djvu_exception 

1103 

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

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

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

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

1108 

1109 try: 

1110 _, status, message = history_views.execute_and_record_func( 

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

1112 ) 

1113 except Timeout as exception: 

1114 return HttpResponse(exception, status=408) 

1115 except Exception as exception: 

1116 return HttpResponseServerError(exception) 

1117 

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

1119 return JsonResponse(data) 

1120 

1121 def update_pcj_editor(self, updated_articles): 

1122 for article in updated_articles: 

1123 data = { 

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

1125 "article_number": article.article_number, 

1126 } 

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

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

1129 

1130 

1131class DeployTranslatedArticleAPIView(CsrfExemptMixin, View): 

1132 article = None 

1133 

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

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

1136 

1137 translation = None 

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

1139 if trans_article.lang == lang: 

1140 translation = trans_article 

1141 

1142 if translation is None: 

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

1144 

1145 collection = self.article.get_top_collection() 

1146 colid = collection.pid 

1147 container = self.article.my_container 

1148 

1149 if translation.date_published is None: 

1150 # Add date posted 

1151 cmd = ptf_cmds.publishResourcePtfCmd() 

1152 cmd.set_resource(translation) 

1153 updated_articles = cmd.do() 

1154 

1155 # Recompile PDF to add the date posted 

1156 try: 

1157 tex.create_frontpage(colid, container, updated_articles, test=False, lang=lang) 

1158 except Exception: 

1159 raise PDFException( 

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

1161 ) 

1162 

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

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

1165 resolver.copy_binary_files( 

1166 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

1167 ) 

1168 

1169 # Deploy in prod 

1170 xml = ptf_cmds.exportPtfCmd( 

1171 { 

1172 "pid": self.article.pid, 

1173 "with_djvu": False, 

1174 "article_standalone": True, 

1175 "collection_pid": colid, 

1176 "export_to_website": True, 

1177 "export_folder": settings.LOG_DIR, 

1178 } 

1179 ).do() 

1180 xml_file = io.StringIO(xml) 

1181 files = {"xml": xml_file} 

1182 

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

1184 if not server_url: 

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

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

1187 header = {} 

1188 

1189 try: 

1190 response = requests.post( 

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

1192 ) # verify: ignore TLS certificate 

1193 status = response.status_code 

1194 except requests.exceptions.ConnectionError: 

1195 raise ServerUnderMaintenance( 

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

1197 ) 

1198 

1199 # Register translation in Crossref 

1200 if 199 < status < 205: 

1201 if self.article.allow_crossref(): 

1202 try: 

1203 recordDOI(translation) 

1204 except Exception: 

1205 raise DOIException( 

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

1207 ) 

1208 

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

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

1211 self.article = model_helpers.get_article_by_doi(doi) 

1212 if self.article is None: 

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

1214 

1215 try: 

1216 _, status, message = history_views.execute_and_record_func( 

1217 "deploy", 

1218 self.article.pid, 

1219 self.article.get_top_collection().pid, 

1220 self.internal_do, 

1221 "website", 

1222 ) 

1223 except Timeout as exception: 

1224 return HttpResponse(exception, status=408) 

1225 except Exception as exception: 

1226 return HttpResponseServerError(exception) 

1227 

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

1229 return JsonResponse(data) 

1230 

1231 

1232class DeleteJatsIssueAPIView(View): 

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

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

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

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

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

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

1239 status = 200 

1240 

1241 issue = model_helpers.get_container(pid) 

1242 if not issue: 

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

1244 try: 

1245 mersenneSite = model_helpers.get_site_mersenne(colid) 

1246 

1247 if site == "ptf_tools": 

1248 if issue.is_deployed(mersenneSite): 

1249 issue.undeploy(mersenneSite) 

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

1251 article.undeploy(mersenneSite) 

1252 

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

1254 

1255 cmd = ptf_cmds.addContainerPtfCmd( 

1256 { 

1257 "pid": issue.pid, 

1258 "ctype": "issue", 

1259 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

1260 } 

1261 ) 

1262 cmd.set_provider(p) 

1263 cmd.add_collection(issue.get_collection()) 

1264 cmd.set_object_to_be_deleted(issue) 

1265 cmd.undo() 

1266 

1267 else: 

1268 if site == "numdam": 

1269 server_url = settings.NUMDAM_PRE_URL 

1270 else: 

1271 collection = issue.get_collection() 

1272 server_url = getattr(collection, site)() 

1273 

1274 if not server_url: 

1275 message = "The collection has no " + site 

1276 status = 500 

1277 else: 

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

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

1280 status = response.status_code 

1281 

1282 if status == 404: 

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

1284 elif status > 204: 

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

1286 message = body[:1000] 

1287 else: 

1288 status = 200 

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

1290 if site == "website": 

1291 if issue.is_deployed(mersenneSite): 

1292 issue.undeploy(mersenneSite) 

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

1294 article.undeploy(mersenneSite) 

1295 # delete article binary files 

1296 folder = article.get_relative_folder() 

1297 resolver.delete_object_folder( 

1298 folder, 

1299 to_folder=settings.MERSENNE_PROD_DATA_FORLDER, 

1300 ) 

1301 # delete issue binary files 

1302 folder = issue.get_relative_folder() 

1303 resolver.delete_object_folder( 

1304 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER 

1305 ) 

1306 

1307 except Timeout as exception: 

1308 return HttpResponse(exception, status=408) 

1309 except Exception as exception: 

1310 return HttpResponseServerError(exception) 

1311 

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

1313 return JsonResponse(data) 

1314 

1315 

1316class ArchiveIssueAPIView(View): 

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

1318 try: 

1319 pid = kwargs["pid"] 

1320 colid = kwargs["colid"] 

1321 except IndexError: 

1322 raise Http404 

1323 

1324 try: 

1325 cmd = ptf_cmds.archiveIssuePtfCmd( 

1326 { 

1327 "pid": pid, 

1328 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1329 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

1330 } 

1331 ) 

1332 result_, status, message = history_views.execute_and_record_func( 

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

1334 ) 

1335 except Exception as exception: 

1336 return HttpResponseServerError(exception) 

1337 

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

1339 return JsonResponse(data) 

1340 

1341 

1342class CreateDjvuAPIView(View): 

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

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

1345 

1346 resource = model_helpers.get_resource(pid) 

1347 cmd = ptf_cmds.addDjvuPtfCmd() 

1348 cmd.set_resource(resource) 

1349 cmd.do() 

1350 

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

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

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

1354 

1355 try: 

1356 _, status, message = history_views.execute_and_record_func( 

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

1358 ) 

1359 except Exception as exception: 

1360 return HttpResponseServerError(exception) 

1361 

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

1363 return JsonResponse(data) 

1364 

1365 

1366class PTFToolsHomeView(LoginRequiredMixin, View): 

1367 """ 

1368 Home Page. 

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

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

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

1372 - Comment moderator -> Comments dashboard 

1373 - Others -> 404 response 

1374 """ 

1375 

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

1377 # Staff or user with authorized collections 

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

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

1380 

1381 colids = get_authorized_collections(request.user) 

1382 is_mod = is_comment_moderator(request.user) 

1383 

1384 # The user has no rights 

1385 if not (colids or is_mod): 

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

1387 # Comment moderator only 

1388 elif not colids: 

1389 return HttpResponseRedirect(reverse("comment_list")) 

1390 

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

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

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

1394 

1395 # User with multiple authorized collections - Special home 

1396 context = {} 

1397 context["overview"] = True 

1398 

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

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

1401 

1402 # Comments summary 

1403 try: 

1404 error, comments_data = get_comments_for_home(request.user) 

1405 except AttributeError: 

1406 error, comments_data = True, {} 

1407 

1408 context["comment_server_ok"] = False 

1409 

1410 if not error: 

1411 context["comment_server_ok"] = True 

1412 if comments_data: 

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

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

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

1416 

1417 # TODO: Translations summary 

1418 context["translation_server_ok"] = False 

1419 

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

1421 context["collections"] = sorted( 

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

1423 ) 

1424 

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

1426 

1427 

1428class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin): 

1429 columns = 5 

1430 

1431 def get_common_context_data(self, **kwargs): 

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

1433 now = timezone.now() 

1434 curyear = now.year 

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

1436 

1437 context["collections"] = settings.MERSENNE_COLLECTIONS 

1438 context["containers_to_be_published"] = [] 

1439 context["last_col_events"] = [] 

1440 

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

1442 clockss_gap = get_gap(now, event) 

1443 

1444 context["years"] = years 

1445 context["clockss_gap"] = clockss_gap 

1446 

1447 return context 

1448 

1449 def calculate_articles_and_pages(self, pid, years): 

1450 data_by_year = [] 

1451 total_articles = [0] * len(years) 

1452 total_pages = [0] * len(years) 

1453 

1454 for year in years: 

1455 articles = self.get_articles_for_year(pid, year) 

1456 articles_count = articles.count() 

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

1458 

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

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

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

1462 

1463 return data_by_year, total_articles, total_pages 

1464 

1465 def get_articles_for_year(self, pid, year): 

1466 return Article.objects.filter( 

1467 Q(my_container__my_collection__pid=pid) 

1468 & ( 

1469 Q(date_published__year=year, date_online_first__isnull=True) 

1470 | Q(date_online_first__year=year) 

1471 ) 

1472 ).prefetch_related("resourcecount_set") 

1473 

1474 

1475class PublishedArticlesDashboardView(BaseMersenneDashboardView): 

1476 template_name = "dashboard/published_articles.html" 

1477 

1478 def get_context_data(self, **kwargs): 

1479 context = self.get_common_context_data(**kwargs) 

1480 years = context["years"] 

1481 

1482 published_articles = [] 

1483 total_published_articles = [ 

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

1485 ] 

1486 

1487 for pid in settings.MERSENNE_COLLECTIONS: 

1488 if pid != "MERSENNE": 

1489 articles_data, total_articles, total_pages = self.calculate_articles_and_pages( 

1490 pid, years 

1491 ) 

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

1493 

1494 for i, year in enumerate(years): 

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

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

1497 

1498 context["published_articles"] = published_articles 

1499 context["total_published_articles"] = total_published_articles 

1500 

1501 return context 

1502 

1503 

1504class CreatedVolumesDashboardView(BaseMersenneDashboardView): 

1505 template_name = "dashboard/created_volumes.html" 

1506 

1507 def get_context_data(self, **kwargs): 

1508 context = self.get_common_context_data(**kwargs) 

1509 years = context["years"] 

1510 

1511 created_volumes = [] 

1512 total_created_volumes = [ 

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

1514 ] 

1515 

1516 for pid in settings.MERSENNE_COLLECTIONS: 

1517 if pid != "MERSENNE": 

1518 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages( 

1519 pid, years 

1520 ) 

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

1522 

1523 for i, year in enumerate(years): 

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

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

1526 

1527 context["created_volumes"] = created_volumes 

1528 context["total_created_volumes"] = total_created_volumes 

1529 

1530 return context 

1531 

1532 def calculate_volumes_and_pages(self, pid, years): 

1533 data_by_year = [] 

1534 total_articles = [0] * len(years) 

1535 total_pages = [0] * len(years) 

1536 

1537 for year in years: 

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

1539 articles_count = 0 

1540 page_count = 0 

1541 

1542 for issue in issues: 

1543 articles = issue.article_set.filter( 

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

1545 ).prefetch_related("resourcecount_set") 

1546 

1547 articles_count += articles.count() 

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

1549 

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

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

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

1553 

1554 return data_by_year, total_articles, total_pages 

1555 

1556 

1557class ReferencingChoice(View): 

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

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

1560 return redirect( 

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

1562 ) 

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

1564 comp = ReferencingCheckerWos() 

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

1566 if journal is None: 

1567 return render( 

1568 request, 

1569 "dashboard/referencing.html", 

1570 { 

1571 "error": "Collection not found", 

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

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

1574 }, 

1575 ) 

1576 return render( 

1577 request, 

1578 "dashboard/referencing.html", 

1579 { 

1580 "journal": journal, 

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

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

1583 }, 

1584 ) 

1585 

1586 

1587class ReferencingWosFileView(View): 

1588 template_name = "dashboard/referencing.html" 

1589 

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

1591 colid = request.POST["colid"] 

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

1593 message = "No file uploaded" 

1594 return render( 

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

1596 ) 

1597 uploaded_file = request.FILES["risfile"] 

1598 comp = ReferencingCheckerWos() 

1599 journal = comp.check_references(colid, uploaded_file) 

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

1601 

1602 

1603class ReferencingDashboardView(BaseMersenneDashboardView): 

1604 template_name = "dashboard/referencing.html" 

1605 

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

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

1608 comp = ReferencingCheckerAds() 

1609 journal = comp.check_references(colid) 

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

1611 

1612 

1613class BaseCollectionView(TemplateView): 

1614 def get_context_data(self, **kwargs): 

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

1616 aid = context.get("aid") 

1617 year = context.get("year") 

1618 

1619 if aid and year: 

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

1621 

1622 return context 

1623 

1624 def get_collection(self, aid, year): 

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

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

1627 

1628 

1629class ArticleListView(BaseCollectionView): 

1630 template_name = "collection-list.html" 

1631 

1632 def get_collection(self, aid, year): 

1633 return Article.objects.filter( 

1634 Q(my_container__my_collection__pid=aid) 

1635 & ( 

1636 Q(date_published__year=year, date_online_first__isnull=True) 

1637 | Q(date_online_first__year=year) 

1638 ) 

1639 ).prefetch_related("resourcecount_set") 

1640 

1641 

1642class VolumeListView(BaseCollectionView): 

1643 template_name = "collection-list.html" 

1644 

1645 def get_collection(self, aid, year): 

1646 return Article.objects.filter( 

1647 Q(my_container__my_collection__pid=aid, my_container__year=year) 

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

1649 ).prefetch_related("resourcecount_set") 

1650 

1651 

1652class DOAJResourceRegisterView(View): 

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

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

1655 resource = model_helpers.get_resource(pid) 

1656 if resource is None: 

1657 raise Http404 

1658 

1659 try: 

1660 data = {} 

1661 doaj_meta, response = doaj_pid_register(pid) 

1662 if response is None: 

1663 return HttpResponse(status=204) 

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

1665 data.update(doaj_meta) 

1666 else: 

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

1668 except Timeout as exception: 

1669 return HttpResponse(exception, status=408) 

1670 except Exception as exception: 

1671 return HttpResponseServerError(exception) 

1672 return JsonResponse(data) 

1673 

1674 

1675class CROSSREFResourceRegisterView(View): 

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

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

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

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

1680 if not request.user.is_superuser: 

1681 force = None 

1682 

1683 resource = model_helpers.get_resource(pid) 

1684 if resource is None: 

1685 raise Http404 

1686 

1687 resource = resource.cast() 

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

1689 try: 

1690 data = meth(resource, force) 

1691 except Timeout as exception: 

1692 return HttpResponse(exception, status=408) 

1693 except Exception as exception: 

1694 return HttpResponseServerError(exception) 

1695 return JsonResponse(data) 

1696 

1697 def recordDOIArticle(self, article, force=None): 

1698 result = {"status": 404} 

1699 if ( 

1700 article.doi 

1701 and not article.do_not_publish 

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

1703 ): 

1704 if article.my_container.year is None: # or article.my_container.year == '0': 

1705 article.my_container.year = datetime.now().strftime("%Y") 

1706 result = recordDOI(article) 

1707 return result 

1708 

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

1710 return recordDOI(collection) 

1711 

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

1713 data = {"status": 200, "message": "tout va bien"} 

1714 

1715 if container.ctype == "issue": 

1716 if container.doi: 

1717 result = recordDOI(container) 

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

1719 return result 

1720 if force == "force": 

1721 articles = container.article_set.exclude( 

1722 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True 

1723 ) 

1724 else: 

1725 articles = container.article_set.exclude( 

1726 doi__isnull=True, 

1727 do_not_publish=True, 

1728 date_published__isnull=True, 

1729 date_online_first__isnull=True, 

1730 ) 

1731 

1732 for article in articles: 

1733 result = self.recordDOIArticle(article, force) 

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

1735 data = result 

1736 else: 

1737 return recordDOI(container) 

1738 return data 

1739 

1740 

1741class CROSSREFResourceCheckStatusView(View): 

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

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

1744 resource = model_helpers.get_resource(pid) 

1745 if resource is None: 

1746 raise Http404 

1747 resource = resource.cast() 

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

1749 try: 

1750 meth(resource) 

1751 except Timeout as exception: 

1752 return HttpResponse(exception, status=408) 

1753 except Exception as exception: 

1754 return HttpResponseServerError(exception) 

1755 

1756 data = {"status": 200, "message": "tout va bien"} 

1757 return JsonResponse(data) 

1758 

1759 def checkDOIArticle(self, article): 

1760 if article.my_container.year is None or article.my_container.year == "0": 

1761 article.my_container.year = datetime.now().strftime("%Y") 

1762 checkDOI(article) 

1763 

1764 def checkDOICollection(self, collection): 

1765 checkDOI(collection) 

1766 

1767 def checkDOIContainer(self, container): 

1768 if container.doi is not None: 

1769 checkDOI(container) 

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

1771 self.checkDOIArticle(article) 

1772 

1773 

1774class RegisterPubmedFormView(FormView): 

1775 template_name = "record_pubmed_dialog.html" 

1776 form_class = RegisterPubmedForm 

1777 

1778 def get_context_data(self, **kwargs): 

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

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

1781 context["helper"] = PtfLargeModalFormHelper 

1782 return context 

1783 

1784 

1785class RegisterPubmedView(View): 

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

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

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

1789 

1790 article = model_helpers.get_article(pid) 

1791 if article is None: 

1792 raise Http404 

1793 try: 

1794 recordPubmed(article, update_article) 

1795 except Exception as exception: 

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

1797 return HttpResponseServerError(exception) 

1798 

1799 return HttpResponseRedirect( 

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

1801 ) 

1802 

1803 

1804class PTFToolsContainerView(TemplateView): 

1805 template_name = "" 

1806 

1807 def get_context_data(self, **kwargs): 

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

1809 

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

1811 if container is None: 

1812 raise Http404 

1813 citing_articles = container.citations() 

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

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

1816 book_parts = ( 

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

1818 ) 

1819 references = False 

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

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

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

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

1824 references = True 

1825 context.update( 

1826 { 

1827 "book": container, 

1828 "book_parts": list(book_parts), 

1829 "source": source, 

1830 "citing_articles": citing_articles, 

1831 "references": references, 

1832 "test_website": container.get_top_collection() 

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

1834 .location, 

1835 "prod_website": container.get_top_collection() 

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

1837 .location, 

1838 } 

1839 ) 

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

1841 else: 

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

1843 for article in articles: 

1844 try: 

1845 last_match = ( 

1846 history_models.HistoryEvent.objects.filter( 

1847 pid=article.pid, 

1848 type="matching", 

1849 ) 

1850 .only("created_on") 

1851 .latest("created_on") 

1852 ) 

1853 except history_models.HistoryEvent.DoesNotExist as _: 

1854 article.last_match = None 

1855 else: 

1856 article.last_match = last_match.created_on 

1857 

1858 # article1 = articles.first() 

1859 # date = article1.deployed_date() 

1860 # TODO next_issue, previous_issue 

1861 

1862 # check DOI est maintenant une commande à part 

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

1864 # articlesWithStatus = [] 

1865 # for article in articles: 

1866 # checkDOIExistence(article) 

1867 # articlesWithStatus.append(article) 

1868 

1869 test_location = prod_location = "" 

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

1871 if qs: 

1872 test_location = qs.first().location 

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

1874 if qs: 

1875 prod_location = qs.first().location 

1876 context.update( 

1877 { 

1878 "issue": container, 

1879 "articles": articles, 

1880 "source": source, 

1881 "citing_articles": citing_articles, 

1882 "test_website": test_location, 

1883 "prod_website": prod_location, 

1884 } 

1885 ) 

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

1887 

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

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

1890 return context 

1891 

1892 

1893class ExtLinkInline(InlineFormSetFactory): 

1894 model = ExtLink 

1895 form_class = ExtLinkForm 

1896 factory_kwargs = {"extra": 0} 

1897 

1898 

1899class ResourceIdInline(InlineFormSetFactory): 

1900 model = ResourceId 

1901 form_class = ResourceIdForm 

1902 factory_kwargs = {"extra": 0} 

1903 

1904 

1905class IssueDetailAPIView(View): 

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

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

1908 deployed_date = issue.deployed_date() 

1909 result = { 

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

1911 if deployed_date 

1912 else None, 

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

1914 "all_doi_are_registered": issue.all_doi_are_registered(), 

1915 "registered_in_doaj": issue.registered_in_doaj(), 

1916 "doi": issue.my_collection.doi, 

1917 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(), 

1918 } 

1919 try: 

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

1921 except history_models.HistoryEvent.DoesNotExist as _: 

1922 pass 

1923 else: 

1924 result["latest"] = latest.data["message"] 

1925 result["latest_target"] = latest.data.get("target", "") 

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

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

1928 ) 

1929 

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

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

1932 try: 

1933 result[event_type] = timezone.localtime( 

1934 history_models.HistoryEvent.objects.filter( 

1935 type=event_type, 

1936 status="OK", 

1937 pid__startswith=issue.pid, 

1938 ) 

1939 .latest("created_on") 

1940 .created_on 

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

1942 except history_models.HistoryEvent.DoesNotExist as _: 

1943 result[event_type] = "" 

1944 return JsonResponse(result) 

1945 

1946 

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

1948 model = Collection 

1949 form_class = CollectionForm 

1950 inlines = [ResourceIdInline, ExtLinkInline] 

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

1952 

1953 def get_context_data(self, **kwargs): 

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

1955 context["helper"] = PtfFormHelper 

1956 context["formset_helper"] = FormSetHelper 

1957 return context 

1958 

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

1960 if description: 

1961 la = Abstract( 

1962 resource=collection, 

1963 tag="description", 

1964 lang=lang, 

1965 seq=seq, 

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

1967 value_html=description, 

1968 value_tex=description, 

1969 ) 

1970 la.save() 

1971 

1972 def form_valid(self, form): 

1973 if form.instance.abbrev: 

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

1975 else: 

1976 form.instance.title_xml = ( 

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

1978 ) 

1979 

1980 form.instance.title_html = form.instance.title_tex 

1981 form.instance.title_sort = form.instance.title_tex 

1982 result = super().form_valid(form) 

1983 

1984 collection = self.object 

1985 collection.abstract_set.all().delete() 

1986 

1987 seq = 1 

1988 description = form.cleaned_data["description_en"] 

1989 if description: 

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

1991 seq += 1 

1992 description = form.cleaned_data["description_fr"] 

1993 if description: 

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

1995 

1996 return result 

1997 

1998 def get_success_url(self): 

1999 messages.success(self.request, "La Collection a été modifiée avec succès") 

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

2001 

2002 

2003class CollectionCreate(CollectionFormView, CreateWithInlinesView): 

2004 """ 

2005 Warning : Not yet finished 

2006 Automatic site membership creation is still missing 

2007 """ 

2008 

2009 

2010class CollectionUpdate(CollectionFormView, UpdateWithInlinesView): 

2011 slug_field = "pid" 

2012 slug_url_kwarg = "pid" 

2013 

2014 

2015def suggest_load_journal_dois(colid): 

2016 articles = ( 

2017 Article.objects.filter(my_container__my_collection__pid=colid) 

2018 .filter(doi__isnull=False) 

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

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

2021 ) 

2022 

2023 try: 

2024 articles = sorted( 

2025 articles, 

2026 key=lambda d: ( 

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

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

2029 ), 

2030 ) 

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

2032 pass 

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

2034 

2035 

2036def get_context_with_volumes(journal): 

2037 result = model_helpers.get_volumes_in_collection(journal) 

2038 volume_count = result["volume_count"] 

2039 collections = [] 

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

2041 item = model_helpers.get_volumes_in_collection(ancestor) 

2042 volume_count = max(0, volume_count) 

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

2044 collections.append(item) 

2045 

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

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

2048 collections.append(result) 

2049 

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

2051 collections.sort( 

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

2053 reverse=True, 

2054 ) 

2055 

2056 context = { 

2057 "journal": journal, 

2058 "sorted_issues": result["sorted_issues"], 

2059 "volume_count": volume_count, 

2060 "max_width": result["max_width"], 

2061 "collections": collections, 

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

2063 } 

2064 return context 

2065 

2066 

2067class CollectionDetail( 

2068 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin 

2069): 

2070 model = Collection 

2071 slug_field = "pid" 

2072 slug_url_kwarg = "pid" 

2073 template_name = "ptf/collection_detail.html" 

2074 

2075 def test_func(self): 

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

2077 

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

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

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

2081 

2082 def get_context_data(self, **kwargs): 

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

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

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

2086 ) 

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

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

2089 

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

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

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

2093 pid=context["issue_to_appear_pid"] 

2094 ).exists() 

2095 try: 

2096 latest_error = history_models.HistoryEvent.objects.filter( 

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

2098 ).latest("created_on") 

2099 except history_models.HistoryEvent.DoesNotExist as _: 

2100 pass 

2101 else: 

2102 message = latest_error.data["message"] 

2103 i = message.find(" - ") 

2104 latest_exception = message[:i] 

2105 latest_error_message = message[i + 3 :] 

2106 context["latest_exception"] = latest_exception 

2107 context["latest_exception_date"] = latest_error.created_on 

2108 context["latest_exception_type"] = latest_error.type 

2109 context["latest_error_message"] = latest_error_message 

2110 

2111 archive_in_error = history_models.HistoryEvent.objects.filter( 

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

2113 ).exists() 

2114 

2115 context["archive_in_error"] = archive_in_error 

2116 

2117 return context 

2118 

2119 def get_queryset(self): 

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

2121 

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

2123 query |= ancestor.content.all() 

2124 

2125 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int") 

2126 

2127 

2128class ContainerEditView(FormView): 

2129 template_name = "container_form.html" 

2130 form_class = ContainerForm 

2131 

2132 def get_success_url(self): 

2133 if self.kwargs["pid"]: 

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

2135 return reverse("mersenne_dashboard/published_articles") 

2136 

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

2138 messages.success(self.request, "Le fascicule a été modifié") 

2139 

2140 def get_form_kwargs(self): 

2141 kwargs = super().get_form_kwargs() 

2142 if "pid" not in self.kwargs: 

2143 self.kwargs["pid"] = None 

2144 if "colid" not in self.kwargs: 

2145 self.kwargs["colid"] = None 

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

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

2148 # It is used when you submit a new container 

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

2150 

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

2152 self.kwargs["pid"] 

2153 ) 

2154 return kwargs 

2155 

2156 def get_context_data(self, **kwargs): 

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

2158 

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

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

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

2162 

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

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

2165 

2166 return context 

2167 

2168 def form_valid(self, form): 

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

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

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

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

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

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

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

2176 

2177 collection = None 

2178 issue = self.kwargs["container"] 

2179 if issue is not None: 

2180 collection = issue.my_collection 

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

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

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

2184 else: 

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

2186 

2187 if collection is None: 

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

2189 

2190 # Icon 

2191 new_icon_location = "" 

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

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

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

2195 

2196 icon_filename = resolver.get_disk_location( 

2197 settings.MERSENNE_TEST_DATA_FOLDER, 

2198 collection.pid, 

2199 file_extension, 

2200 new_pid, 

2201 None, 

2202 True, 

2203 ) 

2204 

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

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

2207 destination.write(chunk) 

2208 

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

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

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

2212 if name == "special_issue_create": 

2213 self.kwargs["name"] = name 

2214 if self.kwargs["container"]: 

2215 # Edit Issue 

2216 issue = self.kwargs["container"] 

2217 if issue is None: 

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

2219 

2220 issue.pid = new_pid 

2221 issue.title_tex = issue.title_html = new_title 

2222 issue.title_xml = build_title_xml( 

2223 title=new_title, 

2224 lang=issue.lang, 

2225 title_type="issue-title", 

2226 ) 

2227 

2228 trans_lang = "" 

2229 if issue.trans_lang != "" and issue.trans_lang != "und": 

2230 trans_lang = issue.trans_lang 

2231 elif new_trans_title != "": 

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

2233 issue.trans_lang = trans_lang 

2234 

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

2236 issue.trans_title_html = "" 

2237 issue.trans_title_tex = "" 

2238 title_xml = build_title_xml( 

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

2240 ) 

2241 try: 

2242 trans_title_object = Title.objects.get(resource=issue, lang=trans_lang) 

2243 trans_title_object.title_html = new_trans_title 

2244 trans_title_object.title_xml = title_xml 

2245 trans_title_object.save() 

2246 except Title.DoesNotExist: 

2247 trans_title = Title( 

2248 resource=issue, 

2249 lang=trans_lang, 

2250 type="main", 

2251 title_html=new_trans_title, 

2252 title_xml=title_xml, 

2253 ) 

2254 trans_title.save() 

2255 issue.year = new_year 

2256 issue.volume = new_volume 

2257 issue.volume_int = make_int(new_volume) 

2258 issue.number = new_number 

2259 issue.number_int = make_int(new_number) 

2260 issue.save() 

2261 else: 

2262 xissue = create_issuedata() 

2263 

2264 xissue.ctype = "issue" 

2265 xissue.pid = new_pid 

2266 xissue.lang = "en" 

2267 xissue.title_tex = new_title 

2268 xissue.title_html = new_title 

2269 xissue.title_xml = build_title_xml( 

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

2271 ) 

2272 

2273 if new_trans_title != "": 

2274 trans_lang = "fr" 

2275 title_xml = build_title_xml( 

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

2277 ) 

2278 title = create_titledata( 

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

2280 ) 

2281 issue.titles = [title] 

2282 

2283 xissue.year = new_year 

2284 xissue.volume = new_volume 

2285 xissue.number = new_number 

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

2287 

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

2289 cmd.add_collection(collection) 

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

2291 issue = cmd.do() 

2292 

2293 self.kwargs["pid"] = new_pid 

2294 

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

2296 params = { 

2297 "icon_location": new_icon_location, 

2298 } 

2299 cmd = ptf_cmds.updateContainerPtfCmd(params) 

2300 cmd.set_resource(issue) 

2301 cmd.do() 

2302 

2303 publisher = model_helpers.get_publisher(new_publisher) 

2304 if not publisher: 

2305 xpub = create_publisherdata() 

2306 xpub.name = new_publisher 

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

2308 issue.my_publisher = publisher 

2309 issue.save() 

2310 

2311 self.set_success_message() 

2312 

2313 return super().form_valid(form) 

2314 

2315 

2316# class ArticleEditView(FormView): 

2317# template_name = 'article_form.html' 

2318# form_class = ArticleForm 

2319# 

2320# def get_success_url(self): 

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

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

2323# return reverse('mersenne_dashboard/published_articles') 

2324# 

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

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

2327# 

2328# def get_form_kwargs(self): 

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

2330# 

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

2332# # Article creation: pid is None 

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

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

2335# # Article edit: issue_id is not passed 

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

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

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

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

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

2341# 

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

2343# return kwargs 

2344# 

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

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

2347# 

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

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

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

2351# 

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

2353# 

2354# article = context['article'] 

2355# if article: 

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

2357# context['kwds_fr'] = None 

2358# context['kwds_en'] = None 

2359# kwd_gps = article.get_non_msc_kwds() 

2360# for kwd_gp in kwd_gps: 

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

2362# if kwd_gp.value_xml: 

2363# kwd_ = types.SimpleNamespace() 

2364# kwd_.value = kwd_gp.value_tex 

2365# context['kwd_unstructured_fr'] = kwd_ 

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

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

2368# if kwd_gp.value_xml: 

2369# kwd_ = types.SimpleNamespace() 

2370# kwd_.value = kwd_gp.value_tex 

2371# context['kwd_unstructured_en'] = kwd_ 

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

2373# 

2374# # Article creation: init pid 

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

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

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

2378# 

2379# return context 

2380# 

2381# def form_valid(self, form): 

2382# 

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

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

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

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

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

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

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

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

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

2392# 

2393# # TODO support MathML 

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

2395# # We need to pass trans_title to get_title_xml 

2396# # Meanwhile, ignore new_title_xml 

2397# new_title_xml = jats_parser.get_title_xml(new_title) 

2398# new_title_html = new_title 

2399# 

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

2401# i = 1 

2402# new_authors = [] 

2403# old_author_contributions = [] 

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

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

2406# 

2407# while authors_count > 0: 

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

2409# 

2410# if prefix is not None: 

2411# addresses = [] 

2412# if len(old_author_contributions) >= i: 

2413# old_author_contribution = old_author_contributions[i - 1] 

2414# addresses = [contrib_address.address for contrib_address in 

2415# old_author_contribution.get_addresses()] 

2416# 

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

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

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

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

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

2422# deceased_before_publication = deceased == 'on' 

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

2424# equal_contrib = equal_contrib == 'on' 

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

2426# corresponding = corresponding == 'on' 

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

2428# 

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

2430# params['deceased_before_publication'] = deceased_before_publication 

2431# params['equal_contrib'] = equal_contrib 

2432# params['corresponding'] = corresponding 

2433# params['addresses'] = addresses 

2434# params['email'] = email 

2435# 

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

2437# 

2438# new_authors.append(params) 

2439# 

2440# authors_count -= 1 

2441# i += 1 

2442# 

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

2444# i = 1 

2445# new_kwds_fr = [] 

2446# while kwds_fr_count > 0: 

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

2448# new_kwds_fr.append(value) 

2449# kwds_fr_count -= 1 

2450# i += 1 

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

2452# 

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

2454# i = 1 

2455# new_kwds_en = [] 

2456# while kwds_en_count > 0: 

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

2458# new_kwds_en.append(value) 

2459# kwds_en_count -= 1 

2460# i += 1 

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

2462# 

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

2464# # Edit article 

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

2466# else: 

2467# # New article 

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

2469# 

2470# if container is None: 

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

2472# 

2473# collection = container.my_collection 

2474# 

2475# # Copy PDF file & extract full text 

2476# body = '' 

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

2478# collection.pid, 

2479# "pdf", 

2480# container.pid, 

2481# new_pid, 

2482# True) 

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

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

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

2486# destination.write(chunk) 

2487# 

2488# # Extract full text from the PDF 

2489# body = utils.pdf_to_text(pdf_filename) 

2490# 

2491# # Icon 

2492# new_icon_location = '' 

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

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

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

2496# 

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

2498# collection.pid, 

2499# file_extension, 

2500# container.pid, 

2501# new_pid, 

2502# True) 

2503# 

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

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

2506# destination.write(chunk) 

2507# 

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

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

2510# 

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

2512# # Edit article 

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

2514# article.fpage = new_fpage 

2515# article.lpage = new_lpage 

2516# article.page_range = new_page_range 

2517# article.coi_statement = new_coi_statement 

2518# article.show_body = new_show_body 

2519# article.do_not_publish = new_do_not_publish 

2520# article.save() 

2521# 

2522# else: 

2523# # New article 

2524# params = { 

2525# 'pid': new_pid, 

2526# 'title_xml': new_title_xml, 

2527# 'title_html': new_title_html, 

2528# 'title_tex': new_title, 

2529# 'fpage': new_fpage, 

2530# 'lpage': new_lpage, 

2531# 'page_range': new_page_range, 

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

2533# 'body': body, 

2534# 'coi_statement': new_coi_statement, 

2535# 'show_body': new_show_body, 

2536# 'do_not_publish': new_do_not_publish 

2537# } 

2538# 

2539# xarticle = create_articledata() 

2540# xarticle.pid = new_pid 

2541# xarticle.title_xml = new_title_xml 

2542# xarticle.title_html = new_title_html 

2543# xarticle.title_tex = new_title 

2544# xarticle.fpage = new_fpage 

2545# xarticle.lpage = new_lpage 

2546# xarticle.page_range = new_page_range 

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

2548# xarticle.body = body 

2549# xarticle.coi_statement = new_coi_statement 

2550# params['xobj'] = xarticle 

2551# 

2552# cmd = ptf_cmds.addArticlePtfCmd(params) 

2553# cmd.set_container(container) 

2554# cmd.add_collection(container.my_collection) 

2555# article = cmd.do() 

2556# 

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

2558# 

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

2560# params = { 

2561# # 'title_xml': new_title_xml, 

2562# # 'title_html': new_title_html, 

2563# # 'title_tex': new_title, 

2564# 'authors': new_authors, 

2565# 'page_count': new_page_count, 

2566# 'icon_location': new_icon_location, 

2567# 'body': body, 

2568# 'use_kwds': True, 

2569# 'kwds_fr': new_kwds_fr, 

2570# 'kwds_en': new_kwds_en, 

2571# 'kwd_uns_fr': new_kwd_uns_fr, 

2572# 'kwd_uns_en': new_kwd_uns_en 

2573# } 

2574# cmd = ptf_cmds.updateArticlePtfCmd(params) 

2575# cmd.set_article(article) 

2576# cmd.do() 

2577# 

2578# self.set_success_message() 

2579# 

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

2581 

2582 

2583@require_http_methods(["POST"]) 

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

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

2586 

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

2588 

2589 article = model_helpers.get_article(pid) 

2590 if article: 

2591 article.do_not_publish = not article.do_not_publish 

2592 article.save() 

2593 else: 

2594 raise Http404 

2595 

2596 return HttpResponseRedirect(next) 

2597 

2598 

2599@require_http_methods(["POST"]) 

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

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

2602 

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

2604 

2605 article = model_helpers.get_article(pid) 

2606 if article: 

2607 article.show_body = not article.show_body 

2608 article.save() 

2609 else: 

2610 raise Http404 

2611 

2612 return HttpResponseRedirect(next) 

2613 

2614 

2615class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView): 

2616 """ 

2617 API to get/post article metadata 

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

2619 """ 

2620 

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

2622 """ 

2623 we define here what fields we want in the form 

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

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

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

2627 self.fields_to_update = [ 

2628 "lang", 

2629 "atype", 

2630 "contributors", 

2631 "abstracts", 

2632 "kwds", 

2633 "titles", 

2634 "trans_title_html", 

2635 "title_html", 

2636 "title_xml", 

2637 "streams", 

2638 "ext_links", 

2639 ] 

2640 self.additional_fields = [ 

2641 "pid", 

2642 "doi", 

2643 "container_pid", 

2644 "pdf", 

2645 "illustration", 

2646 ] 

2647 self.editorial_tools = ["translation", "sidebar", "lang_selection"] 

2648 self.article_container_pid = "" 

2649 self.back_url = "trammel" 

2650 

2651 def save_data(self, data_article): 

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

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

2654 params = { 

2655 "pid": data_article.pid, 

2656 "export_folder": settings.MERSENNE_TMP_FOLDER, 

2657 "export_all": True, 

2658 "with_binary_files": False, 

2659 } 

2660 ptf_cmds.exportExtraDataPtfCmd(params).do() 

2661 

2662 def restore_data(self, article): 

2663 ptf_cmds.importExtraDataPtfCmd( 

2664 { 

2665 "pid": article.pid, 

2666 "import_folder": settings.MERSENNE_TMP_FOLDER, 

2667 } 

2668 ).do() 

2669 

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

2671 data = super().get(request, *args, **kwargs) 

2672 return data 

2673 

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

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

2676 if response["message"] == "OK": 

2677 return redirect( 

2678 "api-edit-article", 

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

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

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

2682 ) 

2683 else: 

2684 raise Http404 

2685 

2686 

2687class ArticleEditWithVueView(LoginRequiredMixin, TemplateView): 

2688 template_name = "article_form.html" 

2689 

2690 def get_success_url(self): 

2691 if self.kwargs["doi"]: 

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

2693 return reverse("mersenne_dashboard/published_articles") 

2694 

2695 def get_context_data(self, **kwargs): 

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

2697 if "doi" in self.kwargs: 

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

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

2700 

2701 return context 

2702 

2703 

2704class ArticleDeleteView(View): 

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

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

2707 article = get_object_or_404(Article, pid=pid) 

2708 

2709 try: 

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

2711 article.undeploy(mersenneSite) 

2712 

2713 cmd = ptf_cmds.addArticlePtfCmd( 

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

2715 ) 

2716 cmd.set_container(article.my_container) 

2717 cmd.set_object_to_be_deleted(article) 

2718 cmd.undo() 

2719 except Exception as exception: 

2720 return HttpResponseServerError(exception) 

2721 

2722 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200} 

2723 return JsonResponse(data) 

2724 

2725 

2726def get_messages_in_queue(): 

2727 app = Celery("ptf-tools") 

2728 # tasks = list(current_app.tasks) 

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

2730 print(tasks) 

2731 # i = app.control.inspect() 

2732 

2733 with app.connection_or_acquire() as conn: 

2734 remaining = conn.default_channel.queue_declare(queue="celery", passive=True).message_count 

2735 return remaining 

2736 

2737 

2738class FailedTasksListView(ListView): 

2739 model = TaskResult 

2740 queryset = TaskResult.objects.filter( 

2741 status="FAILURE", 

2742 task_name="ptf_tools.tasks.archive_numdam_issue", 

2743 ) 

2744 

2745 

2746class FailedTasksDeleteView(DeleteView): 

2747 model = TaskResult 

2748 success_url = reverse_lazy("tasks-failed") 

2749 

2750 

2751class FailedTasksRetryView(SingleObjectMixin, RedirectView): 

2752 model = TaskResult 

2753 

2754 @staticmethod 

2755 def retry_task(task): 

2756 colid, pid = (arg.strip("'") for arg in task.task_args.strip("()").split(", ")) 

2757 run_task(ArchiveNumdamIssueTask(colid, pid)) 

2758 

2759 task.delete() 

2760 

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

2762 self.retry_task(self.get_object()) 

2763 return reverse("tasks-failed") 

2764 

2765 

2766class NumdamView(TemplateView, history_views.HistoryContextMixin): 

2767 template_name = "numdam.html" 

2768 

2769 def get_context_data(self, **kwargs): 

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

2771 

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

2773 

2774 pre_issues = [] 

2775 prod_issues = [] 

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

2777 try: 

2778 response = requests.get(url) 

2779 if response.status_code == 200: 

2780 data = response.json() 

2781 if "issues" in data: 

2782 pre_issues = data["issues"] 

2783 except Exception: 

2784 pass 

2785 

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

2787 response = requests.get(url) 

2788 if response.status_code == 200: 

2789 data = response.json() 

2790 if "issues" in data: 

2791 prod_issues = data["issues"] 

2792 

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

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

2795 grouped = [ 

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

2797 ] 

2798 grouped_removed = [ 

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

2800 ] 

2801 context["added_issues"] = grouped 

2802 context["removed_issues"] = grouped_removed 

2803 

2804 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS 

2805 return context 

2806 

2807 

2808class TasksProgressView(View): 

2809 def get(self, *args, **kwargs): 

2810 task_name = self.kwargs.get("task", "archive_numdam_issue") 

2811 successes = TaskResult.objects.filter( 

2812 task_name=f"ptf_tools.tasks.{task_name}", status="SUCCESS" 

2813 ).count() 

2814 fails = TaskResult.objects.filter( 

2815 task_name=f"ptf_tools.tasks.{task_name}", status="FAILURE" 

2816 ).count() 

2817 last_task = ( 

2818 TaskResult.objects.filter( 

2819 task_name=f"ptf_tools.tasks.{task_name}", 

2820 status="SUCCESS", 

2821 ) 

2822 .order_by("-date_done") 

2823 .first() 

2824 ) 

2825 if last_task: 

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

2827 remaining = get_messages_in_queue() 

2828 all = successes + remaining 

2829 progress = int(successes * 100 / all) if all else 0 

2830 error_rate = int(fails * 100 / all) if all else 0 

2831 status = "consuming_queue" if (successes or fails) and not progress == 100 else "polling" 

2832 data = { 

2833 "status": status, 

2834 "progress": progress, 

2835 "total": all, 

2836 "remaining": remaining, 

2837 "successes": successes, 

2838 "fails": fails, 

2839 "error_rate": error_rate, 

2840 "last_task": last_task, 

2841 } 

2842 return JsonResponse(data) 

2843 

2844 

2845class NumdamArchiveView(RedirectView): 

2846 @staticmethod 

2847 def reset_task_results(): 

2848 TaskResult.objects.all().delete() 

2849 

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

2851 self.colid = kwargs["colid"] 

2852 

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

2854 return Http404 

2855 

2856 # we make sure archiving is not already running 

2857 if not get_messages_in_queue(): 

2858 self.reset_task_results() 

2859 response = requests.get(f"{settings.NUMDAM_URL}/api-all-collections/") 

2860 if response.status_code == 200: 

2861 data = sorted(response.json()["collections"]) 

2862 

2863 if self.colid != "ALL" and self.colid not in data: 

2864 return Http404 

2865 

2866 colids = [self.colid] if self.colid != "ALL" else data 

2867 

2868 with open( 

2869 os.path.join(settings.LOG_DIR, "archive.log"), "w", encoding="utf-8" 

2870 ) as file_: 

2871 file_.write("Archive " + " ".join([colid for colid in colids]) + "\n") 

2872 

2873 for colid in colids: 

2874 if colid not in settings.MERSENNE_COLLECTIONS: 

2875 run_task(ArchiveNumdamCollectionTask, colid) 

2876 

2877 return reverse("numdam") 

2878 

2879 

2880class DeployAllNumdamAPIView(View): 

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

2882 pids = [] 

2883 

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

2885 pids.append(obj.pid) 

2886 

2887 return pids 

2888 

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

2890 try: 

2891 pids, status, message = history_views.execute_and_record_func( 

2892 "deploy", "numdam", "numdam", self.internal_do, "numdam" 

2893 ) 

2894 except Exception as exception: 

2895 return HttpResponseServerError(exception) 

2896 

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

2898 return JsonResponse(data) 

2899 

2900 

2901class NumdamDeleteAPIView(View): 

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

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

2904 

2905 try: 

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

2907 obj.delete() 

2908 except Exception as exception: 

2909 return HttpResponseServerError(exception) 

2910 

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

2912 return JsonResponse(data) 

2913 

2914 

2915class ExtIdApiDetail(View): 

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

2917 extid = get_object_or_404( 

2918 ExtId, 

2919 resource__pid=kwargs["pid"], 

2920 id_type=kwargs["what"], 

2921 ) 

2922 return JsonResponse( 

2923 { 

2924 "pk": extid.pk, 

2925 "href": extid.get_href(), 

2926 "fetch": reverse( 

2927 "api-fetch-id", 

2928 args=( 

2929 extid.resource.pk, 

2930 extid.id_value, 

2931 extid.id_type, 

2932 "extid", 

2933 ), 

2934 ), 

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

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

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

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

2939 "is_valid": extid.checked, 

2940 } 

2941 ) 

2942 

2943 

2944class ExtIdFormTemplate(TemplateView): 

2945 template_name = "common/externalid_form.html" 

2946 

2947 def get_context_data(self, **kwargs): 

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

2949 context["sequence"] = kwargs["sequence"] 

2950 return context 

2951 

2952 

2953class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

2954 def get_context_data(self, **kwargs): 

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

2956 context["helper"] = PtfFormHelper 

2957 return context 

2958 

2959 def get_success_url(self): 

2960 self.post_process() 

2961 return self.object.bibitem.resource.get_absolute_url() 

2962 

2963 def post_process(self): 

2964 cmd = xml_cmds.updateBibitemCitationXmlCmd() 

2965 cmd.set_bibitem(self.object.bibitem) 

2966 cmd.do() 

2967 model_helpers.post_resource_updated(self.object.bibitem.resource) 

2968 

2969 

2970class BibItemIdCreate(BibItemIdFormView, CreateView): 

2971 model = BibItemId 

2972 form_class = BibItemIdForm 

2973 

2974 def get_context_data(self, **kwargs): 

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

2976 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"]) 

2977 return context 

2978 

2979 def get_initial(self): 

2980 initial = super().get_initial() 

2981 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"]) 

2982 return initial 

2983 

2984 def form_valid(self, form): 

2985 form.instance.checked = False 

2986 return super().form_valid(form) 

2987 

2988 

2989class BibItemIdUpdate(BibItemIdFormView, UpdateView): 

2990 model = BibItemId 

2991 form_class = BibItemIdForm 

2992 

2993 def get_context_data(self, **kwargs): 

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

2995 context["bibitem"] = self.object.bibitem 

2996 return context 

2997 

2998 

2999class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

3000 def get_context_data(self, **kwargs): 

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

3002 context["helper"] = PtfFormHelper 

3003 return context 

3004 

3005 def get_success_url(self): 

3006 self.post_process() 

3007 return self.object.resource.get_absolute_url() 

3008 

3009 def post_process(self): 

3010 model_helpers.post_resource_updated(self.object.resource) 

3011 

3012 

3013class ExtIdCreate(ExtIdFormView, CreateView): 

3014 model = ExtId 

3015 form_class = ExtIdForm 

3016 

3017 def get_context_data(self, **kwargs): 

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

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

3020 return context 

3021 

3022 def get_initial(self): 

3023 initial = super().get_initial() 

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

3025 return initial 

3026 

3027 def form_valid(self, form): 

3028 form.instance.checked = False 

3029 return super().form_valid(form) 

3030 

3031 

3032class ExtIdUpdate(ExtIdFormView, UpdateView): 

3033 model = ExtId 

3034 form_class = ExtIdForm 

3035 

3036 def get_context_data(self, **kwargs): 

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

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

3039 return context 

3040 

3041 

3042class BibItemIdApiDetail(View): 

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

3044 bibitemid = get_object_or_404( 

3045 BibItemId, 

3046 bibitem__resource__pid=kwargs["pid"], 

3047 bibitem__sequence=kwargs["seq"], 

3048 id_type=kwargs["what"], 

3049 ) 

3050 return JsonResponse( 

3051 { 

3052 "pk": bibitemid.pk, 

3053 "href": bibitemid.get_href(), 

3054 "fetch": reverse( 

3055 "api-fetch-id", 

3056 args=( 

3057 bibitemid.bibitem.pk, 

3058 bibitemid.id_value, 

3059 bibitemid.id_type, 

3060 "bibitemid", 

3061 ), 

3062 ), 

3063 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")), 

3064 "uncheck": reverse( 

3065 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive") 

3066 ), 

3067 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}), 

3068 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")), 

3069 "is_valid": bibitemid.checked, 

3070 } 

3071 ) 

3072 

3073 

3074class UpdateTexmfZipAPIView(View): 

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

3076 def copy_zip_files(src_folder, dest_folder): 

3077 os.makedirs(dest_folder, exist_ok=True) 

3078 

3079 zip_files = [ 

3080 os.path.join(src_folder, f) 

3081 for f in os.listdir(src_folder) 

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

3083 ] 

3084 for zip_file in zip_files: 

3085 resolver.copy_file(zip_file, dest_folder) 

3086 

3087 # Exceptions: specific zip/gz files 

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

3089 resolver.copy_file(zip_file, dest_folder) 

3090 

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

3092 resolver.copy_file(zip_file, dest_folder) 

3093 

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

3095 resolver.copy_file(gz_file, dest_folder) 

3096 

3097 src_folder = settings.CEDRAM_DISTRIB_FOLDER 

3098 

3099 dest_folder = os.path.join( 

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

3101 ) 

3102 

3103 try: 

3104 copy_zip_files(src_folder, dest_folder) 

3105 except Exception as exception: 

3106 return HttpResponseServerError(exception) 

3107 

3108 try: 

3109 dest_folder = os.path.join( 

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

3111 ) 

3112 copy_zip_files(src_folder, dest_folder) 

3113 except Exception as exception: 

3114 return HttpResponseServerError(exception) 

3115 

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

3117 return JsonResponse(data) 

3118 

3119 

3120class TestView(TemplateView): 

3121 template_name = "mersenne.html" 

3122 

3123 def get_context_data(self, **kwargs): 

3124 super().get_context_data(**kwargs) 

3125 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True) 

3126 model_data_converter.db_to_issue_data(issue) 

3127 

3128 

3129class TrammelTasksProgressView(View): 

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

3131 """ 

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

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

3134 """ 

3135 task_name = task 

3136 

3137 def get_event_data(): 

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

3139 

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

3141 remaining_messages = get_messages_in_queue() 

3142 

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

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

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

3146 

3147 all_tasks_count = all_tasks.count() 

3148 success_count = successed_tasks.count() 

3149 fail_count = failed_tasks.count() 

3150 

3151 all_count = all_tasks_count + remaining_messages 

3152 remaining_count = all_count - success_count - fail_count 

3153 

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

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

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

3157 

3158 last_task = successed_tasks.first() 

3159 last_task = ( 

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

3161 if last_task 

3162 else "" 

3163 ) 

3164 

3165 # SSE event format 

3166 event_data = { 

3167 "status": status, 

3168 "success_rate": success_rate, 

3169 "error_rate": error_rate, 

3170 "all_count": all_count, 

3171 "remaining_count": remaining_count, 

3172 "success_count": success_count, 

3173 "fail_count": fail_count, 

3174 "last_task": last_task, 

3175 } 

3176 

3177 return event_data 

3178 

3179 def stream_response(data): 

3180 # Send initial response headers 

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

3182 

3183 data = get_event_data() 

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

3185 if format == "json": 

3186 response = JsonResponse(data) 

3187 else: 

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

3189 return response 

3190 

3191 

3192class TrammelFailedTasksListView(ListView): 

3193 model = TaskResult 

3194 queryset = TaskResult.objects.filter( 

3195 status="FAILURE", 

3196 task_name="ptf_tools.tasks.archive_trammel_collection", 

3197 ) 

3198 

3199 

3200user_signed_up.connect(update_user_from_invite)