Coverage for src/ptf_tools/views/base_views.py: 18%
1685 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-27 13:01 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-27 13:01 +0000
1import io
2import json
3import logging
4import os
5import re
6from datetime import datetime
7from itertools import groupby
9import jsonpickle
10import requests
11from allauth.account.signals import user_signed_up
12from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin
13from celery import Celery, current_app
14from django.conf import settings
15from django.contrib import messages
16from django.contrib.auth.mixins import UserPassesTestMixin
17from django.db.models import Q
18from django.http import (
19 Http404,
20 HttpRequest,
21 HttpResponse,
22 HttpResponseRedirect,
23 HttpResponseServerError,
24 JsonResponse,
25)
26from django.shortcuts import get_object_or_404, redirect, render
27from django.urls import resolve, reverse
28from django.utils import timezone
29from django.utils.decorators import method_decorator
30from django.views.decorators.csrf import csrf_exempt
31from django.views.decorators.http import require_http_methods
32from django.views.generic import ListView, TemplateView, View
33from django.views.generic.base import RedirectView
34from django.views.generic.detail import SingleObjectMixin
35from django.views.generic.edit import CreateView, FormView, UpdateView
36from django_celery_results.models import TaskResult
37from extra_views import (
38 CreateWithInlinesView,
39 InlineFormSetFactory,
40 NamedFormsetsMixin,
41 UpdateWithInlinesView,
42)
43from ptf import model_data_converter, model_helpers, utils
44from ptf.cmds import ptf_cmds, xml_cmds
45from ptf.cmds.base_cmds import make_int
46from ptf.cmds.xml.jats.builder.issue import build_title_xml
47from ptf.cmds.xml.xml_utils import replace_html_entities
48from ptf.display import resolver
49from ptf.exceptions import DOIException, ServerUnderMaintenance
50from ptf.model_data import create_issuedata, create_publisherdata, create_titledata
51from ptf.models import (
52 Abstract,
53 Article,
54 Collection,
55 Container,
56 ExtId,
57 ExtLink,
58 Resource,
59 ResourceId,
60)
61from ptf.views import ArticleView, ItemViewClassFactory
62from ptf_back.locks import (
63 is_tex_conversion_locked,
64 release_tex_conversion_lock,
65)
66from ptf_back.tex import create_frontpage
67from ptf_back.tex.tex_tasks import convert_article_tex
68from ptf_external.back.crossref.doi import checkDOI, recordDOI, recordPendingPublication
70# from ptf.views import ArticleEditFormWithVueAPIView
71from ptf_external.views import ArticleEditFormWithVueAPIView
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} : {exception!s}",
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 result[0]
315 self.diffs = result[1]
316 self.xissue = result[2]
318 if True:
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=f"Balises non parsées lors de l'import : {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=f"Balises non parsées lors de l'import : {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:
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 f"Create/Update deployed_date and date_published on all articles for {pid}\n"
1008 )
1010 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
1011 cmd = ptf_cmds.publishResourcePtfCmd()
1012 cmd.set_resource(resource)
1013 updated_articles = cmd.do()
1015 create_frontpage(colid, container, updated_articles, test=False)
1017 mersenneSite = model_helpers.get_site_mersenne(colid)
1018 # create or update deployed_date on container and articles
1019 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
1021 for art in articles_to_deploy:
1022 if (
1023 art.doi
1024 and (art.date_published or art.date_online_first)
1025 and art.my_container.fyear is None
1026 ):
1027 art.my_container.fyear = datetime.now().year
1028 # BUG ? update the container but no save() ?
1030 file_.write(
1031 f"Publication date of {art.pid} : Online First: {art.date_online_first}, Published: {art.date_published}\n"
1032 )
1034 if article is None:
1035 resolver.copy_binary_files(
1036 container,
1037 settings.MERSENNE_TEST_DATA_FOLDER,
1038 settings.MERSENNE_PROD_DATA_FOLDER,
1039 )
1041 for art in articles_to_deploy:
1042 resolver.copy_binary_files(
1043 art,
1044 settings.MERSENNE_TEST_DATA_FOLDER,
1045 settings.MERSENNE_PROD_DATA_FOLDER,
1046 )
1048 elif site == "test_website":
1049 # create date_pre_published on articles without date_pre_published
1050 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
1051 cmd.set_resource(resource)
1052 updated_articles = cmd.do()
1054 create_frontpage(colid, container, updated_articles)
1056 export_to_website = site == "website"
1058 if article is None:
1059 with_djvu = site == "numdam"
1060 xml = ptf_cmds.exportPtfCmd(
1061 {
1062 "pid": pid,
1063 "with_djvu": with_djvu,
1064 "export_to_website": export_to_website,
1065 }
1066 ).do()
1067 body = xml.encode("utf8")
1069 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1070 url = server_url + reverse("issue_upload")
1071 else:
1072 url = server_url + reverse("book_upload")
1074 # verify=False: ignore TLS certificate
1075 response = requests.post(url, data=body, verify=False)
1076 # response = requests.post(url, files=files, verify=False)
1077 else:
1078 xml = ptf_cmds.exportPtfCmd(
1079 {
1080 "pid": pid,
1081 "with_djvu": False,
1082 "article_standalone": True,
1083 "collection_pid": collection.pid,
1084 "export_to_website": export_to_website,
1085 "export_folder": settings.LOG_DIR,
1086 }
1087 ).do()
1088 # Unlike containers that send their XML as the body of the POST request,
1089 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1090 xml_file = io.StringIO(xml)
1091 files = {"xml": xml_file}
1093 url = server_url + reverse(
1094 "article_in_issue_upload", kwargs={"pid": container.pid}
1095 )
1096 # verify=False: ignore TLS certificate
1097 header = {}
1098 logger.debug(f"Sending POST request to {url}")
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(self, 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 = 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 and self.article.allow_crossref():
1268 try:
1269 recordDOI(translation)
1270 except Exception:
1271 raise DOIException(
1272 "Error while recording the DOI. Please contact the centre Mersenne"
1273 )
1275 def get(self, request, *args, **kwargs):
1276 doi = kwargs.get("doi", None)
1277 self.article = model_helpers.get_article_by_doi(doi)
1278 if self.article is None:
1279 raise Http404(f"{doi} does not exist")
1281 try:
1282 _, status, message = history_views.execute_and_record_func(
1283 "deploy",
1284 self.article.pid,
1285 self.article.get_top_collection().pid,
1286 self.internal_do,
1287 "website",
1288 )
1289 except Timeout as exception:
1290 return HttpResponse(exception, status=408)
1291 except Exception as exception:
1292 logger.exception(f"Failed to post translation for {self.article.pid}")
1293 return HttpResponseServerError(exception)
1295 data = {"message": message, "status": status}
1296 return JsonResponse(data)
1299class DeleteJatsIssueAPIView(View):
1300 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1301 def get(self, request, *args, **kwargs):
1302 pid = self.kwargs.get("pid", None)
1303 colid = self.kwargs.get("colid", None)
1304 site = self.kwargs.get("site", None)
1305 message = f"The issue has correctly been removed from {site}"
1306 status = 200
1308 issue = model_helpers.get_container(pid)
1309 if not issue:
1310 raise Http404(f"{pid} does not exist")
1311 try:
1312 if check_lock():
1313 message = "Trammel is under maintenance. Please try again later."
1314 messages.error(self.request, message)
1315 raise ServerUnderMaintenance(message)
1317 mersenneSite = model_helpers.get_site_mersenne(colid)
1319 if site == "ptf_tools":
1320 if issue.is_deployed(mersenneSite):
1321 issue.undeploy(mersenneSite)
1322 for article in issue.article_set.all():
1323 article.undeploy(mersenneSite)
1325 p = model_helpers.get_provider("mathdoc-id")
1327 cmd = ptf_cmds.addContainerPtfCmd(
1328 {
1329 "pid": issue.pid,
1330 "ctype": "issue",
1331 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1332 }
1333 )
1334 cmd.set_provider(p)
1335 cmd.add_collection(issue.get_collection())
1336 cmd.set_object_to_be_deleted(issue)
1337 cmd.undo()
1339 else:
1340 if site == "numdam":
1341 server_url = settings.NUMDAM_PRE_URL
1342 else:
1343 collection = issue.get_collection()
1344 server_url = getattr(collection, site)()
1346 if not server_url:
1347 raise Http404(f"The collection has no {site}")
1348 else:
1349 # Delete on site
1350 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1351 response = requests.delete(url, verify=False)
1352 status = response.status_code
1354 if status == 404:
1355 raise Http404(
1356 "Issue not found on {site}. Please make sure the article exists."
1357 )
1358 elif status > 204:
1359 raise HTTPError(
1360 response.text.encode("utf8")[:1000],
1361 response=response,
1362 )
1363 else:
1364 status = 200
1366 # Unpublish issue in collection site (site_register.json)
1367 if site == "website" and issue.is_deployed(mersenneSite):
1368 issue.undeploy(mersenneSite)
1369 for article in issue.article_set.all():
1370 article.undeploy(mersenneSite)
1371 # Delete article binary files
1372 folder = article.get_relative_folder()
1373 resolver.delete_object_folder(
1374 folder,
1375 to_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1376 )
1377 # Delete issue binary files
1378 folder = issue.get_relative_folder()
1379 resolver.delete_object_folder(
1380 folder, to_folder=settings.MERSENNE_PROD_DATA_FOLDER
1381 )
1383 except ValueError as exception:
1384 if site in {"ptf_tools", "numdam", "website", "test_website"}:
1385 raise
1386 message, status = str(exception), 400
1387 except Timeout:
1388 message = f"{site} is under maintenance. Please try again later."
1389 status = 503
1390 except Http404 as exception:
1391 message, status = str(exception), 404
1392 except HTTPError as exception:
1393 message, status = str(exception), 502
1394 except ServerUnderMaintenance as exception:
1395 message, status = str(exception), 503
1396 except requests.exceptions.ConnectionError:
1397 message, status = (
1398 f"{site} is under maintenance. Please try again later.",
1399 503,
1400 )
1401 except Exception as exception:
1402 logger.exception("Unexpected error when deleting article %s from %s", pid, site)
1403 message, status = str(exception), 500
1405 return JsonResponse({"message": message, "status": status}, status=status)
1408class ArchiveIssueAPIView(View):
1409 def get(self, request, *args, **kwargs):
1410 try:
1411 pid = kwargs["pid"]
1412 colid = kwargs["colid"]
1413 except IndexError:
1414 raise Http404
1416 try:
1417 cmd = ptf_cmds.archiveIssuePtfCmd(
1418 {
1419 "pid": pid,
1420 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1421 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1422 "needs_publication_date": True,
1423 }
1424 )
1425 _result, _status, message = history_views.execute_and_record_func(
1426 "archive", pid, colid, cmd.do
1427 )
1428 except Exception as exception:
1429 return HttpResponseServerError(exception)
1431 data = {"message": message, "status": 200}
1432 return JsonResponse(data)
1435class CreateDjvuAPIView(View):
1436 def internal_do(self, *args, **kwargs):
1437 pid = self.kwargs.get("pid", None)
1439 resource = model_helpers.get_resource(pid)
1440 cmd = ptf_cmds.addDjvuPtfCmd()
1441 cmd.set_resource(resource)
1442 cmd.do()
1444 def get(self, request, *args, **kwargs):
1445 pid = self.kwargs.get("pid", None)
1446 colid = pid.split("_")[0]
1448 try:
1449 _, status, message = history_views.execute_and_record_func(
1450 "numdam", pid, colid, self.internal_do
1451 )
1452 except Exception as exception:
1453 return HttpResponseServerError(exception)
1455 data = {"message": message, "status": status}
1456 return JsonResponse(data)
1459class PTFToolsHomeView(LoginRequiredMixin, View):
1460 """
1461 Home Page.
1462 - Admin & staff -> Render blank home.html
1463 - User with unique authorized collection -> Redirect to collection details page
1464 - User with multiple authorized collections -> Render home.html with data
1465 - Comment moderator -> Comments dashboard
1466 - Others -> 404 response
1467 """
1469 def get(self, request, *args, **kwargs) -> HttpResponse:
1470 # Staff or user with authorized collections
1471 if request.user.is_staff or request.user.is_superuser:
1472 return render(request, "home.html")
1474 colids = get_authorized_collections(request.user)
1475 is_mod = is_comment_moderator(request.user)
1477 # The user has no rights
1478 if not (colids or is_mod):
1479 raise Http404("No collections associated with your account.")
1480 # Comment moderator only
1481 elif not colids:
1482 return HttpResponseRedirect(reverse("comment_list"))
1484 # User with unique collection -> Redirect to collection detail page
1485 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1486 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1488 # User with multiple authorized collections - Special home
1489 context = {}
1490 context["overview"] = True
1492 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1493 all_collections = {c["pid"]: c for c in all_collections}
1495 # Comments summary
1496 try:
1497 error, comments_data = get_comments_for_home(request.user)
1498 except AttributeError:
1499 error, comments_data = True, {}
1501 context["comment_server_ok"] = False
1503 if not error:
1504 context["comment_server_ok"] = True
1505 if comments_data:
1506 for col_id, comment_nb in comments_data.items():
1507 if col_id.upper() in all_collections: 1507 ↛ 1506line 1507 didn't jump to line 1506 because the condition on line 1507 was always true
1508 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1510 # TODO: Translations summary
1511 context["translation_server_ok"] = False
1513 # Sort the collections according to the number of pending comments
1514 context["collections"] = sorted(
1515 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1516 )
1518 return render(request, "home.html", context)
1521class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1522 columns = 5
1524 def get_common_context_data(self, **kwargs):
1525 context = super().get_context_data(**kwargs)
1526 now = timezone.now()
1527 curyear = now.year
1528 years = range(curyear - self.columns + 1, curyear + 1)
1530 context["collections"] = settings.MERSENNE_COLLECTIONS
1531 context["containers_to_be_published"] = []
1532 context["last_col_events"] = []
1534 event = get_history_last_event_by("clockss", "ALL")
1535 clockss_gap = get_gap(now, event)
1537 context["years"] = years
1538 context["clockss_gap"] = clockss_gap
1540 return context
1542 def calculate_articles_and_pages(self, pid, years):
1543 data_by_year = []
1544 total_articles = [0] * len(years)
1545 total_pages = [0] * len(years)
1547 for year in years:
1548 articles = self.get_articles_for_year(pid, year)
1549 articles_count = articles.count()
1550 page_count = sum(article.get_article_page_count() for article in articles)
1552 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1553 total_articles[year - years[0]] += articles_count
1554 total_pages[year - years[0]] += page_count
1556 return data_by_year, total_articles, total_pages
1558 def get_articles_for_year(self, pid, year):
1559 return Article.objects.filter(
1560 Q(my_container__my_collection__pid=pid)
1561 & (
1562 Q(date_published__year=year, date_online_first__isnull=True)
1563 | Q(date_online_first__year=year)
1564 )
1565 ).prefetch_related("resourcecount_set")
1568class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1569 template_name = "dashboard/published_articles.html"
1571 def get_context_data(self, **kwargs):
1572 context = self.get_common_context_data(**kwargs)
1573 years = context["years"]
1575 published_articles = []
1576 total_published_articles = [
1577 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1578 ]
1580 for pid in settings.MERSENNE_COLLECTIONS:
1581 if pid != "MERSENNE":
1582 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1583 pid, years
1584 )
1585 published_articles.append({"pid": pid, "years": articles_data})
1587 for i, year in enumerate(years):
1588 total_published_articles[i]["total_articles"] += total_articles[i]
1589 total_published_articles[i]["total_pages"] += total_pages[i]
1591 context["published_articles"] = published_articles
1592 context["total_published_articles"] = total_published_articles
1594 return context
1597class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1598 template_name = "dashboard/created_volumes.html"
1600 def get_context_data(self, **kwargs):
1601 context = self.get_common_context_data(**kwargs)
1602 years = context["years"]
1604 created_volumes = []
1605 total_created_volumes = [
1606 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1607 ]
1609 for pid in settings.MERSENNE_COLLECTIONS:
1610 if pid != "MERSENNE":
1611 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1612 pid, years
1613 )
1614 created_volumes.append({"pid": pid, "years": volumes_data})
1616 for i, _ in enumerate(years):
1617 total_created_volumes[i]["total_articles"] += total_articles[i]
1618 total_created_volumes[i]["total_pages"] += total_pages[i]
1620 context["created_volumes"] = created_volumes
1621 context["total_created_volumes"] = total_created_volumes
1623 return context
1625 def calculate_volumes_and_pages(self, pid, years):
1626 data_by_year = []
1627 total_articles = [0] * len(years)
1628 total_pages = [0] * len(years)
1630 for year in years:
1631 issues = Container.objects.filter(my_collection__pid=pid, fyear=year)
1632 articles_count = 0
1633 page_count = 0
1635 for issue in issues:
1636 articles = issue.article_set.filter(
1637 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1638 ).prefetch_related("resourcecount_set")
1640 articles_count += articles.count()
1641 page_count += sum(article.get_article_page_count() for article in articles)
1643 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1644 total_articles[year - years[0]] += articles_count
1645 total_pages[year - years[0]] += page_count
1647 return data_by_year, total_articles, total_pages
1650class ReferencingChoice(View):
1651 def post(self, request, *args, **kwargs):
1652 if request.POST.get("optSite") == "ads":
1653 return redirect(
1654 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1655 )
1656 elif request.POST.get("optSite") == "wos":
1657 comp = ReferencingCheckerWos()
1658 journal = comp.make_journal(request.POST.get("selectCol"))
1659 if journal is None:
1660 return render(
1661 request,
1662 "dashboard/referencing.html",
1663 {
1664 "error": "Collection not found",
1665 "colid": request.POST.get("selectCol"),
1666 "optSite": request.POST.get("optSite"),
1667 },
1668 )
1669 return render(
1670 request,
1671 "dashboard/referencing.html",
1672 {
1673 "journal": journal,
1674 "colid": request.POST.get("selectCol"),
1675 "optSite": request.POST.get("optSite"),
1676 },
1677 )
1680class ReferencingWosFileView(View):
1681 template_name = "dashboard/referencing.html"
1683 def post(self, request, *args, **kwargs):
1684 colid = request.POST["colid"]
1685 if request.FILES.get("risfile") is None:
1686 message = "No file uploaded"
1687 return render(
1688 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1689 )
1690 uploaded_file = request.FILES["risfile"]
1691 comp = ReferencingCheckerWos()
1692 journal = comp.check_references(colid, uploaded_file)
1693 return render(request, self.template_name, {"journal": journal})
1696class ReferencingDashboardView(BaseMersenneDashboardView):
1697 template_name = "dashboard/referencing.html"
1699 def get(self, request, *args, **kwargs):
1700 colid = self.kwargs.get("colid", None)
1701 comp = ReferencingCheckerAds()
1702 journal = comp.check_references(colid)
1703 return render(request, self.template_name, {"journal": journal})
1706class BaseCollectionView(TemplateView):
1707 def get_context_data(self, **kwargs):
1708 context = super().get_context_data(**kwargs)
1709 aid = context.get("aid")
1710 year = context.get("year")
1712 if aid and year:
1713 context["collection"] = self.get_collection(aid, year)
1715 return context
1717 def get_collection(self, aid, year):
1718 """Method to be overridden by subclasses to fetch the appropriate collection"""
1719 raise NotImplementedError("Subclasses must implement get_collection method")
1722class ArticleListView(BaseCollectionView):
1723 template_name = "collection-list.html"
1725 def get_collection(self, aid, year):
1726 return Article.objects.filter(
1727 Q(my_container__my_collection__pid=aid)
1728 & (
1729 Q(date_published__year=year, date_online_first__isnull=True)
1730 | Q(date_online_first__year=year)
1731 )
1732 ).prefetch_related("resourcecount_set")
1735class VolumeListView(BaseCollectionView):
1736 template_name = "collection-list.html"
1738 def get_collection(self, aid, year):
1739 return Article.objects.filter(
1740 Q(my_container__my_collection__pid=aid, my_container__fyear=year)
1741 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1742 ).prefetch_related("resourcecount_set")
1745class DOAJResourceRegisterView(View):
1746 def get(self, request, *args, **kwargs):
1747 pid = kwargs.get("pid", None)
1748 resource = model_helpers.get_resource(pid)
1749 if resource is None:
1750 raise Http404
1751 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1752 resource.colid, None
1753 ):
1754 raise RuntimeError("Pending publications should not be deployed")
1756 try:
1757 data = {}
1758 doaj_meta, response = doaj_pid_register(pid)
1759 if response is None:
1760 return HttpResponse(status=204)
1761 elif doaj_meta and 200 <= response.status_code <= 299:
1762 data.update(doaj_meta)
1763 else:
1764 return HttpResponse(status=response.status_code, reason=response.text)
1765 except Timeout as exception:
1766 return HttpResponse(exception, status=408)
1767 except Exception as exception:
1768 return HttpResponseServerError(exception)
1769 return JsonResponse(data)
1772class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View):
1773 """
1774 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml
1775 """
1777 def get(self, request, *args, **kwargs):
1778 pid = kwargs.get("pid")
1779 if not pid:
1780 raise Http404("Missing pid")
1782 article = Article.objects.filter(pid=pid).first()
1783 if not article:
1784 raise Http404(f"Article not found: {pid}")
1786 colid = article.get_collection().pid
1787 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
1788 return JsonResponse(
1789 {"status": 403, "message": f"Tex conversions are disabled in {colid}"}
1790 )
1792 if is_tex_conversion_locked(pid):
1793 logger.warning("Conversion rejected (lock exists) for %s", pid)
1794 return JsonResponse(
1795 {"status": 409, "message": f"A conversion is already running for {pid}"}
1796 )
1798 logger.info("No lock → scheduling conversion for %s", pid)
1800 try:
1801 convert_article_tex.delay(pid=pid, user_pk=request.user.pk)
1802 except Exception:
1803 logger.exception("Failed to enqueue task for %s", pid)
1804 release_tex_conversion_lock(pid)
1805 raise
1807 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"})
1810class CROSSREFResourceRegisterView(View):
1811 def get(self, request, *args, **kwargs):
1812 pid = kwargs.get("pid", None)
1813 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1814 force = kwargs.get("force", None)
1815 if not request.user.is_superuser:
1816 force = None
1818 resource = model_helpers.get_resource(pid)
1819 if resource is None:
1820 raise Http404
1822 resource = resource.cast()
1823 meth = getattr(self, "recordDOI" + resource.classname)
1824 try:
1825 data = meth(resource, force)
1826 except Timeout as exception:
1827 return HttpResponse(exception, status=408)
1828 except Exception as exception:
1829 return HttpResponseServerError(exception)
1830 return JsonResponse(data)
1832 def recordDOIArticle(self, article: "Article", force=None):
1833 result = {"status": 404}
1834 if (
1835 article.doi
1836 and not article.do_not_publish
1837 and (article.date_published or article.date_online_first or force == "force")
1838 ):
1839 if article.my_container.fyear == 0:
1840 article.my_container.fyear = datetime.now().year
1841 result = recordDOI(article)
1842 return result
1844 def recordDOICollection(self, collection, force=None):
1845 return recordDOI(collection)
1847 def recordDOIContainer(self, container, force=None):
1848 data = {"status": 200, "message": "All DOI successfully checked"}
1850 if container.ctype == "issue":
1851 if container.doi:
1852 result = recordDOI(container)
1853 if result["status"] != 200:
1854 return result
1855 if force == "force":
1856 articles = container.article_set.exclude(
1857 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1858 )
1859 else:
1860 articles = container.article_set.exclude(
1861 doi__isnull=True,
1862 do_not_publish=True,
1863 date_published__isnull=True,
1864 date_online_first__isnull=True,
1865 )
1867 for article in articles:
1868 result = self.recordDOIArticle(article, force)
1869 if result["status"] != 200:
1870 data = result
1871 else:
1872 return recordDOI(container)
1873 return data
1876class CROSSREFResourceCheckStatusView(View):
1877 def get(self, request, *args, **kwargs):
1878 pid = kwargs.get("pid", None)
1879 resource = model_helpers.get_resource(pid)
1880 if resource is None:
1881 raise Http404
1882 resource = resource.cast()
1883 meth = getattr(self, "checkDOI" + resource.classname)
1884 try:
1885 meth(resource)
1886 except Timeout as exception:
1887 return HttpResponse(exception, status=408)
1888 except Exception as exception:
1889 return HttpResponseServerError(exception)
1891 data = {"status": 200, "message": "DOI successfully checked"}
1892 return JsonResponse(data)
1894 def checkDOIArticle(self, article: "Article"):
1895 if article.my_container.fyear == 0:
1896 article.my_container.fyear = datetime.now().year
1897 checkDOI(article)
1899 def checkDOICollection(self, collection):
1900 checkDOI(collection)
1902 def checkDOIContainer(self, container):
1903 if container.doi is not None:
1904 checkDOI(container)
1905 for article in container.article_set.all():
1906 self.checkDOIArticle(article)
1909class CROSSREFResourcePendingPublicationRegisterView(View):
1910 def get(self, request, *args, **kwargs):
1911 pid = kwargs.get("pid", None)
1912 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1914 resource = model_helpers.get_resource(pid)
1915 if resource is None:
1916 raise Http404
1918 resource = resource.cast()
1919 meth = getattr(self, "recordPendingPublication" + resource.classname)
1920 try:
1921 data = meth(resource)
1922 except Timeout as exception:
1923 return HttpResponse(exception, status=408)
1924 except Exception as exception:
1925 return HttpResponseServerError(exception)
1926 return JsonResponse(data)
1928 def recordPendingPublicationArticle(self, article):
1929 result = {"status": 404}
1930 if article.doi and not article.date_published and not article.date_online_first:
1931 if article.my_container.fyear is None or article.my_container.fyear == "0":
1932 article.my_container.fyear = datetime.now().year
1933 result = recordPendingPublication(article)
1934 return result
1937class RegisterPubmedFormView(FormView):
1938 template_name = "record_pubmed_dialog.html"
1939 form_class = RegisterPubmedForm
1941 def get_context_data(self, **kwargs):
1942 context = super().get_context_data(**kwargs)
1943 context["pid"] = self.kwargs["pid"]
1944 context["helper"] = PtfLargeModalFormHelper
1945 return context
1948class RegisterPubmedView(View):
1949 def get(self, request, *args, **kwargs):
1950 pid = kwargs.get("pid", None)
1951 update_article = self.request.GET.get("update_article", "on") == "on"
1953 article = model_helpers.get_article(pid)
1954 if article is None:
1955 raise Http404
1956 try:
1957 recordPubmed(article, update_article)
1958 except Exception as exception:
1959 messages.error("Unable to register the article in PubMed")
1960 return HttpResponseServerError(exception)
1962 return HttpResponseRedirect(
1963 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1964 )
1967class PTFToolsArticleView(ArticleView):
1968 def get_context_data(self, **kwargs):
1969 context = super().get_context_data(**kwargs)
1971 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(self.obj)
1973 qs = self.obj.get_top_collection().extlink_set.filter(rel="test_website")
1974 if qs:
1975 test_website = qs.first().location
1976 context["test_website"] = test_website
1977 qs = self.obj.get_top_collection().extlink_set.filter(rel="website")
1978 if qs:
1979 prod_location = qs.first().location
1980 context["prod_website"] = prod_location
1982 return context
1985ItemViewClassFactory.views["article"] = PTFToolsArticleView
1988class PTFToolsContainerView(TemplateView):
1989 template_name = ""
1991 def get_context_data(self, **kwargs):
1992 context = super().get_context_data(**kwargs)
1994 container = model_helpers.get_container(self.kwargs.get("pid"))
1995 if container is None:
1996 raise Http404
1997 citing_articles = container.citations()
1998 source = self.request.GET.get("source", None)
1999 if container.ctype.startswith("book"):
2000 book_parts = (
2001 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
2002 )
2003 references = False
2004 if container.ctype == "book-monograph":
2005 # on regarde si il y a au moins une bibliographie
2006 for art in container.article_set.all():
2007 if art.bibitem_set.count() > 0:
2008 references = True
2009 context.update(
2010 {
2011 "book": container,
2012 "book_parts": list(book_parts),
2013 "source": source,
2014 "citing_articles": citing_articles,
2015 "references": references,
2016 "test_website": container.get_top_collection()
2017 .extlink_set.get(rel="test_website")
2018 .location,
2019 "prod_website": container.get_top_collection()
2020 .extlink_set.get(rel="website")
2021 .location,
2022 }
2023 )
2024 self.template_name = "book-toc.html"
2025 else:
2026 articles = container.article_set.all().order_by("seq")
2027 for article in articles:
2028 try:
2029 last_match = (
2030 history_models.HistoryEvent.objects.filter(
2031 pid=article.pid,
2032 type="matching",
2033 )
2034 .only("created_on")
2035 .latest("created_on")
2036 )
2037 except history_models.HistoryEvent.DoesNotExist as _:
2038 article.last_match = None
2039 else:
2040 article.last_match = last_match.created_on
2042 # article1 = articles.first()
2043 # date = article1.deployed_date()
2044 # TODO next_issue, previous_issue
2046 # check DOI est maintenant une commande à part
2047 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
2048 # articlesWithStatus = []
2049 # for article in articles:
2050 # checkDOIExistence(article)
2051 # articlesWithStatus.append(article)
2053 test_location = prod_location = ""
2054 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
2055 if qs:
2056 test_location = qs.first().location
2057 qs = container.get_top_collection().extlink_set.filter(rel="website")
2058 if qs:
2059 prod_location = qs.first().location
2060 context.update(
2061 {
2062 "issue": container,
2063 "articles": articles,
2064 "source": source,
2065 "citing_articles": citing_articles,
2066 "test_website": test_location,
2067 "prod_website": prod_location,
2068 }
2069 )
2071 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
2072 context["is_issue_pending_publication"] = True
2073 if container.get_top_collection().pid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
2074 context["is_excluded_from_tex_conversion"] = True
2075 self.template_name = "issue-items.html"
2077 context["allow_crossref"] = container.allow_crossref()
2078 context["coltype"] = container.my_collection.coltype
2079 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(container)
2080 return context
2083class ExtLinkInline(InlineFormSetFactory):
2084 model = ExtLink
2085 form_class = ExtLinkForm
2086 factory_kwargs = {"extra": 0}
2089class ResourceIdInline(InlineFormSetFactory):
2090 model = ResourceId
2091 form_class = ResourceIdForm
2092 factory_kwargs = {"extra": 0}
2095class IssueDetailAPIView(View):
2096 def get(self, request, *args, **kwargs):
2097 issue = get_object_or_404(Container, pid=kwargs["pid"])
2098 deployed_date = issue.deployed_date()
2099 result = {
2100 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2101 if deployed_date
2102 else None,
2103 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2104 "all_doi_are_registered": issue.all_doi_are_registered(),
2105 "registered_in_doaj": issue.registered_in_doaj(),
2106 "doi": issue.my_collection.doi,
2107 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2108 }
2109 try:
2110 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2111 except history_models.HistoryEvent.DoesNotExist as _:
2112 pass
2113 else:
2114 result["latest"] = latest.message
2115 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2116 "%Y-%m-%d %H:%M"
2117 )
2119 result["latest_type"] = latest.type.capitalize()
2120 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2121 try:
2122 result[event_type] = timezone.localtime(
2123 history_models.HistoryEvent.objects.filter(
2124 type=event_type,
2125 status="OK",
2126 pid__startswith=issue.pid,
2127 )
2128 .latest("created_on")
2129 .created_on
2130 ).strftime("%Y-%m-%d %H:%M")
2131 except history_models.HistoryEvent.DoesNotExist as _:
2132 result[event_type] = ""
2133 return JsonResponse(result)
2136class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2137 model = Collection
2138 form_class = CollectionForm
2139 inlines = [ResourceIdInline, ExtLinkInline]
2140 inlines_names = ["resource_ids_form", "ext_links_form"]
2142 def get_context_data(self, **kwargs):
2143 context = super().get_context_data(**kwargs)
2144 context["helper"] = PtfFormHelper
2145 context["formset_helper"] = FormSetHelper
2146 return context
2148 def add_description(self, collection, description, lang, seq):
2149 if description:
2150 la = Abstract(
2151 resource=collection,
2152 tag="description",
2153 lang=lang,
2154 seq=seq,
2155 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2156 value_html=description,
2157 value_tex=description,
2158 )
2159 la.save()
2161 def form_valid(self, form):
2162 if form.instance.abbrev:
2163 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2164 else:
2165 form.instance.title_xml = (
2166 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2167 )
2169 form.instance.title_html = form.instance.title_tex
2170 form.instance.title_sort = form.instance.title_tex
2171 result = super().form_valid(form)
2173 collection = self.object
2174 collection.abstract_set.all().delete()
2176 seq = 1
2177 description = form.cleaned_data["description_en"]
2178 if description:
2179 self.add_description(collection, description, "en", seq)
2180 seq += 1
2181 description = form.cleaned_data["description_fr"]
2182 if description:
2183 self.add_description(collection, description, "fr", seq)
2185 return result
2187 def get_success_url(self):
2188 messages.success(
2189 self.request, f'The collection "{self.object.pid}" has been successfully updated'
2190 )
2191 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2194class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2195 """
2196 Warning : Not yet finished
2197 Automatic site membership creation is still missing
2198 """
2201class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2202 slug_field = "pid"
2203 slug_url_kwarg = "pid"
2206def suggest_load_journal_dois(colid):
2207 articles = (
2208 Article.objects.filter(my_container__my_collection__pid=colid)
2209 .filter(doi__isnull=False)
2210 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2211 .values_list("doi", flat=True)
2212 )
2214 try:
2215 articles = sorted(
2216 articles,
2217 key=lambda d: (
2218 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2219 int(re.search(r".(\d+)$", d).group(1)),
2220 ),
2221 )
2222 except: # noqa: E722 (we'll look later)
2223 pass
2224 return [f'<option value="{doi}">' for doi in articles]
2227def get_context_with_volumes(journal):
2228 result = model_helpers.get_volumes_in_collection(journal)
2229 volume_count = result["volume_count"]
2230 collections = []
2231 for ancestor in journal.ancestors.all():
2232 item = model_helpers.get_volumes_in_collection(ancestor)
2233 volume_count = max(0, volume_count)
2234 item.update({"journal": ancestor})
2235 collections.append(item)
2237 # add the parent collection to its children list and sort it by date
2238 result.update({"journal": journal})
2239 collections.append(result)
2241 collections = [c for c in collections if c["sorted_issues"]]
2242 collections.sort(
2243 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2244 reverse=True,
2245 )
2247 context = {
2248 "journal": journal,
2249 "sorted_issues": result["sorted_issues"],
2250 "volume_count": volume_count,
2251 "max_width": result["max_width"],
2252 "collections": collections,
2253 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2254 }
2255 return context
2258class CollectionDetail(
2259 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2260):
2261 model = Collection
2262 slug_field = "pid"
2263 slug_url_kwarg = "pid"
2264 template_name = "ptf/collection_detail.html"
2266 def test_func(self):
2267 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2269 def get(self, request, *args, **kwargs):
2270 self.object = self.get_object(queryset=Collection.objects.all())
2271 return super().get(request, *args, **kwargs)
2273 def get_context_data(self, **kwargs):
2274 context = super().get_context_data(**kwargs)
2275 context["object_list"] = context["object_list"].filter(
2276 Q(ctype="issue") | Q(ctype="book-lecture-notes") | Q(ctype="book-monograph")
2277 )
2278 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2279 context.update(get_context_with_volumes(self.object))
2281 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2282 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2283 context["issue_to_appear"] = Container.objects.filter(
2284 pid=context["issue_to_appear_pid"]
2285 ).exists()
2286 try:
2287 latest_error = history_models.HistoryEvent.objects.filter(
2288 status="ERROR", col=self.object
2289 ).latest("created_on")
2290 except history_models.HistoryEvent.DoesNotExist as _:
2291 pass
2292 else:
2293 message = latest_error.message
2294 if message:
2295 i = message.find(" - ")
2296 latest_exception = message[:i]
2297 latest_error_message = message[i + 3 :]
2298 context["latest_exception"] = latest_exception
2299 context["latest_exception_date"] = latest_error.created_on
2300 context["latest_exception_type"] = latest_error.type
2301 context["latest_error_message"] = latest_error_message
2303 archive_in_error = history_models.HistoryEvent.objects.filter(
2304 status="ERROR", col=self.object, type="archive"
2305 ).exists()
2307 context["archive_in_error"] = archive_in_error
2309 return context
2311 def get_queryset(self):
2312 query = self.object.content.all()
2314 for ancestor in self.object.ancestors.all():
2315 query |= ancestor.content.all()
2317 return query.order_by("-fyear", "-vseries", "-volume", "-volume_int", "-number_int")
2320class ContainerEditView(FormView):
2321 template_name = "container_form.html"
2322 form_class = ContainerForm
2324 def get_success_url(self):
2325 if self.kwargs["pid"]:
2326 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2327 return reverse("mersenne_dashboard/published_articles")
2329 def set_success_message(self): # pylint: disable=no-self-use
2330 messages.success(self.request, "Booklet updated")
2332 def get_form_kwargs(self):
2333 kwargs = super().get_form_kwargs()
2334 if "pid" not in self.kwargs:
2335 self.kwargs["pid"] = None
2336 if "colid" not in self.kwargs:
2337 self.kwargs["colid"] = None
2338 if "data" in kwargs and "colid" in kwargs["data"]:
2339 # colid is passed as a hidden param in the form.
2340 # It is used when you submit a new container
2341 self.kwargs["colid"] = kwargs["data"]["colid"]
2343 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2344 self.kwargs["pid"]
2345 )
2346 return kwargs
2348 def get_context_data(self, **kwargs):
2349 context = super().get_context_data(**kwargs)
2351 context["pid"] = self.kwargs["pid"]
2352 context["colid"] = self.kwargs["colid"]
2353 context["container"] = self.kwargs["container"]
2355 context["edit_container"] = context["pid"] is not None
2356 context["name"] = resolve(self.request.path_info).url_name
2358 return context
2360 def form_valid(self, form):
2361 new_pid = form.cleaned_data.get("pid")
2362 new_title = form.cleaned_data.get("title")
2363 new_trans_title = form.cleaned_data.get("trans_title")
2364 new_publisher = form.cleaned_data.get("publisher")
2365 new_year = form.cleaned_data.get("year")
2366 new_volume = form.cleaned_data.get("volume")
2367 new_number = form.cleaned_data.get("number")
2369 collection = None
2370 issue = self.kwargs["container"]
2371 if issue is not None:
2372 collection = issue.my_collection
2373 elif self.kwargs["colid"] is not None:
2374 if "CR" in self.kwargs["colid"]:
2375 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2376 else:
2377 collection = model_helpers.get_collection(self.kwargs["colid"])
2379 if collection is None:
2380 raise ValueError("Collection for " + new_pid + " does not exist")
2382 # Icon
2383 new_icon_location = ""
2384 if "icon" in self.request.FILES:
2385 filename = os.path.basename(self.request.FILES["icon"].name)
2386 file_extension = filename.split(".")[1]
2388 icon_filename = resolver.get_disk_location(
2389 settings.MERSENNE_TEST_DATA_FOLDER,
2390 collection.pid,
2391 file_extension,
2392 new_pid,
2393 None,
2394 True,
2395 )
2397 with open(icon_filename, "wb+") as destination:
2398 destination.writelines(self.request.FILES["icon"].chunks())
2400 folder = resolver.get_relative_folder(collection.pid, new_pid)
2401 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2402 name = resolve(self.request.path_info).url_name
2403 if name == "special_issue_create":
2404 self.kwargs["name"] = name
2405 if self.kwargs["container"]:
2406 # Edit Issue
2407 issue = self.kwargs["container"]
2408 if issue is None:
2409 raise ValueError(self.kwargs["pid"] + " does not exist")
2411 issue.pid = new_pid
2412 issue.title_tex = issue.title_html = new_title
2413 issue.title_xml = build_title_xml(
2414 title=new_title,
2415 lang=issue.lang,
2416 title_type="issue-title",
2417 )
2419 trans_lang = ""
2420 if new_trans_title != "":
2421 trans_lang = "fr" if issue.lang == "en" else "en"
2423 if trans_lang != "" and new_trans_title != "":
2424 title_xml = build_title_xml(
2425 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2426 )
2428 issue.title_set.update_or_create(
2429 lang=trans_lang,
2430 type="main",
2431 defaults={"title_html": new_trans_title, "title_xml": title_xml},
2432 )
2434 issue.fyear = new_year
2435 issue.volume = new_volume
2436 issue.volume_int = make_int(new_volume)
2437 issue.number = new_number
2438 issue.number_int = make_int(new_number)
2439 issue.save()
2440 else:
2441 xissue = create_issuedata()
2443 xissue.ctype = "issue"
2444 xissue.pid = new_pid
2445 xissue.lang = "en"
2446 xissue.title_tex = new_title
2447 xissue.title_html = new_title
2448 xissue.title_xml = build_title_xml(
2449 title=new_title, lang=xissue.lang, title_type="issue-title"
2450 )
2452 if new_trans_title != "":
2453 trans_lang = "fr"
2454 title_xml = build_title_xml(
2455 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2456 )
2457 title = create_titledata(
2458 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2459 )
2460 issue.titles = [title]
2462 xissue.fyear = new_year
2463 xissue.volume = new_volume
2464 xissue.number = new_number
2465 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2467 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2468 cmd.add_collection(collection)
2469 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2470 issue = cmd.do()
2472 self.kwargs["pid"] = new_pid
2474 # Add objects related to the article: contribs, datastream, counts...
2475 params = {
2476 "icon_location": new_icon_location,
2477 }
2478 cmd = ptf_cmds.updateContainerPtfCmd(params)
2479 cmd.set_resource(issue)
2480 cmd.do()
2482 publisher = model_helpers.get_publisher(new_publisher)
2483 if not publisher:
2484 xpub = create_publisherdata()
2485 xpub.name = new_publisher
2486 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2487 issue.my_publisher = publisher
2488 issue.save()
2490 self.set_success_message()
2492 return super().form_valid(form)
2495# class ArticleEditView(FormView):
2496# template_name = 'article_form.html'
2497# form_class = ArticleForm
2498#
2499# def get_success_url(self):
2500# if self.kwargs['pid']:
2501# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2502# return reverse('mersenne_dashboard/published_articles')
2503#
2504# def set_success_message(self): # pylint: disable=no-self-use
2505# messages.success(self.request, "L'article a été modifié")
2506#
2507# def get_form_kwargs(self):
2508# kwargs = super(ArticleEditView, self).get_form_kwargs()
2509#
2510# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2511# # Article creation: pid is None
2512# self.kwargs['pid'] = None
2513# if 'issue_id' not in self.kwargs:
2514# # Article edit: issue_id is not passed
2515# self.kwargs['issue_id'] = None
2516# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2517# # colid is passed as a hidden param in the form.
2518# # It is used when you submit a new container
2519# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2520#
2521# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2522# return kwargs
2523#
2524# def get_context_data(self, **kwargs):
2525# context = super(ArticleEditView, self).get_context_data(**kwargs)
2526#
2527# context['pid'] = self.kwargs['pid']
2528# context['issue_id'] = self.kwargs['issue_id']
2529# context['article'] = self.kwargs['article']
2530#
2531# context['edit_article'] = context['pid'] is not None
2532#
2533# article = context['article']
2534# if article:
2535# context['author_contributions'] = article.get_author_contributions()
2536# context['kwds_fr'] = None
2537# context['kwds_en'] = None
2538# kwd_gps = article.get_non_msc_kwds()
2539# for kwd_gp in kwd_gps:
2540# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2541# if kwd_gp.value_xml:
2542# kwd_ = types.SimpleNamespace()
2543# kwd_.value = kwd_gp.value_tex
2544# context['kwd_unstructured_fr'] = kwd_
2545# context['kwds_fr'] = kwd_gp.kwd_set.all()
2546# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2547# if kwd_gp.value_xml:
2548# kwd_ = types.SimpleNamespace()
2549# kwd_.value = kwd_gp.value_tex
2550# context['kwd_unstructured_en'] = kwd_
2551# context['kwds_en'] = kwd_gp.kwd_set.all()
2552#
2553# # Article creation: init pid
2554# if context['issue_id'] and context['pid'] is None:
2555# issue = model_helpers.get_container(context['issue_id'])
2556# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2557#
2558# return context
2559#
2560# def form_valid(self, form):
2561#
2562# new_pid = form.cleaned_data.get('pid')
2563# new_title = form.cleaned_data.get('title')
2564# new_fpage = form.cleaned_data.get('fpage')
2565# new_lpage = form.cleaned_data.get('lpage')
2566# new_page_range = form.cleaned_data.get('page_range')
2567# new_page_count = form.cleaned_data.get('page_count')
2568# new_coi_statement = form.cleaned_data.get('coi_statement')
2569# new_show_body = form.cleaned_data.get('show_body')
2570# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2571#
2572# # TODO support MathML
2573# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2574# # We need to pass trans_title to get_title_xml
2575# # Meanwhile, ignore new_title_xml
2576# new_title_xml = jats_parser.get_title_xml(new_title)
2577# new_title_html = new_title
2578#
2579# authors_count = int(self.request.POST.get('authors_count', "0"))
2580# i = 1
2581# new_authors = []
2582# old_author_contributions = []
2583# if self.kwargs['article']:
2584# old_author_contributions = self.kwargs['article'].get_author_contributions()
2585#
2586# while authors_count > 0:
2587# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2588#
2589# if prefix is not None:
2590# addresses = []
2591# if len(old_author_contributions) >= i:
2592# old_author_contribution = old_author_contributions[i - 1]
2593# addresses = [contrib_address.address for contrib_address in
2594# old_author_contribution.get_addresses()]
2595#
2596# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2597# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2598# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2599# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2600# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2601# deceased_before_publication = deceased == 'on'
2602# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2603# equal_contrib = equal_contrib == 'on'
2604# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2605# corresponding = corresponding == 'on'
2606# email = self.request.POST.get('email-' + str(i), None)
2607#
2608# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2609# params['deceased_before_publication'] = deceased_before_publication
2610# params['equal_contrib'] = equal_contrib
2611# params['corresponding'] = corresponding
2612# params['addresses'] = addresses
2613# params['email'] = email
2614#
2615# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2616#
2617# new_authors.append(params)
2618#
2619# authors_count -= 1
2620# i += 1
2621#
2622# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2623# i = 1
2624# new_kwds_fr = []
2625# while kwds_fr_count > 0:
2626# value = self.request.POST.get('kwd-fr-' + str(i), None)
2627# new_kwds_fr.append(value)
2628# kwds_fr_count -= 1
2629# i += 1
2630# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2631#
2632# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2633# i = 1
2634# new_kwds_en = []
2635# while kwds_en_count > 0:
2636# value = self.request.POST.get('kwd-en-' + str(i), None)
2637# new_kwds_en.append(value)
2638# kwds_en_count -= 1
2639# i += 1
2640# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2641#
2642# if self.kwargs['article']:
2643# # Edit article
2644# container = self.kwargs['article'].my_container
2645# else:
2646# # New article
2647# container = model_helpers.get_container(self.kwargs['issue_id'])
2648#
2649# if container is None:
2650# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2651#
2652# collection = container.my_collection
2653#
2654# # Copy PDF file & extract full text
2655# body = ''
2656# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2657# collection.pid,
2658# "pdf",
2659# container.pid,
2660# new_pid,
2661# True)
2662# if 'pdf' in self.request.FILES:
2663# with open(pdf_filename, 'wb+') as destination:
2664# for chunk in self.request.FILES['pdf'].chunks():
2665# destination.write(chunk)
2666#
2667# # Extract full text from the PDF
2668# body = utils.pdf_to_text(pdf_filename)
2669#
2670# # Icon
2671# new_icon_location = ''
2672# if 'icon' in self.request.FILES:
2673# filename = os.path.basename(self.request.FILES['icon'].name)
2674# file_extension = filename.split('.')[1]
2675#
2676# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2677# collection.pid,
2678# file_extension,
2679# container.pid,
2680# new_pid,
2681# True)
2682#
2683# with open(icon_filename, 'wb+') as destination:
2684# for chunk in self.request.FILES['icon'].chunks():
2685# destination.write(chunk)
2686#
2687# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2688# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2689#
2690# if self.kwargs['article']:
2691# # Edit article
2692# article = self.kwargs['article']
2693# article.fpage = new_fpage
2694# article.lpage = new_lpage
2695# article.page_range = new_page_range
2696# article.coi_statement = new_coi_statement
2697# article.show_body = new_show_body
2698# article.do_not_publish = new_do_not_publish
2699# article.save()
2700#
2701# else:
2702# # New article
2703# params = {
2704# 'pid': new_pid,
2705# 'title_xml': new_title_xml,
2706# 'title_html': new_title_html,
2707# 'title_tex': new_title,
2708# 'fpage': new_fpage,
2709# 'lpage': new_lpage,
2710# 'page_range': new_page_range,
2711# 'seq': container.article_set.count() + 1,
2712# 'body': body,
2713# 'coi_statement': new_coi_statement,
2714# 'show_body': new_show_body,
2715# 'do_not_publish': new_do_not_publish
2716# }
2717#
2718# xarticle = create_articledata()
2719# xarticle.pid = new_pid
2720# xarticle.title_xml = new_title_xml
2721# xarticle.title_html = new_title_html
2722# xarticle.title_tex = new_title
2723# xarticle.fpage = new_fpage
2724# xarticle.lpage = new_lpage
2725# xarticle.page_range = new_page_range
2726# xarticle.seq = container.article_set.count() + 1
2727# xarticle.body = body
2728# xarticle.coi_statement = new_coi_statement
2729# params['xobj'] = xarticle
2730#
2731# cmd = ptf_cmds.addArticlePtfCmd(params)
2732# cmd.set_container(container)
2733# cmd.add_collection(container.my_collection)
2734# article = cmd.do()
2735#
2736# self.kwargs['pid'] = new_pid
2737#
2738# # Add objects related to the article: contribs, datastream, counts...
2739# params = {
2740# # 'title_xml': new_title_xml,
2741# # 'title_html': new_title_html,
2742# # 'title_tex': new_title,
2743# 'authors': new_authors,
2744# 'page_count': new_page_count,
2745# 'icon_location': new_icon_location,
2746# 'body': body,
2747# 'use_kwds': True,
2748# 'kwds_fr': new_kwds_fr,
2749# 'kwds_en': new_kwds_en,
2750# 'kwd_uns_fr': new_kwd_uns_fr,
2751# 'kwd_uns_en': new_kwd_uns_en
2752# }
2753# cmd = ptf_cmds.updateArticlePtfCmd(params)
2754# cmd.set_article(article)
2755# cmd.do()
2756#
2757# self.set_success_message()
2758#
2759# return super(ArticleEditView, self).form_valid(form)
2762@require_http_methods(["POST"])
2763def do_not_publish_article(request, *args, **kwargs):
2764 next = request.headers.get("referer")
2766 pid = kwargs.get("pid", "")
2768 article = model_helpers.get_article(pid)
2769 if article:
2770 article.do_not_publish = not article.do_not_publish
2771 article.save()
2772 else:
2773 raise Http404
2775 return HttpResponseRedirect(next)
2778@require_http_methods(["POST"])
2779def show_article_body(request, *args, **kwargs):
2780 next = request.headers.get("referer")
2782 pid = kwargs.get("pid", "")
2784 article = model_helpers.get_article(pid)
2785 if article:
2786 article.show_body = not article.show_body
2787 article.save()
2788 else:
2789 raise Http404
2791 return HttpResponseRedirect(next)
2794class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2795 """
2796 API to get/post article metadata
2797 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2798 """
2800 def __init__(self, *args, **kwargs):
2801 """
2802 we define here what fields we want in the form
2803 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2804 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2805 super().__init__(*args, **kwargs)
2806 self.fields_to_update = [
2807 "lang",
2808 "atype",
2809 "contributors",
2810 "abstracts",
2811 "kwds",
2812 "titles",
2813 "title_html",
2814 "title_xml",
2815 "title_tex",
2816 "streams",
2817 "ext_links",
2818 "date_accepted",
2819 "history_dates",
2820 "subjs",
2821 "bibitems",
2822 "references",
2823 ]
2824 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2825 self.additional_fields = [
2826 "doi",
2827 "pid",
2828 "container_pid",
2829 "pdf",
2830 "illustration",
2831 "dates",
2832 "msc_keywords",
2833 ]
2834 self.editorial_tools = [
2835 "translation",
2836 "sidebar",
2837 "lang_selection",
2838 "back_to_article_option",
2839 "msc_keywords",
2840 ]
2841 self.validation = {
2842 "required_checks": [],
2843 "blocking_checks": [],
2844 }
2845 self.article_container_pid = ""
2846 self.back_url = "trammel"
2848 def save_data(self, data_article):
2849 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2850 # The icons are not preserved since we can add/edit/delete them in VueJs
2851 params = {
2852 "pid": data_article.pid,
2853 "export_folder": settings.MERSENNE_TMP_FOLDER,
2854 "export_all": True,
2855 "with_binary_files": False,
2856 }
2857 ptf_cmds.exportExtraDataPtfCmd(params).do()
2859 def restore_data(self, article):
2860 ptf_cmds.importExtraDataPtfCmd(
2861 {
2862 "pid": article.pid,
2863 "import_folder": settings.MERSENNE_TMP_FOLDER,
2864 "import_bibitemid": False,
2865 }
2866 ).do()
2868 @method_decorator(csrf_exempt)
2869 def dispatch(self, request, *args, **kwargs):
2870 user_role = request.user.roles.all()
2871 if user_role:
2872 new_fields_to_update = []
2873 new_additional_fields = []
2874 new_editorial_tools = []
2875 # if a user have > 1 role, fields accessible shall be cumulative
2876 for role in user_role:
2877 role_fields_to_updates, role_additional_fields = role.get_real_fields_to_update()
2878 new_fields_to_update += role_fields_to_updates
2879 new_additional_fields += role_additional_fields
2880 new_editorial_tools += role.editorial_tools
2881 # in case two roles have some fields in common
2882 self.fields_to_update = list(set(new_fields_to_update))
2883 self.additional_fields = list(set(new_additional_fields))
2884 self.editorial_tools = list(set(new_editorial_tools))
2886 self.additional_fields += ["doi", "pid", "container_pid"]
2887 return super().dispatch(request, *args, **kwargs)
2889 def post(self, request, *args, **kwargs):
2890 response = super().post(request, *args, **kwargs)
2891 if response.status_code in [200, 302]: 2891 ↛ 2899line 2891 didn't jump to line 2899 because the condition on line 2891 was always true
2892 return redirect(
2893 "api-edit-article",
2894 colid=kwargs.get("colid", ""),
2895 containerPid=kwargs.get("containerPid"),
2896 doi=kwargs.get("doi", ""),
2897 )
2898 else:
2899 raise Http404
2902class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2903 template_name = "article_form.html"
2905 def get_success_url(self):
2906 if self.kwargs["doi"]:
2907 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2908 return reverse("mersenne_dashboard/published_articles")
2910 def get_context_data(self, **kwargs):
2911 context = super().get_context_data(**kwargs)
2912 if "doi" in self.kwargs:
2913 article = model_helpers.get_article_by_doi(self.kwargs["doi"])
2914 context["article"] = article
2915 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(article)
2916 context["pid"] = context["article"].pid
2918 context["container_pid"] = kwargs.get("container_pid", "")
2919 return context
2922class ArticleDeleteView(View):
2923 def get(self, request, *args, **kwargs):
2924 pid = self.kwargs.get("pid", None)
2925 article = get_object_or_404(Article, pid=pid)
2926 site = self.kwargs.get("site", None)
2927 message = f"Article successfully removed from {site}"
2928 status = 200
2930 try:
2931 if check_lock():
2932 message = "Trammel is under maintenance. Please try again later."
2933 messages.error(self.request, message)
2934 raise ServerUnderMaintenance(message)
2936 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2938 if site == "trammel":
2939 if article.is_deployed(mersenneSite):
2940 article.undeploy(mersenneSite)
2942 cmd = ptf_cmds.addArticlePtfCmd(
2943 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2944 )
2945 cmd.set_container(article.my_container)
2946 cmd.set_object_to_be_deleted(article)
2947 cmd.undo()
2949 elif site in ["website", "test_website"]:
2950 collection = article.get_collection()
2951 server_url = getattr(collection, site)()
2952 if not server_url:
2953 raise Http404(f"The collection has no {site}")
2955 # Delete on site
2956 url = server_url + reverse("article_delete", kwargs={"pid": pid})
2957 response = requests.delete(url, verify=False)
2958 status = response.status_code
2960 if status == 404:
2961 raise Http404(
2962 f"Article not found on {site}. Please make sure the article exists."
2963 )
2964 elif status > 204:
2965 exception = HTTPError(
2966 response.text.encode("utf8")[:1000],
2967 response=response,
2968 )
2969 raise exception
2970 else:
2971 if site == "website" and article.is_deployed(mersenneSite):
2972 article.undeploy(mersenneSite)
2973 # Delete article binary files
2974 folder = article.get_relative_folder()
2975 resolver.delete_object_folder(
2976 folder,
2977 to_folder=settings.MERSENNE_PROD_DATA_FOLDER,
2978 )
2979 else:
2980 raise ValueError(f"{site} is not a supported site")
2982 except ValueError as exception:
2983 if site in {"trammel", "website", "test_website"}:
2984 raise
2985 message, status = str(exception), 400
2986 except Http404 as exception:
2987 message, status = str(exception), 404
2988 except HTTPError as exception:
2989 message, status = str(exception), 502
2990 except ServerUnderMaintenance as exception:
2991 message, status = str(exception), 503
2992 except requests.exceptions.ConnectionError:
2993 message, status = (
2994 f"{site} is under maintenance. Please try again later.",
2995 503,
2996 )
2997 except Exception as exception:
2998 logger.exception("Unexpected error when deleting article %s from %s", pid, site)
2999 message, status = str(exception), 500
3001 return JsonResponse({"message": message, "status": status}, status=status)
3004def get_messages_in_queue():
3005 app = Celery("ptf-tools")
3006 # tasks = list(current_app.tasks)
3007 tasks = sorted(name for name in current_app.tasks if name.startswith("celery"))
3008 print(tasks)
3009 # i = app.control.inspect()
3011 with app.connection_or_acquire() as conn:
3012 remaining = conn.default_channel.queue_declare(
3013 queue="coordinator", passive=True
3014 ).message_count
3015 return remaining
3018class NumdamView(TemplateView, history_views.HistoryContextMixin):
3019 template_name = "numdam.html"
3021 def get_context_data(self, **kwargs):
3022 context = super().get_context_data(**kwargs)
3024 context["objs"] = ResourceInNumdam.objects.all()
3026 pre_issues = []
3027 prod_issues = []
3028 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
3029 try:
3030 response = requests.get(url)
3031 if response.status_code == 200:
3032 data = response.json()
3033 if "issues" in data:
3034 pre_issues = data["issues"]
3035 except Exception:
3036 pass
3038 url = f"{settings.NUMDAM_URL}/api-all-issues/"
3039 response = requests.get(url)
3040 if response.status_code == 200:
3041 data = response.json()
3042 if "issues" in data:
3043 prod_issues = data["issues"]
3045 new = sorted(set(pre_issues).difference(prod_issues))
3046 removed = sorted(set(prod_issues).difference(pre_issues))
3047 grouped = [
3048 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
3049 ]
3050 grouped_removed = [
3051 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
3052 ]
3053 context["added_issues"] = grouped
3054 context["removed_issues"] = grouped_removed
3056 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
3057 return context
3060class NumdamArchiveView(RedirectView):
3061 @staticmethod
3062 def reset_task_results():
3063 TaskResult.objects.all().delete()
3065 def get_redirect_url(self, *args, **kwargs):
3066 self.colid = kwargs["colid"]
3068 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
3069 return Http404
3071 # we make sure archiving is not already running
3072 # if not get_messages_in_queue():
3073 # self.reset_task_results()
3075 if self.colid == "ALL":
3076 archive_numdam_collections.delay()
3077 else:
3078 archive_numdam_collection.s(self.colid).delay()
3080 return reverse("numdam")
3083class DeployAllNumdamAPIView(View):
3084 def internal_do(self, *args, **kwargs):
3085 pids = []
3087 for obj in ResourceInNumdam.objects.all():
3088 pids.append(obj.pid)
3090 return pids
3092 def get(self, request, *args, **kwargs):
3093 try:
3094 pids, status, message = history_views.execute_and_record_func(
3095 "deploy", "numdam", "ALL", self.internal_do, "numdam"
3096 )
3097 except Exception as exception:
3098 return HttpResponseServerError(exception)
3100 data = {"message": message, "ids": pids, "status": status}
3101 return JsonResponse(data)
3104class NumdamDeleteAPIView(View):
3105 def get(self, request, *args, **kwargs):
3106 pid = self.kwargs.get("pid", None)
3108 try:
3109 obj = ResourceInNumdam.objects.get(pid=pid)
3110 obj.delete()
3111 except Exception as exception:
3112 return HttpResponseServerError(exception)
3114 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
3115 return JsonResponse(data)
3118class ExtIdApiDetail(View):
3119 def get(self, request, *args, **kwargs):
3120 extid = get_object_or_404(
3121 ExtId,
3122 resource__pid=kwargs["pid"],
3123 id_type=kwargs["what"],
3124 )
3125 return JsonResponse(
3126 {
3127 "pk": extid.pk,
3128 "href": extid.get_href(),
3129 "fetch": reverse(
3130 "api-fetch-id",
3131 args=(
3132 extid.resource.pk,
3133 extid.id_value,
3134 extid.id_type,
3135 "extid",
3136 ),
3137 ),
3138 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3139 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3140 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3141 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3142 "is_valid": extid.checked,
3143 }
3144 )
3147class ExtIdFormTemplate(TemplateView):
3148 template_name = "common/externalid_form.html"
3150 def get_context_data(self, **kwargs):
3151 context = super().get_context_data(**kwargs)
3152 context["sequence"] = kwargs["sequence"]
3153 return context
3156class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3157 def get_context_data(self, **kwargs):
3158 context = super().get_context_data(**kwargs)
3159 context["helper"] = PtfFormHelper
3160 return context
3162 def get_success_url(self):
3163 self.post_process()
3164 return self.object.resource.get_absolute_url()
3166 def post_process(self):
3167 model_helpers.post_resource_updated(self.object.resource)
3170class ExtIdCreate(ExtIdFormView, CreateView):
3171 model = ExtId
3172 form_class = ExtIdForm
3174 def get_context_data(self, **kwargs):
3175 context = super().get_context_data(**kwargs)
3176 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3177 return context
3179 def get_initial(self):
3180 initial = super().get_initial()
3181 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3182 return initial
3184 def form_valid(self, form):
3185 form.instance.checked = False
3186 return super().form_valid(form)
3189class ExtIdUpdate(ExtIdFormView, UpdateView):
3190 model = ExtId
3191 form_class = ExtIdForm
3193 def get_context_data(self, **kwargs):
3194 context = super().get_context_data(**kwargs)
3195 context["resource"] = self.object.resource
3196 return context
3199class UpdateTexmfZipAPIView(View):
3200 def get(self, request, *args, **kwargs):
3201 def copy_zip_files(src_folder, dest_folder):
3202 os.makedirs(dest_folder, exist_ok=True)
3204 zip_files = [
3205 os.path.join(src_folder, f)
3206 for f in os.listdir(src_folder)
3207 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3208 ]
3209 for zip_file in zip_files:
3210 resolver.copy_file(zip_file, dest_folder)
3212 # Exceptions: specific zip/gz files
3213 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3214 resolver.copy_file(zip_file, dest_folder)
3216 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3217 resolver.copy_file(zip_file, dest_folder)
3219 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3220 resolver.copy_file(gz_file, dest_folder)
3222 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3224 dest_folder = os.path.join(
3225 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3226 )
3228 try:
3229 copy_zip_files(src_folder, dest_folder)
3230 except Exception as exception:
3231 return HttpResponseServerError(exception)
3233 try:
3234 dest_folder = os.path.join(
3235 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3236 )
3237 copy_zip_files(src_folder, dest_folder)
3238 except Exception as exception:
3239 return HttpResponseServerError(exception)
3241 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3242 return JsonResponse(data)
3245class TrammelTasksProgressView(View):
3246 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3247 """
3248 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3249 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3250 """
3251 task_name = task
3253 def get_event_data():
3254 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3256 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3257 remaining_messages = get_messages_in_queue()
3259 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3260 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3261 failed_tasks = all_tasks.filter(status="FAILURE")
3263 all_tasks_count = all_tasks.count()
3264 success_count = successed_tasks.count()
3265 fail_count = failed_tasks.count()
3267 all_count = all_tasks_count + remaining_messages
3268 remaining_count = all_count - success_count - fail_count
3270 success_rate = int(success_count * 100 / all_count) if all_count else 0
3271 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3272 status = "consuming_queue" if remaining_count != 0 else "polling"
3274 last_task = successed_tasks.first()
3275 last_task = (
3276 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3277 if last_task
3278 else ""
3279 )
3281 # SSE event format
3282 event_data = {
3283 "status": status,
3284 "success_rate": success_rate,
3285 "error_rate": error_rate,
3286 "all_count": all_count,
3287 "remaining_count": remaining_count,
3288 "success_count": success_count,
3289 "fail_count": fail_count,
3290 "last_task": last_task,
3291 }
3293 return event_data
3295 def stream_response(data):
3296 # Send initial response headers
3297 yield f"data: {json.dumps(data)}\n\n"
3299 data = get_event_data()
3300 format = request.GET.get("format", "stream")
3301 if format == "json":
3302 response = JsonResponse(data)
3303 else:
3304 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3305 return response
3308user_signed_up.connect(update_user_from_invite)