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
« 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
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
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
98def view_404(request: HttpRequest):
99 """
100 Dummy view raising HTTP 404 exception.
101 """
102 raise Http404
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 """
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")
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)
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
139 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
142def check_lock():
143 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
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 )
155class ImportCedricsArticleFormView(FormView):
156 template_name = "import_article.html"
157 form_class = ImportArticleForm
159 def dispatch(self, request, *args, **kwargs):
160 self.colid = self.kwargs["colid"]
161 return super().dispatch(request, *args, **kwargs)
163 def get_success_url(self):
164 if self.colid:
165 return reverse("collection-detail", kwargs={"pid": self.colid})
166 return "/"
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
174 def get_form_kwargs(self):
175 kwargs = super().get_form_kwargs()
176 kwargs["colid"] = self.colid
177 return kwargs
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)
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()
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))
196 import_args = [self]
197 import_kwargs = {}
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 )
211 messages.success(
212 self.request, f"L'article {self.article_pid} a été importé avec succès"
213 )
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 )
221 return redirect(self.get_success_url())
224class ImportCedricsIssueView(FormView):
225 template_name = "import_container.html"
226 form_class = ImportContainerForm
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)
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 "/"
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
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
252 def form_valid(self, form):
253 self.filename = form.cleaned_data["filename"].split("/")[-1]
254 return super().form_valid(form)
257class DiffCedricsIssueView(FormView):
258 template_name = "diff_container_form.html"
259 form_class = DiffContainerForm
260 diffs = None
261 xissue = None
262 xissue_encoded = None
264 def get_success_url(self):
265 return reverse("collection-detail", kwargs={"pid": self.colid})
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)
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"
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())
293 no_conflict = result[0]
294 self.diffs = result[1]
295 self.xissue = result[2]
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)
305 return super().get(request, *args, **kwargs)
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)
312 return super().post(request, *args, **kwargs)
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
322 def get_form_kwargs(self):
323 kwargs = super().get_form_kwargs()
324 kwargs["colid"] = self.colid
325 return kwargs
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 }
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)
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 )
349 return result
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
367 params = {
368 "colid": self.colid,
369 "xissue": self.xissue,
370 "input_file": self.filename,
371 }
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)
380 cmd.do()
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]
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)
406 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
407 return super().form_valid(form)
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
427 data = {"bibtex": all_bibtex}
428 return JsonResponse(data)
431class MatchingAPIView(View):
432 def get(self, request, *args, **kwargs):
433 pid = self.kwargs.get("pid", None)
435 url = settings.MATCHING_URL
436 headers = {"Content-Type": "application/xml"}
438 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
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()
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]}
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()
457 resource = model_helpers.get_resource(pid)
458 obj = resource.cast()
459 colid = obj.get_collection().pid
461 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
463 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
464 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
465 )
466 cmd.do()
468 print("Matching finished")
469 return JsonResponse(data)
472class ImportAllAPIView(View):
473 def internal_do(self, *args, **kwargs):
474 pid = self.kwargs.get("pid", None)
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")
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)
499 obj = resource.cast()
501 if obj.classname != "Collection":
502 raise ValueError(pid + " does not contain a collection")
504 cmd = xml_cmds.collectEntireCollectionXmlCmd(
505 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
506 )
507 pids = cmd.do()
509 return pids
511 def get(self, request, *args, **kwargs):
512 pid = self.kwargs.get("pid", None)
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)
523 data = {"message": message, "ids": pids, "status": status}
524 return JsonResponse(data)
527class DeployAllAPIView(View):
528 def internal_do(self, *args, **kwargs):
529 pid = self.kwargs.get("pid", None)
530 site = self.kwargs.get("site", None)
532 pids = []
534 collection = model_helpers.get_collection(pid)
535 if not collection:
536 raise RuntimeError(pid + " does not exist")
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)
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)
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)
555 return pids
557 def get(self, request, *args, **kwargs):
558 pid = self.kwargs.get("pid", None)
559 site = self.kwargs.get("site", None)
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)
570 data = {"message": message, "ids": pids, "status": status}
571 return JsonResponse(data)
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"
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)
592 def internal_do(self, *args, **kwargs):
593 """
594 Called by history_views.execute_and_record_func to do the actual job.
595 """
597 issue_pid = self.issue.pid
598 colid = self.collection.pid
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")
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)
617 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
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)
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)
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})
642 self.pid = self.kwargs.get("pid", None)
643 self.site = self.kwargs.get("site", "test_website")
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()
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 )
659 except Timeout as exception:
660 return HttpResponse(exception, status=408)
661 except Exception as exception:
662 return HttpResponseServerError(exception)
664 data = {"message": message, "status": status}
665 return JsonResponse(data)
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 """
675 def internal_do(self, *args, **kwargs):
676 collection = kwargs["collection"]
677 pids = []
678 colid = collection.pid
680 logfile = os.path.join(settings.LOG_DIR, "archive.log")
681 if os.path.isfile(logfile):
682 os.remove(logfile)
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()
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)
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)
707 return pids
709 def get(self, request, *args, **kwargs):
710 pid = self.kwargs.get("pid", None)
712 collection = model_helpers.get_collection(pid)
713 if not collection:
714 return HttpResponse(f"{pid} does not exist", status=400)
716 dict_ = {"collection": collection}
717 args_ = [self]
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)
728 data = {"message": message, "ids": pids, "status": status}
729 return JsonResponse(data)
732class CreateAllDjvuAPIView(View):
733 def internal_do(self, *args, **kwargs):
734 issue = kwargs["issue"]
735 pids = [issue.pid]
737 for article in issue.article_set.all():
738 pids.append(article.pid)
740 return pids
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")
748 try:
749 dict_ = {"issue": issue}
750 args_ = [self]
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)
765 data = {"message": message, "ids": pids, "status": status}
766 return JsonResponse(data)
769class ImportJatsContainerAPIView(View):
770 def internal_do(self, *args, **kwargs):
771 pid = self.kwargs.get("pid", None)
772 colid = self.kwargs.get("colid", None)
774 if pid and colid:
775 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
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 )
792 if not container:
793 raise RuntimeError("Error: the container " + pid + " was not imported")
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")
808 def get(self, request, *args, **kwargs):
809 pid = self.kwargs.get("pid", None)
810 colid = self.kwargs.get("colid", None)
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)
821 data = {"message": message, "status": status}
822 return JsonResponse(data)
825class DeployCollectionAPIView(View):
826 # Update collection.xml on a site (with its images)
828 def internal_do(self, *args, **kwargs):
829 colid = self.kwargs.get("colid", None)
830 site = self.kwargs.get("site", None)
832 collection = model_helpers.get_collection(colid)
833 if not collection:
834 raise RuntimeError(f"{colid} does not exist")
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}")
843 # check_collection creates or updates the collection (XML, image...)
844 check_collection(collection, server_url, site)
846 def get(self, request, *args, **kwargs):
847 colid = self.kwargs.get("colid", None)
848 site = self.kwargs.get("site", None)
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)
859 data = {"message": message, "status": status}
860 return JsonResponse(data)
863class DeployJatsResourceAPIView(View):
864 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
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)
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})
878 resource = model_helpers.get_resource(pid)
879 if not resource:
880 raise RuntimeError(f"{pid} does not exist")
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)
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")
897 collection = container.get_top_collection()
898 colid = collection.pid
899 djvu_exception = None
901 if site == "numdam":
902 server_url = settings.NUMDAM_PRE_URL
903 ResourceInNumdam.objects.get_or_create(pid=container.pid)
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}")
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)
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 )
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()
942 tex.create_frontpage(colid, container, updated_articles, test=False)
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_)
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() ?
954 file_.write(
955 "Publication date of {} : Online First: {}, Published: {}\n".format(
956 art.pid, art.date_online_first, art.date_published
957 )
958 )
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 )
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 )
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()
980 tex.create_frontpage(colid, container, updated_articles)
982 export_to_website = site == "website"
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")
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")
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}
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)
1026 status = response.status_code
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)
1038 for art in articles_to_deploy:
1039 # record DOI automatically when deploying in prod
1041 if art.doi and art.allow_crossref():
1042 recordDOI(art)
1044 if colid == "CRBIOL":
1045 recordPubmed(
1046 art, force_update=False, updated_articles=updated_articles
1047 )
1049 if colid == "PCJ":
1050 self.update_pcj_editor(updated_articles)
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()
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
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)
1084 elif status == 503:
1085 raise ServerUnderMaintenance(response.text)
1086 else:
1087 raise RuntimeError(response.text)
1089 if djvu_exception:
1090 raise djvu_exception
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)
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)
1106 data = {"message": message, "status": status}
1107 return JsonResponse(data)
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)
1119class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1120 article = None
1122 def internal_do(self, *args, **kwargs):
1123 lang = self.kwargs.get("lang", None)
1125 translation = None
1126 for trans_article in self.article.translations.all():
1127 if trans_article.lang == lang:
1128 translation = trans_article
1130 if translation is None:
1131 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1133 collection = self.article.get_top_collection()
1134 colid = collection.pid
1135 container = self.article.my_container
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()
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 )
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 )
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}
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 = {}
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 )
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 )
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")
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)
1216 data = {"message": message, "status": status}
1217 return JsonResponse(data)
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
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)
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)
1241 p = model_helpers.get_provider("mathdoc-id")
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()
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)()
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
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 )
1295 except Timeout as exception:
1296 return HttpResponse(exception, status=408)
1297 except Exception as exception:
1298 return HttpResponseServerError(exception)
1300 data = {"message": message, "status": status}
1301 return JsonResponse(data)
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
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)
1326 data = {"message": message, "status": 200}
1327 return JsonResponse(data)
1330class CreateDjvuAPIView(View):
1331 def internal_do(self, *args, **kwargs):
1332 pid = self.kwargs.get("pid", None)
1334 resource = model_helpers.get_resource(pid)
1335 cmd = ptf_cmds.addDjvuPtfCmd()
1336 cmd.set_resource(resource)
1337 cmd.do()
1339 def get(self, request, *args, **kwargs):
1340 pid = self.kwargs.get("pid", None)
1341 colid = pid.split("_")[0]
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)
1350 data = {"message": message, "status": status}
1351 return JsonResponse(data)
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 """
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")
1369 colids = get_authorized_collections(request.user)
1370 is_mod = is_comment_moderator(request.user)
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"))
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]}))
1383 # User with multiple authorized collections - Special home
1384 context = {}
1385 context["overview"] = True
1387 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1388 all_collections = {c["pid"]: c for c in all_collections}
1390 # Comments summary
1391 try:
1392 error, comments_data = get_comments_for_home(request.user)
1393 except AttributeError:
1394 error, comments_data = True, {}
1396 context["comment_server_ok"] = False
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
1405 # TODO: Translations summary
1406 context["translation_server_ok"] = False
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 )
1413 return render(request, "home.html", context)
1416class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1417 columns = 5
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)
1425 context["collections"] = settings.MERSENNE_COLLECTIONS
1426 context["containers_to_be_published"] = []
1427 context["last_col_events"] = []
1429 event = history_models.get_history_last_event_by("clockss", "ALL")
1430 clockss_gap = history_models.get_gap(now, event)
1432 context["years"] = years
1433 context["clockss_gap"] = clockss_gap
1435 return context
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)
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)
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
1451 return data_by_year, total_articles, total_pages
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")
1463class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1464 template_name = "dashboard/published_articles.html"
1466 def get_context_data(self, **kwargs):
1467 context = self.get_common_context_data(**kwargs)
1468 years = context["years"]
1470 published_articles = []
1471 total_published_articles = [
1472 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1473 ]
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})
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]
1486 context["published_articles"] = published_articles
1487 context["total_published_articles"] = total_published_articles
1489 return context
1492class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1493 template_name = "dashboard/created_volumes.html"
1495 def get_context_data(self, **kwargs):
1496 context = self.get_common_context_data(**kwargs)
1497 years = context["years"]
1499 created_volumes = []
1500 total_created_volumes = [
1501 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1502 ]
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})
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]
1515 context["created_volumes"] = created_volumes
1516 context["total_created_volumes"] = total_created_volumes
1518 return context
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)
1525 for year in years:
1526 issues = Container.objects.filter(my_collection__pid=pid, year=year)
1527 articles_count = 0
1528 page_count = 0
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")
1535 articles_count += articles.count()
1536 page_count += sum(article.get_article_page_count() for article in articles)
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
1542 return data_by_year, total_articles, total_pages
1545class ReferencingDashboardView(BaseMersenneDashboardView):
1546 template_name = "dashboard/referencing.html"
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})
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")
1561 if aid and year:
1562 context["collection"] = self.get_collection(aid, year)
1564 return context
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")
1571class ArticleListView(BaseCollectionView):
1572 template_name = "collection-list.html"
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")
1584class VolumeListView(BaseCollectionView):
1585 template_name = "collection-list.html"
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")
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
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)
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
1625 resource = model_helpers.get_resource(pid)
1626 if resource is None:
1627 raise Http404
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)
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
1651 def recordDOICollection(self, collection, force=None):
1652 return recordDOI(collection)
1654 def recordDOIContainer(self, container, force=None):
1655 data = {"status": 200, "message": "tout va bien"}
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 )
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
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)
1698 data = {"status": 200, "message": "tout va bien"}
1699 return JsonResponse(data)
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)
1706 def checkDOICollection(self, collection):
1707 get_or_create_doibatch(collection)
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)
1716class RegisterPubmedFormView(FormView):
1717 template_name = "record_pubmed_dialog.html"
1718 form_class = RegisterPubmedForm
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
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"
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)
1741 return HttpResponseRedirect(
1742 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1743 )
1746class PTFToolsContainerView(TemplateView):
1747 template_name = ""
1749 def get_context_data(self, **kwargs):
1750 context = super().get_context_data(**kwargs)
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
1800 # article1 = articles.first()
1801 # date = article1.deployed_date()
1802 # TODO next_issue, previous_issue
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)
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"
1830 context["allow_crossref"] = container.allow_crossref()
1831 context["coltype"] = container.my_collection.coltype
1832 return context
1835class ExtLinkInline(InlineFormSetFactory):
1836 model = ExtLink
1837 form_class = ExtLinkForm
1838 factory_kwargs = {"extra": 0}
1841class ResourceIdInline(InlineFormSetFactory):
1842 model = ResourceId
1843 form_class = ResourceIdForm
1844 factory_kwargs = {"extra": 0}
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 )
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)
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"]
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
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()
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 )
1924 form.instance.title_html = form.instance.title_tex
1925 form.instance.title_sort = form.instance.title_tex
1926 result = super().form_valid(form)
1928 collection = self.object
1929 collection.abstract_set.all().delete()
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)
1940 return result
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})
1947class CollectionCreate(CollectionFormView, CreateWithInlinesView):
1948 """
1949 Warning : Not yet finished
1950 Automatic site membership creation is still missing
1951 """
1954class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
1955 slug_field = "pid"
1956 slug_url_kwarg = "pid"
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 )
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]
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)
1990 # add the parent collection to its children list and sort it by date
1991 result.update({"journal": journal})
1992 collections.append(result)
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 )
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
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"
2019 def test_func(self):
2020 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2022 def get(self, request, *args, **kwargs):
2023 self.object = self.get_object(queryset=Collection.objects.all())
2024 return super().get(request, *args, **kwargs)
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))
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
2057 def get_queryset(self):
2058 query = self.object.content.all()
2060 for ancestor in self.object.ancestors.all():
2061 query |= ancestor.content.all()
2063 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2066class ContainerEditView(FormView):
2067 template_name = "container_form.html"
2068 form_class = ContainerForm
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")
2075 def set_success_message(self): # pylint: disable=no-self-use
2076 messages.success(self.request, "Le fascicule a été modifié")
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"]
2089 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2090 self.kwargs["pid"]
2091 )
2092 return kwargs
2094 def get_context_data(self, **kwargs):
2095 context = super().get_context_data(**kwargs)
2097 context["pid"] = self.kwargs["pid"]
2098 context["colid"] = self.kwargs["colid"]
2099 context["container"] = self.kwargs["container"]
2101 context["edit_container"] = context["pid"] is not None
2102 context["name"] = resolve(self.request.path_info).url_name
2104 return context
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")
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"])
2125 if collection is None:
2126 raise ValueError("Collection for " + new_pid + " does not exist")
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]
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 )
2143 with open(icon_filename, "wb+") as destination:
2144 for chunk in self.request.FILES["icon"].chunks():
2145 destination.write(chunk)
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")
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 )
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
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()
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 )
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]
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")
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()
2231 self.kwargs["pid"] = new_pid
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()
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()
2249 self.set_success_message()
2251 return super().form_valid(form)
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)
2521@require_http_methods(["POST"])
2522def do_not_publish_article(request, *args, **kwargs):
2523 next = request.headers.get("referer")
2525 pid = kwargs.get("pid", "")
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
2534 return HttpResponseRedirect(next)
2537@require_http_methods(["POST"])
2538def show_article_body(request, *args, **kwargs):
2539 next = request.headers.get("referer")
2541 pid = kwargs.get("pid", "")
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
2550 return HttpResponseRedirect(next)
2553class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2554 """
2555 API to get/post article metadata
2556 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2557 """
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"
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()
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()
2608 def get(self, request, *args, **kwargs):
2609 data = super().get(request, *args, **kwargs)
2610 return data
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
2625class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2626 template_name = "article_form.html"
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")
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
2639 return context
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)
2647 try:
2648 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2649 article.undeploy(mersenneSite)
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)
2660 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2661 return JsonResponse(data)
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()
2671 with app.connection_or_acquire() as conn:
2672 remaining = conn.default_channel.queue_declare(queue="celery", passive=True).message_count
2673 return remaining
2676class FailedTasksListView(ListView):
2677 model = TaskResult
2678 queryset = TaskResult.objects.filter(
2679 status="FAILURE",
2680 task_name="ptf_tools.tasks.archive_numdam_issue",
2681 )
2684class FailedTasksDeleteView(DeleteView):
2685 model = TaskResult
2686 success_url = reverse_lazy("tasks-failed")
2689class FailedTasksRetryView(SingleObjectMixin, RedirectView):
2690 model = TaskResult
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()
2698 def get_redirect_url(self, *args, **kwargs):
2699 self.retry_task(self.get_object())
2700 return reverse("tasks-failed")
2703class NumdamView(TemplateView, history_views.HistoryContextMixin):
2704 template_name = "numdam.html"
2706 def get_context_data(self, **kwargs):
2707 context = super().get_context_data(**kwargs)
2709 context["objs"] = ResourceInNumdam.objects.all()
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
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"]
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
2741 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2742 return context
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)
2782class TasksView(TemplateView):
2783 template_name = "tasks.html"
2785 def get_context_data(self, **kwargs):
2786 context = super().get_context_data(**kwargs)
2787 context["tasks"] = TaskResult.objects.all()
2788 return context
2791class NumdamArchiveView(RedirectView):
2792 @staticmethod
2793 def reset_task_results():
2794 TaskResult.objects.all().delete()
2796 def get_redirect_url(self, *args, **kwargs):
2797 self.colid = kwargs["colid"]
2799 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2800 return Http404
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"])
2809 if self.colid != "ALL" and self.colid not in data:
2810 return Http404
2812 colids = [self.colid] if self.colid != "ALL" else data
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")
2819 for colid in colids:
2820 if colid not in settings.MERSENNE_COLLECTIONS:
2821 archive_numdam_collection.delay(colid)
2822 return reverse("numdam")
2825class DeployAllNumdamAPIView(View):
2826 def internal_do(self, *args, **kwargs):
2827 pids = []
2829 for obj in ResourceInNumdam.objects.all():
2830 pids.append(obj.pid)
2832 return pids
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)
2842 data = {"message": message, "ids": pids, "status": status}
2843 return JsonResponse(data)
2846class NumdamDeleteAPIView(View):
2847 def get(self, request, *args, **kwargs):
2848 pid = self.kwargs.get("pid", None)
2850 try:
2851 obj = ResourceInNumdam.objects.get(pid=pid)
2852 obj.delete()
2853 except Exception as exception:
2854 return HttpResponseServerError(exception)
2856 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
2857 return JsonResponse(data)
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 )
2889class ExtIdFormTemplate(TemplateView):
2890 template_name = "common/externalid_form.html"
2892 def get_context_data(self, **kwargs):
2893 context = super().get_context_data(**kwargs)
2894 context["sequence"] = kwargs["sequence"]
2895 return context
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
2904 def get_success_url(self):
2905 self.post_process()
2906 return self.object.bibitem.resource.get_absolute_url()
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)
2915class BibItemIdCreate(BibItemIdFormView, CreateView):
2916 model = BibItemId
2917 form_class = BibItemIdForm
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
2924 def get_initial(self):
2925 initial = super().get_initial()
2926 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
2927 return initial
2929 def form_valid(self, form):
2930 form.instance.checked = False
2931 return super().form_valid(form)
2934class BibItemIdUpdate(BibItemIdFormView, UpdateView):
2935 model = BibItemId
2936 form_class = BibItemIdForm
2938 def get_context_data(self, **kwargs):
2939 context = super().get_context_data(**kwargs)
2940 context["bibitem"] = self.object.bibitem
2941 return context
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
2950 def get_success_url(self):
2951 self.post_process()
2952 return self.object.resource.get_absolute_url()
2954 def post_process(self):
2955 model_helpers.post_resource_updated(self.object.resource)
2958class ExtIdCreate(ExtIdFormView, CreateView):
2959 model = ExtId
2960 form_class = ExtIdForm
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
2967 def get_initial(self):
2968 initial = super().get_initial()
2969 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
2970 return initial
2972 def form_valid(self, form):
2973 form.instance.checked = False
2974 return super().form_valid(form)
2977class ExtIdUpdate(ExtIdFormView, UpdateView):
2978 model = ExtId
2979 form_class = ExtIdForm
2981 def get_context_data(self, **kwargs):
2982 context = super().get_context_data(**kwargs)
2983 context["resource"] = self.object.resource
2984 return context
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 )
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)
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)
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)
3036 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3037 resolver.copy_file(zip_file, dest_folder)
3039 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3040 resolver.copy_file(gz_file, dest_folder)
3042 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3044 dest_folder = os.path.join(
3045 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3046 )
3048 try:
3049 copy_zip_files(src_folder, dest_folder)
3050 except Exception as exception:
3051 return HttpResponseServerError(exception)
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)
3061 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3062 return JsonResponse(data)
3065class TestView(TemplateView):
3066 template_name = "mersenne.html"
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)
3074class TrammelArchiveView(RedirectView):
3075 @staticmethod
3076 def reset_task_results():
3077 TaskResult.objects.all().delete()
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", "")
3091 if self.colid != "ALL" and self.colid not in settings.MERSENNE_COLLECTIONS:
3092 return Http404
3094 colids = [self.colid] if self.colid != "ALL" else settings.MERSENNE_COLLECTIONS
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")
3101 for colid in colids:
3102 archive_trammel_collection.delay(
3103 colid, self.mathdoc_archive, self.binary_files_folder
3104 )
3106 if self.colid == "ALL":
3107 return reverse("home")
3108 else:
3109 return reverse("collection-detail", kwargs={"pid": self.colid})
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")
3120 def get_event_data():
3121 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
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()
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")
3130 all_tasks_count = all_tasks.count()
3131 success_count = successed_tasks.count()
3132 fail_count = failed_tasks.count()
3134 all_count = all_tasks_count + remaining_messages
3135 remaining_count = all_count - success_count - fail_count
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"
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 )
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 }
3160 return event_data
3162 def stream_response(data):
3163 # Send initial response headers
3164 yield f"data: {json.dumps(data)}\n\n"
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
3175class TrammelFailedTasksListView(ListView):
3176 model = TaskResult
3177 queryset = TaskResult.objects.filter(
3178 status="FAILURE",
3179 task_name="ptf_tools.tasks.archive_trammel_resource",
3180 )