Coverage for src/ptf_tools/views/base_views.py: 18%
1689 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-13 07:53 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-13 07:53 +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 external.back.crossref.doi import checkDOI, recordDOI, recordPendingPublication
38from extra_views import (
39 CreateWithInlinesView,
40 InlineFormSetFactory,
41 NamedFormsetsMixin,
42 UpdateWithInlinesView,
43)
45# from ptf.views import ArticleEditFormWithVueAPIView
46from matching_back.views import ArticleEditFormWithVueAPIView
47from ptf import model_data_converter, model_helpers, utils
48from ptf.cmds import ptf_cmds, xml_cmds
49from ptf.cmds.base_cmds import make_int
50from ptf.cmds.xml.jats.builder.issue import build_title_xml
51from ptf.cmds.xml.xml_utils import replace_html_entities
52from ptf.display import resolver
53from ptf.exceptions import DOIException, ServerUnderMaintenance
54from ptf.model_data import create_issuedata, create_publisherdata, create_titledata
55from ptf.models import (
56 Abstract,
57 Article,
58 Collection,
59 Container,
60 ExtId,
61 ExtLink,
62 Resource,
63 ResourceId,
64)
65from ptf.views import ArticleView, ItemViewClassFactory
66from ptf_back.locks import (
67 is_tex_conversion_locked,
68 release_tex_conversion_lock,
69)
70from ptf_back.tex import create_frontpage
71from ptf_back.tex.tex_tasks import convert_article_tex
72from pubmed.views import recordPubmed
73from requests import Timeout
74from requests.exceptions import HTTPError
75from task.tasks.archiving_tasks import archive_resource
77from comments_moderation.utils import get_comments_for_home, is_comment_moderator
78from history import models as history_models
79from history import views as history_views
80from history.utils import (
81 get_gap,
82 get_history_last_event_by,
83 get_last_unsolved_error,
84)
85from ptf_tools.doaj import doaj_pid_register
86from ptf_tools.forms import (
87 CollectionForm,
88 ContainerForm,
89 DiffContainerForm,
90 ExtIdForm,
91 ExtLinkForm,
92 FormSetHelper,
93 ImportArticleForm,
94 ImportContainerForm,
95 ImportEditflowArticleForm,
96 PtfFormHelper,
97 PtfLargeModalFormHelper,
98 PtfModalFormHelper,
99 RegisterPubmedForm,
100 ResourceIdForm,
101 get_article_choices,
102)
103from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos
104from ptf_tools.models import ResourceInNumdam
105from ptf_tools.signals import update_user_from_invite
106from ptf_tools.tasks import (
107 archive_numdam_collection,
108 archive_numdam_collections,
109)
110from ptf_tools.templatetags.tools_helpers import get_authorized_collections
111from ptf_tools.utils import is_authorized_editor
112from ptf_tools.views.components import breadcrumb
114logger = logging.getLogger(__name__)
117def view_404(request: HttpRequest, *args, **kwargs):
118 """
119 Dummy view raising HTTP 404 exception.
120 """
121 raise Http404
124def check_collection(collection, server_url, server_type):
125 """
126 Check if a collection exists on a serveur (test/prod)
127 and upload the collection (XML, image) if necessary
128 """
130 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
131 response = requests.get(url, verify=False)
132 # First, upload the collection XML
133 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
134 body = xml.encode("utf8")
136 url = server_url + reverse("upload-serials")
137 if response.status_code == 200:
138 # PUT http verb is used for update
139 response = requests.put(url, data=body, verify=False)
140 else:
141 # POST http verb is used for creation
142 response = requests.post(url, data=body, verify=False)
144 # Second, copy the collection images
145 # There is no need to copy files for the test server
146 # Files were already copied in /mersenne_test_data during the ptf_tools import
147 # We only need to copy files from /mersenne_test_data to
148 # /mersenne_prod_data during an upload to prod
149 if server_type == "website":
150 resolver.copy_binary_files(
151 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
152 )
153 elif server_type == "numdam":
154 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
155 if collection.pid in settings.NUMDAM_COLLECTIONS:
156 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
158 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
161def check_lock():
162 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
165def load_cedrics_article_choices(request):
166 colid = request.GET.get("colid")
167 issue = request.GET.get("issue")
168 article_choices = get_article_choices(colid, issue)
169 return render(
170 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
171 )
174class ImportCedricsArticleFormView(FormView):
175 template_name = "import_article.html"
176 form_class = ImportArticleForm
178 def dispatch(self, request, *args, **kwargs):
179 self.colid = self.kwargs["colid"]
180 return super().dispatch(request, *args, **kwargs)
182 def get_success_url(self):
183 if self.colid:
184 return reverse("collection-detail", kwargs={"pid": self.colid})
185 return "/"
187 def get_context_data(self, **kwargs):
188 context = super().get_context_data(**kwargs)
189 context["colid"] = self.colid
190 context["helper"] = PtfModalFormHelper
191 return context
193 def get_form_kwargs(self):
194 kwargs = super().get_form_kwargs()
195 kwargs["colid"] = self.colid
196 return kwargs
198 def form_valid(self, form):
199 self.issue = form.cleaned_data["issue"]
200 self.article = form.cleaned_data["article"]
201 return super().form_valid(form)
203 def import_cedrics_article(self, *args, **kwargs):
204 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
205 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
206 )
207 cmd.do()
209 def post(self, request, *args, **kwargs):
210 self.colid = self.kwargs.get("colid", None)
211 issue = request.POST["issue"]
212 self.article_pid = request.POST["article"]
213 self.issue_pid = os.path.basename(os.path.dirname(issue))
215 import_args = [self]
216 import_kwargs = {}
218 try:
219 _, status, message = history_views.execute_and_record_func(
220 "import",
221 f"{self.issue_pid} / {self.article_pid}",
222 self.colid,
223 self.import_cedrics_article,
224 "",
225 False,
226 None,
227 None,
228 *import_args,
229 **import_kwargs,
230 )
232 messages.success(
233 self.request, f"L'article {self.article_pid} a été importé avec succès"
234 )
236 except Exception as exception:
237 messages.error(
238 self.request,
239 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}",
240 )
242 return redirect(self.get_success_url())
245class ImportCedricsIssueView(FormView):
246 template_name = "import_container.html"
247 form_class = ImportContainerForm
249 def dispatch(self, request, *args, **kwargs):
250 self.colid = self.kwargs["colid"]
251 self.to_appear = self.request.GET.get("to_appear", False)
252 return super().dispatch(request, *args, **kwargs)
254 def get_success_url(self):
255 if self.filename:
256 return reverse(
257 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
258 )
259 return "/"
261 def get_context_data(self, **kwargs):
262 context = super().get_context_data(**kwargs)
263 context["colid"] = self.colid
264 context["helper"] = PtfModalFormHelper
265 return context
267 def get_form_kwargs(self):
268 kwargs = super().get_form_kwargs()
269 kwargs["colid"] = self.colid
270 kwargs["to_appear"] = self.to_appear
271 return kwargs
273 def form_valid(self, form):
274 self.filename = form.cleaned_data["filename"].split("/")[-1]
275 return super().form_valid(form)
278class DiffCedricsIssueView(FormView):
279 template_name = "diff_container_form.html"
280 form_class = DiffContainerForm
281 diffs = None
282 xissue = None
283 xissue_encoded = None
285 def get_success_url(self):
286 return reverse("collection-detail", kwargs={"pid": self.colid})
288 def dispatch(self, request, *args, **kwargs):
289 self.colid = self.kwargs["colid"]
290 # self.filename = self.kwargs['filename']
291 return super().dispatch(request, *args, **kwargs)
293 def get(self, request, *args, **kwargs):
294 self.filename = request.GET["filename"]
295 self.remove_mail = request.GET.get("remove_email", "off")
296 self.remove_date_prod = request.GET.get("remove_date_prod", "off")
297 self.remove_email = self.remove_mail == "on"
298 self.remove_date_prod = self.remove_date_prod == "on"
300 try:
301 result, status, message = history_views.execute_and_record_func(
302 "import",
303 os.path.basename(self.filename),
304 self.colid,
305 self.diff_cedrics_issue,
306 "",
307 True,
308 )
309 except Exception as exception:
310 pid = self.filename.split("/")[-1]
311 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
312 return HttpResponseRedirect(self.get_success_url())
314 no_conflict = result[0]
315 self.diffs = result[1]
316 self.xissue = result[2]
318 if True or no_conflict:
319 # Proceed with the import
320 self.form_valid(self.get_form())
321 return redirect(self.get_success_url())
322 else:
323 # Display the diff template
324 self.xissue_encoded = jsonpickle.encode(self.xissue)
326 return super().get(request, *args, **kwargs)
328 def post(self, request, *args, **kwargs):
329 self.filename = request.POST["filename"]
330 data = request.POST["xissue_encoded"]
331 self.xissue = jsonpickle.decode(data)
333 return super().post(request, *args, **kwargs)
335 def get_context_data(self, **kwargs):
336 context = super().get_context_data(**kwargs)
337 context["colid"] = self.colid
338 context["diff"] = self.diffs
339 context["filename"] = self.filename
340 context["xissue_encoded"] = self.xissue_encoded
341 return context
343 def get_form_kwargs(self):
344 kwargs = super().get_form_kwargs()
345 kwargs["colid"] = self.colid
346 return kwargs
348 def diff_cedrics_issue(self, *args, **kwargs):
349 params = {
350 "colid": self.colid,
351 "input_file": self.filename,
352 "remove_email": self.remove_mail,
353 "remove_date_prod": self.remove_date_prod,
354 "diff_only": True,
355 }
357 if settings.IMPORT_CEDRICS_DIRECTLY:
358 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
359 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
360 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
361 else:
362 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
364 result = cmd.do()
365 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
366 messages.warning(
367 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings
368 )
370 return result
372 def import_cedrics_issue(self, *args, **kwargs):
373 # modify xissue with data_issue if params to override
374 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
375 issue = model_helpers.get_container(self.xissue.pid)
376 if issue:
377 data_issue = model_data_converter.db_to_issue_data(issue)
378 for xarticle in self.xissue.articles:
379 filter_articles = [
380 article for article in data_issue.articles if article.doi == xarticle.doi
381 ]
382 if len(filter_articles) > 0:
383 db_article = filter_articles[0]
384 xarticle.coi_statement = db_article.coi_statement
385 xarticle.kwds = db_article.kwds
386 xarticle.contrib_groups = db_article.contrib_groups
388 params = {
389 "colid": self.colid,
390 "xissue": self.xissue,
391 "input_file": self.filename,
392 }
394 if settings.IMPORT_CEDRICS_DIRECTLY:
395 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
396 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
397 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
398 else:
399 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
401 cmd.do()
403 def form_valid(self, form):
404 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
405 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
406 else:
407 import_kwargs = {}
408 import_args = [self]
410 try:
411 _, status, message = history_views.execute_and_record_func(
412 "import",
413 self.xissue.pid,
414 self.kwargs["colid"],
415 self.import_cedrics_issue,
416 "",
417 False,
418 None,
419 None,
420 *import_args,
421 **import_kwargs,
422 )
423 except Exception as exception:
424 messages.error(
425 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
426 )
427 return super().form_invalid(form)
429 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
430 return super().form_valid(form)
433class ImportEditflowArticleFormView(FormView):
434 template_name = "import_editflow_article.html"
435 form_class = ImportEditflowArticleForm
437 def dispatch(self, request, *args, **kwargs):
438 self.colid = self.kwargs["colid"]
439 return super().dispatch(request, *args, **kwargs)
441 def get_success_url(self):
442 if self.colid:
443 return reverse("collection-detail", kwargs={"pid": self.colid})
444 return "/"
446 def get_context_data(self, **kwargs):
447 context = super().get_context_data(**kwargs)
448 context["colid"] = self.colid
449 context["helper"] = PtfLargeModalFormHelper
450 return context
452 def get_form_kwargs(self):
453 kwargs = super().get_form_kwargs()
454 kwargs["colid"] = self.colid
455 return kwargs
457 def form_valid(self, form):
458 try:
459 if not self.colid:
460 raise ValueError("Missing collection id")
462 issue_name = settings.ISSUE_PENDING_PUBLICATION_PIDS.get(self.colid)
463 if not issue_name:
464 raise ValueError(
465 "Issue not found in Pending Publications PIDs. Did you forget to add it?"
466 )
468 issue = model_helpers.get_container(issue_name)
469 if not issue:
470 raise ValueError("No issue found")
472 editflow_xml_file = form.cleaned_data["editflow_xml_file"]
473 body = editflow_xml_file.read().decode("utf-8")
475 cmd = xml_cmds.addArticleXmlCmd(
476 {
477 "body": body,
478 "issue": issue,
479 "assign_doi": True,
480 "standalone": True,
481 "from_folder": settings.RESOURCES_ROOT,
482 }
483 )
484 cmd.set_collection(issue.get_collection())
485 cmd.do()
487 messages.success(
488 self.request,
489 f'Editflow article successfully imported into issue "{issue_name}"',
490 )
492 except Exception as exception:
493 messages.error(
494 self.request,
495 f"Import failed: {exception}",
496 )
497 return super().form_invalid(form)
499 return super().form_valid(form)
502class MatchingAPIView(View):
503 def get(self, request, *args, **kwargs):
504 pid = self.kwargs.get("pid", None)
506 url = settings.MATCHING_URL
507 headers = {"Content-Type": "application/xml"}
509 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
511 if settings.DEBUG:
512 print("Issue exported to /tmp/issue.xml")
513 f = open("/tmp/issue.xml", "w")
514 f.write(body.encode("utf8"))
515 f.close()
517 r = requests.post(url, data=body.encode("utf8"), headers=headers)
518 body = r.text.encode("utf8")
519 data = {"status": r.status_code, "message": body[:1000]}
521 if settings.DEBUG:
522 print("Matching received, new issue exported to /tmp/issue1.xml")
523 f = open("/tmp/issue1.xml", "w")
524 text = body
525 f.write(text)
526 f.close()
528 resource = model_helpers.get_resource(pid)
529 obj = resource.cast()
530 colid = obj.get_collection().pid
532 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
534 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
535 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
536 )
537 cmd.do()
539 print("Matching finished")
540 return JsonResponse(data)
543class ImportAllAPIView(View):
544 def internal_do(self, *args, **kwargs):
545 pid = self.kwargs.get("pid", None)
547 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
548 if not os.path.isdir(root_folder):
549 raise ValueError(root_folder + " does not exist")
551 resource = model_helpers.get_resource(pid)
552 if not resource:
553 file = os.path.join(root_folder, pid + ".xml")
554 body = utils.get_file_content_in_utf8(file)
555 journals = xml_cmds.addCollectionsXmlCmd(
556 {
557 "body": body,
558 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
559 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
560 }
561 ).do()
562 if not journals:
563 raise ValueError(file + " does not contain a collection")
564 resource = journals[0]
565 # resolver.copy_binary_files(
566 # resource,
567 # settings.MATHDOC_ARCHIVE_FOLDER,
568 # settings.MERSENNE_TEST_DATA_FOLDER)
570 obj = resource.cast()
572 if obj.classname != "Collection":
573 raise ValueError(pid + " does not contain a collection")
575 cmd = xml_cmds.collectEntireCollectionXmlCmd(
576 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
577 )
578 pids = cmd.do()
580 return pids
582 def get(self, request, *args, **kwargs):
583 pid = self.kwargs.get("pid", None)
585 try:
586 pids, status, message = history_views.execute_and_record_func(
587 "import", pid, pid, self.internal_do
588 )
589 except Timeout as exception:
590 return HttpResponse(exception, status=408)
591 except Exception as exception:
592 return HttpResponseServerError(exception)
594 data = {"message": message, "ids": pids, "status": status}
595 return JsonResponse(data)
598class DeployAllAPIView(View):
599 def internal_do(self, *args, **kwargs):
600 pid = self.kwargs.get("pid", None)
601 site = self.kwargs.get("site", None)
603 pids = []
605 collection = model_helpers.get_collection(pid)
606 if not collection:
607 raise RuntimeError(pid + " does not exist")
609 if site == "numdam":
610 server_url = settings.NUMDAM_PRE_URL
611 elif site != "ptf_tools":
612 server_url = getattr(collection, site)()
613 if not server_url:
614 raise RuntimeError("The collection has no " + site)
616 if site != "ptf_tools":
617 # check if the collection exists on the server
618 # if not, check_collection will upload the collection (XML,
619 # image...)
620 check_collection(collection, server_url, site)
622 for issue in collection.content.all():
623 if site != "website" or (site == "website" and issue.are_all_articles_published()):
624 pids.append(issue.pid)
626 return pids
628 def get(self, request, *args, **kwargs):
629 pid = self.kwargs.get("pid", None)
630 site = self.kwargs.get("site", None)
632 try:
633 pids, status, message = history_views.execute_and_record_func(
634 "deploy", pid, pid, self.internal_do, site
635 )
636 except Timeout as exception:
637 return HttpResponse(exception, status=408)
638 except Exception as exception:
639 return HttpResponseServerError(exception)
641 data = {"message": message, "ids": pids, "status": status}
642 return JsonResponse(data)
645class AddIssuePDFView(View):
646 def __init(self, *args, **kwargs):
647 super().__init__(*args, **kwargs)
648 self.pid = None
649 self.issue = None
650 self.collection = None
651 self.site = "test_website"
653 def post_to_site(self, url):
654 response = requests.post(url, verify=False)
655 status = response.status_code
656 if not (199 < status < 205):
657 messages.error(self.request, response.text)
658 if status == 503:
659 raise ServerUnderMaintenance(response.text)
660 else:
661 raise RuntimeError(response.text)
663 def internal_do(self, *args, **kwargs):
664 """
665 Called by history_views.execute_and_record_func to do the actual job.
666 """
668 issue_pid = self.issue.pid
669 colid = self.collection.pid
671 if self.site == "website":
672 # Copy the PDF from the test to the production folder
673 resolver.copy_binary_files(
674 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
675 )
676 else:
677 # Copy the PDF from the cedram to the test folder
678 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
679 from_path = os.path.join(from_folder, issue_pid + ".pdf")
680 if not os.path.isfile(from_path):
681 raise Http404(f"{from_path} does not exist")
683 to_path = resolver.get_disk_location(
684 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
685 )
686 resolver.copy_file(from_path, to_path)
688 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
690 if self.site == "test_website":
691 # Post to ptf-tools: it will add a Datastream to the issue
692 absolute_url = self.request.build_absolute_uri(url)
693 self.post_to_site(absolute_url)
695 server_url = getattr(self.collection, self.site)()
696 absolute_url = server_url + url
697 # Post to the test or production website
698 self.post_to_site(absolute_url)
700 def get(self, request, *args, **kwargs):
701 """
702 Send an issue PDF to the test or production website
703 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
704 :param args:
705 :param kwargs:
706 :return:
707 """
708 if check_lock():
709 m = "Trammel is under maintenance. Please try again later."
710 messages.error(self.request, m)
711 return JsonResponse({"message": m, "status": 503})
713 self.pid = self.kwargs.get("pid", None)
714 self.site = self.kwargs.get("site", "test_website")
716 self.issue = model_helpers.get_container(self.pid)
717 if not self.issue:
718 raise Http404(f"{self.pid} does not exist")
719 self.collection = self.issue.get_top_collection()
721 try:
722 pids, status, message = history_views.execute_and_record_func(
723 "deploy",
724 self.pid,
725 self.collection.pid,
726 self.internal_do,
727 f"add issue PDF to {self.site}",
728 )
730 except Timeout as exception:
731 return HttpResponse(exception, status=408)
732 except Exception as exception:
733 return HttpResponseServerError(exception)
735 data = {"message": message, "status": status}
736 return JsonResponse(data)
739class ArchiveAllAPIView(View):
740 """
741 - archive le xml de la collection ainsi que les binaires liés
742 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
743 @return array of issues pid
744 """
746 def internal_do(self, *args, **kwargs):
747 collection = kwargs["collection"]
748 pids = []
749 colid = collection.pid
751 logfile = os.path.join(settings.LOG_DIR, "archive.log")
752 if os.path.isfile(logfile):
753 os.remove(logfile)
755 ptf_cmds.exportPtfCmd(
756 {
757 "pid": colid,
758 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
759 "with_binary_files": True,
760 "for_archive": True,
761 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
762 }
763 ).do()
765 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
766 if os.path.isfile(cedramcls):
767 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
768 resolver.create_folder(dest_folder)
769 resolver.copy_file(cedramcls, dest_folder)
771 for issue in collection.content.all():
772 qs = issue.article_set.filter(
773 date_online_first__isnull=True, date_published__isnull=True
774 )
775 if qs.count() == 0:
776 pids.append(issue.pid)
778 return pids
780 def get(self, request, *args, **kwargs):
781 pid = self.kwargs.get("pid", None)
783 collection = model_helpers.get_collection(pid)
784 if not collection:
785 return HttpResponse(f"{pid} does not exist", status=400)
787 dict_ = {"collection": collection}
788 args_ = [self]
790 try:
791 pids, status, message = history_views.execute_and_record_func(
792 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_
793 )
794 except Timeout as exception:
795 return HttpResponse(exception, status=408)
796 except Exception as exception:
797 return HttpResponseServerError(exception)
799 data = {"message": message, "ids": pids, "status": status}
800 return JsonResponse(data)
803class CreateAllDjvuAPIView(View):
804 def internal_do(self, *args, **kwargs):
805 issue = kwargs["issue"]
806 pids = [issue.pid]
808 for article in issue.article_set.all():
809 pids.append(article.pid)
811 return pids
813 def get(self, request, *args, **kwargs):
814 pid = self.kwargs.get("pid", None)
815 issue = model_helpers.get_container(pid)
816 if not issue:
817 raise Http404(f"{pid} does not exist")
819 try:
820 dict_ = {"issue": issue}
821 args_ = [self]
823 pids, status, message = history_views.execute_and_record_func(
824 "numdam",
825 pid,
826 issue.get_collection().pid,
827 self.internal_do,
828 "",
829 False,
830 None,
831 None,
832 *args_,
833 **dict_,
834 )
835 except Exception as exception:
836 return HttpResponseServerError(exception)
838 data = {"message": message, "ids": pids, "status": status}
839 return JsonResponse(data)
842class ImportJatsContainerAPIView(View):
843 def internal_do(self, *args, **kwargs):
844 pid = self.kwargs.get("pid", None)
845 colid = self.kwargs.get("colid", None)
847 if pid and colid:
848 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
850 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
851 {
852 "body": body,
853 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
854 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
855 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
856 }
857 )
858 container = cmd.do()
859 if len(cmd.warnings) > 0:
860 messages.warning(
861 self.request,
862 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
863 )
865 if not container:
866 raise RuntimeError("Error: the container " + pid + " was not imported")
868 # resolver.copy_binary_files(
869 # container,
870 # settings.MATHDOC_ARCHIVE_FOLDER,
871 # settings.MERSENNE_TEST_DATA_FOLDER)
872 #
873 # for article in container.article_set.all():
874 # resolver.copy_binary_files(
875 # article,
876 # settings.MATHDOC_ARCHIVE_FOLDER,
877 # settings.MERSENNE_TEST_DATA_FOLDER)
878 else:
879 raise RuntimeError("colid or pid are not defined")
881 def get(self, request, *args, **kwargs):
882 pid = self.kwargs.get("pid", None)
883 colid = self.kwargs.get("colid", None)
885 try:
886 _, status, message = history_views.execute_and_record_func(
887 "import", pid, colid, self.internal_do
888 )
889 except Timeout as exception:
890 return HttpResponse(exception, status=408)
891 except Exception as exception:
892 return HttpResponseServerError(exception)
894 data = {"message": message, "status": status}
895 return JsonResponse(data)
898class DeployCollectionAPIView(View):
899 # Update collection.xml on a site (with its images)
901 def internal_do(self, *args, **kwargs):
902 colid = self.kwargs.get("colid", None)
903 site = self.kwargs.get("site", None)
905 collection = model_helpers.get_collection(colid)
906 if not collection:
907 raise RuntimeError(f"{colid} does not exist")
909 if site == "numdam":
910 server_url = settings.NUMDAM_PRE_URL
911 else:
912 server_url = getattr(collection, site)()
913 if not server_url:
914 raise RuntimeError(f"The collection has no {site}")
916 # check_collection creates or updates the collection (XML, image...)
917 check_collection(collection, server_url, site)
919 def get(self, request, *args, **kwargs):
920 colid = self.kwargs.get("colid", None)
921 site = self.kwargs.get("site", None)
923 try:
924 _, status, message = history_views.execute_and_record_func(
925 "deploy", colid, colid, self.internal_do, site
926 )
927 except Timeout as exception:
928 return HttpResponse(exception, status=408)
929 except Exception as exception:
930 return HttpResponseServerError(exception)
932 data = {"message": message, "status": status}
933 return JsonResponse(data)
936class DeployJatsResourceAPIView(View):
937 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
939 def internal_do(self, *args, **kwargs):
940 pid = self.kwargs.get("pid", None)
941 colid = self.kwargs.get("colid", None)
942 site = self.kwargs.get("site", None)
944 if site in ["ptf_tools", "trammel"]:
945 raise RuntimeError("You can't deploy on Trammel itself.")
946 if check_lock():
947 msg = "Trammel is under maintenance. Please try again later."
948 messages.error(self.request, msg)
949 return JsonResponse({"messages": msg, "status": 503})
951 resource = model_helpers.get_resource(pid)
952 if not resource:
953 raise RuntimeError(f"{pid} does not exist")
955 obj = resource.cast()
956 article = None
957 if obj.classname == "Article":
958 article = obj
959 container = article.my_container
960 articles_to_deploy = [article]
961 else:
962 container = obj
963 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
965 if container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(colid, None):
966 raise RuntimeError("Pending publications should not be deployed")
967 if site == "website" and article is not None and article.do_not_publish:
968 raise RuntimeError(f"{pid} is marked as Do not publish. You can't deploy it.")
969 if site == "numdam" and article is not None:
970 raise RuntimeError("You can only deploy issues to Numdam")
972 collection = container.get_top_collection()
973 colid = collection.pid
974 djvu_exception = None
976 if site == "numdam":
977 server_url = settings.NUMDAM_PRE_URL
978 ResourceInNumdam.objects.get_or_create(pid=container.pid)
980 # 06/12/2022: DjVu are no longer added with Mersenne articles
981 # Add Djvu (before exporting the XML)
982 if False and int(container.fyear) < 2020:
983 for art in container.article_set.all():
984 try:
985 cmd = ptf_cmds.addDjvuPtfCmd()
986 cmd.set_resource(art)
987 cmd.do()
988 except Exception as e:
989 # Djvu are optional.
990 # Allow the deployment, but record the exception in the history
991 djvu_exception = e
992 else:
993 server_url = getattr(collection, site)()
994 if not server_url:
995 raise RuntimeError(f"The collection has no {site}")
997 # check if the collection exists on the server
998 # if not, check_collection will upload the collection (XML,
999 # image...)
1000 if article is None:
1001 check_collection(collection, server_url, site)
1003 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
1004 # Create/update deployed date and published date on all container articles
1005 if site == "website":
1006 file_.write(
1007 "Create/Update deployed_date and date_published on all articles for {}\n".format(
1008 pid
1009 )
1010 )
1012 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
1013 cmd = ptf_cmds.publishResourcePtfCmd()
1014 cmd.set_resource(resource)
1015 updated_articles = cmd.do()
1017 create_frontpage(colid, container, updated_articles, test=False)
1019 mersenneSite = model_helpers.get_site_mersenne(colid)
1020 # create or update deployed_date on container and articles
1021 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
1023 for art in articles_to_deploy:
1024 if art.doi and (art.date_published or art.date_online_first):
1025 if art.my_container.fyear is None:
1026 art.my_container.fyear = datetime.now().year
1027 # BUG ? update the container but no save() ?
1029 file_.write(
1030 "Publication date of {} : Online First: {}, Published: {}\n".format(
1031 art.pid, art.date_online_first, art.date_published
1032 )
1033 )
1035 if article is None:
1036 resolver.copy_binary_files(
1037 container,
1038 settings.MERSENNE_TEST_DATA_FOLDER,
1039 settings.MERSENNE_PROD_DATA_FOLDER,
1040 )
1042 for art in articles_to_deploy:
1043 resolver.copy_binary_files(
1044 art,
1045 settings.MERSENNE_TEST_DATA_FOLDER,
1046 settings.MERSENNE_PROD_DATA_FOLDER,
1047 )
1049 elif site == "test_website":
1050 # create date_pre_published on articles without date_pre_published
1051 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
1052 cmd.set_resource(resource)
1053 updated_articles = cmd.do()
1055 create_frontpage(colid, container, updated_articles)
1057 export_to_website = site == "website"
1059 if article is None:
1060 with_djvu = site == "numdam"
1061 xml = ptf_cmds.exportPtfCmd(
1062 {
1063 "pid": pid,
1064 "with_djvu": with_djvu,
1065 "export_to_website": export_to_website,
1066 }
1067 ).do()
1068 body = xml.encode("utf8")
1070 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1071 url = server_url + reverse("issue_upload")
1072 else:
1073 url = server_url + reverse("book_upload")
1075 # verify=False: ignore TLS certificate
1076 response = requests.post(url, data=body, verify=False)
1077 # response = requests.post(url, files=files, verify=False)
1078 else:
1079 xml = ptf_cmds.exportPtfCmd(
1080 {
1081 "pid": pid,
1082 "with_djvu": False,
1083 "article_standalone": True,
1084 "collection_pid": collection.pid,
1085 "export_to_website": export_to_website,
1086 "export_folder": settings.LOG_DIR,
1087 }
1088 ).do()
1089 # Unlike containers that send their XML as the body of the POST request,
1090 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1091 xml_file = io.StringIO(xml)
1092 files = {"xml": xml_file}
1094 url = server_url + reverse(
1095 "article_in_issue_upload", kwargs={"pid": container.pid}
1096 )
1097 # verify=False: ignore TLS certificate
1098 header = {}
1099 response = requests.post(url, headers=header, files=files, verify=False)
1101 status = response.status_code
1103 if 199 < status < 205:
1104 # There is no need to copy files for the test server
1105 # Files were already copied in /mersenne_test_data during the ptf_tools import
1106 # We only need to copy files from /mersenne_test_data to
1107 # /mersenne_prod_data during an upload to prod
1108 if site == "website":
1109 # TODO mettre ici le record doi pour un issue publié
1110 if container.doi:
1111 recordDOI(container)
1113 for art in articles_to_deploy:
1114 # record DOI automatically when deploying in prod
1116 if art.doi and art.allow_crossref():
1117 recordDOI(art)
1119 if colid == "CRBIOL":
1120 recordPubmed(
1121 art, force_update=False, updated_articles=updated_articles
1122 )
1124 if colid == "PCJ":
1125 self.update_pcj_editor(updated_articles)
1127 # Archive the container or the article
1128 if article is None:
1129 archive_resource.delay(
1130 pid,
1131 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1132 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1133 )
1135 else:
1136 archive_resource.delay(
1137 pid,
1138 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1139 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1140 article_doi=article.doi,
1141 )
1142 # cmd = ptf_cmds.archiveIssuePtfCmd({
1143 # "pid": pid,
1144 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1145 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1146 # cmd.set_article(article) # set_article allows archiving only the article
1147 # cmd.do()
1149 elif site == "numdam":
1150 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1151 if colid in settings.NUMDAM_COLLECTIONS:
1152 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1154 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1155 for article in container.article_set.all():
1156 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1158 elif status == 503:
1159 raise ServerUnderMaintenance(response.text)
1160 else:
1161 raise RuntimeError(response.text)
1163 if djvu_exception:
1164 raise djvu_exception
1166 def get(self, request, *args, **kwargs):
1167 pid = self.kwargs.get("pid", None)
1168 colid = self.kwargs.get("colid", None)
1169 site = self.kwargs.get("site", None)
1171 try:
1172 _, status, message = history_views.execute_and_record_func(
1173 "deploy",
1174 pid,
1175 colid,
1176 self.internal_do,
1177 f"Article successfully transferred to {site}",
1178 )
1179 except Timeout as exception:
1180 return HttpResponse(exception, status=408)
1181 except Exception as exception:
1182 return HttpResponseServerError(exception)
1184 data = {"message": message, "status": status}
1185 return JsonResponse(data)
1187 def update_pcj_editor(updated_articles):
1188 for article in updated_articles:
1189 data = {
1190 "date_published": article.date_published.strftime("%Y-%m-%d"),
1191 "article_number": article.article_number,
1192 }
1193 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1194 requests.post(url, json=data, verify=False)
1197class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1198 article = None
1200 def internal_do(self, *args, **kwargs):
1201 lang = self.kwargs.get("lang", None)
1203 translation = None
1204 for trans_article in self.article.translations.all():
1205 if trans_article.lang == lang:
1206 translation = trans_article
1208 if translation is None:
1209 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1211 collection = self.article.get_top_collection()
1212 colid = collection.pid
1213 container = self.article.my_container
1215 if translation.date_published is None:
1216 # Add date posted
1217 cmd = ptf_cmds.publishResourcePtfCmd()
1218 cmd.set_resource(translation)
1219 cmd.do()
1220 # updated_articles = cmd.do()
1222 # # Recompile PDF to add the date posted
1223 # try:
1224 # create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1225 # except Exception:
1226 # raise PDFException(
1227 # "Unable to compile the article PDF. Please contact the centre Mersenne"
1228 # )
1230 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1231 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1232 resolver.copy_binary_files(
1233 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1234 )
1236 # Deploy in prod
1237 xml = ptf_cmds.exportPtfCmd(
1238 {
1239 "pid": self.article.pid,
1240 "with_djvu": False,
1241 "article_standalone": True,
1242 "collection_pid": colid,
1243 "export_to_website": True,
1244 "export_folder": settings.LOG_DIR,
1245 }
1246 ).do()
1247 xml_file = io.StringIO(xml)
1248 files = {"xml": xml_file}
1250 server_url = getattr(collection, "website")()
1251 if not server_url:
1252 raise RuntimeError("The collection has no website")
1253 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1254 header = {}
1256 try:
1257 response = requests.post(
1258 url, headers=header, files=files, verify=False
1259 ) # verify: ignore TLS certificate
1260 status = response.status_code
1261 except requests.exceptions.ConnectionError:
1262 raise ServerUnderMaintenance(
1263 "The journal is under maintenance. Please try again later."
1264 )
1266 # Register translation in Crossref
1267 if 199 < status < 205:
1268 if self.article.allow_crossref():
1269 try:
1270 recordDOI(translation)
1271 except Exception:
1272 raise DOIException(
1273 "Error while recording the DOI. Please contact the centre Mersenne"
1274 )
1276 def get(self, request, *args, **kwargs):
1277 doi = kwargs.get("doi", None)
1278 self.article = model_helpers.get_article_by_doi(doi)
1279 if self.article is None:
1280 raise Http404(f"{doi} does not exist")
1282 try:
1283 _, status, message = history_views.execute_and_record_func(
1284 "deploy",
1285 self.article.pid,
1286 self.article.get_top_collection().pid,
1287 self.internal_do,
1288 "website",
1289 )
1290 except Timeout as exception:
1291 return HttpResponse(exception, status=408)
1292 except Exception as exception:
1293 logger.exception(f"Failed to post translation for {self.article.pid}")
1294 return HttpResponseServerError(exception)
1296 data = {"message": message, "status": status}
1297 return JsonResponse(data)
1300class DeleteJatsIssueAPIView(View):
1301 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1302 def get(self, request, *args, **kwargs):
1303 pid = self.kwargs.get("pid", None)
1304 colid = self.kwargs.get("colid", None)
1305 site = self.kwargs.get("site", None)
1306 message = f"The issue has correctly been removed from {site}"
1307 status = 200
1309 issue = model_helpers.get_container(pid)
1310 if not issue:
1311 raise Http404(f"{pid} does not exist")
1312 try:
1313 if check_lock():
1314 message = "Trammel is under maintenance. Please try again later."
1315 messages.error(self.request, message)
1316 raise ServerUnderMaintenance(message)
1318 mersenneSite = model_helpers.get_site_mersenne(colid)
1320 if site == "ptf_tools":
1321 if issue.is_deployed(mersenneSite):
1322 issue.undeploy(mersenneSite)
1323 for article in issue.article_set.all():
1324 article.undeploy(mersenneSite)
1326 p = model_helpers.get_provider("mathdoc-id")
1328 cmd = ptf_cmds.addContainerPtfCmd(
1329 {
1330 "pid": issue.pid,
1331 "ctype": "issue",
1332 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1333 }
1334 )
1335 cmd.set_provider(p)
1336 cmd.add_collection(issue.get_collection())
1337 cmd.set_object_to_be_deleted(issue)
1338 cmd.undo()
1340 else:
1341 if site == "numdam":
1342 server_url = settings.NUMDAM_PRE_URL
1343 else:
1344 collection = issue.get_collection()
1345 server_url = getattr(collection, site)()
1347 if not server_url:
1348 raise Http404(f"The collection has no {site}")
1349 else:
1350 # Delete on site
1351 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1352 response = requests.delete(url, verify=False)
1353 status = response.status_code
1355 if status == 404:
1356 raise Http404(
1357 "Issue not found on {site}. Please make sure the article exists."
1358 )
1359 elif status > 204:
1360 raise HTTPError(
1361 response.text.encode("utf8")[:1000],
1362 response=response,
1363 )
1364 else:
1365 status = 200
1367 # Unpublish issue in collection site (site_register.json)
1368 if site == "website":
1369 if issue.is_deployed(mersenneSite):
1370 issue.undeploy(mersenneSite)
1371 for article in issue.article_set.all():
1372 article.undeploy(mersenneSite)
1373 # Delete article binary files
1374 folder = article.get_relative_folder()
1375 resolver.delete_object_folder(
1376 folder,
1377 to_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1378 )
1379 # Delete issue binary files
1380 folder = issue.get_relative_folder()
1381 resolver.delete_object_folder(
1382 folder, to_folder=settings.MERSENNE_PROD_DATA_FOLDER
1383 )
1385 except ValueError as exception:
1386 if site in {"ptf_tools", "numdam", "website", "test_website"}:
1387 raise
1388 message, status = str(exception), 400
1389 except Timeout:
1390 message = f"{site} is under maintenance. Please try again later."
1391 status = 503
1392 except Http404 as exception:
1393 message, status = str(exception), 404
1394 except HTTPError as exception:
1395 message, status = str(exception), 502
1396 except ServerUnderMaintenance as exception:
1397 message, status = str(exception), 503
1398 except requests.exceptions.ConnectionError:
1399 message, status = (
1400 f"{site} is under maintenance. Please try again later.",
1401 503,
1402 )
1403 except Exception as exception:
1404 logger.exception("Unexpected error when deleting article %s from %s", pid, site)
1405 message, status = str(exception), 500
1407 return JsonResponse({"message": message, "status": status}, status=status)
1410class ArchiveIssueAPIView(View):
1411 def get(self, request, *args, **kwargs):
1412 try:
1413 pid = kwargs["pid"]
1414 colid = kwargs["colid"]
1415 except IndexError:
1416 raise Http404
1418 try:
1419 cmd = ptf_cmds.archiveIssuePtfCmd(
1420 {
1421 "pid": pid,
1422 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1423 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1424 "needs_publication_date": True,
1425 }
1426 )
1427 result_, status, message = history_views.execute_and_record_func(
1428 "archive", pid, colid, cmd.do
1429 )
1430 except Exception as exception:
1431 return HttpResponseServerError(exception)
1433 data = {"message": message, "status": 200}
1434 return JsonResponse(data)
1437class CreateDjvuAPIView(View):
1438 def internal_do(self, *args, **kwargs):
1439 pid = self.kwargs.get("pid", None)
1441 resource = model_helpers.get_resource(pid)
1442 cmd = ptf_cmds.addDjvuPtfCmd()
1443 cmd.set_resource(resource)
1444 cmd.do()
1446 def get(self, request, *args, **kwargs):
1447 pid = self.kwargs.get("pid", None)
1448 colid = pid.split("_")[0]
1450 try:
1451 _, status, message = history_views.execute_and_record_func(
1452 "numdam", pid, colid, self.internal_do
1453 )
1454 except Exception as exception:
1455 return HttpResponseServerError(exception)
1457 data = {"message": message, "status": status}
1458 return JsonResponse(data)
1461class PTFToolsHomeView(LoginRequiredMixin, View):
1462 """
1463 Home Page.
1464 - Admin & staff -> Render blank home.html
1465 - User with unique authorized collection -> Redirect to collection details page
1466 - User with multiple authorized collections -> Render home.html with data
1467 - Comment moderator -> Comments dashboard
1468 - Others -> 404 response
1469 """
1471 def get(self, request, *args, **kwargs) -> HttpResponse:
1472 # Staff or user with authorized collections
1473 if request.user.is_staff or request.user.is_superuser:
1474 return render(request, "home.html")
1476 colids = get_authorized_collections(request.user)
1477 is_mod = is_comment_moderator(request.user)
1479 # The user has no rights
1480 if not (colids or is_mod):
1481 raise Http404("No collections associated with your account.")
1482 # Comment moderator only
1483 elif not colids:
1484 return HttpResponseRedirect(reverse("comment_list"))
1486 # User with unique collection -> Redirect to collection detail page
1487 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1488 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1490 # User with multiple authorized collections - Special home
1491 context = {}
1492 context["overview"] = True
1494 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1495 all_collections = {c["pid"]: c for c in all_collections}
1497 # Comments summary
1498 try:
1499 error, comments_data = get_comments_for_home(request.user)
1500 except AttributeError:
1501 error, comments_data = True, {}
1503 context["comment_server_ok"] = False
1505 if not error:
1506 context["comment_server_ok"] = True
1507 if comments_data:
1508 for col_id, comment_nb in comments_data.items():
1509 if col_id.upper() in all_collections: 1509 ↛ 1508line 1509 didn't jump to line 1508 because the condition on line 1509 was always true
1510 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1512 # TODO: Translations summary
1513 context["translation_server_ok"] = False
1515 # Sort the collections according to the number of pending comments
1516 context["collections"] = sorted(
1517 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1518 )
1520 return render(request, "home.html", context)
1523class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1524 columns = 5
1526 def get_common_context_data(self, **kwargs):
1527 context = super().get_context_data(**kwargs)
1528 now = timezone.now()
1529 curyear = now.year
1530 years = range(curyear - self.columns + 1, curyear + 1)
1532 context["collections"] = settings.MERSENNE_COLLECTIONS
1533 context["containers_to_be_published"] = []
1534 context["last_col_events"] = []
1536 event = get_history_last_event_by("clockss", "ALL")
1537 clockss_gap = get_gap(now, event)
1539 context["years"] = years
1540 context["clockss_gap"] = clockss_gap
1542 return context
1544 def calculate_articles_and_pages(self, pid, years):
1545 data_by_year = []
1546 total_articles = [0] * len(years)
1547 total_pages = [0] * len(years)
1549 for year in years:
1550 articles = self.get_articles_for_year(pid, year)
1551 articles_count = articles.count()
1552 page_count = sum(article.get_article_page_count() for article in articles)
1554 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1555 total_articles[year - years[0]] += articles_count
1556 total_pages[year - years[0]] += page_count
1558 return data_by_year, total_articles, total_pages
1560 def get_articles_for_year(self, pid, year):
1561 return Article.objects.filter(
1562 Q(my_container__my_collection__pid=pid)
1563 & (
1564 Q(date_published__year=year, date_online_first__isnull=True)
1565 | Q(date_online_first__year=year)
1566 )
1567 ).prefetch_related("resourcecount_set")
1570class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1571 template_name = "dashboard/published_articles.html"
1573 def get_context_data(self, **kwargs):
1574 context = self.get_common_context_data(**kwargs)
1575 years = context["years"]
1577 published_articles = []
1578 total_published_articles = [
1579 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1580 ]
1582 for pid in settings.MERSENNE_COLLECTIONS:
1583 if pid != "MERSENNE":
1584 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1585 pid, years
1586 )
1587 published_articles.append({"pid": pid, "years": articles_data})
1589 for i, year in enumerate(years):
1590 total_published_articles[i]["total_articles"] += total_articles[i]
1591 total_published_articles[i]["total_pages"] += total_pages[i]
1593 context["published_articles"] = published_articles
1594 context["total_published_articles"] = total_published_articles
1596 return context
1599class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1600 template_name = "dashboard/created_volumes.html"
1602 def get_context_data(self, **kwargs):
1603 context = self.get_common_context_data(**kwargs)
1604 years = context["years"]
1606 created_volumes = []
1607 total_created_volumes = [
1608 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1609 ]
1611 for pid in settings.MERSENNE_COLLECTIONS:
1612 if pid != "MERSENNE":
1613 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1614 pid, years
1615 )
1616 created_volumes.append({"pid": pid, "years": volumes_data})
1618 for i, _ in enumerate(years):
1619 total_created_volumes[i]["total_articles"] += total_articles[i]
1620 total_created_volumes[i]["total_pages"] += total_pages[i]
1622 context["created_volumes"] = created_volumes
1623 context["total_created_volumes"] = total_created_volumes
1625 return context
1627 def calculate_volumes_and_pages(self, pid, years):
1628 data_by_year = []
1629 total_articles = [0] * len(years)
1630 total_pages = [0] * len(years)
1632 for year in years:
1633 issues = Container.objects.filter(my_collection__pid=pid, fyear=year)
1634 articles_count = 0
1635 page_count = 0
1637 for issue in issues:
1638 articles = issue.article_set.filter(
1639 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1640 ).prefetch_related("resourcecount_set")
1642 articles_count += articles.count()
1643 page_count += sum(article.get_article_page_count() for article in articles)
1645 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1646 total_articles[year - years[0]] += articles_count
1647 total_pages[year - years[0]] += page_count
1649 return data_by_year, total_articles, total_pages
1652class ReferencingChoice(View):
1653 def post(self, request, *args, **kwargs):
1654 if request.POST.get("optSite") == "ads":
1655 return redirect(
1656 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1657 )
1658 elif request.POST.get("optSite") == "wos":
1659 comp = ReferencingCheckerWos()
1660 journal = comp.make_journal(request.POST.get("selectCol"))
1661 if journal is None:
1662 return render(
1663 request,
1664 "dashboard/referencing.html",
1665 {
1666 "error": "Collection not found",
1667 "colid": request.POST.get("selectCol"),
1668 "optSite": request.POST.get("optSite"),
1669 },
1670 )
1671 return render(
1672 request,
1673 "dashboard/referencing.html",
1674 {
1675 "journal": journal,
1676 "colid": request.POST.get("selectCol"),
1677 "optSite": request.POST.get("optSite"),
1678 },
1679 )
1682class ReferencingWosFileView(View):
1683 template_name = "dashboard/referencing.html"
1685 def post(self, request, *args, **kwargs):
1686 colid = request.POST["colid"]
1687 if request.FILES.get("risfile") is None:
1688 message = "No file uploaded"
1689 return render(
1690 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1691 )
1692 uploaded_file = request.FILES["risfile"]
1693 comp = ReferencingCheckerWos()
1694 journal = comp.check_references(colid, uploaded_file)
1695 return render(request, self.template_name, {"journal": journal})
1698class ReferencingDashboardView(BaseMersenneDashboardView):
1699 template_name = "dashboard/referencing.html"
1701 def get(self, request, *args, **kwargs):
1702 colid = self.kwargs.get("colid", None)
1703 comp = ReferencingCheckerAds()
1704 journal = comp.check_references(colid)
1705 return render(request, self.template_name, {"journal": journal})
1708class BaseCollectionView(TemplateView):
1709 def get_context_data(self, **kwargs):
1710 context = super().get_context_data(**kwargs)
1711 aid = context.get("aid")
1712 year = context.get("year")
1714 if aid and year:
1715 context["collection"] = self.get_collection(aid, year)
1717 return context
1719 def get_collection(self, aid, year):
1720 """Method to be overridden by subclasses to fetch the appropriate collection"""
1721 raise NotImplementedError("Subclasses must implement get_collection method")
1724class ArticleListView(BaseCollectionView):
1725 template_name = "collection-list.html"
1727 def get_collection(self, aid, year):
1728 return Article.objects.filter(
1729 Q(my_container__my_collection__pid=aid)
1730 & (
1731 Q(date_published__year=year, date_online_first__isnull=True)
1732 | Q(date_online_first__year=year)
1733 )
1734 ).prefetch_related("resourcecount_set")
1737class VolumeListView(BaseCollectionView):
1738 template_name = "collection-list.html"
1740 def get_collection(self, aid, year):
1741 return Article.objects.filter(
1742 Q(my_container__my_collection__pid=aid, my_container__fyear=year)
1743 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1744 ).prefetch_related("resourcecount_set")
1747class DOAJResourceRegisterView(View):
1748 def get(self, request, *args, **kwargs):
1749 pid = kwargs.get("pid", None)
1750 resource = model_helpers.get_resource(pid)
1751 if resource is None:
1752 raise Http404
1753 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1754 resource.colid, None
1755 ):
1756 raise RuntimeError("Pending publications should not be deployed")
1758 try:
1759 data = {}
1760 doaj_meta, response = doaj_pid_register(pid)
1761 if response is None:
1762 return HttpResponse(status=204)
1763 elif doaj_meta and 200 <= response.status_code <= 299:
1764 data.update(doaj_meta)
1765 else:
1766 return HttpResponse(status=response.status_code, reason=response.text)
1767 except Timeout as exception:
1768 return HttpResponse(exception, status=408)
1769 except Exception as exception:
1770 return HttpResponseServerError(exception)
1771 return JsonResponse(data)
1774class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View):
1775 """
1776 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml
1777 """
1779 def get(self, request, *args, **kwargs):
1780 pid = kwargs.get("pid")
1781 if not pid:
1782 raise Http404("Missing pid")
1784 article = Article.objects.filter(pid=pid).first()
1785 if not article:
1786 raise Http404(f"Article not found: {pid}")
1788 colid = article.get_collection().pid
1789 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
1790 return JsonResponse(
1791 {"status": 403, "message": f"Tex conversions are disabled in {colid}"}
1792 )
1794 if is_tex_conversion_locked(pid):
1795 logger.warning("Conversion rejected (lock exists) for %s", pid)
1796 return JsonResponse(
1797 {"status": 409, "message": f"A conversion is already running for {pid}"}
1798 )
1800 logger.info("No lock → scheduling conversion for %s", pid)
1802 try:
1803 convert_article_tex.delay(pid=pid, user_pk=request.user.pk)
1804 except Exception:
1805 logger.exception("Failed to enqueue task for %s", pid)
1806 release_tex_conversion_lock(pid)
1807 raise
1809 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"})
1812class CROSSREFResourceRegisterView(View):
1813 def get(self, request, *args, **kwargs):
1814 pid = kwargs.get("pid", None)
1815 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1816 force = kwargs.get("force", None)
1817 if not request.user.is_superuser:
1818 force = None
1820 resource = model_helpers.get_resource(pid)
1821 if resource is None:
1822 raise Http404
1824 resource = resource.cast()
1825 meth = getattr(self, "recordDOI" + resource.classname)
1826 try:
1827 data = meth(resource, force)
1828 except Timeout as exception:
1829 return HttpResponse(exception, status=408)
1830 except Exception as exception:
1831 return HttpResponseServerError(exception)
1832 return JsonResponse(data)
1834 def recordDOIArticle(self, article: "Article", force=None):
1835 result = {"status": 404}
1836 if (
1837 article.doi
1838 and not article.do_not_publish
1839 and (article.date_published or article.date_online_first or force == "force")
1840 ):
1841 if article.my_container.fyear == 0:
1842 article.my_container.fyear = datetime.now().year
1843 result = recordDOI(article)
1844 return result
1846 def recordDOICollection(self, collection, force=None):
1847 return recordDOI(collection)
1849 def recordDOIContainer(self, container, force=None):
1850 data = {"status": 200, "message": "All DOI successfully checked"}
1852 if container.ctype == "issue":
1853 if container.doi:
1854 result = recordDOI(container)
1855 if result["status"] != 200:
1856 return result
1857 if force == "force":
1858 articles = container.article_set.exclude(
1859 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1860 )
1861 else:
1862 articles = container.article_set.exclude(
1863 doi__isnull=True,
1864 do_not_publish=True,
1865 date_published__isnull=True,
1866 date_online_first__isnull=True,
1867 )
1869 for article in articles:
1870 result = self.recordDOIArticle(article, force)
1871 if result["status"] != 200:
1872 data = result
1873 else:
1874 return recordDOI(container)
1875 return data
1878class CROSSREFResourceCheckStatusView(View):
1879 def get(self, request, *args, **kwargs):
1880 pid = kwargs.get("pid", None)
1881 resource = model_helpers.get_resource(pid)
1882 if resource is None:
1883 raise Http404
1884 resource = resource.cast()
1885 meth = getattr(self, "checkDOI" + resource.classname)
1886 try:
1887 meth(resource)
1888 except Timeout as exception:
1889 return HttpResponse(exception, status=408)
1890 except Exception as exception:
1891 return HttpResponseServerError(exception)
1893 data = {"status": 200, "message": "DOI successfully checked"}
1894 return JsonResponse(data)
1896 def checkDOIArticle(self, article: "Article"):
1897 if article.my_container.fyear == 0:
1898 article.my_container.fyear = datetime.now().year
1899 checkDOI(article)
1901 def checkDOICollection(self, collection):
1902 checkDOI(collection)
1904 def checkDOIContainer(self, container):
1905 if container.doi is not None:
1906 checkDOI(container)
1907 for article in container.article_set.all():
1908 self.checkDOIArticle(article)
1911class CROSSREFResourcePendingPublicationRegisterView(View):
1912 def get(self, request, *args, **kwargs):
1913 pid = kwargs.get("pid", None)
1914 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1916 resource = model_helpers.get_resource(pid)
1917 if resource is None:
1918 raise Http404
1920 resource = resource.cast()
1921 meth = getattr(self, "recordPendingPublication" + resource.classname)
1922 try:
1923 data = meth(resource)
1924 except Timeout as exception:
1925 return HttpResponse(exception, status=408)
1926 except Exception as exception:
1927 return HttpResponseServerError(exception)
1928 return JsonResponse(data)
1930 def recordPendingPublicationArticle(self, article):
1931 result = {"status": 404}
1932 if article.doi and not article.date_published and not article.date_online_first:
1933 if article.my_container.fyear is None or article.my_container.fyear == "0":
1934 article.my_container.fyear = datetime.now().year
1935 result = recordPendingPublication(article)
1936 return result
1939class RegisterPubmedFormView(FormView):
1940 template_name = "record_pubmed_dialog.html"
1941 form_class = RegisterPubmedForm
1943 def get_context_data(self, **kwargs):
1944 context = super().get_context_data(**kwargs)
1945 context["pid"] = self.kwargs["pid"]
1946 context["helper"] = PtfLargeModalFormHelper
1947 return context
1950class RegisterPubmedView(View):
1951 def get(self, request, *args, **kwargs):
1952 pid = kwargs.get("pid", None)
1953 update_article = self.request.GET.get("update_article", "on") == "on"
1955 article = model_helpers.get_article(pid)
1956 if article is None:
1957 raise Http404
1958 try:
1959 recordPubmed(article, update_article)
1960 except Exception as exception:
1961 messages.error("Unable to register the article in PubMed")
1962 return HttpResponseServerError(exception)
1964 return HttpResponseRedirect(
1965 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1966 )
1969class PTFToolsArticleView(ArticleView):
1970 def get_context_data(self, **kwargs):
1971 context = super().get_context_data(**kwargs)
1973 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(self.obj)
1975 qs = self.obj.get_top_collection().extlink_set.filter(rel="test_website")
1976 if qs:
1977 test_website = qs.first().location
1978 context["test_website"] = test_website
1979 qs = self.obj.get_top_collection().extlink_set.filter(rel="website")
1980 if qs:
1981 prod_location = qs.first().location
1982 context["prod_website"] = prod_location
1984 return context
1987ItemViewClassFactory.views["article"] = PTFToolsArticleView
1990class PTFToolsContainerView(TemplateView):
1991 template_name = ""
1993 def get_context_data(self, **kwargs):
1994 context = super().get_context_data(**kwargs)
1996 container = model_helpers.get_container(self.kwargs.get("pid"))
1997 if container is None:
1998 raise Http404
1999 citing_articles = container.citations()
2000 source = self.request.GET.get("source", None)
2001 if container.ctype.startswith("book"):
2002 book_parts = (
2003 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
2004 )
2005 references = False
2006 if container.ctype == "book-monograph":
2007 # on regarde si il y a au moins une bibliographie
2008 for art in container.article_set.all():
2009 if art.bibitem_set.count() > 0:
2010 references = True
2011 context.update(
2012 {
2013 "book": container,
2014 "book_parts": list(book_parts),
2015 "source": source,
2016 "citing_articles": citing_articles,
2017 "references": references,
2018 "test_website": container.get_top_collection()
2019 .extlink_set.get(rel="test_website")
2020 .location,
2021 "prod_website": container.get_top_collection()
2022 .extlink_set.get(rel="website")
2023 .location,
2024 }
2025 )
2026 self.template_name = "book-toc.html"
2027 else:
2028 articles = container.article_set.all().order_by("seq")
2029 for article in articles:
2030 try:
2031 last_match = (
2032 history_models.HistoryEvent.objects.filter(
2033 pid=article.pid,
2034 type="matching",
2035 )
2036 .only("created_on")
2037 .latest("created_on")
2038 )
2039 except history_models.HistoryEvent.DoesNotExist as _:
2040 article.last_match = None
2041 else:
2042 article.last_match = last_match.created_on
2044 # article1 = articles.first()
2045 # date = article1.deployed_date()
2046 # TODO next_issue, previous_issue
2048 # check DOI est maintenant une commande à part
2049 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
2050 # articlesWithStatus = []
2051 # for article in articles:
2052 # checkDOIExistence(article)
2053 # articlesWithStatus.append(article)
2055 test_location = prod_location = ""
2056 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
2057 if qs:
2058 test_location = qs.first().location
2059 qs = container.get_top_collection().extlink_set.filter(rel="website")
2060 if qs:
2061 prod_location = qs.first().location
2062 context.update(
2063 {
2064 "issue": container,
2065 "articles": articles,
2066 "source": source,
2067 "citing_articles": citing_articles,
2068 "test_website": test_location,
2069 "prod_website": prod_location,
2070 }
2071 )
2073 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
2074 context["is_issue_pending_publication"] = True
2075 if container.get_top_collection().pid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
2076 context["is_excluded_from_tex_conversion"] = True
2077 self.template_name = "issue-items.html"
2079 context["allow_crossref"] = container.allow_crossref()
2080 context["coltype"] = container.my_collection.coltype
2081 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(container)
2082 return context
2085class ExtLinkInline(InlineFormSetFactory):
2086 model = ExtLink
2087 form_class = ExtLinkForm
2088 factory_kwargs = {"extra": 0}
2091class ResourceIdInline(InlineFormSetFactory):
2092 model = ResourceId
2093 form_class = ResourceIdForm
2094 factory_kwargs = {"extra": 0}
2097class IssueDetailAPIView(View):
2098 def get(self, request, *args, **kwargs):
2099 issue = get_object_or_404(Container, pid=kwargs["pid"])
2100 deployed_date = issue.deployed_date()
2101 result = {
2102 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2103 if deployed_date
2104 else None,
2105 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2106 "all_doi_are_registered": issue.all_doi_are_registered(),
2107 "registered_in_doaj": issue.registered_in_doaj(),
2108 "doi": issue.my_collection.doi,
2109 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2110 }
2111 try:
2112 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2113 except history_models.HistoryEvent.DoesNotExist as _:
2114 pass
2115 else:
2116 result["latest"] = latest.message
2117 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2118 "%Y-%m-%d %H:%M"
2119 )
2121 result["latest_type"] = latest.type.capitalize()
2122 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2123 try:
2124 result[event_type] = timezone.localtime(
2125 history_models.HistoryEvent.objects.filter(
2126 type=event_type,
2127 status="OK",
2128 pid__startswith=issue.pid,
2129 )
2130 .latest("created_on")
2131 .created_on
2132 ).strftime("%Y-%m-%d %H:%M")
2133 except history_models.HistoryEvent.DoesNotExist as _:
2134 result[event_type] = ""
2135 return JsonResponse(result)
2138class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2139 model = Collection
2140 form_class = CollectionForm
2141 inlines = [ResourceIdInline, ExtLinkInline]
2142 inlines_names = ["resource_ids_form", "ext_links_form"]
2144 def get_context_data(self, **kwargs):
2145 context = super().get_context_data(**kwargs)
2146 context["helper"] = PtfFormHelper
2147 context["formset_helper"] = FormSetHelper
2148 return context
2150 def add_description(self, collection, description, lang, seq):
2151 if description:
2152 la = Abstract(
2153 resource=collection,
2154 tag="description",
2155 lang=lang,
2156 seq=seq,
2157 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2158 value_html=description,
2159 value_tex=description,
2160 )
2161 la.save()
2163 def form_valid(self, form):
2164 if form.instance.abbrev:
2165 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2166 else:
2167 form.instance.title_xml = (
2168 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2169 )
2171 form.instance.title_html = form.instance.title_tex
2172 form.instance.title_sort = form.instance.title_tex
2173 result = super().form_valid(form)
2175 collection = self.object
2176 collection.abstract_set.all().delete()
2178 seq = 1
2179 description = form.cleaned_data["description_en"]
2180 if description:
2181 self.add_description(collection, description, "en", seq)
2182 seq += 1
2183 description = form.cleaned_data["description_fr"]
2184 if description:
2185 self.add_description(collection, description, "fr", seq)
2187 return result
2189 def get_success_url(self):
2190 messages.success(
2191 self.request, f'The collection "{self.object.pid}" has been successfully updated'
2192 )
2193 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2196class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2197 """
2198 Warning : Not yet finished
2199 Automatic site membership creation is still missing
2200 """
2203class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2204 slug_field = "pid"
2205 slug_url_kwarg = "pid"
2208def suggest_load_journal_dois(colid):
2209 articles = (
2210 Article.objects.filter(my_container__my_collection__pid=colid)
2211 .filter(doi__isnull=False)
2212 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2213 .values_list("doi", flat=True)
2214 )
2216 try:
2217 articles = sorted(
2218 articles,
2219 key=lambda d: (
2220 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2221 int(re.search(r".(\d+)$", d).group(1)),
2222 ),
2223 )
2224 except: # noqa: E722 (we'll look later)
2225 pass
2226 return [f'<option value="{doi}">' for doi in articles]
2229def get_context_with_volumes(journal):
2230 result = model_helpers.get_volumes_in_collection(journal)
2231 volume_count = result["volume_count"]
2232 collections = []
2233 for ancestor in journal.ancestors.all():
2234 item = model_helpers.get_volumes_in_collection(ancestor)
2235 volume_count = max(0, volume_count)
2236 item.update({"journal": ancestor})
2237 collections.append(item)
2239 # add the parent collection to its children list and sort it by date
2240 result.update({"journal": journal})
2241 collections.append(result)
2243 collections = [c for c in collections if c["sorted_issues"]]
2244 collections.sort(
2245 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2246 reverse=True,
2247 )
2249 context = {
2250 "journal": journal,
2251 "sorted_issues": result["sorted_issues"],
2252 "volume_count": volume_count,
2253 "max_width": result["max_width"],
2254 "collections": collections,
2255 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2256 }
2257 return context
2260class CollectionDetail(
2261 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2262):
2263 model = Collection
2264 slug_field = "pid"
2265 slug_url_kwarg = "pid"
2266 template_name = "ptf/collection_detail.html"
2268 def test_func(self):
2269 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2271 def get(self, request, *args, **kwargs):
2272 self.object = self.get_object(queryset=Collection.objects.all())
2273 return super().get(request, *args, **kwargs)
2275 def get_context_data(self, **kwargs):
2276 context = super().get_context_data(**kwargs)
2277 context["object_list"] = context["object_list"].filter(
2278 Q(ctype="issue") | Q(ctype="book-lecture-notes") | Q(ctype="book-monograph")
2279 )
2280 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2281 context.update(get_context_with_volumes(self.object))
2283 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2284 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2285 context["issue_to_appear"] = Container.objects.filter(
2286 pid=context["issue_to_appear_pid"]
2287 ).exists()
2288 try:
2289 latest_error = history_models.HistoryEvent.objects.filter(
2290 status="ERROR", col=self.object
2291 ).latest("created_on")
2292 except history_models.HistoryEvent.DoesNotExist as _:
2293 pass
2294 else:
2295 message = latest_error.message
2296 if message:
2297 i = message.find(" - ")
2298 latest_exception = message[:i]
2299 latest_error_message = message[i + 3 :]
2300 context["latest_exception"] = latest_exception
2301 context["latest_exception_date"] = latest_error.created_on
2302 context["latest_exception_type"] = latest_error.type
2303 context["latest_error_message"] = latest_error_message
2305 archive_in_error = history_models.HistoryEvent.objects.filter(
2306 status="ERROR", col=self.object, type="archive"
2307 ).exists()
2309 context["archive_in_error"] = archive_in_error
2311 return context
2313 def get_queryset(self):
2314 query = self.object.content.all()
2316 for ancestor in self.object.ancestors.all():
2317 query |= ancestor.content.all()
2319 return query.order_by("-fyear", "-vseries", "-volume", "-volume_int", "-number_int")
2322class ContainerEditView(FormView):
2323 template_name = "container_form.html"
2324 form_class = ContainerForm
2326 def get_success_url(self):
2327 if self.kwargs["pid"]:
2328 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2329 return reverse("mersenne_dashboard/published_articles")
2331 def set_success_message(self): # pylint: disable=no-self-use
2332 messages.success(self.request, "Booklet updated")
2334 def get_form_kwargs(self):
2335 kwargs = super().get_form_kwargs()
2336 if "pid" not in self.kwargs:
2337 self.kwargs["pid"] = None
2338 if "colid" not in self.kwargs:
2339 self.kwargs["colid"] = None
2340 if "data" in kwargs and "colid" in kwargs["data"]:
2341 # colid is passed as a hidden param in the form.
2342 # It is used when you submit a new container
2343 self.kwargs["colid"] = kwargs["data"]["colid"]
2345 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2346 self.kwargs["pid"]
2347 )
2348 return kwargs
2350 def get_context_data(self, **kwargs):
2351 context = super().get_context_data(**kwargs)
2353 context["pid"] = self.kwargs["pid"]
2354 context["colid"] = self.kwargs["colid"]
2355 context["container"] = self.kwargs["container"]
2357 context["edit_container"] = context["pid"] is not None
2358 context["name"] = resolve(self.request.path_info).url_name
2360 return context
2362 def form_valid(self, form):
2363 new_pid = form.cleaned_data.get("pid")
2364 new_title = form.cleaned_data.get("title")
2365 new_trans_title = form.cleaned_data.get("trans_title")
2366 new_publisher = form.cleaned_data.get("publisher")
2367 new_year = form.cleaned_data.get("year")
2368 new_volume = form.cleaned_data.get("volume")
2369 new_number = form.cleaned_data.get("number")
2371 collection = None
2372 issue = self.kwargs["container"]
2373 if issue is not None:
2374 collection = issue.my_collection
2375 elif self.kwargs["colid"] is not None:
2376 if "CR" in self.kwargs["colid"]:
2377 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2378 else:
2379 collection = model_helpers.get_collection(self.kwargs["colid"])
2381 if collection is None:
2382 raise ValueError("Collection for " + new_pid + " does not exist")
2384 # Icon
2385 new_icon_location = ""
2386 if "icon" in self.request.FILES:
2387 filename = os.path.basename(self.request.FILES["icon"].name)
2388 file_extension = filename.split(".")[1]
2390 icon_filename = resolver.get_disk_location(
2391 settings.MERSENNE_TEST_DATA_FOLDER,
2392 collection.pid,
2393 file_extension,
2394 new_pid,
2395 None,
2396 True,
2397 )
2399 with open(icon_filename, "wb+") as destination:
2400 for chunk in self.request.FILES["icon"].chunks():
2401 destination.write(chunk)
2403 folder = resolver.get_relative_folder(collection.pid, new_pid)
2404 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2405 name = resolve(self.request.path_info).url_name
2406 if name == "special_issue_create":
2407 self.kwargs["name"] = name
2408 if self.kwargs["container"]:
2409 # Edit Issue
2410 issue = self.kwargs["container"]
2411 if issue is None:
2412 raise ValueError(self.kwargs["pid"] + " does not exist")
2414 issue.pid = new_pid
2415 issue.title_tex = issue.title_html = new_title
2416 issue.title_xml = build_title_xml(
2417 title=new_title,
2418 lang=issue.lang,
2419 title_type="issue-title",
2420 )
2422 trans_lang = ""
2423 if new_trans_title != "":
2424 trans_lang = "fr" if issue.lang == "en" else "en"
2426 if trans_lang != "" and new_trans_title != "":
2427 title_xml = build_title_xml(
2428 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2429 )
2431 issue.title_set.update_or_create(
2432 lang=trans_lang,
2433 type="main",
2434 defaults={"title_html": new_trans_title, "title_xml": title_xml},
2435 )
2437 issue.fyear = new_year
2438 issue.volume = new_volume
2439 issue.volume_int = make_int(new_volume)
2440 issue.number = new_number
2441 issue.number_int = make_int(new_number)
2442 issue.save()
2443 else:
2444 xissue = create_issuedata()
2446 xissue.ctype = "issue"
2447 xissue.pid = new_pid
2448 xissue.lang = "en"
2449 xissue.title_tex = new_title
2450 xissue.title_html = new_title
2451 xissue.title_xml = build_title_xml(
2452 title=new_title, lang=xissue.lang, title_type="issue-title"
2453 )
2455 if new_trans_title != "":
2456 trans_lang = "fr"
2457 title_xml = build_title_xml(
2458 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2459 )
2460 title = create_titledata(
2461 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2462 )
2463 issue.titles = [title]
2465 xissue.fyear = new_year
2466 xissue.volume = new_volume
2467 xissue.number = new_number
2468 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2470 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2471 cmd.add_collection(collection)
2472 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2473 issue = cmd.do()
2475 self.kwargs["pid"] = new_pid
2477 # Add objects related to the article: contribs, datastream, counts...
2478 params = {
2479 "icon_location": new_icon_location,
2480 }
2481 cmd = ptf_cmds.updateContainerPtfCmd(params)
2482 cmd.set_resource(issue)
2483 cmd.do()
2485 publisher = model_helpers.get_publisher(new_publisher)
2486 if not publisher:
2487 xpub = create_publisherdata()
2488 xpub.name = new_publisher
2489 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2490 issue.my_publisher = publisher
2491 issue.save()
2493 self.set_success_message()
2495 return super().form_valid(form)
2498# class ArticleEditView(FormView):
2499# template_name = 'article_form.html'
2500# form_class = ArticleForm
2501#
2502# def get_success_url(self):
2503# if self.kwargs['pid']:
2504# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2505# return reverse('mersenne_dashboard/published_articles')
2506#
2507# def set_success_message(self): # pylint: disable=no-self-use
2508# messages.success(self.request, "L'article a été modifié")
2509#
2510# def get_form_kwargs(self):
2511# kwargs = super(ArticleEditView, self).get_form_kwargs()
2512#
2513# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2514# # Article creation: pid is None
2515# self.kwargs['pid'] = None
2516# if 'issue_id' not in self.kwargs:
2517# # Article edit: issue_id is not passed
2518# self.kwargs['issue_id'] = None
2519# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2520# # colid is passed as a hidden param in the form.
2521# # It is used when you submit a new container
2522# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2523#
2524# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2525# return kwargs
2526#
2527# def get_context_data(self, **kwargs):
2528# context = super(ArticleEditView, self).get_context_data(**kwargs)
2529#
2530# context['pid'] = self.kwargs['pid']
2531# context['issue_id'] = self.kwargs['issue_id']
2532# context['article'] = self.kwargs['article']
2533#
2534# context['edit_article'] = context['pid'] is not None
2535#
2536# article = context['article']
2537# if article:
2538# context['author_contributions'] = article.get_author_contributions()
2539# context['kwds_fr'] = None
2540# context['kwds_en'] = None
2541# kwd_gps = article.get_non_msc_kwds()
2542# for kwd_gp in kwd_gps:
2543# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2544# if kwd_gp.value_xml:
2545# kwd_ = types.SimpleNamespace()
2546# kwd_.value = kwd_gp.value_tex
2547# context['kwd_unstructured_fr'] = kwd_
2548# context['kwds_fr'] = kwd_gp.kwd_set.all()
2549# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2550# if kwd_gp.value_xml:
2551# kwd_ = types.SimpleNamespace()
2552# kwd_.value = kwd_gp.value_tex
2553# context['kwd_unstructured_en'] = kwd_
2554# context['kwds_en'] = kwd_gp.kwd_set.all()
2555#
2556# # Article creation: init pid
2557# if context['issue_id'] and context['pid'] is None:
2558# issue = model_helpers.get_container(context['issue_id'])
2559# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2560#
2561# return context
2562#
2563# def form_valid(self, form):
2564#
2565# new_pid = form.cleaned_data.get('pid')
2566# new_title = form.cleaned_data.get('title')
2567# new_fpage = form.cleaned_data.get('fpage')
2568# new_lpage = form.cleaned_data.get('lpage')
2569# new_page_range = form.cleaned_data.get('page_range')
2570# new_page_count = form.cleaned_data.get('page_count')
2571# new_coi_statement = form.cleaned_data.get('coi_statement')
2572# new_show_body = form.cleaned_data.get('show_body')
2573# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2574#
2575# # TODO support MathML
2576# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2577# # We need to pass trans_title to get_title_xml
2578# # Meanwhile, ignore new_title_xml
2579# new_title_xml = jats_parser.get_title_xml(new_title)
2580# new_title_html = new_title
2581#
2582# authors_count = int(self.request.POST.get('authors_count', "0"))
2583# i = 1
2584# new_authors = []
2585# old_author_contributions = []
2586# if self.kwargs['article']:
2587# old_author_contributions = self.kwargs['article'].get_author_contributions()
2588#
2589# while authors_count > 0:
2590# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2591#
2592# if prefix is not None:
2593# addresses = []
2594# if len(old_author_contributions) >= i:
2595# old_author_contribution = old_author_contributions[i - 1]
2596# addresses = [contrib_address.address for contrib_address in
2597# old_author_contribution.get_addresses()]
2598#
2599# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2600# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2601# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2602# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2603# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2604# deceased_before_publication = deceased == 'on'
2605# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2606# equal_contrib = equal_contrib == 'on'
2607# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2608# corresponding = corresponding == 'on'
2609# email = self.request.POST.get('email-' + str(i), None)
2610#
2611# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2612# params['deceased_before_publication'] = deceased_before_publication
2613# params['equal_contrib'] = equal_contrib
2614# params['corresponding'] = corresponding
2615# params['addresses'] = addresses
2616# params['email'] = email
2617#
2618# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2619#
2620# new_authors.append(params)
2621#
2622# authors_count -= 1
2623# i += 1
2624#
2625# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2626# i = 1
2627# new_kwds_fr = []
2628# while kwds_fr_count > 0:
2629# value = self.request.POST.get('kwd-fr-' + str(i), None)
2630# new_kwds_fr.append(value)
2631# kwds_fr_count -= 1
2632# i += 1
2633# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2634#
2635# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2636# i = 1
2637# new_kwds_en = []
2638# while kwds_en_count > 0:
2639# value = self.request.POST.get('kwd-en-' + str(i), None)
2640# new_kwds_en.append(value)
2641# kwds_en_count -= 1
2642# i += 1
2643# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2644#
2645# if self.kwargs['article']:
2646# # Edit article
2647# container = self.kwargs['article'].my_container
2648# else:
2649# # New article
2650# container = model_helpers.get_container(self.kwargs['issue_id'])
2651#
2652# if container is None:
2653# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2654#
2655# collection = container.my_collection
2656#
2657# # Copy PDF file & extract full text
2658# body = ''
2659# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2660# collection.pid,
2661# "pdf",
2662# container.pid,
2663# new_pid,
2664# True)
2665# if 'pdf' in self.request.FILES:
2666# with open(pdf_filename, 'wb+') as destination:
2667# for chunk in self.request.FILES['pdf'].chunks():
2668# destination.write(chunk)
2669#
2670# # Extract full text from the PDF
2671# body = utils.pdf_to_text(pdf_filename)
2672#
2673# # Icon
2674# new_icon_location = ''
2675# if 'icon' in self.request.FILES:
2676# filename = os.path.basename(self.request.FILES['icon'].name)
2677# file_extension = filename.split('.')[1]
2678#
2679# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2680# collection.pid,
2681# file_extension,
2682# container.pid,
2683# new_pid,
2684# True)
2685#
2686# with open(icon_filename, 'wb+') as destination:
2687# for chunk in self.request.FILES['icon'].chunks():
2688# destination.write(chunk)
2689#
2690# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2691# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2692#
2693# if self.kwargs['article']:
2694# # Edit article
2695# article = self.kwargs['article']
2696# article.fpage = new_fpage
2697# article.lpage = new_lpage
2698# article.page_range = new_page_range
2699# article.coi_statement = new_coi_statement
2700# article.show_body = new_show_body
2701# article.do_not_publish = new_do_not_publish
2702# article.save()
2703#
2704# else:
2705# # New article
2706# params = {
2707# 'pid': new_pid,
2708# 'title_xml': new_title_xml,
2709# 'title_html': new_title_html,
2710# 'title_tex': new_title,
2711# 'fpage': new_fpage,
2712# 'lpage': new_lpage,
2713# 'page_range': new_page_range,
2714# 'seq': container.article_set.count() + 1,
2715# 'body': body,
2716# 'coi_statement': new_coi_statement,
2717# 'show_body': new_show_body,
2718# 'do_not_publish': new_do_not_publish
2719# }
2720#
2721# xarticle = create_articledata()
2722# xarticle.pid = new_pid
2723# xarticle.title_xml = new_title_xml
2724# xarticle.title_html = new_title_html
2725# xarticle.title_tex = new_title
2726# xarticle.fpage = new_fpage
2727# xarticle.lpage = new_lpage
2728# xarticle.page_range = new_page_range
2729# xarticle.seq = container.article_set.count() + 1
2730# xarticle.body = body
2731# xarticle.coi_statement = new_coi_statement
2732# params['xobj'] = xarticle
2733#
2734# cmd = ptf_cmds.addArticlePtfCmd(params)
2735# cmd.set_container(container)
2736# cmd.add_collection(container.my_collection)
2737# article = cmd.do()
2738#
2739# self.kwargs['pid'] = new_pid
2740#
2741# # Add objects related to the article: contribs, datastream, counts...
2742# params = {
2743# # 'title_xml': new_title_xml,
2744# # 'title_html': new_title_html,
2745# # 'title_tex': new_title,
2746# 'authors': new_authors,
2747# 'page_count': new_page_count,
2748# 'icon_location': new_icon_location,
2749# 'body': body,
2750# 'use_kwds': True,
2751# 'kwds_fr': new_kwds_fr,
2752# 'kwds_en': new_kwds_en,
2753# 'kwd_uns_fr': new_kwd_uns_fr,
2754# 'kwd_uns_en': new_kwd_uns_en
2755# }
2756# cmd = ptf_cmds.updateArticlePtfCmd(params)
2757# cmd.set_article(article)
2758# cmd.do()
2759#
2760# self.set_success_message()
2761#
2762# return super(ArticleEditView, self).form_valid(form)
2765@require_http_methods(["POST"])
2766def do_not_publish_article(request, *args, **kwargs):
2767 next = request.headers.get("referer")
2769 pid = kwargs.get("pid", "")
2771 article = model_helpers.get_article(pid)
2772 if article:
2773 article.do_not_publish = not article.do_not_publish
2774 article.save()
2775 else:
2776 raise Http404
2778 return HttpResponseRedirect(next)
2781@require_http_methods(["POST"])
2782def show_article_body(request, *args, **kwargs):
2783 next = request.headers.get("referer")
2785 pid = kwargs.get("pid", "")
2787 article = model_helpers.get_article(pid)
2788 if article:
2789 article.show_body = not article.show_body
2790 article.save()
2791 else:
2792 raise Http404
2794 return HttpResponseRedirect(next)
2797class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2798 """
2799 API to get/post article metadata
2800 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2801 """
2803 def __init__(self, *args, **kwargs):
2804 """
2805 we define here what fields we want in the form
2806 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2807 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2808 super().__init__(*args, **kwargs)
2809 self.fields_to_update = [
2810 "lang",
2811 "atype",
2812 "contributors",
2813 "abstracts",
2814 "kwds",
2815 "titles",
2816 "title_html",
2817 "title_xml",
2818 "title_tex",
2819 "streams",
2820 "ext_links",
2821 "date_accepted",
2822 "history_dates",
2823 "subjs",
2824 "bibitems",
2825 "references",
2826 ]
2827 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2828 self.additional_fields = [
2829 "doi",
2830 "pid",
2831 "container_pid",
2832 "pdf",
2833 "illustration",
2834 "dates",
2835 "msc_keywords",
2836 ]
2837 self.editorial_tools = [
2838 "translation",
2839 "sidebar",
2840 "lang_selection",
2841 "back_to_article_option",
2842 "msc_keywords",
2843 ]
2844 self.validation = {
2845 "required_checks": [],
2846 "blocking_checks": [],
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":
2975 if article.is_deployed(mersenneSite):
2976 article.undeploy(mersenneSite)
2977 # Delete article binary files
2978 folder = article.get_relative_folder()
2979 resolver.delete_object_folder(
2980 folder,
2981 to_folder=settings.MERSENNE_PROD_DATA_FOLDER,
2982 )
2983 else:
2984 raise ValueError(f"{site} is not a supported site")
2986 except ValueError as exception:
2987 if site in {"trammel", "website", "test_website"}:
2988 raise
2989 message, status = str(exception), 400
2990 except Http404 as exception:
2991 message, status = str(exception), 404
2992 except HTTPError as exception:
2993 message, status = str(exception), 502
2994 except ServerUnderMaintenance as exception:
2995 message, status = str(exception), 503
2996 except requests.exceptions.ConnectionError:
2997 message, status = (
2998 f"{site} is under maintenance. Please try again later.",
2999 503,
3000 )
3001 except Exception as exception:
3002 logger.exception("Unexpected error when deleting article %s from %s", pid, site)
3003 message, status = str(exception), 500
3005 return JsonResponse({"message": message, "status": status}, status=status)
3008def get_messages_in_queue():
3009 app = Celery("ptf-tools")
3010 # tasks = list(current_app.tasks)
3011 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
3012 print(tasks)
3013 # i = app.control.inspect()
3015 with app.connection_or_acquire() as conn:
3016 remaining = conn.default_channel.queue_declare(
3017 queue="coordinator", passive=True
3018 ).message_count
3019 return remaining
3022class NumdamView(TemplateView, history_views.HistoryContextMixin):
3023 template_name = "numdam.html"
3025 def get_context_data(self, **kwargs):
3026 context = super().get_context_data(**kwargs)
3028 context["objs"] = ResourceInNumdam.objects.all()
3030 pre_issues = []
3031 prod_issues = []
3032 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
3033 try:
3034 response = requests.get(url)
3035 if response.status_code == 200:
3036 data = response.json()
3037 if "issues" in data:
3038 pre_issues = data["issues"]
3039 except Exception:
3040 pass
3042 url = f"{settings.NUMDAM_URL}/api-all-issues/"
3043 response = requests.get(url)
3044 if response.status_code == 200:
3045 data = response.json()
3046 if "issues" in data:
3047 prod_issues = data["issues"]
3049 new = sorted(list(set(pre_issues).difference(prod_issues)))
3050 removed = sorted(list(set(prod_issues).difference(pre_issues)))
3051 grouped = [
3052 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
3053 ]
3054 grouped_removed = [
3055 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
3056 ]
3057 context["added_issues"] = grouped
3058 context["removed_issues"] = grouped_removed
3060 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
3061 return context
3064class NumdamArchiveView(RedirectView):
3065 @staticmethod
3066 def reset_task_results():
3067 TaskResult.objects.all().delete()
3069 def get_redirect_url(self, *args, **kwargs):
3070 self.colid = kwargs["colid"]
3072 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
3073 return Http404
3075 # we make sure archiving is not already running
3076 # if not get_messages_in_queue():
3077 # self.reset_task_results()
3079 if self.colid == "ALL":
3080 archive_numdam_collections.delay()
3081 else:
3082 archive_numdam_collection.s(self.colid).delay()
3084 return reverse("numdam")
3087class DeployAllNumdamAPIView(View):
3088 def internal_do(self, *args, **kwargs):
3089 pids = []
3091 for obj in ResourceInNumdam.objects.all():
3092 pids.append(obj.pid)
3094 return pids
3096 def get(self, request, *args, **kwargs):
3097 try:
3098 pids, status, message = history_views.execute_and_record_func(
3099 "deploy", "numdam", "ALL", self.internal_do, "numdam"
3100 )
3101 except Exception as exception:
3102 return HttpResponseServerError(exception)
3104 data = {"message": message, "ids": pids, "status": status}
3105 return JsonResponse(data)
3108class NumdamDeleteAPIView(View):
3109 def get(self, request, *args, **kwargs):
3110 pid = self.kwargs.get("pid", None)
3112 try:
3113 obj = ResourceInNumdam.objects.get(pid=pid)
3114 obj.delete()
3115 except Exception as exception:
3116 return HttpResponseServerError(exception)
3118 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
3119 return JsonResponse(data)
3122class ExtIdApiDetail(View):
3123 def get(self, request, *args, **kwargs):
3124 extid = get_object_or_404(
3125 ExtId,
3126 resource__pid=kwargs["pid"],
3127 id_type=kwargs["what"],
3128 )
3129 return JsonResponse(
3130 {
3131 "pk": extid.pk,
3132 "href": extid.get_href(),
3133 "fetch": reverse(
3134 "api-fetch-id",
3135 args=(
3136 extid.resource.pk,
3137 extid.id_value,
3138 extid.id_type,
3139 "extid",
3140 ),
3141 ),
3142 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3143 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3144 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3145 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3146 "is_valid": extid.checked,
3147 }
3148 )
3151class ExtIdFormTemplate(TemplateView):
3152 template_name = "common/externalid_form.html"
3154 def get_context_data(self, **kwargs):
3155 context = super().get_context_data(**kwargs)
3156 context["sequence"] = kwargs["sequence"]
3157 return context
3160class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3161 def get_context_data(self, **kwargs):
3162 context = super().get_context_data(**kwargs)
3163 context["helper"] = PtfFormHelper
3164 return context
3166 def get_success_url(self):
3167 self.post_process()
3168 return self.object.resource.get_absolute_url()
3170 def post_process(self):
3171 model_helpers.post_resource_updated(self.object.resource)
3174class ExtIdCreate(ExtIdFormView, CreateView):
3175 model = ExtId
3176 form_class = ExtIdForm
3178 def get_context_data(self, **kwargs):
3179 context = super().get_context_data(**kwargs)
3180 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3181 return context
3183 def get_initial(self):
3184 initial = super().get_initial()
3185 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3186 return initial
3188 def form_valid(self, form):
3189 form.instance.checked = False
3190 return super().form_valid(form)
3193class ExtIdUpdate(ExtIdFormView, UpdateView):
3194 model = ExtId
3195 form_class = ExtIdForm
3197 def get_context_data(self, **kwargs):
3198 context = super().get_context_data(**kwargs)
3199 context["resource"] = self.object.resource
3200 return context
3203class UpdateTexmfZipAPIView(View):
3204 def get(self, request, *args, **kwargs):
3205 def copy_zip_files(src_folder, dest_folder):
3206 os.makedirs(dest_folder, exist_ok=True)
3208 zip_files = [
3209 os.path.join(src_folder, f)
3210 for f in os.listdir(src_folder)
3211 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3212 ]
3213 for zip_file in zip_files:
3214 resolver.copy_file(zip_file, dest_folder)
3216 # Exceptions: specific zip/gz files
3217 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3218 resolver.copy_file(zip_file, dest_folder)
3220 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3221 resolver.copy_file(zip_file, dest_folder)
3223 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3224 resolver.copy_file(gz_file, dest_folder)
3226 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3228 dest_folder = os.path.join(
3229 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3230 )
3232 try:
3233 copy_zip_files(src_folder, dest_folder)
3234 except Exception as exception:
3235 return HttpResponseServerError(exception)
3237 try:
3238 dest_folder = os.path.join(
3239 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3240 )
3241 copy_zip_files(src_folder, dest_folder)
3242 except Exception as exception:
3243 return HttpResponseServerError(exception)
3245 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3246 return JsonResponse(data)
3249class TrammelTasksProgressView(View):
3250 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3251 """
3252 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3253 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3254 """
3255 task_name = task
3257 def get_event_data():
3258 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3260 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3261 remaining_messages = get_messages_in_queue()
3263 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3264 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3265 failed_tasks = all_tasks.filter(status="FAILURE")
3267 all_tasks_count = all_tasks.count()
3268 success_count = successed_tasks.count()
3269 fail_count = failed_tasks.count()
3271 all_count = all_tasks_count + remaining_messages
3272 remaining_count = all_count - success_count - fail_count
3274 success_rate = int(success_count * 100 / all_count) if all_count else 0
3275 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3276 status = "consuming_queue" if remaining_count != 0 else "polling"
3278 last_task = successed_tasks.first()
3279 last_task = (
3280 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3281 if last_task
3282 else ""
3283 )
3285 # SSE event format
3286 event_data = {
3287 "status": status,
3288 "success_rate": success_rate,
3289 "error_rate": error_rate,
3290 "all_count": all_count,
3291 "remaining_count": remaining_count,
3292 "success_count": success_count,
3293 "fail_count": fail_count,
3294 "last_task": last_task,
3295 }
3297 return event_data
3299 def stream_response(data):
3300 # Send initial response headers
3301 yield f"data: {json.dumps(data)}\n\n"
3303 data = get_event_data()
3304 format = request.GET.get("format", "stream")
3305 if format == "json":
3306 response = JsonResponse(data)
3307 else:
3308 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3309 return response
3312user_signed_up.connect(update_user_from_invite)