Coverage for src/ptf_tools/views/base_views.py: 18%
1686 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-10 14:28 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-10 14:28 +0000
1import io
2import json
3import logging
4import os
5import re
6from datetime import datetime
7from itertools import groupby
9import jsonpickle
10import requests
11from allauth.account.signals import user_signed_up
12from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin
13from celery import Celery, current_app
14from django.conf import settings
15from django.contrib import messages
16from django.contrib.auth.mixins import UserPassesTestMixin
17from django.db.models import Q
18from django.http import (
19 Http404,
20 HttpRequest,
21 HttpResponse,
22 HttpResponseRedirect,
23 HttpResponseServerError,
24 JsonResponse,
25)
26from django.shortcuts import get_object_or_404, redirect, render
27from django.urls import resolve, reverse
28from django.utils import timezone
29from django.utils.decorators import method_decorator
30from django.views.decorators.csrf import csrf_exempt
31from django.views.decorators.http import require_http_methods
32from django.views.generic import ListView, TemplateView, View
33from django.views.generic.base import RedirectView
34from django.views.generic.detail import SingleObjectMixin
35from django.views.generic.edit import CreateView, FormView, UpdateView
36from django_celery_results.models import TaskResult
37from extra_views import (
38 CreateWithInlinesView,
39 InlineFormSetFactory,
40 NamedFormsetsMixin,
41 UpdateWithInlinesView,
42)
43from ptf import model_data_converter, model_helpers, utils
44from ptf.cmds import ptf_cmds, xml_cmds
45from ptf.cmds.base_cmds import make_int
46from ptf.cmds.xml.jats.builder.issue import build_title_xml
47from ptf.cmds.xml.xml_utils import replace_html_entities
48from ptf.display import resolver
49from ptf.exceptions import DOIException, ServerUnderMaintenance
50from ptf.model_data import create_issuedata, create_publisherdata, create_titledata
51from ptf.models import (
52 Abstract,
53 Article,
54 Collection,
55 Container,
56 ExtId,
57 ExtLink,
58 Resource,
59 ResourceId,
60)
61from ptf.views import ArticleView, ItemViewClassFactory
62from ptf_back.cmds.xml_cmds import addorUpdateCedricsArticleWithUnicodeXmlCmd
63from ptf_back.locks import (
64 is_tex_conversion_locked,
65 release_tex_conversion_lock,
66)
67from ptf_back.tex import create_frontpage
68from ptf_back.tex.tex_tasks import convert_article_tex
69from ptf_external.back.crossref.doi import checkDOI, recordDOI, recordPendingPublication
71# from ptf.views import ArticleEditFormWithVueAPIView
72from ptf_external.views import ArticleEditFormWithVueAPIView
73from pubmed.views import recordPubmed
74from requests import Timeout
75from requests.exceptions import HTTPError
76from task.tasks.archiving_tasks import archive_resource
78from comments_moderation.utils import get_comments_for_home, is_comment_moderator
79from history import models as history_models
80from history import views as history_views
81from history.utils import (
82 get_gap,
83 get_history_last_event_by,
84 get_last_unsolved_error,
85)
86from ptf_tools.doaj import doaj_pid_register
87from ptf_tools.forms import (
88 CollectionForm,
89 ContainerForm,
90 DiffContainerForm,
91 ExtIdForm,
92 ExtLinkForm,
93 FormSetHelper,
94 ImportArticleForm,
95 ImportContainerForm,
96 ImportEditflowArticleForm,
97 PtfFormHelper,
98 PtfLargeModalFormHelper,
99 PtfModalFormHelper,
100 RegisterPubmedForm,
101 ResourceIdForm,
102 get_article_choices,
103)
104from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos
105from ptf_tools.models import ResourceInNumdam
106from ptf_tools.signals import update_user_from_invite
107from ptf_tools.tasks import (
108 archive_numdam_collection,
109 archive_numdam_collections,
110)
111from ptf_tools.templatetags.tools_helpers import get_authorized_collections
112from ptf_tools.utils import is_authorized_editor
113from ptf_tools.views.components import breadcrumb
115logger = logging.getLogger(__name__)
118def view_404(request: HttpRequest, *args, **kwargs):
119 """
120 Dummy view raising HTTP 404 exception.
121 """
122 raise Http404
125def check_collection(collection, server_url, server_type):
126 """
127 Check if a collection exists on a serveur (test/prod)
128 and upload the collection (XML, image) if necessary
129 """
131 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
132 response = requests.get(url, verify=False)
133 # First, upload the collection XML
134 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
135 body = xml.encode("utf8")
137 url = server_url + reverse("upload-serials")
138 if response.status_code == 200:
139 # PUT http verb is used for update
140 response = requests.put(url, data=body, verify=False)
141 else:
142 # POST http verb is used for creation
143 response = requests.post(url, data=body, verify=False)
145 # Second, copy the collection images
146 # There is no need to copy files for the test server
147 # Files were already copied in /mersenne_test_data during the ptf_tools import
148 # We only need to copy files from /mersenne_test_data to
149 # /mersenne_prod_data during an upload to prod
150 if server_type == "website":
151 resolver.copy_binary_files(
152 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
153 )
154 elif server_type == "numdam":
155 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
156 if collection.pid in settings.NUMDAM_COLLECTIONS:
157 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
159 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
162def check_lock():
163 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
166def load_cedrics_article_choices(request):
167 colid = request.GET.get("colid")
168 issue = request.GET.get("issue")
169 article_choices = get_article_choices(colid, issue)
170 return render(
171 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
172 )
175class ImportCedricsArticleFormView(FormView):
176 template_name = "import_article.html"
177 form_class = ImportArticleForm
179 def dispatch(self, request, *args, **kwargs):
180 self.colid = self.kwargs["colid"]
181 return super().dispatch(request, *args, **kwargs)
183 def get_success_url(self):
184 if self.colid:
185 return reverse("collection-detail", kwargs={"pid": self.colid})
186 return "/"
188 def get_context_data(self, **kwargs):
189 context = super().get_context_data(**kwargs)
190 context["colid"] = self.colid
191 context["helper"] = PtfModalFormHelper
192 return context
194 def get_form_kwargs(self):
195 kwargs = super().get_form_kwargs()
196 kwargs["colid"] = self.colid
197 return kwargs
199 def form_valid(self, form):
200 self.issue = form.cleaned_data["issue"]
201 self.article = form.cleaned_data["article"]
202 return super().form_valid(form)
204 def import_cedrics_article(self, *args, **kwargs):
205 cmd = addorUpdateCedricsArticleWithUnicodeXmlCmd(
206 {
207 "container_pid": self.issue_pid,
208 "article_folder_name": self.article_pid,
209 "convert_tex_to_unicode": self.convert_tex_to_unicode,
210 }
211 )
212 cmd.do()
214 def post(self, request, *args, **kwargs):
215 self.colid = self.kwargs.get("colid", None)
216 issue = request.POST["issue"]
217 self.article_pid = request.POST["article"]
218 self.issue_pid = os.path.basename(os.path.dirname(issue))
219 # Only present in the POST data when the checkbox is checked
220 self.convert_tex_to_unicode = request.POST.get("convert_tex_to_unicode", False)
222 import_args = [self]
223 import_kwargs = {}
225 try:
226 _, _status, _message = history_views.execute_and_record_func(
227 "import",
228 f"{self.issue_pid} / {self.article_pid}",
229 self.colid,
230 self.import_cedrics_article,
231 "",
232 False,
233 None,
234 None,
235 *import_args,
236 **import_kwargs,
237 )
239 messages.success(
240 self.request, f"L'article {self.article_pid} a été importé avec succès"
241 )
243 except Exception as exception:
244 messages.error(
245 self.request,
246 f"Echec de l'import de l'article {self.article_pid} : {exception!s}",
247 )
249 return redirect(self.get_success_url())
252class ImportCedricsIssueView(FormView):
253 template_name = "import_container.html"
254 form_class = ImportContainerForm
256 def dispatch(self, request, *args, **kwargs):
257 self.colid = self.kwargs["colid"]
258 self.to_appear = self.request.GET.get("to_appear", False)
259 return super().dispatch(request, *args, **kwargs)
261 def get_success_url(self):
262 if self.filename:
263 return reverse(
264 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
265 )
266 return "/"
268 def get_context_data(self, **kwargs):
269 context = super().get_context_data(**kwargs)
270 context["colid"] = self.colid
271 context["helper"] = PtfModalFormHelper
272 return context
274 def get_form_kwargs(self):
275 kwargs = super().get_form_kwargs()
276 kwargs["colid"] = self.colid
277 kwargs["to_appear"] = self.to_appear
278 return kwargs
280 def form_valid(self, form):
281 self.filename = form.cleaned_data["filename"].split("/")[-1]
282 return super().form_valid(form)
285class DiffCedricsIssueView(FormView):
286 template_name = "diff_container_form.html"
287 form_class = DiffContainerForm
288 diffs = None
289 xissue = None
290 xissue_encoded = None
292 def get_success_url(self):
293 return reverse("collection-detail", kwargs={"pid": self.colid})
295 def dispatch(self, request, *args, **kwargs):
296 self.colid = self.kwargs["colid"]
297 # self.filename = self.kwargs['filename']
298 return super().dispatch(request, *args, **kwargs)
300 def get(self, request, *args, **kwargs):
301 self.filename = request.GET["filename"]
302 self.remove_mail = request.GET.get("remove_email", "off")
303 self.remove_date_prod = request.GET.get("remove_date_prod", "off")
304 self.remove_email = self.remove_mail == "on"
305 self.remove_date_prod = self.remove_date_prod == "on"
307 try:
308 result, _status, _message = history_views.execute_and_record_func(
309 "import",
310 os.path.basename(self.filename),
311 self.colid,
312 self.diff_cedrics_issue,
313 "",
314 True,
315 )
316 except Exception as exception:
317 pid = self.filename.split("/")[-1]
318 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
319 return HttpResponseRedirect(self.get_success_url())
321 result[0]
322 self.diffs = result[1]
323 self.xissue = result[2]
325 if True:
326 # Proceed with the import
327 self.form_valid(self.get_form())
328 return redirect(self.get_success_url())
329 else:
330 # Display the diff template
331 self.xissue_encoded = jsonpickle.encode(self.xissue)
333 return super().get(request, *args, **kwargs)
335 def post(self, request, *args, **kwargs):
336 self.filename = request.POST["filename"]
337 data = request.POST["xissue_encoded"]
338 self.xissue = jsonpickle.decode(data)
340 return super().post(request, *args, **kwargs)
342 def get_context_data(self, **kwargs):
343 context = super().get_context_data(**kwargs)
344 context["colid"] = self.colid
345 context["diff"] = self.diffs
346 context["filename"] = self.filename
347 context["xissue_encoded"] = self.xissue_encoded
348 return context
350 def get_form_kwargs(self):
351 kwargs = super().get_form_kwargs()
352 kwargs["colid"] = self.colid
353 return kwargs
355 def diff_cedrics_issue(self, *args, **kwargs):
356 params = {
357 "colid": self.colid,
358 "input_file": self.filename,
359 "remove_email": self.remove_mail,
360 "remove_date_prod": self.remove_date_prod,
361 "diff_only": True,
362 }
364 if settings.IMPORT_CEDRICS_DIRECTLY:
365 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
366 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
367 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
368 else:
369 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
371 result = cmd.do()
372 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
373 messages.warning(
374 self.request, message=f"Balises non parsées lors de l'import : {cmd.warnings}"
375 )
377 return result
379 def import_cedrics_issue(self, *args, **kwargs):
380 # modify xissue with data_issue if params to override
381 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
382 issue = model_helpers.get_container(self.xissue.pid)
383 if issue:
384 data_issue = model_data_converter.db_to_issue_data(issue)
385 for xarticle in self.xissue.articles:
386 filter_articles = [
387 article for article in data_issue.articles if article.doi == xarticle.doi
388 ]
389 if len(filter_articles) > 0:
390 db_article = filter_articles[0]
391 xarticle.coi_statement = db_article.coi_statement
392 xarticle.kwds = db_article.kwds
393 xarticle.contrib_groups = db_article.contrib_groups
395 params = {
396 "colid": self.colid,
397 "xissue": self.xissue,
398 "input_file": self.filename,
399 }
401 if settings.IMPORT_CEDRICS_DIRECTLY:
402 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
403 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
404 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
405 else:
406 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
408 cmd.do()
410 def form_valid(self, form):
411 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
412 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
413 else:
414 import_kwargs = {}
415 import_args = [self]
417 try:
418 _, _status, _message = history_views.execute_and_record_func(
419 "import",
420 self.xissue.pid,
421 self.kwargs["colid"],
422 self.import_cedrics_issue,
423 "",
424 False,
425 None,
426 None,
427 *import_args,
428 **import_kwargs,
429 )
430 except Exception as exception:
431 messages.error(
432 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
433 )
434 return super().form_invalid(form)
436 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
437 return super().form_valid(form)
440class ImportEditflowArticleFormView(FormView):
441 template_name = "import_editflow_article.html"
442 form_class = ImportEditflowArticleForm
444 def dispatch(self, request, *args, **kwargs):
445 self.colid = self.kwargs["colid"]
446 return super().dispatch(request, *args, **kwargs)
448 def get_success_url(self):
449 if self.colid:
450 return reverse("collection-detail", kwargs={"pid": self.colid})
451 return "/"
453 def get_context_data(self, **kwargs):
454 context = super().get_context_data(**kwargs)
455 context["colid"] = self.colid
456 context["helper"] = PtfLargeModalFormHelper
457 return context
459 def get_form_kwargs(self):
460 kwargs = super().get_form_kwargs()
461 kwargs["colid"] = self.colid
462 return kwargs
464 def form_valid(self, form):
465 try:
466 if not self.colid:
467 raise ValueError("Missing collection id")
469 issue_name = settings.ISSUE_PENDING_PUBLICATION_PIDS.get(self.colid)
470 if not issue_name:
471 raise ValueError(
472 "Issue not found in Pending Publications PIDs. Did you forget to add it?"
473 )
475 issue = model_helpers.get_container(issue_name)
476 if not issue:
477 raise ValueError("No issue found")
479 editflow_xml_file = form.cleaned_data["editflow_xml_file"]
480 body = editflow_xml_file.read().decode("utf-8")
482 cmd = xml_cmds.addArticleXmlCmd(
483 {
484 "body": body,
485 "issue": issue,
486 "assign_doi": True,
487 "standalone": True,
488 "from_folder": settings.RESOURCES_ROOT,
489 }
490 )
491 cmd.set_collection(issue.get_collection())
492 cmd.do()
494 messages.success(
495 self.request,
496 f'Editflow article successfully imported into issue "{issue_name}"',
497 )
499 except Exception as exception:
500 messages.error(
501 self.request,
502 f"Import failed: {exception}",
503 )
504 return super().form_invalid(form)
506 return super().form_valid(form)
509class MatchingAPIView(View):
510 def get(self, request, *args, **kwargs):
511 pid = self.kwargs.get("pid", None)
513 url = settings.MATCHING_URL
514 headers = {"Content-Type": "application/xml"}
516 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
518 if settings.DEBUG:
519 print("Issue exported to /tmp/issue.xml")
520 f = open("/tmp/issue.xml", "w")
521 f.write(body.encode("utf8"))
522 f.close()
524 r = requests.post(url, data=body.encode("utf8"), headers=headers)
525 body = r.text.encode("utf8")
526 data = {"status": r.status_code, "message": body[:1000]}
528 if settings.DEBUG:
529 print("Matching received, new issue exported to /tmp/issue1.xml")
530 f = open("/tmp/issue1.xml", "w")
531 text = body
532 f.write(text)
533 f.close()
535 resource = model_helpers.get_resource(pid)
536 obj = resource.cast()
537 colid = obj.get_collection().pid
539 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
541 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
542 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
543 )
544 cmd.do()
546 print("Matching finished")
547 return JsonResponse(data)
550class ImportAllAPIView(View):
551 def internal_do(self, *args, **kwargs):
552 pid = self.kwargs.get("pid", None)
554 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
555 if not os.path.isdir(root_folder):
556 raise ValueError(root_folder + " does not exist")
558 resource = model_helpers.get_resource(pid)
559 if not resource:
560 file = os.path.join(root_folder, pid + ".xml")
561 body = utils.get_file_content_in_utf8(file)
562 journals = xml_cmds.addCollectionsXmlCmd(
563 {
564 "body": body,
565 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
566 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
567 }
568 ).do()
569 if not journals:
570 raise ValueError(file + " does not contain a collection")
571 resource = journals[0]
572 # resolver.copy_binary_files(
573 # resource,
574 # settings.MATHDOC_ARCHIVE_FOLDER,
575 # settings.MERSENNE_TEST_DATA_FOLDER)
577 obj = resource.cast()
579 if obj.classname != "Collection":
580 raise ValueError(pid + " does not contain a collection")
582 cmd = xml_cmds.collectEntireCollectionXmlCmd(
583 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
584 )
585 pids = cmd.do()
587 return pids
589 def get(self, request, *args, **kwargs):
590 pid = self.kwargs.get("pid", None)
592 try:
593 pids, status, message = history_views.execute_and_record_func(
594 "import", pid, pid, self.internal_do
595 )
596 except Timeout as exception:
597 return HttpResponse(exception, status=408)
598 except Exception as exception:
599 return HttpResponseServerError(exception)
601 data = {"message": message, "ids": pids, "status": status}
602 return JsonResponse(data)
605class DeployAllAPIView(View):
606 def internal_do(self, *args, **kwargs):
607 pid = self.kwargs.get("pid", None)
608 site = self.kwargs.get("site", None)
610 pids = []
612 collection = model_helpers.get_collection(pid)
613 if not collection:
614 raise RuntimeError(pid + " does not exist")
616 if site == "numdam":
617 server_url = settings.NUMDAM_PRE_URL
618 elif site != "ptf_tools":
619 server_url = getattr(collection, site)()
620 if not server_url:
621 raise RuntimeError("The collection has no " + site)
623 if site != "ptf_tools":
624 # check if the collection exists on the server
625 # if not, check_collection will upload the collection (XML,
626 # image...)
627 check_collection(collection, server_url, site)
629 for issue in collection.content.all():
630 if site != "website" or (site == "website" and issue.are_all_articles_published()):
631 pids.append(issue.pid)
633 return pids
635 def get(self, request, *args, **kwargs):
636 pid = self.kwargs.get("pid", None)
637 site = self.kwargs.get("site", None)
639 try:
640 pids, status, message = history_views.execute_and_record_func(
641 "deploy", pid, pid, self.internal_do, site
642 )
643 except Timeout as exception:
644 return HttpResponse(exception, status=408)
645 except Exception as exception:
646 return HttpResponseServerError(exception)
648 data = {"message": message, "ids": pids, "status": status}
649 return JsonResponse(data)
652class AddIssuePDFView(View):
653 def __init(self, *args, **kwargs):
654 super().__init__(*args, **kwargs)
655 self.pid = None
656 self.issue = None
657 self.collection = None
658 self.site = "test_website"
660 def post_to_site(self, url):
661 response = requests.post(url, verify=False)
662 status = response.status_code
663 if not (199 < status < 205):
664 messages.error(self.request, response.text)
665 if status == 503:
666 raise ServerUnderMaintenance(response.text)
667 else:
668 raise RuntimeError(response.text)
670 def internal_do(self, *args, **kwargs):
671 """
672 Called by history_views.execute_and_record_func to do the actual job.
673 """
675 issue_pid = self.issue.pid
676 colid = self.collection.pid
678 if self.site == "website":
679 # Copy the PDF from the test to the production folder
680 resolver.copy_binary_files(
681 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
682 )
683 else:
684 # Copy the PDF from the cedram to the test folder
685 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
686 from_path = os.path.join(from_folder, issue_pid + ".pdf")
687 if not os.path.isfile(from_path):
688 raise Http404(f"{from_path} does not exist")
690 to_path = resolver.get_disk_location(
691 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
692 )
693 resolver.copy_file(from_path, to_path)
695 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
697 if self.site == "test_website":
698 # Post to ptf-tools: it will add a Datastream to the issue
699 absolute_url = self.request.build_absolute_uri(url)
700 self.post_to_site(absolute_url)
702 server_url = getattr(self.collection, self.site)()
703 absolute_url = server_url + url
704 # Post to the test or production website
705 self.post_to_site(absolute_url)
707 def get(self, request, *args, **kwargs):
708 """
709 Send an issue PDF to the test or production website
710 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
711 :param args:
712 :param kwargs:
713 :return:
714 """
715 if check_lock():
716 m = "Trammel is under maintenance. Please try again later."
717 messages.error(self.request, m)
718 return JsonResponse({"message": m, "status": 503})
720 self.pid = self.kwargs.get("pid", None)
721 self.site = self.kwargs.get("site", "test_website")
723 self.issue = model_helpers.get_container(self.pid)
724 if not self.issue:
725 raise Http404(f"{self.pid} does not exist")
726 self.collection = self.issue.get_top_collection()
728 try:
729 _pids, status, message = history_views.execute_and_record_func(
730 "deploy",
731 self.pid,
732 self.collection.pid,
733 self.internal_do,
734 f"add issue PDF to {self.site}",
735 )
737 except Timeout as exception:
738 return HttpResponse(exception, status=408)
739 except Exception as exception:
740 return HttpResponseServerError(exception)
742 data = {"message": message, "status": status}
743 return JsonResponse(data)
746class ArchiveAllAPIView(View):
747 """
748 - archive le xml de la collection ainsi que les binaires liés
749 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
750 @return array of issues pid
751 """
753 def internal_do(self, *args, **kwargs):
754 collection = kwargs["collection"]
755 pids = []
756 colid = collection.pid
758 logfile = os.path.join(settings.LOG_DIR, "archive.log")
759 if os.path.isfile(logfile):
760 os.remove(logfile)
762 ptf_cmds.exportPtfCmd(
763 {
764 "pid": colid,
765 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
766 "with_binary_files": True,
767 "for_archive": True,
768 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
769 }
770 ).do()
772 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
773 if os.path.isfile(cedramcls):
774 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
775 resolver.create_folder(dest_folder)
776 resolver.copy_file(cedramcls, dest_folder)
778 for issue in collection.content.all():
779 qs = issue.article_set.filter(
780 date_online_first__isnull=True, date_published__isnull=True
781 )
782 if qs.count() == 0:
783 pids.append(issue.pid)
785 return pids
787 def get(self, request, *args, **kwargs):
788 pid = self.kwargs.get("pid", None)
790 collection = model_helpers.get_collection(pid)
791 if not collection:
792 return HttpResponse(f"{pid} does not exist", status=400)
794 dict_ = {"collection": collection}
795 args_ = [self]
797 try:
798 pids, status, message = history_views.execute_and_record_func(
799 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_
800 )
801 except Timeout as exception:
802 return HttpResponse(exception, status=408)
803 except Exception as exception:
804 return HttpResponseServerError(exception)
806 data = {"message": message, "ids": pids, "status": status}
807 return JsonResponse(data)
810class CreateAllDjvuAPIView(View):
811 def internal_do(self, *args, **kwargs):
812 issue = kwargs["issue"]
813 pids = [issue.pid]
815 for article in issue.article_set.all():
816 pids.append(article.pid)
818 return pids
820 def get(self, request, *args, **kwargs):
821 pid = self.kwargs.get("pid", None)
822 issue = model_helpers.get_container(pid)
823 if not issue:
824 raise Http404(f"{pid} does not exist")
826 try:
827 dict_ = {"issue": issue}
828 args_ = [self]
830 pids, status, message = history_views.execute_and_record_func(
831 "numdam",
832 pid,
833 issue.get_collection().pid,
834 self.internal_do,
835 "",
836 False,
837 None,
838 None,
839 *args_,
840 **dict_,
841 )
842 except Exception as exception:
843 return HttpResponseServerError(exception)
845 data = {"message": message, "ids": pids, "status": status}
846 return JsonResponse(data)
849class ImportJatsContainerAPIView(View):
850 def internal_do(self, *args, **kwargs):
851 pid = self.kwargs.get("pid", None)
852 colid = self.kwargs.get("colid", None)
854 if pid and colid:
855 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
857 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
858 {
859 "body": body,
860 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
861 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
862 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
863 }
864 )
865 container = cmd.do()
866 if len(cmd.warnings) > 0:
867 messages.warning(
868 self.request,
869 message=f"Balises non parsées lors de l'import : {cmd.warnings}",
870 )
872 if not container:
873 raise RuntimeError("Error: the container " + pid + " was not imported")
875 # resolver.copy_binary_files(
876 # container,
877 # settings.MATHDOC_ARCHIVE_FOLDER,
878 # settings.MERSENNE_TEST_DATA_FOLDER)
879 #
880 # for article in container.article_set.all():
881 # resolver.copy_binary_files(
882 # article,
883 # settings.MATHDOC_ARCHIVE_FOLDER,
884 # settings.MERSENNE_TEST_DATA_FOLDER)
885 else:
886 raise RuntimeError("colid or pid are not defined")
888 def get(self, request, *args, **kwargs):
889 pid = self.kwargs.get("pid", None)
890 colid = self.kwargs.get("colid", None)
892 try:
893 _, status, message = history_views.execute_and_record_func(
894 "import", pid, colid, self.internal_do
895 )
896 except Timeout as exception:
897 return HttpResponse(exception, status=408)
898 except Exception as exception:
899 return HttpResponseServerError(exception)
901 data = {"message": message, "status": status}
902 return JsonResponse(data)
905class DeployCollectionAPIView(View):
906 # Update collection.xml on a site (with its images)
908 def internal_do(self, *args, **kwargs):
909 colid = self.kwargs.get("colid", None)
910 site = self.kwargs.get("site", None)
912 collection = model_helpers.get_collection(colid)
913 if not collection:
914 raise RuntimeError(f"{colid} does not exist")
916 if site == "numdam":
917 server_url = settings.NUMDAM_PRE_URL
918 else:
919 server_url = getattr(collection, site)()
920 if not server_url:
921 raise RuntimeError(f"The collection has no {site}")
923 # check_collection creates or updates the collection (XML, image...)
924 check_collection(collection, server_url, site)
926 def get(self, request, *args, **kwargs):
927 colid = self.kwargs.get("colid", None)
928 site = self.kwargs.get("site", None)
930 try:
931 _, status, message = history_views.execute_and_record_func(
932 "deploy", colid, colid, self.internal_do, site
933 )
934 except Timeout as exception:
935 return HttpResponse(exception, status=408)
936 except Exception as exception:
937 return HttpResponseServerError(exception)
939 data = {"message": message, "status": status}
940 return JsonResponse(data)
943class DeployJatsResourceAPIView(View):
944 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
946 def internal_do(self, *args, **kwargs):
947 pid = self.kwargs.get("pid", None)
948 colid = self.kwargs.get("colid", None)
949 site = self.kwargs.get("site", None)
951 if site in ["ptf_tools", "trammel"]:
952 raise RuntimeError("You can't deploy on Trammel itself.")
953 if check_lock():
954 msg = "Trammel is under maintenance. Please try again later."
955 messages.error(self.request, msg)
956 return JsonResponse({"messages": msg, "status": 503})
958 resource = model_helpers.get_resource(pid)
959 if not resource:
960 raise RuntimeError(f"{pid} does not exist")
962 obj = resource.cast()
963 article = None
964 if obj.classname == "Article":
965 article = obj
966 container = article.my_container
967 articles_to_deploy = [article]
968 else:
969 container = obj
970 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
972 if container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(colid, None):
973 raise RuntimeError("Pending publications should not be deployed")
974 if site == "website" and article is not None and article.do_not_publish:
975 raise RuntimeError(f"{pid} is marked as Do not publish. You can't deploy it.")
976 if site == "numdam" and article is not None:
977 raise RuntimeError("You can only deploy issues to Numdam")
979 collection = container.get_top_collection()
980 colid = collection.pid
981 djvu_exception = None
983 if site == "numdam":
984 server_url = settings.NUMDAM_PRE_URL
985 ResourceInNumdam.objects.get_or_create(pid=container.pid)
987 # 06/12/2022: DjVu are no longer added with Mersenne articles
988 # Add Djvu (before exporting the XML)
989 if False:
990 for art in container.article_set.all():
991 try:
992 cmd = ptf_cmds.addDjvuPtfCmd()
993 cmd.set_resource(art)
994 cmd.do()
995 except Exception as e:
996 # Djvu are optional.
997 # Allow the deployment, but record the exception in the history
998 djvu_exception = e
999 else:
1000 server_url = getattr(collection, site)()
1001 if not server_url:
1002 raise RuntimeError(f"The collection has no {site}")
1004 # check if the collection exists on the server
1005 # if not, check_collection will upload the collection (XML,
1006 # image...)
1007 if article is None:
1008 check_collection(collection, server_url, site)
1010 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
1011 # Create/update deployed date and published date on all container articles
1012 if site == "website":
1013 file_.write(
1014 f"Create/Update deployed_date and date_published on all articles for {pid}\n"
1015 )
1017 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
1018 cmd = ptf_cmds.publishResourcePtfCmd()
1019 cmd.set_resource(resource)
1020 updated_articles = cmd.do()
1022 create_frontpage(colid, container, updated_articles, test=False)
1024 mersenneSite = model_helpers.get_site_mersenne(colid)
1025 # create or update deployed_date on container and articles
1026 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
1028 for art in articles_to_deploy:
1029 if (
1030 art.doi
1031 and (art.date_published or art.date_online_first)
1032 and art.my_container.fyear is None
1033 ):
1034 art.my_container.fyear = datetime.now().year
1035 # BUG ? update the container but no save() ?
1037 file_.write(
1038 f"Publication date of {art.pid} : Online First: {art.date_online_first}, Published: {art.date_published}\n"
1039 )
1041 if article is None:
1042 resolver.copy_binary_files(
1043 container,
1044 settings.MERSENNE_TEST_DATA_FOLDER,
1045 settings.MERSENNE_PROD_DATA_FOLDER,
1046 )
1048 for art in articles_to_deploy:
1049 resolver.copy_binary_files(
1050 art,
1051 settings.MERSENNE_TEST_DATA_FOLDER,
1052 settings.MERSENNE_PROD_DATA_FOLDER,
1053 )
1055 elif site == "test_website":
1056 # create date_pre_published on articles without date_pre_published
1057 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
1058 cmd.set_resource(resource)
1059 updated_articles = cmd.do()
1061 create_frontpage(colid, container, updated_articles)
1063 export_to_website = site == "website"
1065 if article is None:
1066 with_djvu = site == "numdam"
1067 xml = ptf_cmds.exportPtfCmd(
1068 {
1069 "pid": pid,
1070 "with_djvu": with_djvu,
1071 "export_to_website": export_to_website,
1072 }
1073 ).do()
1074 body = xml.encode("utf8")
1076 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1077 url = server_url + reverse("issue_upload")
1078 else:
1079 url = server_url + reverse("book_upload")
1081 # verify=False: ignore TLS certificate
1082 response = requests.post(url, data=body, verify=False)
1083 # response = requests.post(url, files=files, verify=False)
1084 else:
1085 xml = ptf_cmds.exportPtfCmd(
1086 {
1087 "pid": pid,
1088 "with_djvu": False,
1089 "article_standalone": True,
1090 "collection_pid": collection.pid,
1091 "export_to_website": export_to_website,
1092 "export_folder": settings.LOG_DIR,
1093 }
1094 ).do()
1095 # Unlike containers that send their XML as the body of the POST request,
1096 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1097 xml_file = io.StringIO(xml)
1098 files = {"xml": xml_file}
1100 url = server_url + reverse(
1101 "article_in_issue_upload", kwargs={"pid": container.pid}
1102 )
1103 # verify=False: ignore TLS certificate
1104 header = {}
1105 logger.debug(f"Sending POST request to {url}")
1106 response = requests.post(url, headers=header, files=files, verify=False)
1108 status = response.status_code
1110 if 199 < status < 205:
1111 # There is no need to copy files for the test server
1112 # Files were already copied in /mersenne_test_data during the ptf_tools import
1113 # We only need to copy files from /mersenne_test_data to
1114 # /mersenne_prod_data during an upload to prod
1115 if site == "website":
1116 # TODO mettre ici le record doi pour un issue publié
1117 if container.doi:
1118 recordDOI(container)
1120 for art in articles_to_deploy:
1121 # record DOI automatically when deploying in prod
1123 if art.doi and art.allow_crossref():
1124 recordDOI(art)
1126 if colid == "CRBIOL":
1127 recordPubmed(
1128 art, force_update=False, updated_articles=updated_articles
1129 )
1131 if colid == "PCJ":
1132 self.update_pcj_editor(updated_articles)
1134 # Archive the container or the article
1135 if article is None:
1136 archive_resource.delay(
1137 pid,
1138 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1139 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1140 )
1142 else:
1143 archive_resource.delay(
1144 pid,
1145 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1146 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1147 article_doi=article.doi,
1148 )
1149 # cmd = ptf_cmds.archiveIssuePtfCmd({
1150 # "pid": pid,
1151 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1152 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1153 # cmd.set_article(article) # set_article allows archiving only the article
1154 # cmd.do()
1156 elif site == "numdam":
1157 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1158 if colid in settings.NUMDAM_COLLECTIONS:
1159 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1161 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1162 for article in container.article_set.all():
1163 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1165 elif status == 503:
1166 raise ServerUnderMaintenance(response.text)
1167 else:
1168 raise RuntimeError(response.text)
1170 if djvu_exception:
1171 raise djvu_exception
1173 def get(self, request, *args, **kwargs):
1174 pid = self.kwargs.get("pid", None)
1175 colid = self.kwargs.get("colid", None)
1176 site = self.kwargs.get("site", None)
1178 try:
1179 _, status, message = history_views.execute_and_record_func(
1180 "deploy",
1181 pid,
1182 colid,
1183 self.internal_do,
1184 f"Article successfully transferred to {site}",
1185 )
1186 except Timeout as exception:
1187 return HttpResponse(exception, status=408)
1188 except Exception as exception:
1189 return HttpResponseServerError(exception)
1191 data = {"message": message, "status": status}
1192 return JsonResponse(data)
1194 def update_pcj_editor(self, updated_articles):
1195 for article in updated_articles:
1196 data = {
1197 "date_published": article.date_published.strftime("%Y-%m-%d"),
1198 "article_number": article.article_number,
1199 }
1200 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1201 requests.post(url, json=data, verify=False)
1204class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1205 article = None
1207 def internal_do(self, *args, **kwargs):
1208 lang = self.kwargs.get("lang", None)
1210 translation = None
1211 for trans_article in self.article.translations.all():
1212 if trans_article.lang == lang:
1213 translation = trans_article
1215 if translation is None:
1216 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1218 collection = self.article.get_top_collection()
1219 colid = collection.pid
1220 container = self.article.my_container
1222 if translation.date_published is None:
1223 # Add date posted
1224 cmd = ptf_cmds.publishResourcePtfCmd()
1225 cmd.set_resource(translation)
1226 cmd.do()
1227 # updated_articles = cmd.do()
1229 # # Recompile PDF to add the date posted
1230 # try:
1231 # create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1232 # except Exception:
1233 # raise PDFException(
1234 # "Unable to compile the article PDF. Please contact the centre Mersenne"
1235 # )
1237 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1238 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1239 resolver.copy_binary_files(
1240 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1241 )
1243 # Deploy in prod
1244 xml = ptf_cmds.exportPtfCmd(
1245 {
1246 "pid": self.article.pid,
1247 "with_djvu": False,
1248 "article_standalone": True,
1249 "collection_pid": colid,
1250 "export_to_website": True,
1251 "export_folder": settings.LOG_DIR,
1252 }
1253 ).do()
1254 xml_file = io.StringIO(xml)
1255 files = {"xml": xml_file}
1257 server_url = collection.website()
1258 if not server_url:
1259 raise RuntimeError("The collection has no website")
1260 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1261 header = {}
1263 try:
1264 response = requests.post(
1265 url, headers=header, files=files, verify=False
1266 ) # verify: ignore TLS certificate
1267 status = response.status_code
1268 except requests.exceptions.ConnectionError:
1269 raise ServerUnderMaintenance(
1270 "The journal is under maintenance. Please try again later."
1271 )
1273 # Register translation in Crossref
1274 if 199 < status < 205 and self.article.allow_crossref():
1275 try:
1276 recordDOI(translation)
1277 except Exception:
1278 raise DOIException(
1279 "Error while recording the DOI. Please contact the centre Mersenne"
1280 )
1282 def get(self, request, *args, **kwargs):
1283 doi = kwargs.get("doi", None)
1284 self.article = model_helpers.get_article_by_doi(doi)
1285 if self.article is None:
1286 raise Http404(f"{doi} does not exist")
1288 try:
1289 _, status, message = history_views.execute_and_record_func(
1290 "deploy",
1291 self.article.pid,
1292 self.article.get_top_collection().pid,
1293 self.internal_do,
1294 "website",
1295 )
1296 except Timeout as exception:
1297 return HttpResponse(exception, status=408)
1298 except Exception as exception:
1299 logger.exception(f"Failed to post translation for {self.article.pid}")
1300 return HttpResponseServerError(exception)
1302 data = {"message": message, "status": status}
1303 return JsonResponse(data)
1306class DeleteJatsIssueAPIView(View):
1307 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1308 def get(self, request, *args, **kwargs):
1309 pid = self.kwargs.get("pid", None)
1310 colid = self.kwargs.get("colid", None)
1311 site = self.kwargs.get("site", None)
1312 message = f"The issue has correctly been removed from {site}"
1313 status = 200
1315 issue = model_helpers.get_container(pid)
1316 if not issue:
1317 raise Http404(f"{pid} does not exist")
1318 try:
1319 if check_lock():
1320 message = "Trammel is under maintenance. Please try again later."
1321 messages.error(self.request, message)
1322 raise ServerUnderMaintenance(message)
1324 mersenneSite = model_helpers.get_site_mersenne(colid)
1326 if site == "ptf_tools":
1327 if issue.is_deployed(mersenneSite):
1328 issue.undeploy(mersenneSite)
1329 for article in issue.article_set.all():
1330 article.undeploy(mersenneSite)
1332 p = model_helpers.get_provider("mathdoc-id")
1334 cmd = ptf_cmds.addContainerPtfCmd(
1335 {
1336 "pid": issue.pid,
1337 "ctype": "issue",
1338 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1339 }
1340 )
1341 cmd.set_provider(p)
1342 cmd.add_collection(issue.get_collection())
1343 cmd.set_object_to_be_deleted(issue)
1344 cmd.undo()
1346 else:
1347 if site == "numdam":
1348 server_url = settings.NUMDAM_PRE_URL
1349 else:
1350 collection = issue.get_collection()
1351 server_url = getattr(collection, site)()
1353 if not server_url:
1354 raise Http404(f"The collection has no {site}")
1355 else:
1356 # Delete on site
1357 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1358 response = requests.delete(url, verify=False)
1359 status = response.status_code
1361 if status == 404:
1362 raise Http404(
1363 "Issue not found on {site}. Please make sure the article exists."
1364 )
1365 elif status > 204:
1366 raise HTTPError(
1367 response.text.encode("utf8")[:1000],
1368 response=response,
1369 )
1370 else:
1371 status = 200
1373 # Unpublish issue in collection site (site_register.json)
1374 if site == "website" and issue.is_deployed(mersenneSite):
1375 issue.undeploy(mersenneSite)
1376 for article in issue.article_set.all():
1377 article.undeploy(mersenneSite)
1378 # Delete article binary files
1379 folder = article.get_relative_folder()
1380 resolver.delete_object_folder(
1381 folder,
1382 to_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1383 )
1384 # Delete issue binary files
1385 folder = issue.get_relative_folder()
1386 resolver.delete_object_folder(
1387 folder, to_folder=settings.MERSENNE_PROD_DATA_FOLDER
1388 )
1390 except ValueError as exception:
1391 if site in {"ptf_tools", "numdam", "website", "test_website"}:
1392 raise
1393 message, status = str(exception), 400
1394 except Timeout:
1395 message = f"{site} is under maintenance. Please try again later."
1396 status = 503
1397 except Http404 as exception:
1398 message, status = str(exception), 404
1399 except HTTPError as exception:
1400 message, status = str(exception), 502
1401 except ServerUnderMaintenance as exception:
1402 message, status = str(exception), 503
1403 except requests.exceptions.ConnectionError:
1404 message, status = (
1405 f"{site} is under maintenance. Please try again later.",
1406 503,
1407 )
1408 except Exception as exception:
1409 logger.exception("Unexpected error when deleting article %s from %s", pid, site)
1410 message, status = str(exception), 500
1412 return JsonResponse({"message": message, "status": status}, status=status)
1415class ArchiveIssueAPIView(View):
1416 def get(self, request, *args, **kwargs):
1417 try:
1418 pid = kwargs["pid"]
1419 colid = kwargs["colid"]
1420 except IndexError:
1421 raise Http404
1423 try:
1424 cmd = ptf_cmds.archiveIssuePtfCmd(
1425 {
1426 "pid": pid,
1427 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1428 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1429 "needs_publication_date": True,
1430 }
1431 )
1432 _result, _status, message = history_views.execute_and_record_func(
1433 "archive", pid, colid, cmd.do
1434 )
1435 except Exception as exception:
1436 return HttpResponseServerError(exception)
1438 data = {"message": message, "status": 200}
1439 return JsonResponse(data)
1442class CreateDjvuAPIView(View):
1443 def internal_do(self, *args, **kwargs):
1444 pid = self.kwargs.get("pid", None)
1446 resource = model_helpers.get_resource(pid)
1447 cmd = ptf_cmds.addDjvuPtfCmd()
1448 cmd.set_resource(resource)
1449 cmd.do()
1451 def get(self, request, *args, **kwargs):
1452 pid = self.kwargs.get("pid", None)
1453 colid = pid.split("_")[0]
1455 try:
1456 _, status, message = history_views.execute_and_record_func(
1457 "numdam", pid, colid, self.internal_do
1458 )
1459 except Exception as exception:
1460 return HttpResponseServerError(exception)
1462 data = {"message": message, "status": status}
1463 return JsonResponse(data)
1466class PTFToolsHomeView(LoginRequiredMixin, View):
1467 """
1468 Home Page.
1469 - Admin & staff -> Render blank home.html
1470 - User with unique authorized collection -> Redirect to collection details page
1471 - User with multiple authorized collections -> Render home.html with data
1472 - Comment moderator -> Comments dashboard
1473 - Others -> 404 response
1474 """
1476 def get(self, request, *args, **kwargs) -> HttpResponse:
1477 # Staff or user with authorized collections
1478 if request.user.is_staff or request.user.is_superuser:
1479 return render(request, "home.html")
1481 colids = get_authorized_collections(request.user)
1482 is_mod = is_comment_moderator(request.user)
1484 # The user has no rights
1485 if not (colids or is_mod):
1486 raise Http404("No collections associated with your account.")
1487 # Comment moderator only
1488 elif not colids:
1489 return HttpResponseRedirect(reverse("comment_list"))
1491 # User with unique collection -> Redirect to collection detail page
1492 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1493 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1495 # User with multiple authorized collections - Special home
1496 context = {}
1497 context["overview"] = True
1499 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1500 all_collections = {c["pid"]: c for c in all_collections}
1502 # Comments summary
1503 try:
1504 error, comments_data = get_comments_for_home(request.user)
1505 except AttributeError:
1506 error, comments_data = True, {}
1508 context["comment_server_ok"] = False
1510 if not error:
1511 context["comment_server_ok"] = True
1512 if comments_data:
1513 for col_id, comment_nb in comments_data.items():
1514 if col_id.upper() in all_collections: 1514 ↛ 1513line 1514 didn't jump to line 1513 because the condition on line 1514 was always true
1515 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1517 # TODO: Translations summary
1518 context["translation_server_ok"] = False
1520 # Sort the collections according to the number of pending comments
1521 context["collections"] = sorted(
1522 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1523 )
1525 return render(request, "home.html", context)
1528class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1529 columns = 5
1531 def get_common_context_data(self, **kwargs):
1532 context = super().get_context_data(**kwargs)
1533 now = timezone.now()
1534 curyear = now.year
1535 years = range(curyear - self.columns + 1, curyear + 1)
1537 context["collections"] = settings.MERSENNE_COLLECTIONS
1538 context["containers_to_be_published"] = []
1539 context["last_col_events"] = []
1541 event = get_history_last_event_by("clockss", "ALL")
1542 clockss_gap = get_gap(now, event)
1544 context["years"] = years
1545 context["clockss_gap"] = clockss_gap
1547 return context
1549 def calculate_articles_and_pages(self, pid, years):
1550 data_by_year = []
1551 total_articles = [0] * len(years)
1552 total_pages = [0] * len(years)
1554 for year in years:
1555 articles = self.get_articles_for_year(pid, year)
1556 articles_count = articles.count()
1557 page_count = sum(article.get_article_page_count() for article in articles)
1559 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1560 total_articles[year - years[0]] += articles_count
1561 total_pages[year - years[0]] += page_count
1563 return data_by_year, total_articles, total_pages
1565 def get_articles_for_year(self, pid, year):
1566 return Article.objects.filter(
1567 Q(my_container__my_collection__pid=pid)
1568 & (
1569 Q(date_published__year=year, date_online_first__isnull=True)
1570 | Q(date_online_first__year=year)
1571 )
1572 ).prefetch_related("resourcecount_set")
1575class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1576 template_name = "dashboard/published_articles.html"
1578 def get_context_data(self, **kwargs):
1579 context = self.get_common_context_data(**kwargs)
1580 years = context["years"]
1582 published_articles = []
1583 total_published_articles = [
1584 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1585 ]
1587 for pid in settings.MERSENNE_COLLECTIONS:
1588 if pid != "MERSENNE":
1589 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1590 pid, years
1591 )
1592 published_articles.append({"pid": pid, "years": articles_data})
1594 for i, year in enumerate(years):
1595 total_published_articles[i]["total_articles"] += total_articles[i]
1596 total_published_articles[i]["total_pages"] += total_pages[i]
1598 context["published_articles"] = published_articles
1599 context["total_published_articles"] = total_published_articles
1601 return context
1604class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1605 template_name = "dashboard/created_volumes.html"
1607 def get_context_data(self, **kwargs):
1608 context = self.get_common_context_data(**kwargs)
1609 years = context["years"]
1611 created_volumes = []
1612 total_created_volumes = [
1613 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1614 ]
1616 for pid in settings.MERSENNE_COLLECTIONS:
1617 if pid != "MERSENNE":
1618 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1619 pid, years
1620 )
1621 created_volumes.append({"pid": pid, "years": volumes_data})
1623 for i, _ in enumerate(years):
1624 total_created_volumes[i]["total_articles"] += total_articles[i]
1625 total_created_volumes[i]["total_pages"] += total_pages[i]
1627 context["created_volumes"] = created_volumes
1628 context["total_created_volumes"] = total_created_volumes
1630 return context
1632 def calculate_volumes_and_pages(self, pid, years):
1633 data_by_year = []
1634 total_articles = [0] * len(years)
1635 total_pages = [0] * len(years)
1637 for year in years:
1638 issues = Container.objects.filter(my_collection__pid=pid, fyear=year)
1639 articles_count = 0
1640 page_count = 0
1642 for issue in issues:
1643 articles = issue.article_set.filter(
1644 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1645 ).prefetch_related("resourcecount_set")
1647 articles_count += articles.count()
1648 page_count += sum(article.get_article_page_count() for article in articles)
1650 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1651 total_articles[year - years[0]] += articles_count
1652 total_pages[year - years[0]] += page_count
1654 return data_by_year, total_articles, total_pages
1657class ReferencingChoice(View):
1658 def post(self, request, *args, **kwargs):
1659 if request.POST.get("optSite") == "ads":
1660 return redirect(
1661 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1662 )
1663 elif request.POST.get("optSite") == "wos":
1664 comp = ReferencingCheckerWos()
1665 journal = comp.make_journal(request.POST.get("selectCol"))
1666 if journal is None:
1667 return render(
1668 request,
1669 "dashboard/referencing.html",
1670 {
1671 "error": "Collection not found",
1672 "colid": request.POST.get("selectCol"),
1673 "optSite": request.POST.get("optSite"),
1674 },
1675 )
1676 return render(
1677 request,
1678 "dashboard/referencing.html",
1679 {
1680 "journal": journal,
1681 "colid": request.POST.get("selectCol"),
1682 "optSite": request.POST.get("optSite"),
1683 },
1684 )
1687class ReferencingWosFileView(View):
1688 template_name = "dashboard/referencing.html"
1690 def post(self, request, *args, **kwargs):
1691 colid = request.POST["colid"]
1692 if request.FILES.get("risfile") is None:
1693 message = "No file uploaded"
1694 return render(
1695 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1696 )
1697 uploaded_file = request.FILES["risfile"]
1698 comp = ReferencingCheckerWos()
1699 journal = comp.check_references(colid, uploaded_file)
1700 return render(request, self.template_name, {"journal": journal})
1703class ReferencingDashboardView(BaseMersenneDashboardView):
1704 template_name = "dashboard/referencing.html"
1706 def get(self, request, *args, **kwargs):
1707 colid = self.kwargs.get("colid", None)
1708 comp = ReferencingCheckerAds()
1709 journal = comp.check_references(colid)
1710 return render(request, self.template_name, {"journal": journal})
1713class BaseCollectionView(TemplateView):
1714 def get_context_data(self, **kwargs):
1715 context = super().get_context_data(**kwargs)
1716 aid = context.get("aid")
1717 year = context.get("year")
1719 if aid and year:
1720 context["collection"] = self.get_collection(aid, year)
1722 return context
1724 def get_collection(self, aid, year):
1725 """Method to be overridden by subclasses to fetch the appropriate collection"""
1726 raise NotImplementedError("Subclasses must implement get_collection method")
1729class ArticleListView(BaseCollectionView):
1730 template_name = "collection-list.html"
1732 def get_collection(self, aid, year):
1733 return Article.objects.filter(
1734 Q(my_container__my_collection__pid=aid)
1735 & (
1736 Q(date_published__year=year, date_online_first__isnull=True)
1737 | Q(date_online_first__year=year)
1738 )
1739 ).prefetch_related("resourcecount_set")
1742class VolumeListView(BaseCollectionView):
1743 template_name = "collection-list.html"
1745 def get_collection(self, aid, year):
1746 return Article.objects.filter(
1747 Q(my_container__my_collection__pid=aid, my_container__fyear=year)
1748 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1749 ).prefetch_related("resourcecount_set")
1752class DOAJResourceRegisterView(View):
1753 def get(self, request, *args, **kwargs):
1754 pid = kwargs.get("pid", None)
1755 resource = model_helpers.get_resource(pid)
1756 if resource is None:
1757 raise Http404
1758 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1759 resource.colid, None
1760 ):
1761 raise RuntimeError("Pending publications should not be deployed")
1763 try:
1764 data = {}
1765 doaj_meta, response = doaj_pid_register(pid)
1766 if response is None:
1767 return HttpResponse(status=204)
1768 elif doaj_meta and 200 <= response.status_code <= 299:
1769 data.update(doaj_meta)
1770 else:
1771 return HttpResponse(status=response.status_code, reason=response.text)
1772 except Timeout as exception:
1773 return HttpResponse(exception, status=408)
1774 except Exception as exception:
1775 return HttpResponseServerError(exception)
1776 return JsonResponse(data)
1779class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View):
1780 """
1781 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml
1782 """
1784 def get(self, request, *args, **kwargs):
1785 pid = kwargs.get("pid")
1786 if not pid:
1787 raise Http404("Missing pid")
1789 article = Article.objects.filter(pid=pid).first()
1790 if not article:
1791 raise Http404(f"Article not found: {pid}")
1793 colid = article.get_collection().pid
1794 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
1795 return JsonResponse(
1796 {"status": 403, "message": f"Tex conversions are disabled in {colid}"}
1797 )
1799 if is_tex_conversion_locked(pid):
1800 logger.warning("Conversion rejected (lock exists) for %s", pid)
1801 return JsonResponse(
1802 {"status": 409, "message": f"A conversion is already running for {pid}"}
1803 )
1805 logger.info("No lock → scheduling conversion for %s", pid)
1807 try:
1808 convert_article_tex.delay(pid=pid, user_pk=request.user.pk)
1809 except Exception:
1810 logger.exception("Failed to enqueue task for %s", pid)
1811 release_tex_conversion_lock(pid)
1812 raise
1814 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"})
1817class CROSSREFResourceRegisterView(View):
1818 def get(self, request, *args, **kwargs):
1819 pid = kwargs.get("pid", None)
1820 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1821 force = kwargs.get("force", None)
1822 if not request.user.is_superuser:
1823 force = None
1825 resource = model_helpers.get_resource(pid)
1826 if resource is None:
1827 raise Http404
1829 resource = resource.cast()
1830 meth = getattr(self, "recordDOI" + resource.classname)
1831 try:
1832 data = meth(resource, force)
1833 except Timeout as exception:
1834 return HttpResponse(exception, status=408)
1835 except Exception as exception:
1836 return HttpResponseServerError(exception)
1837 return JsonResponse(data)
1839 def recordDOIArticle(self, article: "Article", force=None):
1840 result = {"status": 404}
1841 if (
1842 article.doi
1843 and not article.do_not_publish
1844 and (article.date_published or article.date_online_first or force == "force")
1845 ):
1846 if article.my_container.fyear == 0:
1847 article.my_container.fyear = datetime.now().year
1848 result = recordDOI(article)
1849 return result
1851 def recordDOICollection(self, collection, force=None):
1852 return recordDOI(collection)
1854 def recordDOIContainer(self, container, force=None):
1855 data = {"status": 200, "message": "All DOI successfully checked"}
1857 if container.ctype == "issue":
1858 if container.doi:
1859 result = recordDOI(container)
1860 if result["status"] != 200:
1861 return result
1862 if force == "force":
1863 articles = container.article_set.exclude(
1864 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1865 )
1866 else:
1867 articles = container.article_set.exclude(
1868 doi__isnull=True,
1869 do_not_publish=True,
1870 date_published__isnull=True,
1871 date_online_first__isnull=True,
1872 )
1874 for article in articles:
1875 result = self.recordDOIArticle(article, force)
1876 if result["status"] != 200:
1877 data = result
1878 else:
1879 return recordDOI(container)
1880 return data
1883class CROSSREFResourceCheckStatusView(View):
1884 def get(self, request, *args, **kwargs):
1885 pid = kwargs.get("pid", None)
1886 resource = model_helpers.get_resource(pid)
1887 if resource is None:
1888 raise Http404
1889 resource = resource.cast()
1890 meth = getattr(self, "checkDOI" + resource.classname)
1891 try:
1892 meth(resource)
1893 except Timeout as exception:
1894 return HttpResponse(exception, status=408)
1895 except Exception as exception:
1896 return HttpResponseServerError(exception)
1898 data = {"status": 200, "message": "DOI successfully checked"}
1899 return JsonResponse(data)
1901 def checkDOIArticle(self, article: "Article"):
1902 if article.my_container.fyear == 0:
1903 article.my_container.fyear = datetime.now().year
1904 checkDOI(article)
1906 def checkDOICollection(self, collection):
1907 checkDOI(collection)
1909 def checkDOIContainer(self, container):
1910 if container.doi is not None:
1911 checkDOI(container)
1912 for article in container.article_set.all():
1913 self.checkDOIArticle(article)
1916class CROSSREFResourcePendingPublicationRegisterView(View):
1917 def get(self, request, *args, **kwargs):
1918 pid = kwargs.get("pid", None)
1919 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1921 resource = model_helpers.get_resource(pid)
1922 if resource is None:
1923 raise Http404
1925 resource = resource.cast()
1926 meth = getattr(self, "recordPendingPublication" + resource.classname)
1927 try:
1928 data = meth(resource)
1929 except Timeout as exception:
1930 return HttpResponse(exception, status=408)
1931 except Exception as exception:
1932 return HttpResponseServerError(exception)
1933 return JsonResponse(data)
1935 def recordPendingPublicationArticle(self, article):
1936 result = {"status": 404}
1937 if article.doi and not article.date_published and not article.date_online_first:
1938 if article.my_container.fyear is None or article.my_container.fyear == "0":
1939 article.my_container.fyear = datetime.now().year
1940 result = recordPendingPublication(article)
1941 return result
1944class RegisterPubmedFormView(FormView):
1945 template_name = "record_pubmed_dialog.html"
1946 form_class = RegisterPubmedForm
1948 def get_context_data(self, **kwargs):
1949 context = super().get_context_data(**kwargs)
1950 context["pid"] = self.kwargs["pid"]
1951 context["helper"] = PtfLargeModalFormHelper
1952 return context
1955class RegisterPubmedView(View):
1956 def get(self, request, *args, **kwargs):
1957 pid = kwargs.get("pid", None)
1958 update_article = self.request.GET.get("update_article", "on") == "on"
1960 article = model_helpers.get_article(pid)
1961 if article is None:
1962 raise Http404
1963 try:
1964 recordPubmed(article, update_article)
1965 except Exception as exception:
1966 messages.error("Unable to register the article in PubMed")
1967 return HttpResponseServerError(exception)
1969 return HttpResponseRedirect(
1970 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1971 )
1974class PTFToolsArticleView(ArticleView):
1975 def get_context_data(self, **kwargs):
1976 context = super().get_context_data(**kwargs)
1978 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(self.obj)
1980 qs = self.obj.get_top_collection().extlink_set.filter(rel="test_website")
1981 if qs:
1982 test_website = qs.first().location
1983 context["test_website"] = test_website
1984 qs = self.obj.get_top_collection().extlink_set.filter(rel="website")
1985 if qs:
1986 prod_location = qs.first().location
1987 context["prod_website"] = prod_location
1989 return context
1992ItemViewClassFactory.views["article"] = PTFToolsArticleView
1995class PTFToolsContainerView(TemplateView):
1996 template_name = ""
1998 def get_context_data(self, **kwargs):
1999 context = super().get_context_data(**kwargs)
2001 container = model_helpers.get_container(self.kwargs.get("pid"))
2002 if container is None:
2003 raise Http404
2004 citing_articles = container.citations()
2005 source = self.request.GET.get("source", None)
2006 if container.ctype.startswith("book"):
2007 book_parts = (
2008 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
2009 )
2010 references = False
2011 if container.ctype == "book-monograph":
2012 # on regarde si il y a au moins une bibliographie
2013 for art in container.article_set.all():
2014 if art.bibitem_set.count() > 0:
2015 references = True
2016 context.update(
2017 {
2018 "book": container,
2019 "book_parts": list(book_parts),
2020 "source": source,
2021 "citing_articles": citing_articles,
2022 "references": references,
2023 "test_website": container.get_top_collection()
2024 .extlink_set.get(rel="test_website")
2025 .location,
2026 "prod_website": container.get_top_collection()
2027 .extlink_set.get(rel="website")
2028 .location,
2029 }
2030 )
2031 self.template_name = "book-toc.html"
2032 else:
2033 articles = container.article_set.all().order_by("seq")
2034 for article in articles:
2035 try:
2036 last_match = (
2037 history_models.HistoryEvent.objects.filter(
2038 pid=article.pid,
2039 type="matching",
2040 )
2041 .only("created_on")
2042 .latest("created_on")
2043 )
2044 except history_models.HistoryEvent.DoesNotExist as _:
2045 article.last_match = None
2046 else:
2047 article.last_match = last_match.created_on
2049 # article1 = articles.first()
2050 # date = article1.deployed_date()
2051 # TODO next_issue, previous_issue
2053 # check DOI est maintenant une commande à part
2054 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
2055 # articlesWithStatus = []
2056 # for article in articles:
2057 # checkDOIExistence(article)
2058 # articlesWithStatus.append(article)
2060 test_location = prod_location = ""
2061 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
2062 if qs:
2063 test_location = qs.first().location
2064 qs = container.get_top_collection().extlink_set.filter(rel="website")
2065 if qs:
2066 prod_location = qs.first().location
2067 context.update(
2068 {
2069 "issue": container,
2070 "articles": articles,
2071 "source": source,
2072 "citing_articles": citing_articles,
2073 "test_website": test_location,
2074 "prod_website": prod_location,
2075 }
2076 )
2078 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
2079 context["is_issue_pending_publication"] = True
2080 if container.get_top_collection().pid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
2081 context["is_excluded_from_tex_conversion"] = True
2082 self.template_name = "issue-items.html"
2084 context["allow_crossref"] = container.allow_crossref()
2085 context["coltype"] = container.my_collection.coltype
2086 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(container)
2087 return context
2090class ExtLinkInline(InlineFormSetFactory):
2091 model = ExtLink
2092 form_class = ExtLinkForm
2093 factory_kwargs = {"extra": 0}
2096class ResourceIdInline(InlineFormSetFactory):
2097 model = ResourceId
2098 form_class = ResourceIdForm
2099 factory_kwargs = {"extra": 0}
2102class IssueDetailAPIView(View):
2103 def get(self, request, *args, **kwargs):
2104 issue = get_object_or_404(Container, pid=kwargs["pid"])
2105 deployed_date = issue.deployed_date()
2106 result = {
2107 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2108 if deployed_date
2109 else None,
2110 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2111 "all_doi_are_registered": issue.all_doi_are_registered(),
2112 "registered_in_doaj": issue.registered_in_doaj(),
2113 "doi": issue.my_collection.doi,
2114 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2115 }
2116 try:
2117 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2118 except history_models.HistoryEvent.DoesNotExist as _:
2119 pass
2120 else:
2121 result["latest"] = latest.message
2122 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2123 "%Y-%m-%d %H:%M"
2124 )
2126 result["latest_type"] = latest.type.capitalize()
2127 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2128 try:
2129 result[event_type] = timezone.localtime(
2130 history_models.HistoryEvent.objects.filter(
2131 type=event_type,
2132 status="OK",
2133 pid__startswith=issue.pid,
2134 )
2135 .latest("created_on")
2136 .created_on
2137 ).strftime("%Y-%m-%d %H:%M")
2138 except history_models.HistoryEvent.DoesNotExist as _:
2139 result[event_type] = ""
2140 return JsonResponse(result)
2143class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2144 model = Collection
2145 form_class = CollectionForm
2146 inlines = [ResourceIdInline, ExtLinkInline]
2147 inlines_names = ["resource_ids_form", "ext_links_form"]
2149 def get_context_data(self, **kwargs):
2150 context = super().get_context_data(**kwargs)
2151 context["helper"] = PtfFormHelper
2152 context["formset_helper"] = FormSetHelper
2153 return context
2155 def add_description(self, collection, description, lang, seq):
2156 if description:
2157 la = Abstract(
2158 resource=collection,
2159 tag="description",
2160 lang=lang,
2161 seq=seq,
2162 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2163 value_html=description,
2164 value_tex=description,
2165 )
2166 la.save()
2168 def form_valid(self, form):
2169 if form.instance.abbrev:
2170 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2171 else:
2172 form.instance.title_xml = (
2173 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2174 )
2176 form.instance.title_html = form.instance.title_tex
2177 form.instance.title_sort = form.instance.title_tex
2178 result = super().form_valid(form)
2180 collection = self.object
2181 collection.abstract_set.all().delete()
2183 seq = 1
2184 description = form.cleaned_data["description_en"]
2185 if description:
2186 self.add_description(collection, description, "en", seq)
2187 seq += 1
2188 description = form.cleaned_data["description_fr"]
2189 if description:
2190 self.add_description(collection, description, "fr", seq)
2192 return result
2194 def get_success_url(self):
2195 messages.success(
2196 self.request, f'The collection "{self.object.pid}" has been successfully updated'
2197 )
2198 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2201class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2202 """
2203 Warning : Not yet finished
2204 Automatic site membership creation is still missing
2205 """
2208class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2209 slug_field = "pid"
2210 slug_url_kwarg = "pid"
2213def suggest_load_journal_dois(colid):
2214 articles = (
2215 Article.objects.filter(my_container__my_collection__pid=colid)
2216 .filter(doi__isnull=False)
2217 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2218 .values_list("doi", flat=True)
2219 )
2221 try:
2222 articles = sorted(
2223 articles,
2224 key=lambda d: (
2225 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2226 int(re.search(r".(\d+)$", d).group(1)),
2227 ),
2228 )
2229 except: # noqa: E722 (we'll look later)
2230 pass
2231 return [f'<option value="{doi}">' for doi in articles]
2234def get_context_with_volumes(journal):
2235 result = model_helpers.get_volumes_in_collection(journal)
2236 volume_count = result["volume_count"]
2237 collections = []
2238 for ancestor in journal.ancestors.all():
2239 item = model_helpers.get_volumes_in_collection(ancestor)
2240 volume_count = max(0, volume_count)
2241 item.update({"journal": ancestor})
2242 collections.append(item)
2244 # add the parent collection to its children list and sort it by date
2245 result.update({"journal": journal})
2246 collections.append(result)
2248 collections = [c for c in collections if c["sorted_issues"]]
2249 collections.sort(
2250 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2251 reverse=True,
2252 )
2254 context = {
2255 "journal": journal,
2256 "sorted_issues": result["sorted_issues"],
2257 "volume_count": volume_count,
2258 "max_width": result["max_width"],
2259 "collections": collections,
2260 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2261 }
2262 return context
2265class CollectionDetail(
2266 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2267):
2268 model = Collection
2269 slug_field = "pid"
2270 slug_url_kwarg = "pid"
2271 template_name = "ptf/collection_detail.html"
2273 def test_func(self):
2274 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2276 def get(self, request, *args, **kwargs):
2277 self.object = self.get_object(queryset=Collection.objects.all())
2278 return super().get(request, *args, **kwargs)
2280 def get_context_data(self, **kwargs):
2281 context = super().get_context_data(**kwargs)
2282 context["object_list"] = context["object_list"].filter(
2283 Q(ctype="issue") | Q(ctype="book-lecture-notes") | Q(ctype="book-monograph")
2284 )
2285 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2286 context.update(get_context_with_volumes(self.object))
2288 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2289 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2290 context["issue_to_appear"] = Container.objects.filter(
2291 pid=context["issue_to_appear_pid"]
2292 ).exists()
2293 try:
2294 latest_error = history_models.HistoryEvent.objects.filter(
2295 status="ERROR", col=self.object
2296 ).latest("created_on")
2297 except history_models.HistoryEvent.DoesNotExist as _:
2298 pass
2299 else:
2300 message = latest_error.message
2301 if message:
2302 i = message.find(" - ")
2303 latest_exception = message[:i]
2304 latest_error_message = message[i + 3 :]
2305 context["latest_exception"] = latest_exception
2306 context["latest_exception_date"] = latest_error.created_on
2307 context["latest_exception_type"] = latest_error.type
2308 context["latest_error_message"] = latest_error_message
2310 archive_in_error = history_models.HistoryEvent.objects.filter(
2311 status="ERROR", col=self.object, type="archive"
2312 ).exists()
2314 context["archive_in_error"] = archive_in_error
2316 return context
2318 def get_queryset(self):
2319 query = self.object.content.all()
2321 for ancestor in self.object.ancestors.all():
2322 query |= ancestor.content.all()
2324 return query.order_by("-fyear", "-vseries", "-volume", "-volume_int", "-number_int")
2327class ContainerEditView(FormView):
2328 template_name = "container_form.html"
2329 form_class = ContainerForm
2331 def get_success_url(self):
2332 if self.kwargs["pid"]:
2333 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2334 return reverse("mersenne_dashboard/published_articles")
2336 def set_success_message(self): # pylint: disable=no-self-use
2337 messages.success(self.request, "Booklet updated")
2339 def get_form_kwargs(self):
2340 kwargs = super().get_form_kwargs()
2341 if "pid" not in self.kwargs:
2342 self.kwargs["pid"] = None
2343 if "colid" not in self.kwargs:
2344 self.kwargs["colid"] = None
2345 if "data" in kwargs and "colid" in kwargs["data"]:
2346 # colid is passed as a hidden param in the form.
2347 # It is used when you submit a new container
2348 self.kwargs["colid"] = kwargs["data"]["colid"]
2350 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2351 self.kwargs["pid"]
2352 )
2353 return kwargs
2355 def get_context_data(self, **kwargs):
2356 context = super().get_context_data(**kwargs)
2358 context["pid"] = self.kwargs["pid"]
2359 context["colid"] = self.kwargs["colid"]
2360 context["container"] = self.kwargs["container"]
2362 context["edit_container"] = context["pid"] is not None
2363 context["name"] = resolve(self.request.path_info).url_name
2365 return context
2367 def form_valid(self, form):
2368 new_pid = form.cleaned_data.get("pid")
2369 new_title = form.cleaned_data.get("title")
2370 new_trans_title = form.cleaned_data.get("trans_title")
2371 new_publisher = form.cleaned_data.get("publisher")
2372 new_year = form.cleaned_data.get("year")
2373 new_volume = form.cleaned_data.get("volume")
2374 new_number = form.cleaned_data.get("number")
2376 collection = None
2377 issue = self.kwargs["container"]
2378 if issue is not None:
2379 collection = issue.my_collection
2380 elif self.kwargs["colid"] is not None:
2381 if "CR" in self.kwargs["colid"]:
2382 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2383 else:
2384 collection = model_helpers.get_collection(self.kwargs["colid"])
2386 if collection is None:
2387 raise ValueError("Collection for " + new_pid + " does not exist")
2389 # Icon
2390 new_icon_location = ""
2391 if "icon" in self.request.FILES:
2392 filename = os.path.basename(self.request.FILES["icon"].name)
2393 file_extension = filename.split(".")[1]
2395 icon_filename = resolver.get_disk_location(
2396 settings.MERSENNE_TEST_DATA_FOLDER,
2397 collection.pid,
2398 file_extension,
2399 new_pid,
2400 None,
2401 True,
2402 )
2404 with open(icon_filename, "wb+") as destination:
2405 destination.writelines(self.request.FILES["icon"].chunks())
2407 folder = resolver.get_relative_folder(collection.pid, new_pid)
2408 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2409 name = resolve(self.request.path_info).url_name
2410 if name == "special_issue_create":
2411 self.kwargs["name"] = name
2412 if self.kwargs["container"]:
2413 # Edit Issue
2414 issue = self.kwargs["container"]
2415 if issue is None:
2416 raise ValueError(self.kwargs["pid"] + " does not exist")
2418 issue.pid = new_pid
2419 issue.title_tex = issue.title_html = new_title
2420 issue.title_xml = build_title_xml(
2421 title=new_title,
2422 lang=issue.lang,
2423 title_type="issue-title",
2424 )
2426 trans_lang = ""
2427 if new_trans_title != "":
2428 trans_lang = "fr" if issue.lang == "en" else "en"
2430 if trans_lang != "" and new_trans_title != "":
2431 title_xml = build_title_xml(
2432 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2433 )
2435 issue.title_set.update_or_create(
2436 lang=trans_lang,
2437 type="main",
2438 defaults={"title_html": new_trans_title, "title_xml": title_xml},
2439 )
2441 issue.fyear = new_year
2442 issue.volume = new_volume
2443 issue.volume_int = make_int(new_volume)
2444 issue.number = new_number
2445 issue.number_int = make_int(new_number)
2446 issue.save()
2447 else:
2448 xissue = create_issuedata()
2450 xissue.ctype = "issue"
2451 xissue.pid = new_pid
2452 xissue.lang = "en"
2453 xissue.title_tex = new_title
2454 xissue.title_html = new_title
2455 xissue.title_xml = build_title_xml(
2456 title=new_title, lang=xissue.lang, title_type="issue-title"
2457 )
2459 if new_trans_title != "":
2460 trans_lang = "fr"
2461 title_xml = build_title_xml(
2462 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2463 )
2464 title = create_titledata(
2465 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2466 )
2467 issue.titles = [title]
2469 xissue.fyear = new_year
2470 xissue.volume = new_volume
2471 xissue.number = new_number
2472 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2474 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2475 cmd.add_collection(collection)
2476 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2477 issue = cmd.do()
2479 self.kwargs["pid"] = new_pid
2481 # Add objects related to the article: contribs, datastream, counts...
2482 params = {
2483 "icon_location": new_icon_location,
2484 }
2485 cmd = ptf_cmds.updateContainerPtfCmd(params)
2486 cmd.set_resource(issue)
2487 cmd.do()
2489 publisher = model_helpers.get_publisher(new_publisher)
2490 if not publisher:
2491 xpub = create_publisherdata()
2492 xpub.name = new_publisher
2493 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2494 issue.my_publisher = publisher
2495 issue.save()
2497 self.set_success_message()
2499 return super().form_valid(form)
2502# class ArticleEditView(FormView):
2503# template_name = 'article_form.html'
2504# form_class = ArticleForm
2505#
2506# def get_success_url(self):
2507# if self.kwargs['pid']:
2508# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2509# return reverse('mersenne_dashboard/published_articles')
2510#
2511# def set_success_message(self): # pylint: disable=no-self-use
2512# messages.success(self.request, "L'article a été modifié")
2513#
2514# def get_form_kwargs(self):
2515# kwargs = super(ArticleEditView, self).get_form_kwargs()
2516#
2517# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2518# # Article creation: pid is None
2519# self.kwargs['pid'] = None
2520# if 'issue_id' not in self.kwargs:
2521# # Article edit: issue_id is not passed
2522# self.kwargs['issue_id'] = None
2523# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2524# # colid is passed as a hidden param in the form.
2525# # It is used when you submit a new container
2526# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2527#
2528# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2529# return kwargs
2530#
2531# def get_context_data(self, **kwargs):
2532# context = super(ArticleEditView, self).get_context_data(**kwargs)
2533#
2534# context['pid'] = self.kwargs['pid']
2535# context['issue_id'] = self.kwargs['issue_id']
2536# context['article'] = self.kwargs['article']
2537#
2538# context['edit_article'] = context['pid'] is not None
2539#
2540# article = context['article']
2541# if article:
2542# context['author_contributions'] = article.get_author_contributions()
2543# context['kwds_fr'] = None
2544# context['kwds_en'] = None
2545# kwd_gps = article.get_non_msc_kwds()
2546# for kwd_gp in kwd_gps:
2547# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2548# if kwd_gp.value_xml:
2549# kwd_ = types.SimpleNamespace()
2550# kwd_.value = kwd_gp.value_tex
2551# context['kwd_unstructured_fr'] = kwd_
2552# context['kwds_fr'] = kwd_gp.kwd_set.all()
2553# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2554# if kwd_gp.value_xml:
2555# kwd_ = types.SimpleNamespace()
2556# kwd_.value = kwd_gp.value_tex
2557# context['kwd_unstructured_en'] = kwd_
2558# context['kwds_en'] = kwd_gp.kwd_set.all()
2559#
2560# # Article creation: init pid
2561# if context['issue_id'] and context['pid'] is None:
2562# issue = model_helpers.get_container(context['issue_id'])
2563# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2564#
2565# return context
2566#
2567# def form_valid(self, form):
2568#
2569# new_pid = form.cleaned_data.get('pid')
2570# new_title = form.cleaned_data.get('title')
2571# new_fpage = form.cleaned_data.get('fpage')
2572# new_lpage = form.cleaned_data.get('lpage')
2573# new_page_range = form.cleaned_data.get('page_range')
2574# new_page_count = form.cleaned_data.get('page_count')
2575# new_coi_statement = form.cleaned_data.get('coi_statement')
2576# new_show_body = form.cleaned_data.get('show_body')
2577# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2578#
2579# # TODO support MathML
2580# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2581# # We need to pass trans_title to get_title_xml
2582# # Meanwhile, ignore new_title_xml
2583# new_title_xml = jats_parser.get_title_xml(new_title)
2584# new_title_html = new_title
2585#
2586# authors_count = int(self.request.POST.get('authors_count', "0"))
2587# i = 1
2588# new_authors = []
2589# old_author_contributions = []
2590# if self.kwargs['article']:
2591# old_author_contributions = self.kwargs['article'].get_author_contributions()
2592#
2593# while authors_count > 0:
2594# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2595#
2596# if prefix is not None:
2597# addresses = []
2598# if len(old_author_contributions) >= i:
2599# old_author_contribution = old_author_contributions[i - 1]
2600# addresses = [contrib_address.address for contrib_address in
2601# old_author_contribution.get_addresses()]
2602#
2603# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2604# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2605# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2606# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2607# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2608# deceased_before_publication = deceased == 'on'
2609# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2610# equal_contrib = equal_contrib == 'on'
2611# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2612# corresponding = corresponding == 'on'
2613# email = self.request.POST.get('email-' + str(i), None)
2614#
2615# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2616# params['deceased_before_publication'] = deceased_before_publication
2617# params['equal_contrib'] = equal_contrib
2618# params['corresponding'] = corresponding
2619# params['addresses'] = addresses
2620# params['email'] = email
2621#
2622# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2623#
2624# new_authors.append(params)
2625#
2626# authors_count -= 1
2627# i += 1
2628#
2629# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2630# i = 1
2631# new_kwds_fr = []
2632# while kwds_fr_count > 0:
2633# value = self.request.POST.get('kwd-fr-' + str(i), None)
2634# new_kwds_fr.append(value)
2635# kwds_fr_count -= 1
2636# i += 1
2637# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2638#
2639# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2640# i = 1
2641# new_kwds_en = []
2642# while kwds_en_count > 0:
2643# value = self.request.POST.get('kwd-en-' + str(i), None)
2644# new_kwds_en.append(value)
2645# kwds_en_count -= 1
2646# i += 1
2647# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2648#
2649# if self.kwargs['article']:
2650# # Edit article
2651# container = self.kwargs['article'].my_container
2652# else:
2653# # New article
2654# container = model_helpers.get_container(self.kwargs['issue_id'])
2655#
2656# if container is None:
2657# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2658#
2659# collection = container.my_collection
2660#
2661# # Copy PDF file & extract full text
2662# body = ''
2663# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2664# collection.pid,
2665# "pdf",
2666# container.pid,
2667# new_pid,
2668# True)
2669# if 'pdf' in self.request.FILES:
2670# with open(pdf_filename, 'wb+') as destination:
2671# for chunk in self.request.FILES['pdf'].chunks():
2672# destination.write(chunk)
2673#
2674# # Extract full text from the PDF
2675# body = utils.pdf_to_text(pdf_filename)
2676#
2677# # Icon
2678# new_icon_location = ''
2679# if 'icon' in self.request.FILES:
2680# filename = os.path.basename(self.request.FILES['icon'].name)
2681# file_extension = filename.split('.')[1]
2682#
2683# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2684# collection.pid,
2685# file_extension,
2686# container.pid,
2687# new_pid,
2688# True)
2689#
2690# with open(icon_filename, 'wb+') as destination:
2691# for chunk in self.request.FILES['icon'].chunks():
2692# destination.write(chunk)
2693#
2694# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2695# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2696#
2697# if self.kwargs['article']:
2698# # Edit article
2699# article = self.kwargs['article']
2700# article.fpage = new_fpage
2701# article.lpage = new_lpage
2702# article.page_range = new_page_range
2703# article.coi_statement = new_coi_statement
2704# article.show_body = new_show_body
2705# article.do_not_publish = new_do_not_publish
2706# article.save()
2707#
2708# else:
2709# # New article
2710# params = {
2711# 'pid': new_pid,
2712# 'title_xml': new_title_xml,
2713# 'title_html': new_title_html,
2714# 'title_tex': new_title,
2715# 'fpage': new_fpage,
2716# 'lpage': new_lpage,
2717# 'page_range': new_page_range,
2718# 'seq': container.article_set.count() + 1,
2719# 'body': body,
2720# 'coi_statement': new_coi_statement,
2721# 'show_body': new_show_body,
2722# 'do_not_publish': new_do_not_publish
2723# }
2724#
2725# xarticle = create_articledata()
2726# xarticle.pid = new_pid
2727# xarticle.title_xml = new_title_xml
2728# xarticle.title_html = new_title_html
2729# xarticle.title_tex = new_title
2730# xarticle.fpage = new_fpage
2731# xarticle.lpage = new_lpage
2732# xarticle.page_range = new_page_range
2733# xarticle.seq = container.article_set.count() + 1
2734# xarticle.body = body
2735# xarticle.coi_statement = new_coi_statement
2736# params['xobj'] = xarticle
2737#
2738# cmd = ptf_cmds.addArticlePtfCmd(params)
2739# cmd.set_container(container)
2740# cmd.add_collection(container.my_collection)
2741# article = cmd.do()
2742#
2743# self.kwargs['pid'] = new_pid
2744#
2745# # Add objects related to the article: contribs, datastream, counts...
2746# params = {
2747# # 'title_xml': new_title_xml,
2748# # 'title_html': new_title_html,
2749# # 'title_tex': new_title,
2750# 'authors': new_authors,
2751# 'page_count': new_page_count,
2752# 'icon_location': new_icon_location,
2753# 'body': body,
2754# 'use_kwds': True,
2755# 'kwds_fr': new_kwds_fr,
2756# 'kwds_en': new_kwds_en,
2757# 'kwd_uns_fr': new_kwd_uns_fr,
2758# 'kwd_uns_en': new_kwd_uns_en
2759# }
2760# cmd = ptf_cmds.updateArticlePtfCmd(params)
2761# cmd.set_article(article)
2762# cmd.do()
2763#
2764# self.set_success_message()
2765#
2766# return super(ArticleEditView, self).form_valid(form)
2769@require_http_methods(["POST"])
2770def do_not_publish_article(request, *args, **kwargs):
2771 next = request.headers.get("referer")
2773 pid = kwargs.get("pid", "")
2775 article = model_helpers.get_article(pid)
2776 if article:
2777 article.do_not_publish = not article.do_not_publish
2778 article.save()
2779 else:
2780 raise Http404
2782 return HttpResponseRedirect(next)
2785@require_http_methods(["POST"])
2786def show_article_body(request, *args, **kwargs):
2787 next = request.headers.get("referer")
2789 pid = kwargs.get("pid", "")
2791 article = model_helpers.get_article(pid)
2792 if article:
2793 article.show_body = not article.show_body
2794 article.save()
2795 else:
2796 raise Http404
2798 return HttpResponseRedirect(next)
2801class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2802 """
2803 API to get/post article metadata
2804 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2805 """
2807 def __init__(self, *args, **kwargs):
2808 """
2809 we define here what fields we want in the form
2810 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2811 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2812 super().__init__(*args, **kwargs)
2813 self.fields_to_update = [
2814 "lang",
2815 "atype",
2816 "contributors",
2817 "abstracts",
2818 "kwds",
2819 "titles",
2820 "title_html",
2821 "title_xml",
2822 "title_tex",
2823 "streams",
2824 "ext_links",
2825 "date_accepted",
2826 "history_dates",
2827 "subjs",
2828 "bibitems",
2829 "references",
2830 ]
2831 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2832 self.additional_fields = [
2833 "doi",
2834 "pid",
2835 "container_pid",
2836 "pdf",
2837 "illustration",
2838 "dates",
2839 "msc_keywords",
2840 ]
2841 self.editorial_tools = [
2842 "translation",
2843 "sidebar",
2844 "lang_selection",
2845 "back_to_article_option",
2846 "msc_keywords",
2847 ]
2848 self.article_container_pid = ""
2849 self.back_url = "trammel"
2851 def save_data(self, data_article):
2852 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2853 # The icons are not preserved since we can add/edit/delete them in VueJs
2854 params = {
2855 "pid": data_article.pid,
2856 "export_folder": settings.MERSENNE_TMP_FOLDER,
2857 "export_all": True,
2858 "with_binary_files": False,
2859 }
2860 ptf_cmds.exportExtraDataPtfCmd(params).do()
2862 def restore_data(self, article):
2863 ptf_cmds.importExtraDataPtfCmd(
2864 {
2865 "pid": article.pid,
2866 "import_folder": settings.MERSENNE_TMP_FOLDER,
2867 "import_bibitemid": False,
2868 }
2869 ).do()
2871 @method_decorator(csrf_exempt)
2872 def dispatch(self, request, *args, **kwargs):
2873 user_role = request.user.roles.all()
2874 if user_role:
2875 new_fields_to_update = []
2876 new_additional_fields = []
2877 new_editorial_tools = []
2878 # if a user have > 1 role, fields accessible shall be cumulative
2879 for role in user_role:
2880 role_fields_to_updates, role_additional_fields = role.get_real_fields_to_update()
2881 new_fields_to_update += role_fields_to_updates
2882 new_additional_fields += role_additional_fields
2883 new_editorial_tools += role.editorial_tools
2884 # in case two roles have some fields in common
2885 self.fields_to_update = list(set(new_fields_to_update))
2886 self.additional_fields = list(set(new_additional_fields))
2887 self.editorial_tools = list(set(new_editorial_tools))
2889 self.additional_fields += ["doi", "pid", "container_pid"]
2890 return super().dispatch(request, *args, **kwargs)
2892 def post(self, request, *args, **kwargs):
2893 response = super().post(request, *args, **kwargs)
2894 if response.status_code in [200, 302]: 2894 ↛ 2902line 2894 didn't jump to line 2902 because the condition on line 2894 was always true
2895 return redirect(
2896 "api-edit-article",
2897 colid=kwargs.get("colid", ""),
2898 containerPid=kwargs.get("containerPid"),
2899 doi=kwargs.get("doi", ""),
2900 )
2901 else:
2902 raise Http404
2905class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2906 template_name = "article_form.html"
2908 def get_success_url(self):
2909 if self.kwargs["doi"]:
2910 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2911 return reverse("mersenne_dashboard/published_articles")
2913 def get_context_data(self, **kwargs):
2914 context = super().get_context_data(**kwargs)
2915 if "doi" in self.kwargs:
2916 article = model_helpers.get_article_by_doi(self.kwargs["doi"])
2917 context["article"] = article
2918 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(article)
2919 context["pid"] = context["article"].pid
2921 context["container_pid"] = kwargs.get("container_pid", "")
2922 return context
2925class ArticleDeleteView(View):
2926 def get(self, request, *args, **kwargs):
2927 pid = self.kwargs.get("pid", None)
2928 article = get_object_or_404(Article, pid=pid)
2929 site = self.kwargs.get("site", None)
2930 message = f"Article successfully removed from {site}"
2931 status = 200
2933 try:
2934 if check_lock():
2935 message = "Trammel is under maintenance. Please try again later."
2936 messages.error(self.request, message)
2937 raise ServerUnderMaintenance(message)
2939 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2941 if site == "trammel":
2942 if article.is_deployed(mersenneSite):
2943 article.undeploy(mersenneSite)
2945 cmd = ptf_cmds.addArticlePtfCmd(
2946 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2947 )
2948 cmd.set_container(article.my_container)
2949 cmd.set_object_to_be_deleted(article)
2950 cmd.undo()
2952 elif site in ["website", "test_website"]:
2953 collection = article.get_collection()
2954 server_url = getattr(collection, site)()
2955 if not server_url:
2956 raise Http404(f"The collection has no {site}")
2958 # Delete on site
2959 url = server_url + reverse("article_delete", kwargs={"pid": pid})
2960 response = requests.delete(url, verify=False)
2961 status = response.status_code
2963 if status == 404:
2964 raise Http404(
2965 f"Article not found on {site}. Please make sure the article exists."
2966 )
2967 elif status > 204:
2968 exception = HTTPError(
2969 response.text.encode("utf8")[:1000],
2970 response=response,
2971 )
2972 raise exception
2973 else:
2974 if site == "website" and article.is_deployed(mersenneSite):
2975 article.undeploy(mersenneSite)
2976 # Delete article binary files
2977 folder = article.get_relative_folder()
2978 resolver.delete_object_folder(
2979 folder,
2980 to_folder=settings.MERSENNE_PROD_DATA_FOLDER,
2981 )
2982 else:
2983 raise ValueError(f"{site} is not a supported site")
2985 except ValueError as exception:
2986 if site in {"trammel", "website", "test_website"}:
2987 raise
2988 message, status = str(exception), 400
2989 except Http404 as exception:
2990 message, status = str(exception), 404
2991 except HTTPError as exception:
2992 message, status = str(exception), 502
2993 except ServerUnderMaintenance as exception:
2994 message, status = str(exception), 503
2995 except requests.exceptions.ConnectionError:
2996 message, status = (
2997 f"{site} is under maintenance. Please try again later.",
2998 503,
2999 )
3000 except Exception as exception:
3001 logger.exception("Unexpected error when deleting article %s from %s", pid, site)
3002 message, status = str(exception), 500
3004 return JsonResponse({"message": message, "status": status}, status=status)
3007def get_messages_in_queue():
3008 app = Celery("ptf-tools")
3009 # tasks = list(current_app.tasks)
3010 tasks = sorted(name for name in current_app.tasks if name.startswith("celery"))
3011 print(tasks)
3012 # i = app.control.inspect()
3014 with app.connection_or_acquire() as conn:
3015 remaining = conn.default_channel.queue_declare(
3016 queue="coordinator", passive=True
3017 ).message_count
3018 return remaining
3021class NumdamView(TemplateView, history_views.HistoryContextMixin):
3022 template_name = "numdam.html"
3024 def get_context_data(self, **kwargs):
3025 context = super().get_context_data(**kwargs)
3027 context["objs"] = ResourceInNumdam.objects.all()
3029 pre_issues = []
3030 prod_issues = []
3031 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
3032 try:
3033 response = requests.get(url)
3034 if response.status_code == 200:
3035 data = response.json()
3036 if "issues" in data:
3037 pre_issues = data["issues"]
3038 except Exception:
3039 pass
3041 url = f"{settings.NUMDAM_URL}/api-all-issues/"
3042 response = requests.get(url)
3043 if response.status_code == 200:
3044 data = response.json()
3045 if "issues" in data:
3046 prod_issues = data["issues"]
3048 new = sorted(set(pre_issues).difference(prod_issues))
3049 removed = sorted(set(prod_issues).difference(pre_issues))
3050 grouped = [
3051 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
3052 ]
3053 grouped_removed = [
3054 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
3055 ]
3056 context["added_issues"] = grouped
3057 context["removed_issues"] = grouped_removed
3059 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
3060 return context
3063class NumdamArchiveView(RedirectView):
3064 @staticmethod
3065 def reset_task_results():
3066 TaskResult.objects.all().delete()
3068 def get_redirect_url(self, *args, **kwargs):
3069 self.colid = kwargs["colid"]
3071 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
3072 return Http404
3074 # we make sure archiving is not already running
3075 # if not get_messages_in_queue():
3076 # self.reset_task_results()
3078 if self.colid == "ALL":
3079 archive_numdam_collections.delay()
3080 else:
3081 archive_numdam_collection.s(self.colid).delay()
3083 return reverse("numdam")
3086class DeployAllNumdamAPIView(View):
3087 def internal_do(self, *args, **kwargs):
3088 pids = []
3090 for obj in ResourceInNumdam.objects.all():
3091 pids.append(obj.pid)
3093 return pids
3095 def get(self, request, *args, **kwargs):
3096 try:
3097 pids, status, message = history_views.execute_and_record_func(
3098 "deploy", "numdam", "ALL", self.internal_do, "numdam"
3099 )
3100 except Exception as exception:
3101 return HttpResponseServerError(exception)
3103 data = {"message": message, "ids": pids, "status": status}
3104 return JsonResponse(data)
3107class NumdamDeleteAPIView(View):
3108 def get(self, request, *args, **kwargs):
3109 pid = self.kwargs.get("pid", None)
3111 try:
3112 obj = ResourceInNumdam.objects.get(pid=pid)
3113 obj.delete()
3114 except Exception as exception:
3115 return HttpResponseServerError(exception)
3117 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
3118 return JsonResponse(data)
3121class ExtIdApiDetail(View):
3122 def get(self, request, *args, **kwargs):
3123 extid = get_object_or_404(
3124 ExtId,
3125 resource__pid=kwargs["pid"],
3126 id_type=kwargs["what"],
3127 )
3128 return JsonResponse(
3129 {
3130 "pk": extid.pk,
3131 "href": extid.get_href(),
3132 "fetch": reverse(
3133 "api-fetch-id",
3134 args=(
3135 extid.resource.pk,
3136 extid.id_value,
3137 extid.id_type,
3138 "extid",
3139 ),
3140 ),
3141 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3142 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3143 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3144 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3145 "is_valid": extid.checked,
3146 }
3147 )
3150class ExtIdFormTemplate(TemplateView):
3151 template_name = "common/externalid_form.html"
3153 def get_context_data(self, **kwargs):
3154 context = super().get_context_data(**kwargs)
3155 context["sequence"] = kwargs["sequence"]
3156 return context
3159class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3160 def get_context_data(self, **kwargs):
3161 context = super().get_context_data(**kwargs)
3162 context["helper"] = PtfFormHelper
3163 return context
3165 def get_success_url(self):
3166 self.post_process()
3167 return self.object.resource.get_absolute_url()
3169 def post_process(self):
3170 model_helpers.post_resource_updated(self.object.resource)
3173class ExtIdCreate(ExtIdFormView, CreateView):
3174 model = ExtId
3175 form_class = ExtIdForm
3177 def get_context_data(self, **kwargs):
3178 context = super().get_context_data(**kwargs)
3179 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3180 return context
3182 def get_initial(self):
3183 initial = super().get_initial()
3184 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3185 return initial
3187 def form_valid(self, form):
3188 form.instance.checked = False
3189 return super().form_valid(form)
3192class ExtIdUpdate(ExtIdFormView, UpdateView):
3193 model = ExtId
3194 form_class = ExtIdForm
3196 def get_context_data(self, **kwargs):
3197 context = super().get_context_data(**kwargs)
3198 context["resource"] = self.object.resource
3199 return context
3202class UpdateTexmfZipAPIView(View):
3203 def get(self, request, *args, **kwargs):
3204 def copy_zip_files(src_folder, dest_folder):
3205 os.makedirs(dest_folder, exist_ok=True)
3207 zip_files = [
3208 os.path.join(src_folder, f)
3209 for f in os.listdir(src_folder)
3210 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3211 ]
3212 for zip_file in zip_files:
3213 resolver.copy_file(zip_file, dest_folder)
3215 # Exceptions: specific zip/gz files
3216 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3217 resolver.copy_file(zip_file, dest_folder)
3219 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3220 resolver.copy_file(zip_file, dest_folder)
3222 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3223 resolver.copy_file(gz_file, dest_folder)
3225 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3227 dest_folder = os.path.join(
3228 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3229 )
3231 try:
3232 copy_zip_files(src_folder, dest_folder)
3233 except Exception as exception:
3234 return HttpResponseServerError(exception)
3236 try:
3237 dest_folder = os.path.join(
3238 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3239 )
3240 copy_zip_files(src_folder, dest_folder)
3241 except Exception as exception:
3242 return HttpResponseServerError(exception)
3244 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3245 return JsonResponse(data)
3248class TrammelTasksProgressView(View):
3249 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3250 """
3251 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3252 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3253 """
3254 task_name = task
3256 def get_event_data():
3257 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3259 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3260 remaining_messages = get_messages_in_queue()
3262 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3263 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3264 failed_tasks = all_tasks.filter(status="FAILURE")
3266 all_tasks_count = all_tasks.count()
3267 success_count = successed_tasks.count()
3268 fail_count = failed_tasks.count()
3270 all_count = all_tasks_count + remaining_messages
3271 remaining_count = all_count - success_count - fail_count
3273 success_rate = int(success_count * 100 / all_count) if all_count else 0
3274 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3275 status = "consuming_queue" if remaining_count != 0 else "polling"
3277 last_task = successed_tasks.first()
3278 last_task = (
3279 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3280 if last_task
3281 else ""
3282 )
3284 # SSE event format
3285 event_data = {
3286 "status": status,
3287 "success_rate": success_rate,
3288 "error_rate": error_rate,
3289 "all_count": all_count,
3290 "remaining_count": remaining_count,
3291 "success_count": success_count,
3292 "fail_count": fail_count,
3293 "last_task": last_task,
3294 }
3296 return event_data
3298 def stream_response(data):
3299 # Send initial response headers
3300 yield f"data: {json.dumps(data)}\n\n"
3302 data = get_event_data()
3303 format = request.GET.get("format", "stream")
3304 if format == "json":
3305 response = JsonResponse(data)
3306 else:
3307 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3308 return response
3311user_signed_up.connect(update_user_from_invite)