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

1625 statements  

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

1import io 

2import json 

3import os 

4import re 

5from datetime import datetime 

6from itertools import groupby 

7 

8import jsonpickle 

9import requests 

10from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin 

11from celery import Celery, current_app 

12from django.conf import settings 

13from django.contrib import messages 

14from django.contrib.auth.mixins import UserPassesTestMixin 

15from django.db.models import Q 

16from django.http import ( 

17 Http404, 

18 HttpRequest, 

19 HttpResponse, 

20 HttpResponseRedirect, 

21 HttpResponseServerError, 

22 JsonResponse, 

23) 

24from django.shortcuts import get_object_or_404, redirect, render 

25from django.urls import resolve, reverse, reverse_lazy 

26from django.utils import timezone 

27from django.views.decorators.http import require_http_methods 

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

29from django.views.generic.base import RedirectView 

30from django.views.generic.detail import SingleObjectMixin 

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

32from django_celery_results.models import TaskResult 

33from extra_views import ( 

34 CreateWithInlinesView, 

35 InlineFormSetFactory, 

36 NamedFormsetsMixin, 

37 UpdateWithInlinesView, 

38) 

39from ptf import model_data_converter, model_helpers, tex, utils 

40from ptf.cmds import ptf_cmds, xml_cmds 

41from ptf.cmds.base_cmds import make_int 

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

43from ptf.cmds.xml.xml_utils import replace_html_entities 

44from ptf.display import resolver 

45from ptf.exceptions import DOIException, PDFException, ServerUnderMaintenance 

46from ptf.model_data import create_issuedata, create_publisherdata, create_titledata 

47from ptf.models import ( 

48 Abstract, 

49 Article, 

50 BibItem, 

51 BibItemId, 

52 Collection, 

53 Container, 

54 ExtId, 

55 ExtLink, 

56 Resource, 

57 ResourceId, 

58 Title, 

59) 

60from ptf.views import ArticleEditFormWithVueAPIView 

61from pubmed.views import recordPubmed 

62from requests import Timeout 

63 

64from comments_moderation.utils import get_comments_for_home, is_comment_moderator 

65from history import models as history_models 

66from history import views as history_views 

67from ptf_tools.doaj import doaj_pid_register 

68from ptf_tools.doi import get_or_create_doibatch, recordDOI 

69from ptf_tools.forms import ( 

70 BibItemIdForm, 

71 CollectionForm, 

72 ContainerForm, 

73 DiffContainerForm, 

74 ExtIdForm, 

75 ExtLinkForm, 

76 FormSetHelper, 

77 ImportArticleForm, 

78 ImportContainerForm, 

79 PtfFormHelper, 

80 PtfLargeModalFormHelper, 

81 PtfModalFormHelper, 

82 RegisterPubmedForm, 

83 ResourceIdForm, 

84 get_article_choices, 

85) 

86from ptf_tools.indexingChecker import ReferencingChecker 

87from ptf_tools.models import ResourceInNumdam 

88from ptf_tools.tasks import ( 

89 archive_numdam_collection, 

90 archive_numdam_issue, 

91 archive_trammel_collection, 

92 archive_trammel_resource, 

93) 

94from ptf_tools.templatetags.tools_helpers import get_authorized_collections 

95from ptf_tools.utils import is_authorized_editor 

96 

97 

98def view_404(request: HttpRequest): 

99 """ 

100 Dummy view raising HTTP 404 exception. 

101 """ 

102 raise Http404 

103 

104 

105def check_collection(collection, server_url, server_type): 

106 """ 

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

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

109 """ 

110 

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

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

113 # First, upload the collection XML 

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

115 body = xml.encode("utf8") 

116 

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

118 if response.status_code == 200: 

119 # PUT http verb is used for update 

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

121 else: 

122 # POST http verb is used for creation 

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

124 

125 # Second, copy the collection images 

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

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

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

129 # /mersenne_prod_data during an upload to prod 

130 if server_type == "website": 

131 resolver.copy_binary_files( 

132 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

133 ) 

134 elif server_type == "numdam": 

135 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

136 if collection.pid in settings.NUMDAM_COLLECTIONS: 

137 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

138 

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

140 

141 

142def check_lock(): 

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

144 

145 

146def load_cedrics_article_choices(request): 

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

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

149 article_choices = get_article_choices(colid, issue) 

150 return render( 

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

152 ) 

153 

154 

155class ImportCedricsArticleFormView(FormView): 

156 template_name = "import_article.html" 

157 form_class = ImportArticleForm 

158 

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

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

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

162 

163 def get_success_url(self): 

164 if self.colid: 

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

166 return "/" 

167 

168 def get_context_data(self, **kwargs): 

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

170 context["colid"] = self.colid 

171 context["helper"] = PtfModalFormHelper 

172 return context 

173 

174 def get_form_kwargs(self): 

175 kwargs = super().get_form_kwargs() 

176 kwargs["colid"] = self.colid 

177 return kwargs 

178 

179 def form_valid(self, form): 

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

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

182 return super().form_valid(form) 

183 

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

185 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd( 

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

187 ) 

188 cmd.do() 

189 

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

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

192 issue = request.POST["issue"] 

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

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

195 

196 import_args = [self] 

197 import_kwargs = {} 

198 

199 try: 

200 _, status, message = history_views.execute_and_record_func( 

201 "import", 

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

203 self.colid, 

204 self.import_cedrics_article, 

205 "", 

206 False, 

207 *import_args, 

208 **import_kwargs, 

209 ) 

210 

211 messages.success( 

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

213 ) 

214 

215 except Exception as exception: 

216 messages.error( 

217 self.request, 

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

219 ) 

220 

221 return redirect(self.get_success_url()) 

222 

223 

224class ImportCedricsIssueView(FormView): 

225 template_name = "import_container.html" 

226 form_class = ImportContainerForm 

227 

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

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

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

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

232 

233 def get_success_url(self): 

234 if self.filename: 

235 return reverse( 

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

237 ) 

238 return "/" 

239 

240 def get_context_data(self, **kwargs): 

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

242 context["colid"] = self.colid 

243 context["helper"] = PtfModalFormHelper 

244 return context 

245 

246 def get_form_kwargs(self): 

247 kwargs = super().get_form_kwargs() 

248 kwargs["colid"] = self.colid 

249 kwargs["to_appear"] = self.to_appear 

250 return kwargs 

251 

252 def form_valid(self, form): 

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

254 return super().form_valid(form) 

255 

256 

257class DiffCedricsIssueView(FormView): 

258 template_name = "diff_container_form.html" 

259 form_class = DiffContainerForm 

260 diffs = None 

261 xissue = None 

262 xissue_encoded = None 

263 

264 def get_success_url(self): 

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

266 

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

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

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

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

271 

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

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

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

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

276 self.remove_email = self.remove_mail == "on" 

277 self.remove_date_prod = self.remove_date_prod == "on" 

278 

279 try: 

280 result, status, message = history_views.execute_and_record_func( 

281 "import", 

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

283 self.colid, 

284 self.diff_cedrics_issue, 

285 "", 

286 True, 

287 ) 

288 except Exception as exception: 

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

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

291 return HttpResponseRedirect(self.get_success_url()) 

292 

293 no_conflict = result[0] 

294 self.diffs = result[1] 

295 self.xissue = result[2] 

296 

297 if no_conflict: 

298 # Proceed with the import 

299 self.form_valid(self.get_form()) 

300 return redirect(self.get_success_url()) 

301 else: 

302 # Display the diff template 

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

304 

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

306 

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

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

309 data = request.POST["xissue_encoded"] 

310 self.xissue = jsonpickle.decode(data) 

311 

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

313 

314 def get_context_data(self, **kwargs): 

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

316 context["colid"] = self.colid 

317 context["diff"] = self.diffs 

318 context["filename"] = self.filename 

319 context["xissue_encoded"] = self.xissue_encoded 

320 return context 

321 

322 def get_form_kwargs(self): 

323 kwargs = super().get_form_kwargs() 

324 kwargs["colid"] = self.colid 

325 return kwargs 

326 

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

328 params = { 

329 "colid": self.colid, 

330 "input_file": self.filename, 

331 "remove_email": self.remove_mail, 

332 "remove_date_prod": self.remove_date_prod, 

333 "diff_only": True, 

334 } 

335 

336 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

339 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

340 else: 

341 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

342 

343 result = cmd.do() 

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

345 messages.warning( 

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

347 ) 

348 

349 return result 

350 

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

352 # modify xissue with data_issue if params to override 

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

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

355 if issue: 

356 data_issue = model_data_converter.db_to_issue_data(issue) 

357 for xarticle in self.xissue.articles: 

358 filter_articles = [ 

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

360 ] 

361 if len(filter_articles) > 0: 

362 db_article = filter_articles[0] 

363 xarticle.coi_statement = db_article.coi_statement 

364 xarticle.kwds = db_article.kwds 

365 xarticle.contrib_groups = db_article.contrib_groups 

366 

367 params = { 

368 "colid": self.colid, 

369 "xissue": self.xissue, 

370 "input_file": self.filename, 

371 } 

372 

373 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

376 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

377 else: 

378 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

379 

380 cmd.do() 

381 

382 def form_valid(self, form): 

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

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

385 else: 

386 import_kwargs = {} 

387 import_args = [self] 

388 

389 try: 

390 _, status, message = history_views.execute_and_record_func( 

391 "import", 

392 self.xissue.pid, 

393 self.kwargs["colid"], 

394 self.import_cedrics_issue, 

395 "", 

396 False, 

397 *import_args, 

398 **import_kwargs, 

399 ) 

400 except Exception as exception: 

401 messages.error( 

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

403 ) 

404 return super().form_invalid(form) 

405 

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

407 return super().form_valid(form) 

408 

409 

410class BibtexAPIView(View): 

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

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

413 all_bibtex = "" 

414 if pid: 

415 article = model_helpers.get_article(pid) 

416 if article: 

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

418 bibtex_array = bibitem.get_bibtex() 

419 last = len(bibtex_array) 

420 i = 1 

421 for bibtex in bibtex_array: 

422 if i > 1 and i < last: 

423 all_bibtex += " " 

424 all_bibtex += bibtex + "\n" 

425 i += 1 

426 

427 data = {"bibtex": all_bibtex} 

428 return JsonResponse(data) 

429 

430 

431class MatchingAPIView(View): 

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

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

434 

435 url = settings.MATCHING_URL 

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

437 

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

439 

440 if settings.DEBUG: 

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

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

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

444 f.close() 

445 

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

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

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

449 

450 if settings.DEBUG: 

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

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

453 text = body 

454 f.write(text) 

455 f.close() 

456 

457 resource = model_helpers.get_resource(pid) 

458 obj = resource.cast() 

459 colid = obj.get_collection().pid 

460 

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

462 

463 cmd = xml_cmds.addOrUpdateIssueXmlCmd( 

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

465 ) 

466 cmd.do() 

467 

468 print("Matching finished") 

469 return JsonResponse(data) 

470 

471 

472class ImportAllAPIView(View): 

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

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

475 

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

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

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

479 

480 resource = model_helpers.get_resource(pid) 

481 if not resource: 

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

483 body = utils.get_file_content_in_utf8(file) 

484 journals = xml_cmds.addCollectionsXmlCmd( 

485 { 

486 "body": body, 

487 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

488 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

489 } 

490 ).do() 

491 if not journals: 

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

493 resource = journals[0] 

494 # resolver.copy_binary_files( 

495 # resource, 

496 # settings.MATHDOC_ARCHIVE_FOLDER, 

497 # settings.MERSENNE_TEST_DATA_FOLDER) 

498 

499 obj = resource.cast() 

500 

501 if obj.classname != "Collection": 

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

503 

504 cmd = xml_cmds.collectEntireCollectionXmlCmd( 

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

506 ) 

507 pids = cmd.do() 

508 

509 return pids 

510 

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

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

513 

514 try: 

515 pids, status, message = history_views.execute_and_record_func( 

516 "import", pid, pid, self.internal_do 

517 ) 

518 except Timeout as exception: 

519 return HttpResponse(exception, status=408) 

520 except Exception as exception: 

521 return HttpResponseServerError(exception) 

522 

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

524 return JsonResponse(data) 

525 

526 

527class DeployAllAPIView(View): 

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

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

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

531 

532 pids = [] 

533 

534 collection = model_helpers.get_collection(pid) 

535 if not collection: 

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

537 

538 if site == "numdam": 

539 server_url = settings.NUMDAM_PRE_URL 

540 elif site != "ptf_tools": 

541 server_url = getattr(collection, site)() 

542 if not server_url: 

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

544 

545 if site != "ptf_tools": 

546 # check if the collection exists on the server 

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

548 # image...) 

549 check_collection(collection, server_url, site) 

550 

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

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

553 pids.append(issue.pid) 

554 

555 return pids 

556 

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

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

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

560 

561 try: 

562 pids, status, message = history_views.execute_and_record_func( 

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

564 ) 

565 except Timeout as exception: 

566 return HttpResponse(exception, status=408) 

567 except Exception as exception: 

568 return HttpResponseServerError(exception) 

569 

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

571 return JsonResponse(data) 

572 

573 

574class AddIssuePDFView(View): 

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

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

577 self.pid = None 

578 self.issue = None 

579 self.collection = None 

580 self.site = "test_website" 

581 

582 def post_to_site(self, url): 

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

584 status = response.status_code 

585 if not (199 < status < 205): 

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

587 if status == 503: 

588 raise ServerUnderMaintenance(response.text) 

589 else: 

590 raise RuntimeError(response.text) 

591 

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

593 """ 

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

595 """ 

596 

597 issue_pid = self.issue.pid 

598 colid = self.collection.pid 

599 

600 if self.site == "website": 

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

602 resolver.copy_binary_files( 

603 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

604 ) 

605 else: 

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

607 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid) 

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

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

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

611 

612 to_path = resolver.get_disk_location( 

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

614 ) 

615 resolver.copy_file(from_path, to_path) 

616 

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

618 

619 if self.site == "test_website": 

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

621 absolute_url = self.request.build_absolute_uri(url) 

622 self.post_to_site(absolute_url) 

623 

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

625 absolute_url = server_url + url 

626 # Post to the test or production website 

627 self.post_to_site(absolute_url) 

628 

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

630 """ 

631 Send an issue PDF to the test or production website 

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

633 :param args: 

634 :param kwargs: 

635 :return: 

636 """ 

637 if check_lock(): 

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

639 messages.error(self.request, m) 

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

641 

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

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

644 

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

646 if not self.issue: 

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

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

649 

650 try: 

651 pids, status, message = history_views.execute_and_record_func( 

652 "deploy", 

653 self.pid, 

654 self.collection.pid, 

655 self.internal_do, 

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

657 ) 

658 

659 except Timeout as exception: 

660 return HttpResponse(exception, status=408) 

661 except Exception as exception: 

662 return HttpResponseServerError(exception) 

663 

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

665 return JsonResponse(data) 

666 

667 

668class ArchiveAllAPIView(View): 

669 """ 

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

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

672 @return array of issues pid 

673 """ 

674 

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

676 collection = kwargs["collection"] 

677 pids = [] 

678 colid = collection.pid 

679 

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

681 if os.path.isfile(logfile): 

682 os.remove(logfile) 

683 

684 ptf_cmds.exportPtfCmd( 

685 { 

686 "pid": colid, 

687 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

688 "with_binary_files": True, 

689 "for_archive": True, 

690 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

691 } 

692 ).do() 

693 

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

695 if os.path.isfile(cedramcls): 

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

697 resolver.create_folder(dest_folder) 

698 resolver.copy_file(cedramcls, dest_folder) 

699 

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

701 qs = issue.article_set.filter( 

702 date_online_first__isnull=True, date_published__isnull=True 

703 ) 

704 if qs.count() == 0: 

705 pids.append(issue.pid) 

706 

707 return pids 

708 

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

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

711 

712 collection = model_helpers.get_collection(pid) 

713 if not collection: 

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

715 

716 dict_ = {"collection": collection} 

717 args_ = [self] 

718 

719 try: 

720 pids, status, message = history_views.execute_and_record_func( 

721 "archive", pid, pid, self.internal_do, "", False, *args_, **dict_ 

722 ) 

723 except Timeout as exception: 

724 return HttpResponse(exception, status=408) 

725 except Exception as exception: 

726 return HttpResponseServerError(exception) 

727 

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

729 return JsonResponse(data) 

730 

731 

732class CreateAllDjvuAPIView(View): 

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

734 issue = kwargs["issue"] 

735 pids = [issue.pid] 

736 

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

738 pids.append(article.pid) 

739 

740 return pids 

741 

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

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

744 issue = model_helpers.get_container(pid) 

745 if not issue: 

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

747 

748 try: 

749 dict_ = {"issue": issue} 

750 args_ = [self] 

751 

752 pids, status, message = history_views.execute_and_record_func( 

753 "numdam", 

754 pid, 

755 issue.get_collection().pid, 

756 self.internal_do, 

757 "", 

758 False, 

759 *args_, 

760 **dict_, 

761 ) 

762 except Exception as exception: 

763 return HttpResponseServerError(exception) 

764 

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

766 return JsonResponse(data) 

767 

768 

769class ImportJatsContainerAPIView(View): 

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

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

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

773 

774 if pid and colid: 

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

776 

777 cmd = xml_cmds.addOrUpdateContainerXmlCmd( 

778 { 

779 "body": body, 

780 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

781 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

782 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

783 } 

784 ) 

785 container = cmd.do() 

786 if len(cmd.warnings) > 0: 

787 messages.warning( 

788 self.request, 

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

790 ) 

791 

792 if not container: 

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

794 

795 # resolver.copy_binary_files( 

796 # container, 

797 # settings.MATHDOC_ARCHIVE_FOLDER, 

798 # settings.MERSENNE_TEST_DATA_FOLDER) 

799 # 

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

801 # resolver.copy_binary_files( 

802 # article, 

803 # settings.MATHDOC_ARCHIVE_FOLDER, 

804 # settings.MERSENNE_TEST_DATA_FOLDER) 

805 else: 

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

807 

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

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

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

811 

812 try: 

813 _, status, message = history_views.execute_and_record_func( 

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

815 ) 

816 except Timeout as exception: 

817 return HttpResponse(exception, status=408) 

818 except Exception as exception: 

819 return HttpResponseServerError(exception) 

820 

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

822 return JsonResponse(data) 

823 

824 

825class DeployCollectionAPIView(View): 

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

827 

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

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

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

831 

832 collection = model_helpers.get_collection(colid) 

833 if not collection: 

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

835 

836 if site == "numdam": 

837 server_url = settings.NUMDAM_PRE_URL 

838 else: 

839 server_url = getattr(collection, site)() 

840 if not server_url: 

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

842 

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

844 check_collection(collection, server_url, site) 

845 

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

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

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

849 

850 try: 

851 _, status, message = history_views.execute_and_record_func( 

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

853 ) 

854 except Timeout as exception: 

855 return HttpResponse(exception, status=408) 

856 except Exception as exception: 

857 return HttpResponseServerError(exception) 

858 

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

860 return JsonResponse(data) 

861 

862 

863class DeployJatsResourceAPIView(View): 

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

865 

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

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

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

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

870 

871 if site == "ptf_tools": 

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

873 if check_lock(): 

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

875 messages.error(self.request, msg) 

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

877 

878 resource = model_helpers.get_resource(pid) 

879 if not resource: 

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

881 

882 obj = resource.cast() 

883 article = None 

884 if obj.classname == "Article": 

885 article = obj 

886 container = article.my_container 

887 articles_to_deploy = [article] 

888 else: 

889 container = obj 

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

891 

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

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

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

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

896 

897 collection = container.get_top_collection() 

898 colid = collection.pid 

899 djvu_exception = None 

900 

901 if site == "numdam": 

902 server_url = settings.NUMDAM_PRE_URL 

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

904 

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

906 # Add Djvu (before exporting the XML) 

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

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

909 try: 

910 cmd = ptf_cmds.addDjvuPtfCmd() 

911 cmd.set_resource(art) 

912 cmd.do() 

913 except Exception as e: 

914 # Djvu are optional. 

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

916 djvu_exception = e 

917 else: 

918 server_url = getattr(collection, site)() 

919 if not server_url: 

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

921 

922 # check if the collection exists on the server 

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

924 # image...) 

925 if article is None: 

926 check_collection(collection, server_url, site) 

927 

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

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

930 if site == "website": 

931 file_.write( 

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

933 pid 

934 ) 

935 ) 

936 

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

938 cmd = ptf_cmds.publishResourcePtfCmd() 

939 cmd.set_resource(resource) 

940 updated_articles = cmd.do() 

941 

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

943 

944 mersenneSite = model_helpers.get_site_mersenne(colid) 

945 # create or update deployed_date on container and articles 

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

947 

948 for art in articles_to_deploy: 

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

950 if art.my_container.year is None: 

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

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

953 

954 file_.write( 

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

956 art.pid, art.date_online_first, art.date_published 

957 ) 

958 ) 

959 

960 if article is None: 

961 resolver.copy_binary_files( 

962 container, 

963 settings.MERSENNE_TEST_DATA_FOLDER, 

964 settings.MERSENNE_PROD_DATA_FOLDER, 

965 ) 

966 

967 for art in articles_to_deploy: 

968 resolver.copy_binary_files( 

969 art, 

970 settings.MERSENNE_TEST_DATA_FOLDER, 

971 settings.MERSENNE_PROD_DATA_FOLDER, 

972 ) 

973 

974 elif site == "test_website": 

975 # create date_pre_published on articles without date_pre_published 

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

977 cmd.set_resource(resource) 

978 updated_articles = cmd.do() 

979 

980 tex.create_frontpage(colid, container, updated_articles) 

981 

982 export_to_website = site == "website" 

983 

984 if article is None: 

985 with_djvu = site == "numdam" 

986 xml = ptf_cmds.exportPtfCmd( 

987 { 

988 "pid": pid, 

989 "with_djvu": with_djvu, 

990 "export_to_website": export_to_website, 

991 } 

992 ).do() 

993 body = xml.encode("utf8") 

994 

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

996 url = server_url + reverse("issue_upload") 

997 else: 

998 url = server_url + reverse("book_upload") 

999 

1000 # verify=False: ignore TLS certificate 

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

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

1003 else: 

1004 xml = ptf_cmds.exportPtfCmd( 

1005 { 

1006 "pid": pid, 

1007 "with_djvu": False, 

1008 "article_standalone": True, 

1009 "collection_pid": collection.pid, 

1010 "export_to_website": export_to_website, 

1011 "export_folder": settings.LOG_DIR, 

1012 } 

1013 ).do() 

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

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

1016 xml_file = io.StringIO(xml) 

1017 files = {"xml": xml_file} 

1018 

1019 url = server_url + reverse( 

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

1021 ) 

1022 # verify=False: ignore TLS certificate 

1023 header = {} 

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

1025 

1026 status = response.status_code 

1027 

1028 if 199 < status < 205: 

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

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

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

1032 # /mersenne_prod_data during an upload to prod 

1033 if site == "website": 

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

1035 if container.doi: 

1036 recordDOI(container) 

1037 

1038 for art in articles_to_deploy: 

1039 # record DOI automatically when deploying in prod 

1040 

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

1042 recordDOI(art) 

1043 

1044 if colid == "CRBIOL": 

1045 recordPubmed( 

1046 art, force_update=False, updated_articles=updated_articles 

1047 ) 

1048 

1049 if colid == "PCJ": 

1050 self.update_pcj_editor(updated_articles) 

1051 

1052 # Archive the container or the article 

1053 if article is None: 

1054 archive_trammel_resource.delay( 

1055 colid=colid, 

1056 pid=pid, 

1057 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1058 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1059 ) 

1060 else: 

1061 archive_trammel_resource.delay( 

1062 colid=colid, 

1063 pid=pid, 

1064 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1065 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1066 article_doi=article.doi, 

1067 ) 

1068 # cmd = ptf_cmds.archiveIssuePtfCmd({ 

1069 # "pid": pid, 

1070 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1071 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER}) 

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

1073 # cmd.do() 

1074 

1075 elif site == "numdam": 

1076 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

1077 if colid in settings.NUMDAM_COLLECTIONS: 

1078 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

1079 

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

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

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

1083 

1084 elif status == 503: 

1085 raise ServerUnderMaintenance(response.text) 

1086 else: 

1087 raise RuntimeError(response.text) 

1088 

1089 if djvu_exception: 

1090 raise djvu_exception 

1091 

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

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

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

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

1096 

1097 try: 

1098 _, status, message = history_views.execute_and_record_func( 

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

1100 ) 

1101 except Timeout as exception: 

1102 return HttpResponse(exception, status=408) 

1103 except Exception as exception: 

1104 return HttpResponseServerError(exception) 

1105 

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

1107 return JsonResponse(data) 

1108 

1109 def update_pcj_editor(self, updated_articles): 

1110 for article in updated_articles: 

1111 data = { 

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

1113 "article_number": article.article_number, 

1114 } 

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

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

1117 

1118 

1119class DeployTranslatedArticleAPIView(CsrfExemptMixin, View): 

1120 article = None 

1121 

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

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

1124 

1125 translation = None 

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

1127 if trans_article.lang == lang: 

1128 translation = trans_article 

1129 

1130 if translation is None: 

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

1132 

1133 collection = self.article.get_top_collection() 

1134 colid = collection.pid 

1135 container = self.article.my_container 

1136 

1137 if translation.date_published is None: 

1138 # Add date posted 

1139 cmd = ptf_cmds.publishResourcePtfCmd() 

1140 cmd.set_resource(translation) 

1141 updated_articles = cmd.do() 

1142 

1143 # Recompile PDF to add the date posted 

1144 try: 

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

1146 except Exception: 

1147 raise PDFException( 

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

1149 ) 

1150 

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

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

1153 resolver.copy_binary_files( 

1154 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

1155 ) 

1156 

1157 # Deploy in prod 

1158 xml = ptf_cmds.exportPtfCmd( 

1159 { 

1160 "pid": self.article.pid, 

1161 "with_djvu": False, 

1162 "article_standalone": True, 

1163 "collection_pid": colid, 

1164 "export_to_website": True, 

1165 "export_folder": settings.LOG_DIR, 

1166 } 

1167 ).do() 

1168 xml_file = io.StringIO(xml) 

1169 files = {"xml": xml_file} 

1170 

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

1172 if not server_url: 

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

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

1175 header = {} 

1176 

1177 try: 

1178 response = requests.post( 

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

1180 ) # verify: ignore TLS certificate 

1181 status = response.status_code 

1182 except requests.exceptions.ConnectionError: 

1183 raise ServerUnderMaintenance( 

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

1185 ) 

1186 

1187 # Register translation in Crossref 

1188 if 199 < status < 205: 

1189 if self.article.allow_crossref(): 

1190 try: 

1191 recordDOI(translation) 

1192 except Exception: 

1193 raise DOIException( 

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

1195 ) 

1196 

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

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

1199 self.article = model_helpers.get_article_by_doi(doi) 

1200 if self.article is None: 

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

1202 

1203 try: 

1204 _, status, message = history_views.execute_and_record_func( 

1205 "deploy", 

1206 self.article.pid, 

1207 self.article.get_top_collection().pid, 

1208 self.internal_do, 

1209 "website", 

1210 ) 

1211 except Timeout as exception: 

1212 return HttpResponse(exception, status=408) 

1213 except Exception as exception: 

1214 return HttpResponseServerError(exception) 

1215 

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

1217 return JsonResponse(data) 

1218 

1219 

1220class DeleteJatsIssueAPIView(View): 

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

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

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

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

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

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

1227 status = 200 

1228 

1229 issue = model_helpers.get_container(pid) 

1230 if not issue: 

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

1232 try: 

1233 mersenneSite = model_helpers.get_site_mersenne(colid) 

1234 

1235 if site == "ptf_tools": 

1236 if issue.is_deployed(mersenneSite): 

1237 issue.undeploy(mersenneSite) 

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

1239 article.undeploy(mersenneSite) 

1240 

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

1242 

1243 cmd = ptf_cmds.addContainerPtfCmd( 

1244 { 

1245 "pid": issue.pid, 

1246 "ctype": "issue", 

1247 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

1248 } 

1249 ) 

1250 cmd.set_provider(p) 

1251 cmd.add_collection(issue.get_collection()) 

1252 cmd.set_object_to_be_deleted(issue) 

1253 cmd.undo() 

1254 

1255 else: 

1256 if site == "numdam": 

1257 server_url = settings.NUMDAM_PRE_URL 

1258 else: 

1259 collection = issue.get_collection() 

1260 server_url = getattr(collection, site)() 

1261 

1262 if not server_url: 

1263 message = "The collection has no " + site 

1264 status = 500 

1265 else: 

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

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

1268 status = response.status_code 

1269 

1270 if status == 404: 

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

1272 elif status > 204: 

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

1274 message = body[:1000] 

1275 else: 

1276 status = 200 

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

1278 if site == "website": 

1279 if issue.is_deployed(mersenneSite): 

1280 issue.undeploy(mersenneSite) 

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

1282 article.undeploy(mersenneSite) 

1283 # delete article binary files 

1284 folder = article.get_relative_folder() 

1285 resolver.delete_object_folder( 

1286 folder, 

1287 to_folder=settings.MERSENNE_PROD_DATA_FORLDER, 

1288 ) 

1289 # delete issue binary files 

1290 folder = issue.get_relative_folder() 

1291 resolver.delete_object_folder( 

1292 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER 

1293 ) 

1294 

1295 except Timeout as exception: 

1296 return HttpResponse(exception, status=408) 

1297 except Exception as exception: 

1298 return HttpResponseServerError(exception) 

1299 

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

1301 return JsonResponse(data) 

1302 

1303 

1304class ArchiveIssueAPIView(View): 

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

1306 try: 

1307 pid = kwargs["pid"] 

1308 colid = kwargs["colid"] 

1309 except IndexError: 

1310 raise Http404 

1311 

1312 try: 

1313 cmd = ptf_cmds.archiveIssuePtfCmd( 

1314 { 

1315 "pid": pid, 

1316 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1317 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

1318 } 

1319 ) 

1320 result_, status, message = history_views.execute_and_record_func( 

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

1322 ) 

1323 except Exception as exception: 

1324 return HttpResponseServerError(exception) 

1325 

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

1327 return JsonResponse(data) 

1328 

1329 

1330class CreateDjvuAPIView(View): 

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

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

1333 

1334 resource = model_helpers.get_resource(pid) 

1335 cmd = ptf_cmds.addDjvuPtfCmd() 

1336 cmd.set_resource(resource) 

1337 cmd.do() 

1338 

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

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

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

1342 

1343 try: 

1344 _, status, message = history_views.execute_and_record_func( 

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

1346 ) 

1347 except Exception as exception: 

1348 return HttpResponseServerError(exception) 

1349 

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

1351 return JsonResponse(data) 

1352 

1353 

1354class PTFToolsHomeView(LoginRequiredMixin, View): 

1355 """ 

1356 Home Page. 

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

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

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

1360 - Comment moderator -> Comments dashboard 

1361 - Others -> 404 response 

1362 """ 

1363 

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

1365 # Staff or user with authorized collections 

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

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

1368 

1369 colids = get_authorized_collections(request.user) 

1370 is_mod = is_comment_moderator(request.user) 

1371 

1372 # The user has no rights 

1373 if not (colids or is_mod): 

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

1375 # Comment moderator only 

1376 elif not colids: 

1377 return HttpResponseRedirect(reverse("comment_list")) 

1378 

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

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

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

1382 

1383 # User with multiple authorized collections - Special home 

1384 context = {} 

1385 context["overview"] = True 

1386 

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

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

1389 

1390 # Comments summary 

1391 try: 

1392 error, comments_data = get_comments_for_home(request.user) 

1393 except AttributeError: 

1394 error, comments_data = True, {} 

1395 

1396 context["comment_server_ok"] = False 

1397 

1398 if not error: 

1399 context["comment_server_ok"] = True 

1400 if comments_data: 

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

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

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

1404 

1405 # TODO: Translations summary 

1406 context["translation_server_ok"] = False 

1407 

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

1409 context["collections"] = sorted( 

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

1411 ) 

1412 

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

1414 

1415 

1416class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin): 

1417 columns = 5 

1418 

1419 def get_common_context_data(self, **kwargs): 

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

1421 now = timezone.now() 

1422 curyear = now.year 

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

1424 

1425 context["collections"] = settings.MERSENNE_COLLECTIONS 

1426 context["containers_to_be_published"] = [] 

1427 context["last_col_events"] = [] 

1428 

1429 event = history_models.get_history_last_event_by("clockss", "ALL") 

1430 clockss_gap = history_models.get_gap(now, event) 

1431 

1432 context["years"] = years 

1433 context["clockss_gap"] = clockss_gap 

1434 

1435 return context 

1436 

1437 def calculate_articles_and_pages(self, pid, years): 

1438 data_by_year = [] 

1439 total_articles = [0] * len(years) 

1440 total_pages = [0] * len(years) 

1441 

1442 for year in years: 

1443 articles = self.get_articles_for_year(pid, year) 

1444 articles_count = articles.count() 

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

1446 

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

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

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

1450 

1451 return data_by_year, total_articles, total_pages 

1452 

1453 def get_articles_for_year(self, pid, year): 

1454 return Article.objects.filter( 

1455 Q(my_container__my_collection__pid=pid) 

1456 & ( 

1457 Q(date_published__year=year, date_online_first__isnull=True) 

1458 | Q(date_online_first__year=year) 

1459 ) 

1460 ).prefetch_related("resourcecount_set") 

1461 

1462 

1463class PublishedArticlesDashboardView(BaseMersenneDashboardView): 

1464 template_name = "dashboard/published_articles.html" 

1465 

1466 def get_context_data(self, **kwargs): 

1467 context = self.get_common_context_data(**kwargs) 

1468 years = context["years"] 

1469 

1470 published_articles = [] 

1471 total_published_articles = [ 

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

1473 ] 

1474 

1475 for pid in settings.MERSENNE_COLLECTIONS: 

1476 if pid != "MERSENNE": 

1477 articles_data, total_articles, total_pages = self.calculate_articles_and_pages( 

1478 pid, years 

1479 ) 

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

1481 

1482 for i, year in enumerate(years): 

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

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

1485 

1486 context["published_articles"] = published_articles 

1487 context["total_published_articles"] = total_published_articles 

1488 

1489 return context 

1490 

1491 

1492class CreatedVolumesDashboardView(BaseMersenneDashboardView): 

1493 template_name = "dashboard/created_volumes.html" 

1494 

1495 def get_context_data(self, **kwargs): 

1496 context = self.get_common_context_data(**kwargs) 

1497 years = context["years"] 

1498 

1499 created_volumes = [] 

1500 total_created_volumes = [ 

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

1502 ] 

1503 

1504 for pid in settings.MERSENNE_COLLECTIONS: 

1505 if pid != "MERSENNE": 

1506 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages( 

1507 pid, years 

1508 ) 

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

1510 

1511 for i, year in enumerate(years): 

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

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

1514 

1515 context["created_volumes"] = created_volumes 

1516 context["total_created_volumes"] = total_created_volumes 

1517 

1518 return context 

1519 

1520 def calculate_volumes_and_pages(self, pid, years): 

1521 data_by_year = [] 

1522 total_articles = [0] * len(years) 

1523 total_pages = [0] * len(years) 

1524 

1525 for year in years: 

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

1527 articles_count = 0 

1528 page_count = 0 

1529 

1530 for issue in issues: 

1531 articles = issue.article_set.filter( 

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

1533 ).prefetch_related("resourcecount_set") 

1534 

1535 articles_count += articles.count() 

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

1537 

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

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

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

1541 

1542 return data_by_year, total_articles, total_pages 

1543 

1544 

1545class ReferencingDashboardView(BaseMersenneDashboardView): 

1546 template_name = "dashboard/referencing.html" 

1547 

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

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

1550 comp = ReferencingChecker() 

1551 journal = comp.check_references(colid) 

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

1553 

1554 

1555class BaseCollectionView(TemplateView): 

1556 def get_context_data(self, **kwargs): 

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

1558 aid = context.get("aid") 

1559 year = context.get("year") 

1560 

1561 if aid and year: 

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

1563 

1564 return context 

1565 

1566 def get_collection(self, aid, year): 

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

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

1569 

1570 

1571class ArticleListView(BaseCollectionView): 

1572 template_name = "collection-list.html" 

1573 

1574 def get_collection(self, aid, year): 

1575 return Article.objects.filter( 

1576 Q(my_container__my_collection__pid=aid) 

1577 & ( 

1578 Q(date_published__year=year, date_online_first__isnull=True) 

1579 | Q(date_online_first__year=year) 

1580 ) 

1581 ).prefetch_related("resourcecount_set") 

1582 

1583 

1584class VolumeListView(BaseCollectionView): 

1585 template_name = "collection-list.html" 

1586 

1587 def get_collection(self, aid, year): 

1588 return Article.objects.filter( 

1589 Q(my_container__my_collection__pid=aid, my_container__year=year) 

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

1591 ).prefetch_related("resourcecount_set") 

1592 

1593 

1594class DOAJResourceRegisterView(View): 

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

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

1597 resource = model_helpers.get_resource(pid) 

1598 if resource is None: 

1599 raise Http404 

1600 

1601 try: 

1602 data = {} 

1603 doaj_meta, response = doaj_pid_register(pid) 

1604 if response is None: 

1605 return HttpResponse(status=204) 

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

1607 data.update(doaj_meta) 

1608 else: 

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

1610 except Timeout as exception: 

1611 return HttpResponse(exception, status=408) 

1612 except Exception as exception: 

1613 return HttpResponseServerError(exception) 

1614 return JsonResponse(data) 

1615 

1616 

1617class CROSSREFResourceRegisterView(View): 

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

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

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

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

1622 if not request.user.is_superuser: 

1623 force = None 

1624 

1625 resource = model_helpers.get_resource(pid) 

1626 if resource is None: 

1627 raise Http404 

1628 

1629 resource = resource.cast() 

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

1631 try: 

1632 data = meth(resource, force) 

1633 except Timeout as exception: 

1634 return HttpResponse(exception, status=408) 

1635 except Exception as exception: 

1636 return HttpResponseServerError(exception) 

1637 return JsonResponse(data) 

1638 

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

1640 result = {"status": 404} 

1641 if ( 

1642 article.doi 

1643 and not article.do_not_publish 

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

1645 ): 

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

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

1648 result = recordDOI(article) 

1649 return result 

1650 

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

1652 return recordDOI(collection) 

1653 

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

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

1656 

1657 if container.ctype == "issue": 

1658 if container.doi: 

1659 result = recordDOI(container) 

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

1661 return result 

1662 if force == "force": 

1663 articles = container.article_set.exclude( 

1664 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True 

1665 ) 

1666 else: 

1667 articles = container.article_set.exclude( 

1668 doi__isnull=True, 

1669 do_not_publish=True, 

1670 date_published__isnull=True, 

1671 date_online_first__isnull=True, 

1672 ) 

1673 

1674 for article in articles: 

1675 result = self.recordDOIArticle(article, force) 

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

1677 data = result 

1678 else: 

1679 return recordDOI(container) 

1680 return data 

1681 

1682 

1683class CROSSREFResourceCheckStatusView(View): 

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

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

1686 resource = model_helpers.get_resource(pid) 

1687 if resource is None: 

1688 raise Http404 

1689 resource = resource.cast() 

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

1691 try: 

1692 meth(resource) 

1693 except Timeout as exception: 

1694 return HttpResponse(exception, status=408) 

1695 except Exception as exception: 

1696 return HttpResponseServerError(exception) 

1697 

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

1699 return JsonResponse(data) 

1700 

1701 def checkDOIArticle(self, article): 

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

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

1704 get_or_create_doibatch(article) 

1705 

1706 def checkDOICollection(self, collection): 

1707 get_or_create_doibatch(collection) 

1708 

1709 def checkDOIContainer(self, container): 

1710 if container.doi is not None: 

1711 get_or_create_doibatch(container) 

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

1713 self.checkDOIArticle(article) 

1714 

1715 

1716class RegisterPubmedFormView(FormView): 

1717 template_name = "record_pubmed_dialog.html" 

1718 form_class = RegisterPubmedForm 

1719 

1720 def get_context_data(self, **kwargs): 

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

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

1723 context["helper"] = PtfLargeModalFormHelper 

1724 return context 

1725 

1726 

1727class RegisterPubmedView(View): 

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

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

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

1731 

1732 article = model_helpers.get_article(pid) 

1733 if article is None: 

1734 raise Http404 

1735 try: 

1736 recordPubmed(article, update_article) 

1737 except Exception as exception: 

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

1739 return HttpResponseServerError(exception) 

1740 

1741 return HttpResponseRedirect( 

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

1743 ) 

1744 

1745 

1746class PTFToolsContainerView(TemplateView): 

1747 template_name = "" 

1748 

1749 def get_context_data(self, **kwargs): 

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

1751 

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

1753 if container is None: 

1754 raise Http404 

1755 citing_articles = container.citations() 

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

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

1758 book_parts = ( 

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

1760 ) 

1761 references = False 

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

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

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

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

1766 references = True 

1767 context.update( 

1768 { 

1769 "book": container, 

1770 "book_parts": list(book_parts), 

1771 "source": source, 

1772 "citing_articles": citing_articles, 

1773 "references": references, 

1774 "test_website": container.get_top_collection() 

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

1776 .location, 

1777 "prod_website": container.get_top_collection() 

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

1779 .location, 

1780 } 

1781 ) 

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

1783 else: 

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

1785 for article in articles: 

1786 try: 

1787 last_match = ( 

1788 history_models.HistoryEvent.objects.filter( 

1789 pid=article.pid, 

1790 type="matching", 

1791 ) 

1792 .only("created_on") 

1793 .latest("created_on") 

1794 ) 

1795 except history_models.HistoryEvent.DoesNotExist as _: 

1796 article.last_match = None 

1797 else: 

1798 article.last_match = last_match.created_on 

1799 

1800 # article1 = articles.first() 

1801 # date = article1.deployed_date() 

1802 # TODO next_issue, previous_issue 

1803 

1804 # check DOI est maintenant une commande à part 

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

1806 # articlesWithStatus = [] 

1807 # for article in articles: 

1808 # get_or_create_doibatch(article) 

1809 # articlesWithStatus.append(article) 

1810 

1811 test_location = prod_location = "" 

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

1813 if qs: 

1814 test_location = qs.first().location 

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

1816 if qs: 

1817 prod_location = qs.first().location 

1818 context.update( 

1819 { 

1820 "issue": container, 

1821 "articles": articles, 

1822 "source": source, 

1823 "citing_articles": citing_articles, 

1824 "test_website": test_location, 

1825 "prod_website": prod_location, 

1826 } 

1827 ) 

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

1829 

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

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

1832 return context 

1833 

1834 

1835class ExtLinkInline(InlineFormSetFactory): 

1836 model = ExtLink 

1837 form_class = ExtLinkForm 

1838 factory_kwargs = {"extra": 0} 

1839 

1840 

1841class ResourceIdInline(InlineFormSetFactory): 

1842 model = ResourceId 

1843 form_class = ResourceIdForm 

1844 factory_kwargs = {"extra": 0} 

1845 

1846 

1847class IssueDetailAPIView(View): 

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

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

1850 deployed_date = issue.deployed_date() 

1851 result = { 

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

1853 if deployed_date 

1854 else None, 

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

1856 "all_doi_are_registered": issue.all_doi_are_registered(), 

1857 "registered_in_doaj": issue.registered_in_doaj(), 

1858 "doi": issue.my_collection.doi, 

1859 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(), 

1860 } 

1861 try: 

1862 latest = history_models.HistoryEvent.objects.get_last_unsolved_error( 

1863 pid=issue.pid, strict=False 

1864 ) 

1865 except history_models.HistoryEvent.DoesNotExist as _: 

1866 pass 

1867 else: 

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

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

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

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

1872 ) 

1873 

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

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

1876 try: 

1877 result[event_type] = timezone.localtime( 

1878 history_models.HistoryEvent.objects.filter( 

1879 type=event_type, 

1880 status="OK", 

1881 pid__startswith=issue.pid, 

1882 ) 

1883 .latest("created_on") 

1884 .created_on 

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

1886 except history_models.HistoryEvent.DoesNotExist as _: 

1887 result[event_type] = "" 

1888 return JsonResponse(result) 

1889 

1890 

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

1892 model = Collection 

1893 form_class = CollectionForm 

1894 inlines = [ResourceIdInline, ExtLinkInline] 

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

1896 

1897 def get_context_data(self, **kwargs): 

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

1899 context["helper"] = PtfFormHelper 

1900 context["formset_helper"] = FormSetHelper 

1901 return context 

1902 

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

1904 if description: 

1905 la = Abstract( 

1906 resource=collection, 

1907 tag="description", 

1908 lang=lang, 

1909 seq=seq, 

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

1911 value_html=description, 

1912 value_tex=description, 

1913 ) 

1914 la.save() 

1915 

1916 def form_valid(self, form): 

1917 if form.instance.abbrev: 

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

1919 else: 

1920 form.instance.title_xml = ( 

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

1922 ) 

1923 

1924 form.instance.title_html = form.instance.title_tex 

1925 form.instance.title_sort = form.instance.title_tex 

1926 result = super().form_valid(form) 

1927 

1928 collection = self.object 

1929 collection.abstract_set.all().delete() 

1930 

1931 seq = 1 

1932 description = form.cleaned_data["description_en"] 

1933 if description: 

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

1935 seq += 1 

1936 description = form.cleaned_data["description_fr"] 

1937 if description: 

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

1939 

1940 return result 

1941 

1942 def get_success_url(self): 

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

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

1945 

1946 

1947class CollectionCreate(CollectionFormView, CreateWithInlinesView): 

1948 """ 

1949 Warning : Not yet finished 

1950 Automatic site membership creation is still missing 

1951 """ 

1952 

1953 

1954class CollectionUpdate(CollectionFormView, UpdateWithInlinesView): 

1955 slug_field = "pid" 

1956 slug_url_kwarg = "pid" 

1957 

1958 

1959def suggest_load_journal_dois(colid): 

1960 articles = ( 

1961 Article.objects.filter(my_container__my_collection__pid=colid) 

1962 .filter(doi__isnull=False) 

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

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

1965 ) 

1966 

1967 try: 

1968 articles = sorted( 

1969 articles, 

1970 key=lambda d: ( 

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

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

1973 ), 

1974 ) 

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

1976 pass 

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

1978 

1979 

1980def get_context_with_volumes(journal): 

1981 result = model_helpers.get_volumes_in_collection(journal) 

1982 volume_count = result["volume_count"] 

1983 collections = [] 

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

1985 item = model_helpers.get_volumes_in_collection(ancestor) 

1986 volume_count = max(0, volume_count) 

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

1988 collections.append(item) 

1989 

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

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

1992 collections.append(result) 

1993 

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

1995 collections.sort( 

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

1997 reverse=True, 

1998 ) 

1999 

2000 context = { 

2001 "journal": journal, 

2002 "sorted_issues": result["sorted_issues"], 

2003 "volume_count": volume_count, 

2004 "max_width": result["max_width"], 

2005 "collections": collections, 

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

2007 } 

2008 return context 

2009 

2010 

2011class CollectionDetail( 

2012 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin 

2013): 

2014 model = Collection 

2015 slug_field = "pid" 

2016 slug_url_kwarg = "pid" 

2017 template_name = "ptf/collection_detail.html" 

2018 

2019 def test_func(self): 

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

2021 

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

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

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

2025 

2026 def get_context_data(self, **kwargs): 

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

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

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

2030 ) 

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

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

2033 

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

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

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

2037 pid=context["issue_to_appear_pid"] 

2038 ).exists() 

2039 try: 

2040 latest_error = history_models.HistoryEvent.objects.get_last_unsolved_error( 

2041 self.object.pid, 

2042 strict=True, 

2043 ) 

2044 except history_models.HistoryEvent.DoesNotExist as _: 

2045 pass 

2046 else: 

2047 message = latest_error.data["message"] 

2048 i = message.find(" - ") 

2049 latest_exception = message[:i] 

2050 latest_error_message = message[i + 3 :] 

2051 context["latest_exception"] = latest_exception 

2052 context["latest_exception_date"] = latest_error.created_on 

2053 context["latest_exception_type"] = latest_error.type 

2054 context["latest_error_message"] = latest_error_message 

2055 return context 

2056 

2057 def get_queryset(self): 

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

2059 

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

2061 query |= ancestor.content.all() 

2062 

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

2064 

2065 

2066class ContainerEditView(FormView): 

2067 template_name = "container_form.html" 

2068 form_class = ContainerForm 

2069 

2070 def get_success_url(self): 

2071 if self.kwargs["pid"]: 

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

2073 return reverse("mersenne_dashboard/published_articles") 

2074 

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

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

2077 

2078 def get_form_kwargs(self): 

2079 kwargs = super().get_form_kwargs() 

2080 if "pid" not in self.kwargs: 

2081 self.kwargs["pid"] = None 

2082 if "colid" not in self.kwargs: 

2083 self.kwargs["colid"] = None 

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

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

2086 # It is used when you submit a new container 

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

2088 

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

2090 self.kwargs["pid"] 

2091 ) 

2092 return kwargs 

2093 

2094 def get_context_data(self, **kwargs): 

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

2096 

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

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

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

2100 

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

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

2103 

2104 return context 

2105 

2106 def form_valid(self, form): 

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

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

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

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

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

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

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

2114 

2115 collection = None 

2116 issue = self.kwargs["container"] 

2117 if issue is not None: 

2118 collection = issue.my_collection 

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

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

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

2122 else: 

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

2124 

2125 if collection is None: 

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

2127 

2128 # Icon 

2129 new_icon_location = "" 

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

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

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

2133 

2134 icon_filename = resolver.get_disk_location( 

2135 settings.MERSENNE_TEST_DATA_FOLDER, 

2136 collection.pid, 

2137 file_extension, 

2138 new_pid, 

2139 None, 

2140 True, 

2141 ) 

2142 

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

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

2145 destination.write(chunk) 

2146 

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

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

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

2150 if name == "special_issue_create": 

2151 self.kwargs["name"] = name 

2152 if self.kwargs["container"]: 

2153 # Edit Issue 

2154 issue = self.kwargs["container"] 

2155 if issue is None: 

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

2157 

2158 issue.pid = new_pid 

2159 issue.title_tex = issue.title_html = new_title 

2160 issue.title_xml = build_title_xml( 

2161 title=new_title, 

2162 lang=issue.lang, 

2163 title_type="issue-title", 

2164 ) 

2165 

2166 trans_lang = "" 

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

2168 trans_lang = issue.trans_lang 

2169 elif new_trans_title != "": 

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

2171 issue.trans_lang = trans_lang 

2172 

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

2174 issue.trans_title_html = "" 

2175 issue.trans_title_tex = "" 

2176 title_xml = build_title_xml( 

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

2178 ) 

2179 try: 

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

2181 trans_title_object.title_html = new_trans_title 

2182 trans_title_object.title_xml = title_xml 

2183 trans_title_object.save() 

2184 except Title.DoesNotExist: 

2185 trans_title = Title( 

2186 resource=issue, 

2187 lang=trans_lang, 

2188 type="main", 

2189 title_html=new_trans_title, 

2190 title_xml=title_xml, 

2191 ) 

2192 trans_title.save() 

2193 issue.year = new_year 

2194 issue.volume = new_volume 

2195 issue.volume_int = make_int(new_volume) 

2196 issue.number = new_number 

2197 issue.number_int = make_int(new_number) 

2198 issue.save() 

2199 else: 

2200 xissue = create_issuedata() 

2201 

2202 xissue.ctype = "issue" 

2203 xissue.pid = new_pid 

2204 xissue.lang = "en" 

2205 xissue.title_tex = new_title 

2206 xissue.title_html = new_title 

2207 xissue.title_xml = build_title_xml( 

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

2209 ) 

2210 

2211 if new_trans_title != "": 

2212 trans_lang = "fr" 

2213 title_xml = build_title_xml( 

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

2215 ) 

2216 title = create_titledata( 

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

2218 ) 

2219 issue.titles = [title] 

2220 

2221 xissue.year = new_year 

2222 xissue.volume = new_volume 

2223 xissue.number = new_number 

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

2225 

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

2227 cmd.add_collection(collection) 

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

2229 issue = cmd.do() 

2230 

2231 self.kwargs["pid"] = new_pid 

2232 

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

2234 params = { 

2235 "icon_location": new_icon_location, 

2236 } 

2237 cmd = ptf_cmds.updateContainerPtfCmd(params) 

2238 cmd.set_resource(issue) 

2239 cmd.do() 

2240 

2241 publisher = model_helpers.get_publisher(new_publisher) 

2242 if not publisher: 

2243 xpub = create_publisherdata() 

2244 xpub.name = new_publisher 

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

2246 issue.my_publisher = publisher 

2247 issue.save() 

2248 

2249 self.set_success_message() 

2250 

2251 return super().form_valid(form) 

2252 

2253 

2254# class ArticleEditView(FormView): 

2255# template_name = 'article_form.html' 

2256# form_class = ArticleForm 

2257# 

2258# def get_success_url(self): 

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

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

2261# return reverse('mersenne_dashboard/published_articles') 

2262# 

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

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

2265# 

2266# def get_form_kwargs(self): 

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

2268# 

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

2270# # Article creation: pid is None 

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

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

2273# # Article edit: issue_id is not passed 

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

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

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

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

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

2279# 

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

2281# return kwargs 

2282# 

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

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

2285# 

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

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

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

2289# 

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

2291# 

2292# article = context['article'] 

2293# if article: 

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

2295# context['kwds_fr'] = None 

2296# context['kwds_en'] = None 

2297# kwd_gps = article.get_non_msc_kwds() 

2298# for kwd_gp in kwd_gps: 

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

2300# if kwd_gp.value_xml: 

2301# kwd_ = types.SimpleNamespace() 

2302# kwd_.value = kwd_gp.value_tex 

2303# context['kwd_unstructured_fr'] = kwd_ 

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

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

2306# if kwd_gp.value_xml: 

2307# kwd_ = types.SimpleNamespace() 

2308# kwd_.value = kwd_gp.value_tex 

2309# context['kwd_unstructured_en'] = kwd_ 

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

2311# 

2312# # Article creation: init pid 

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

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

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

2316# 

2317# return context 

2318# 

2319# def form_valid(self, form): 

2320# 

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

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

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

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

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

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

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

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

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

2330# 

2331# # TODO support MathML 

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

2333# # We need to pass trans_title to get_title_xml 

2334# # Meanwhile, ignore new_title_xml 

2335# new_title_xml = jats_parser.get_title_xml(new_title) 

2336# new_title_html = new_title 

2337# 

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

2339# i = 1 

2340# new_authors = [] 

2341# old_author_contributions = [] 

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

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

2344# 

2345# while authors_count > 0: 

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

2347# 

2348# if prefix is not None: 

2349# addresses = [] 

2350# if len(old_author_contributions) >= i: 

2351# old_author_contribution = old_author_contributions[i - 1] 

2352# addresses = [contrib_address.address for contrib_address in 

2353# old_author_contribution.get_addresses()] 

2354# 

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

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

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

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

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

2360# deceased_before_publication = deceased == 'on' 

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

2362# equal_contrib = equal_contrib == 'on' 

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

2364# corresponding = corresponding == 'on' 

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

2366# 

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

2368# params['deceased_before_publication'] = deceased_before_publication 

2369# params['equal_contrib'] = equal_contrib 

2370# params['corresponding'] = corresponding 

2371# params['addresses'] = addresses 

2372# params['email'] = email 

2373# 

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

2375# 

2376# new_authors.append(params) 

2377# 

2378# authors_count -= 1 

2379# i += 1 

2380# 

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

2382# i = 1 

2383# new_kwds_fr = [] 

2384# while kwds_fr_count > 0: 

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

2386# new_kwds_fr.append(value) 

2387# kwds_fr_count -= 1 

2388# i += 1 

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

2390# 

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

2392# i = 1 

2393# new_kwds_en = [] 

2394# while kwds_en_count > 0: 

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

2396# new_kwds_en.append(value) 

2397# kwds_en_count -= 1 

2398# i += 1 

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

2400# 

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

2402# # Edit article 

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

2404# else: 

2405# # New article 

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

2407# 

2408# if container is None: 

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

2410# 

2411# collection = container.my_collection 

2412# 

2413# # Copy PDF file & extract full text 

2414# body = '' 

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

2416# collection.pid, 

2417# "pdf", 

2418# container.pid, 

2419# new_pid, 

2420# True) 

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

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

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

2424# destination.write(chunk) 

2425# 

2426# # Extract full text from the PDF 

2427# body = utils.pdf_to_text(pdf_filename) 

2428# 

2429# # Icon 

2430# new_icon_location = '' 

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

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

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

2434# 

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

2436# collection.pid, 

2437# file_extension, 

2438# container.pid, 

2439# new_pid, 

2440# True) 

2441# 

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

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

2444# destination.write(chunk) 

2445# 

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

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

2448# 

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

2450# # Edit article 

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

2452# article.fpage = new_fpage 

2453# article.lpage = new_lpage 

2454# article.page_range = new_page_range 

2455# article.coi_statement = new_coi_statement 

2456# article.show_body = new_show_body 

2457# article.do_not_publish = new_do_not_publish 

2458# article.save() 

2459# 

2460# else: 

2461# # New article 

2462# params = { 

2463# 'pid': new_pid, 

2464# 'title_xml': new_title_xml, 

2465# 'title_html': new_title_html, 

2466# 'title_tex': new_title, 

2467# 'fpage': new_fpage, 

2468# 'lpage': new_lpage, 

2469# 'page_range': new_page_range, 

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

2471# 'body': body, 

2472# 'coi_statement': new_coi_statement, 

2473# 'show_body': new_show_body, 

2474# 'do_not_publish': new_do_not_publish 

2475# } 

2476# 

2477# xarticle = create_articledata() 

2478# xarticle.pid = new_pid 

2479# xarticle.title_xml = new_title_xml 

2480# xarticle.title_html = new_title_html 

2481# xarticle.title_tex = new_title 

2482# xarticle.fpage = new_fpage 

2483# xarticle.lpage = new_lpage 

2484# xarticle.page_range = new_page_range 

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

2486# xarticle.body = body 

2487# xarticle.coi_statement = new_coi_statement 

2488# params['xobj'] = xarticle 

2489# 

2490# cmd = ptf_cmds.addArticlePtfCmd(params) 

2491# cmd.set_container(container) 

2492# cmd.add_collection(container.my_collection) 

2493# article = cmd.do() 

2494# 

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

2496# 

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

2498# params = { 

2499# # 'title_xml': new_title_xml, 

2500# # 'title_html': new_title_html, 

2501# # 'title_tex': new_title, 

2502# 'authors': new_authors, 

2503# 'page_count': new_page_count, 

2504# 'icon_location': new_icon_location, 

2505# 'body': body, 

2506# 'use_kwds': True, 

2507# 'kwds_fr': new_kwds_fr, 

2508# 'kwds_en': new_kwds_en, 

2509# 'kwd_uns_fr': new_kwd_uns_fr, 

2510# 'kwd_uns_en': new_kwd_uns_en 

2511# } 

2512# cmd = ptf_cmds.updateArticlePtfCmd(params) 

2513# cmd.set_article(article) 

2514# cmd.do() 

2515# 

2516# self.set_success_message() 

2517# 

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

2519 

2520 

2521@require_http_methods(["POST"]) 

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

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

2524 

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

2526 

2527 article = model_helpers.get_article(pid) 

2528 if article: 

2529 article.do_not_publish = not article.do_not_publish 

2530 article.save() 

2531 else: 

2532 raise Http404 

2533 

2534 return HttpResponseRedirect(next) 

2535 

2536 

2537@require_http_methods(["POST"]) 

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

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

2540 

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

2542 

2543 article = model_helpers.get_article(pid) 

2544 if article: 

2545 article.show_body = not article.show_body 

2546 article.save() 

2547 else: 

2548 raise Http404 

2549 

2550 return HttpResponseRedirect(next) 

2551 

2552 

2553class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView): 

2554 """ 

2555 API to get/post article metadata 

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

2557 """ 

2558 

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

2560 """ 

2561 we define here what fields we want in the form 

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

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

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

2565 self.fields_to_update = [ 

2566 "lang", 

2567 "atype", 

2568 "contributors", 

2569 "abstracts", 

2570 "kwds", 

2571 "titles", 

2572 "trans_title_html", 

2573 "title_html", 

2574 "title_xml", 

2575 "streams", 

2576 "ext_links", 

2577 ] 

2578 self.additional_fields = [ 

2579 "pid", 

2580 "doi", 

2581 "container_pid", 

2582 "pdf", 

2583 "illustration", 

2584 ] 

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

2586 self.article_container_pid = "" 

2587 self.back_url = "trammel" 

2588 

2589 def save_data(self, data_article): 

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

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

2592 params = { 

2593 "pid": data_article.pid, 

2594 "export_folder": settings.MERSENNE_TMP_FOLDER, 

2595 "export_all": True, 

2596 "with_binary_files": False, 

2597 } 

2598 ptf_cmds.exportExtraDataPtfCmd(params).do() 

2599 

2600 def restore_data(self, article): 

2601 ptf_cmds.importExtraDataPtfCmd( 

2602 { 

2603 "pid": article.pid, 

2604 "import_folder": settings.MERSENNE_TMP_FOLDER, 

2605 } 

2606 ).do() 

2607 

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

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

2610 return data 

2611 

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

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

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

2615 return redirect( 

2616 "api-edit-article", 

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

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

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

2620 ) 

2621 else: 

2622 raise Http404 

2623 

2624 

2625class ArticleEditWithVueView(LoginRequiredMixin, TemplateView): 

2626 template_name = "article_form.html" 

2627 

2628 def get_success_url(self): 

2629 if self.kwargs["doi"]: 

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

2631 return reverse("mersenne_dashboard/published_articles") 

2632 

2633 def get_context_data(self, **kwargs): 

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

2635 if "doi" in self.kwargs: 

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

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

2638 

2639 return context 

2640 

2641 

2642class ArticleDeleteView(View): 

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

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

2645 article = get_object_or_404(Article, pid=pid) 

2646 

2647 try: 

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

2649 article.undeploy(mersenneSite) 

2650 

2651 cmd = ptf_cmds.addArticlePtfCmd( 

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

2653 ) 

2654 cmd.set_container(article.my_container) 

2655 cmd.set_object_to_be_deleted(article) 

2656 cmd.undo() 

2657 except Exception as exception: 

2658 return HttpResponseServerError(exception) 

2659 

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

2661 return JsonResponse(data) 

2662 

2663 

2664def get_messages_in_queue(): 

2665 app = Celery("ptf-tools") 

2666 # tasks = list(current_app.tasks) 

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

2668 print(tasks) 

2669 # i = app.control.inspect() 

2670 

2671 with app.connection_or_acquire() as conn: 

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

2673 return remaining 

2674 

2675 

2676class FailedTasksListView(ListView): 

2677 model = TaskResult 

2678 queryset = TaskResult.objects.filter( 

2679 status="FAILURE", 

2680 task_name="ptf_tools.tasks.archive_numdam_issue", 

2681 ) 

2682 

2683 

2684class FailedTasksDeleteView(DeleteView): 

2685 model = TaskResult 

2686 success_url = reverse_lazy("tasks-failed") 

2687 

2688 

2689class FailedTasksRetryView(SingleObjectMixin, RedirectView): 

2690 model = TaskResult 

2691 

2692 @staticmethod 

2693 def retry_task(task): 

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

2695 archive_numdam_issue.delay(colid, pid) 

2696 task.delete() 

2697 

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

2699 self.retry_task(self.get_object()) 

2700 return reverse("tasks-failed") 

2701 

2702 

2703class NumdamView(TemplateView, history_views.HistoryContextMixin): 

2704 template_name = "numdam.html" 

2705 

2706 def get_context_data(self, **kwargs): 

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

2708 

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

2710 

2711 pre_issues = [] 

2712 prod_issues = [] 

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

2714 try: 

2715 response = requests.get(url) 

2716 if response.status_code == 200: 

2717 data = response.json() 

2718 if "issues" in data: 

2719 pre_issues = data["issues"] 

2720 except Exception: 

2721 pass 

2722 

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

2724 response = requests.get(url) 

2725 if response.status_code == 200: 

2726 data = response.json() 

2727 if "issues" in data: 

2728 prod_issues = data["issues"] 

2729 

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

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

2732 grouped = [ 

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

2734 ] 

2735 grouped_removed = [ 

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

2737 ] 

2738 context["added_issues"] = grouped 

2739 context["removed_issues"] = grouped_removed 

2740 

2741 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS 

2742 return context 

2743 

2744 

2745class TasksProgressView(View): 

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

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

2748 successes = TaskResult.objects.filter( 

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

2750 ).count() 

2751 fails = TaskResult.objects.filter( 

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

2753 ).count() 

2754 last_task = ( 

2755 TaskResult.objects.filter( 

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

2757 status="SUCCESS", 

2758 ) 

2759 .order_by("-date_done") 

2760 .first() 

2761 ) 

2762 if last_task: 

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

2764 remaining = get_messages_in_queue() 

2765 all = successes + remaining 

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

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

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

2769 data = { 

2770 "status": status, 

2771 "progress": progress, 

2772 "total": all, 

2773 "remaining": remaining, 

2774 "successes": successes, 

2775 "fails": fails, 

2776 "error_rate": error_rate, 

2777 "last_task": last_task, 

2778 } 

2779 return JsonResponse(data) 

2780 

2781 

2782class TasksView(TemplateView): 

2783 template_name = "tasks.html" 

2784 

2785 def get_context_data(self, **kwargs): 

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

2787 context["tasks"] = TaskResult.objects.all() 

2788 return context 

2789 

2790 

2791class NumdamArchiveView(RedirectView): 

2792 @staticmethod 

2793 def reset_task_results(): 

2794 TaskResult.objects.all().delete() 

2795 

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

2797 self.colid = kwargs["colid"] 

2798 

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

2800 return Http404 

2801 

2802 # we make sure archiving is not already running 

2803 if not get_messages_in_queue(): 

2804 self.reset_task_results() 

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

2806 if response.status_code == 200: 

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

2808 

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

2810 return Http404 

2811 

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

2813 

2814 with open( 

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

2816 ) as file_: 

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

2818 

2819 for colid in colids: 

2820 if colid not in settings.MERSENNE_COLLECTIONS: 

2821 archive_numdam_collection.delay(colid) 

2822 return reverse("numdam") 

2823 

2824 

2825class DeployAllNumdamAPIView(View): 

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

2827 pids = [] 

2828 

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

2830 pids.append(obj.pid) 

2831 

2832 return pids 

2833 

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

2835 try: 

2836 pids, status, message = history_views.execute_and_record_func( 

2837 "deploy", "numdam", "numdam", self.internal_do, "numdam" 

2838 ) 

2839 except Exception as exception: 

2840 return HttpResponseServerError(exception) 

2841 

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

2843 return JsonResponse(data) 

2844 

2845 

2846class NumdamDeleteAPIView(View): 

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

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

2849 

2850 try: 

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

2852 obj.delete() 

2853 except Exception as exception: 

2854 return HttpResponseServerError(exception) 

2855 

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

2857 return JsonResponse(data) 

2858 

2859 

2860class ExtIdApiDetail(View): 

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

2862 extid = get_object_or_404( 

2863 ExtId, 

2864 resource__pid=kwargs["pid"], 

2865 id_type=kwargs["what"], 

2866 ) 

2867 return JsonResponse( 

2868 { 

2869 "pk": extid.pk, 

2870 "href": extid.get_href(), 

2871 "fetch": reverse( 

2872 "api-fetch-id", 

2873 args=( 

2874 extid.resource.pk, 

2875 extid.id_value, 

2876 extid.id_type, 

2877 "extid", 

2878 ), 

2879 ), 

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

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

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

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

2884 "is_valid": extid.checked, 

2885 } 

2886 ) 

2887 

2888 

2889class ExtIdFormTemplate(TemplateView): 

2890 template_name = "common/externalid_form.html" 

2891 

2892 def get_context_data(self, **kwargs): 

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

2894 context["sequence"] = kwargs["sequence"] 

2895 return context 

2896 

2897 

2898class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

2899 def get_context_data(self, **kwargs): 

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

2901 context["helper"] = PtfFormHelper 

2902 return context 

2903 

2904 def get_success_url(self): 

2905 self.post_process() 

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

2907 

2908 def post_process(self): 

2909 cmd = xml_cmds.updateBibitemCitationXmlCmd() 

2910 cmd.set_bibitem(self.object.bibitem) 

2911 cmd.do() 

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

2913 

2914 

2915class BibItemIdCreate(BibItemIdFormView, CreateView): 

2916 model = BibItemId 

2917 form_class = BibItemIdForm 

2918 

2919 def get_context_data(self, **kwargs): 

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

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

2922 return context 

2923 

2924 def get_initial(self): 

2925 initial = super().get_initial() 

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

2927 return initial 

2928 

2929 def form_valid(self, form): 

2930 form.instance.checked = False 

2931 return super().form_valid(form) 

2932 

2933 

2934class BibItemIdUpdate(BibItemIdFormView, UpdateView): 

2935 model = BibItemId 

2936 form_class = BibItemIdForm 

2937 

2938 def get_context_data(self, **kwargs): 

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

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

2941 return context 

2942 

2943 

2944class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

2945 def get_context_data(self, **kwargs): 

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

2947 context["helper"] = PtfFormHelper 

2948 return context 

2949 

2950 def get_success_url(self): 

2951 self.post_process() 

2952 return self.object.resource.get_absolute_url() 

2953 

2954 def post_process(self): 

2955 model_helpers.post_resource_updated(self.object.resource) 

2956 

2957 

2958class ExtIdCreate(ExtIdFormView, CreateView): 

2959 model = ExtId 

2960 form_class = ExtIdForm 

2961 

2962 def get_context_data(self, **kwargs): 

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

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

2965 return context 

2966 

2967 def get_initial(self): 

2968 initial = super().get_initial() 

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

2970 return initial 

2971 

2972 def form_valid(self, form): 

2973 form.instance.checked = False 

2974 return super().form_valid(form) 

2975 

2976 

2977class ExtIdUpdate(ExtIdFormView, UpdateView): 

2978 model = ExtId 

2979 form_class = ExtIdForm 

2980 

2981 def get_context_data(self, **kwargs): 

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

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

2984 return context 

2985 

2986 

2987class BibItemIdApiDetail(View): 

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

2989 bibitemid = get_object_or_404( 

2990 BibItemId, 

2991 bibitem__resource__pid=kwargs["pid"], 

2992 bibitem__sequence=kwargs["seq"], 

2993 id_type=kwargs["what"], 

2994 ) 

2995 return JsonResponse( 

2996 { 

2997 "pk": bibitemid.pk, 

2998 "href": bibitemid.get_href(), 

2999 "fetch": reverse( 

3000 "api-fetch-id", 

3001 args=( 

3002 bibitemid.bibitem.pk, 

3003 bibitemid.id_value, 

3004 bibitemid.id_type, 

3005 "bibitemid", 

3006 ), 

3007 ), 

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

3009 "uncheck": reverse( 

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

3011 ), 

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

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

3014 "is_valid": bibitemid.checked, 

3015 } 

3016 ) 

3017 

3018 

3019class UpdateTexmfZipAPIView(View): 

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

3021 def copy_zip_files(src_folder, dest_folder): 

3022 os.makedirs(dest_folder, exist_ok=True) 

3023 

3024 zip_files = [ 

3025 os.path.join(src_folder, f) 

3026 for f in os.listdir(src_folder) 

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

3028 ] 

3029 for zip_file in zip_files: 

3030 resolver.copy_file(zip_file, dest_folder) 

3031 

3032 # Exceptions: specific zip/gz files 

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

3034 resolver.copy_file(zip_file, dest_folder) 

3035 

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

3037 resolver.copy_file(zip_file, dest_folder) 

3038 

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

3040 resolver.copy_file(gz_file, dest_folder) 

3041 

3042 src_folder = settings.CEDRAM_DISTRIB_FOLDER 

3043 

3044 dest_folder = os.path.join( 

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

3046 ) 

3047 

3048 try: 

3049 copy_zip_files(src_folder, dest_folder) 

3050 except Exception as exception: 

3051 return HttpResponseServerError(exception) 

3052 

3053 try: 

3054 dest_folder = os.path.join( 

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

3056 ) 

3057 copy_zip_files(src_folder, dest_folder) 

3058 except Exception as exception: 

3059 return HttpResponseServerError(exception) 

3060 

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

3062 return JsonResponse(data) 

3063 

3064 

3065class TestView(TemplateView): 

3066 template_name = "mersenne.html" 

3067 

3068 def get_context_data(self, **kwargs): 

3069 super().get_context_data(**kwargs) 

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

3071 model_data_converter.db_to_issue_data(issue) 

3072 

3073 

3074class TrammelArchiveView(RedirectView): 

3075 @staticmethod 

3076 def reset_task_results(): 

3077 TaskResult.objects.all().delete() 

3078 

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

3080 self.colid = kwargs["colid"] 

3081 self.mathdoc_archive = settings.MATHDOC_ARCHIVE_FOLDER 

3082 self.binary_files_folder = settings.MERSENNE_PROD_DATA_FOLDER 

3083 # Make sure archiving is not already running 

3084 if not get_messages_in_queue(): 

3085 self.reset_task_results() 

3086 if "progress/" in self.colid: 

3087 self.colid = self.colid.replace("progress/", "") 

3088 if "/progress" in self.colid: 

3089 self.colid = self.colid.replace("/progress", "") 

3090 

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

3092 return Http404 

3093 

3094 colids = [self.colid] if self.colid != "ALL" else settings.MERSENNE_COLLECTIONS 

3095 

3096 with open( 

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

3098 ) as file_: 

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

3100 

3101 for colid in colids: 

3102 archive_trammel_collection.delay( 

3103 colid, self.mathdoc_archive, self.binary_files_folder 

3104 ) 

3105 

3106 if self.colid == "ALL": 

3107 return reverse("home") 

3108 else: 

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

3110 

3111 

3112class TrammelTasksProgressView(View): 

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

3114 """ 

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

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

3117 """ 

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

3119 

3120 def get_event_data(): 

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

3122 

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

3124 remaining_messages = get_messages_in_queue() 

3125 

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

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

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

3129 

3130 all_tasks_count = all_tasks.count() 

3131 success_count = successed_tasks.count() 

3132 fail_count = failed_tasks.count() 

3133 

3134 all_count = all_tasks_count + remaining_messages 

3135 remaining_count = all_count - success_count - fail_count 

3136 

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

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

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

3140 

3141 last_task = successed_tasks.first() 

3142 last_task = ( 

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

3144 if last_task 

3145 else "" 

3146 ) 

3147 

3148 # SSE event format 

3149 event_data = { 

3150 "status": status, 

3151 "success_rate": success_rate, 

3152 "error_rate": error_rate, 

3153 "all_count": all_count, 

3154 "remaining_count": remaining_count, 

3155 "success_count": success_count, 

3156 "fail_count": fail_count, 

3157 "last_task": last_task, 

3158 } 

3159 

3160 return event_data 

3161 

3162 def stream_response(data): 

3163 # Send initial response headers 

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

3165 

3166 data = get_event_data() 

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

3168 if format == "json": 

3169 response = JsonResponse(data) 

3170 else: 

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

3172 return response 

3173 

3174 

3175class TrammelFailedTasksListView(ListView): 

3176 model = TaskResult 

3177 queryset = TaskResult.objects.filter( 

3178 status="FAILURE", 

3179 task_name="ptf_tools.tasks.archive_trammel_resource", 

3180 )