Coverage for src/ptf_tools/views/base_views.py: 18%
1684 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-20 07:01 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-20 07: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 external.back.crossref.doi import checkDOI, recordDOI, recordPendingPublication
38from extra_views import (
39 CreateWithInlinesView,
40 InlineFormSetFactory,
41 NamedFormsetsMixin,
42 UpdateWithInlinesView,
43)
45# from ptf.views import ArticleEditFormWithVueAPIView
46from matching_back.views import ArticleEditFormWithVueAPIView
47from ptf import model_data_converter, model_helpers, utils
48from ptf.cmds import ptf_cmds, xml_cmds
49from ptf.cmds.base_cmds import make_int
50from ptf.cmds.xml.jats.builder.issue import build_title_xml
51from ptf.cmds.xml.xml_utils import replace_html_entities
52from ptf.display import resolver
53from ptf.exceptions import DOIException, ServerUnderMaintenance
54from ptf.model_data import create_issuedata, create_publisherdata, create_titledata
55from ptf.models import (
56 Abstract,
57 Article,
58 Collection,
59 Container,
60 ExtId,
61 ExtLink,
62 Resource,
63 ResourceId,
64)
65from ptf.views import ArticleView, ItemViewClassFactory
66from ptf_back.locks import (
67 is_tex_conversion_locked,
68 release_tex_conversion_lock,
69)
70from ptf_back.tex import create_frontpage
71from ptf_back.tex.tex_tasks import convert_article_tex
72from pubmed.views import recordPubmed
73from requests import Timeout
74from requests.exceptions import HTTPError
75from task.tasks.archiving_tasks import archive_resource
77from comments_moderation.utils import get_comments_for_home, is_comment_moderator
78from history import models as history_models
79from history import views as history_views
80from history.utils import (
81 get_gap,
82 get_history_last_event_by,
83 get_last_unsolved_error,
84)
85from ptf_tools.doaj import doaj_pid_register
86from ptf_tools.forms import (
87 CollectionForm,
88 ContainerForm,
89 DiffContainerForm,
90 ExtIdForm,
91 ExtLinkForm,
92 FormSetHelper,
93 ImportArticleForm,
94 ImportContainerForm,
95 ImportEditflowArticleForm,
96 PtfFormHelper,
97 PtfLargeModalFormHelper,
98 PtfModalFormHelper,
99 RegisterPubmedForm,
100 ResourceIdForm,
101 get_article_choices,
102)
103from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos
104from ptf_tools.models import ResourceInNumdam
105from ptf_tools.signals import update_user_from_invite
106from ptf_tools.tasks import (
107 archive_numdam_collection,
108 archive_numdam_collections,
109)
110from ptf_tools.templatetags.tools_helpers import get_authorized_collections
111from ptf_tools.utils import is_authorized_editor
112from ptf_tools.views.components import breadcrumb
114logger = logging.getLogger(__name__)
117def view_404(request: HttpRequest, *args, **kwargs):
118 """
119 Dummy view raising HTTP 404 exception.
120 """
121 raise Http404
124def check_collection(collection, server_url, server_type):
125 """
126 Check if a collection exists on a serveur (test/prod)
127 and upload the collection (XML, image) if necessary
128 """
130 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
131 response = requests.get(url, verify=False)
132 # First, upload the collection XML
133 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
134 body = xml.encode("utf8")
136 url = server_url + reverse("upload-serials")
137 if response.status_code == 200:
138 # PUT http verb is used for update
139 response = requests.put(url, data=body, verify=False)
140 else:
141 # POST http verb is used for creation
142 response = requests.post(url, data=body, verify=False)
144 # Second, copy the collection images
145 # There is no need to copy files for the test server
146 # Files were already copied in /mersenne_test_data during the ptf_tools import
147 # We only need to copy files from /mersenne_test_data to
148 # /mersenne_prod_data during an upload to prod
149 if server_type == "website":
150 resolver.copy_binary_files(
151 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
152 )
153 elif server_type == "numdam":
154 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
155 if collection.pid in settings.NUMDAM_COLLECTIONS:
156 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
158 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
161def check_lock():
162 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
165def load_cedrics_article_choices(request):
166 colid = request.GET.get("colid")
167 issue = request.GET.get("issue")
168 article_choices = get_article_choices(colid, issue)
169 return render(
170 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
171 )
174class ImportCedricsArticleFormView(FormView):
175 template_name = "import_article.html"
176 form_class = ImportArticleForm
178 def dispatch(self, request, *args, **kwargs):
179 self.colid = self.kwargs["colid"]
180 return super().dispatch(request, *args, **kwargs)
182 def get_success_url(self):
183 if self.colid:
184 return reverse("collection-detail", kwargs={"pid": self.colid})
185 return "/"
187 def get_context_data(self, **kwargs):
188 context = super().get_context_data(**kwargs)
189 context["colid"] = self.colid
190 context["helper"] = PtfModalFormHelper
191 return context
193 def get_form_kwargs(self):
194 kwargs = super().get_form_kwargs()
195 kwargs["colid"] = self.colid
196 return kwargs
198 def form_valid(self, form):
199 self.issue = form.cleaned_data["issue"]
200 self.article = form.cleaned_data["article"]
201 return super().form_valid(form)
203 def import_cedrics_article(self, *args, **kwargs):
204 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
205 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
206 )
207 cmd.do()
209 def post(self, request, *args, **kwargs):
210 self.colid = self.kwargs.get("colid", None)
211 issue = request.POST["issue"]
212 self.article_pid = request.POST["article"]
213 self.issue_pid = os.path.basename(os.path.dirname(issue))
215 import_args = [self]
216 import_kwargs = {}
218 try:
219 _, _status, _message = history_views.execute_and_record_func(
220 "import",
221 f"{self.issue_pid} / {self.article_pid}",
222 self.colid,
223 self.import_cedrics_article,
224 "",
225 False,
226 None,
227 None,
228 *import_args,
229 **import_kwargs,
230 )
232 messages.success(
233 self.request, f"L'article {self.article_pid} a été importé avec succès"
234 )
236 except Exception as exception:
237 messages.error(
238 self.request,
239 f"Echec de l'import de l'article {self.article_pid} : {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 response = requests.post(url, headers=header, files=files, verify=False)
1100 status = response.status_code
1102 if 199 < status < 205:
1103 # There is no need to copy files for the test server
1104 # Files were already copied in /mersenne_test_data during the ptf_tools import
1105 # We only need to copy files from /mersenne_test_data to
1106 # /mersenne_prod_data during an upload to prod
1107 if site == "website":
1108 # TODO mettre ici le record doi pour un issue publié
1109 if container.doi:
1110 recordDOI(container)
1112 for art in articles_to_deploy:
1113 # record DOI automatically when deploying in prod
1115 if art.doi and art.allow_crossref():
1116 recordDOI(art)
1118 if colid == "CRBIOL":
1119 recordPubmed(
1120 art, force_update=False, updated_articles=updated_articles
1121 )
1123 if colid == "PCJ":
1124 self.update_pcj_editor(updated_articles)
1126 # Archive the container or the article
1127 if article is None:
1128 archive_resource.delay(
1129 pid,
1130 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1131 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1132 )
1134 else:
1135 archive_resource.delay(
1136 pid,
1137 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1138 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1139 article_doi=article.doi,
1140 )
1141 # cmd = ptf_cmds.archiveIssuePtfCmd({
1142 # "pid": pid,
1143 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1144 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1145 # cmd.set_article(article) # set_article allows archiving only the article
1146 # cmd.do()
1148 elif site == "numdam":
1149 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1150 if colid in settings.NUMDAM_COLLECTIONS:
1151 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1153 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1154 for article in container.article_set.all():
1155 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1157 elif status == 503:
1158 raise ServerUnderMaintenance(response.text)
1159 else:
1160 raise RuntimeError(response.text)
1162 if djvu_exception:
1163 raise djvu_exception
1165 def get(self, request, *args, **kwargs):
1166 pid = self.kwargs.get("pid", None)
1167 colid = self.kwargs.get("colid", None)
1168 site = self.kwargs.get("site", None)
1170 try:
1171 _, status, message = history_views.execute_and_record_func(
1172 "deploy",
1173 pid,
1174 colid,
1175 self.internal_do,
1176 f"Article successfully transferred to {site}",
1177 )
1178 except Timeout as exception:
1179 return HttpResponse(exception, status=408)
1180 except Exception as exception:
1181 return HttpResponseServerError(exception)
1183 data = {"message": message, "status": status}
1184 return JsonResponse(data)
1186 def update_pcj_editor(updated_articles):
1187 for article in updated_articles:
1188 data = {
1189 "date_published": article.date_published.strftime("%Y-%m-%d"),
1190 "article_number": article.article_number,
1191 }
1192 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1193 requests.post(url, json=data, verify=False)
1196class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1197 article = None
1199 def internal_do(self, *args, **kwargs):
1200 lang = self.kwargs.get("lang", None)
1202 translation = None
1203 for trans_article in self.article.translations.all():
1204 if trans_article.lang == lang:
1205 translation = trans_article
1207 if translation is None:
1208 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1210 collection = self.article.get_top_collection()
1211 colid = collection.pid
1212 container = self.article.my_container
1214 if translation.date_published is None:
1215 # Add date posted
1216 cmd = ptf_cmds.publishResourcePtfCmd()
1217 cmd.set_resource(translation)
1218 cmd.do()
1219 # updated_articles = cmd.do()
1221 # # Recompile PDF to add the date posted
1222 # try:
1223 # create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1224 # except Exception:
1225 # raise PDFException(
1226 # "Unable to compile the article PDF. Please contact the centre Mersenne"
1227 # )
1229 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1230 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1231 resolver.copy_binary_files(
1232 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1233 )
1235 # Deploy in prod
1236 xml = ptf_cmds.exportPtfCmd(
1237 {
1238 "pid": self.article.pid,
1239 "with_djvu": False,
1240 "article_standalone": True,
1241 "collection_pid": colid,
1242 "export_to_website": True,
1243 "export_folder": settings.LOG_DIR,
1244 }
1245 ).do()
1246 xml_file = io.StringIO(xml)
1247 files = {"xml": xml_file}
1249 server_url = collection.website()
1250 if not server_url:
1251 raise RuntimeError("The collection has no website")
1252 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1253 header = {}
1255 try:
1256 response = requests.post(
1257 url, headers=header, files=files, verify=False
1258 ) # verify: ignore TLS certificate
1259 status = response.status_code
1260 except requests.exceptions.ConnectionError:
1261 raise ServerUnderMaintenance(
1262 "The journal is under maintenance. Please try again later."
1263 )
1265 # Register translation in Crossref
1266 if 199 < status < 205 and self.article.allow_crossref():
1267 try:
1268 recordDOI(translation)
1269 except Exception:
1270 raise DOIException(
1271 "Error while recording the DOI. Please contact the centre Mersenne"
1272 )
1274 def get(self, request, *args, **kwargs):
1275 doi = kwargs.get("doi", None)
1276 self.article = model_helpers.get_article_by_doi(doi)
1277 if self.article is None:
1278 raise Http404(f"{doi} does not exist")
1280 try:
1281 _, status, message = history_views.execute_and_record_func(
1282 "deploy",
1283 self.article.pid,
1284 self.article.get_top_collection().pid,
1285 self.internal_do,
1286 "website",
1287 )
1288 except Timeout as exception:
1289 return HttpResponse(exception, status=408)
1290 except Exception as exception:
1291 logger.exception(f"Failed to post translation for {self.article.pid}")
1292 return HttpResponseServerError(exception)
1294 data = {"message": message, "status": status}
1295 return JsonResponse(data)
1298class DeleteJatsIssueAPIView(View):
1299 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1300 def get(self, request, *args, **kwargs):
1301 pid = self.kwargs.get("pid", None)
1302 colid = self.kwargs.get("colid", None)
1303 site = self.kwargs.get("site", None)
1304 message = f"The issue has correctly been removed from {site}"
1305 status = 200
1307 issue = model_helpers.get_container(pid)
1308 if not issue:
1309 raise Http404(f"{pid} does not exist")
1310 try:
1311 if check_lock():
1312 message = "Trammel is under maintenance. Please try again later."
1313 messages.error(self.request, message)
1314 raise ServerUnderMaintenance(message)
1316 mersenneSite = model_helpers.get_site_mersenne(colid)
1318 if site == "ptf_tools":
1319 if issue.is_deployed(mersenneSite):
1320 issue.undeploy(mersenneSite)
1321 for article in issue.article_set.all():
1322 article.undeploy(mersenneSite)
1324 p = model_helpers.get_provider("mathdoc-id")
1326 cmd = ptf_cmds.addContainerPtfCmd(
1327 {
1328 "pid": issue.pid,
1329 "ctype": "issue",
1330 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1331 }
1332 )
1333 cmd.set_provider(p)
1334 cmd.add_collection(issue.get_collection())
1335 cmd.set_object_to_be_deleted(issue)
1336 cmd.undo()
1338 else:
1339 if site == "numdam":
1340 server_url = settings.NUMDAM_PRE_URL
1341 else:
1342 collection = issue.get_collection()
1343 server_url = getattr(collection, site)()
1345 if not server_url:
1346 raise Http404(f"The collection has no {site}")
1347 else:
1348 # Delete on site
1349 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1350 response = requests.delete(url, verify=False)
1351 status = response.status_code
1353 if status == 404:
1354 raise Http404(
1355 "Issue not found on {site}. Please make sure the article exists."
1356 )
1357 elif status > 204:
1358 raise HTTPError(
1359 response.text.encode("utf8")[:1000],
1360 response=response,
1361 )
1362 else:
1363 status = 200
1365 # Unpublish issue in collection site (site_register.json)
1366 if site == "website" and issue.is_deployed(mersenneSite):
1367 issue.undeploy(mersenneSite)
1368 for article in issue.article_set.all():
1369 article.undeploy(mersenneSite)
1370 # Delete article binary files
1371 folder = article.get_relative_folder()
1372 resolver.delete_object_folder(
1373 folder,
1374 to_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1375 )
1376 # Delete issue binary files
1377 folder = issue.get_relative_folder()
1378 resolver.delete_object_folder(
1379 folder, to_folder=settings.MERSENNE_PROD_DATA_FOLDER
1380 )
1382 except ValueError as exception:
1383 if site in {"ptf_tools", "numdam", "website", "test_website"}:
1384 raise
1385 message, status = str(exception), 400
1386 except Timeout:
1387 message = f"{site} is under maintenance. Please try again later."
1388 status = 503
1389 except Http404 as exception:
1390 message, status = str(exception), 404
1391 except HTTPError as exception:
1392 message, status = str(exception), 502
1393 except ServerUnderMaintenance as exception:
1394 message, status = str(exception), 503
1395 except requests.exceptions.ConnectionError:
1396 message, status = (
1397 f"{site} is under maintenance. Please try again later.",
1398 503,
1399 )
1400 except Exception as exception:
1401 logger.exception("Unexpected error when deleting article %s from %s", pid, site)
1402 message, status = str(exception), 500
1404 return JsonResponse({"message": message, "status": status}, status=status)
1407class ArchiveIssueAPIView(View):
1408 def get(self, request, *args, **kwargs):
1409 try:
1410 pid = kwargs["pid"]
1411 colid = kwargs["colid"]
1412 except IndexError:
1413 raise Http404
1415 try:
1416 cmd = ptf_cmds.archiveIssuePtfCmd(
1417 {
1418 "pid": pid,
1419 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1420 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1421 "needs_publication_date": True,
1422 }
1423 )
1424 _result, _status, message = history_views.execute_and_record_func(
1425 "archive", pid, colid, cmd.do
1426 )
1427 except Exception as exception:
1428 return HttpResponseServerError(exception)
1430 data = {"message": message, "status": 200}
1431 return JsonResponse(data)
1434class CreateDjvuAPIView(View):
1435 def internal_do(self, *args, **kwargs):
1436 pid = self.kwargs.get("pid", None)
1438 resource = model_helpers.get_resource(pid)
1439 cmd = ptf_cmds.addDjvuPtfCmd()
1440 cmd.set_resource(resource)
1441 cmd.do()
1443 def get(self, request, *args, **kwargs):
1444 pid = self.kwargs.get("pid", None)
1445 colid = pid.split("_")[0]
1447 try:
1448 _, status, message = history_views.execute_and_record_func(
1449 "numdam", pid, colid, self.internal_do
1450 )
1451 except Exception as exception:
1452 return HttpResponseServerError(exception)
1454 data = {"message": message, "status": status}
1455 return JsonResponse(data)
1458class PTFToolsHomeView(LoginRequiredMixin, View):
1459 """
1460 Home Page.
1461 - Admin & staff -> Render blank home.html
1462 - User with unique authorized collection -> Redirect to collection details page
1463 - User with multiple authorized collections -> Render home.html with data
1464 - Comment moderator -> Comments dashboard
1465 - Others -> 404 response
1466 """
1468 def get(self, request, *args, **kwargs) -> HttpResponse:
1469 # Staff or user with authorized collections
1470 if request.user.is_staff or request.user.is_superuser:
1471 return render(request, "home.html")
1473 colids = get_authorized_collections(request.user)
1474 is_mod = is_comment_moderator(request.user)
1476 # The user has no rights
1477 if not (colids or is_mod):
1478 raise Http404("No collections associated with your account.")
1479 # Comment moderator only
1480 elif not colids:
1481 return HttpResponseRedirect(reverse("comment_list"))
1483 # User with unique collection -> Redirect to collection detail page
1484 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1485 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1487 # User with multiple authorized collections - Special home
1488 context = {}
1489 context["overview"] = True
1491 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1492 all_collections = {c["pid"]: c for c in all_collections}
1494 # Comments summary
1495 try:
1496 error, comments_data = get_comments_for_home(request.user)
1497 except AttributeError:
1498 error, comments_data = True, {}
1500 context["comment_server_ok"] = False
1502 if not error:
1503 context["comment_server_ok"] = True
1504 if comments_data:
1505 for col_id, comment_nb in comments_data.items():
1506 if col_id.upper() in all_collections: 1506 ↛ 1505line 1506 didn't jump to line 1505 because the condition on line 1506 was always true
1507 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1509 # TODO: Translations summary
1510 context["translation_server_ok"] = False
1512 # Sort the collections according to the number of pending comments
1513 context["collections"] = sorted(
1514 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1515 )
1517 return render(request, "home.html", context)
1520class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1521 columns = 5
1523 def get_common_context_data(self, **kwargs):
1524 context = super().get_context_data(**kwargs)
1525 now = timezone.now()
1526 curyear = now.year
1527 years = range(curyear - self.columns + 1, curyear + 1)
1529 context["collections"] = settings.MERSENNE_COLLECTIONS
1530 context["containers_to_be_published"] = []
1531 context["last_col_events"] = []
1533 event = get_history_last_event_by("clockss", "ALL")
1534 clockss_gap = get_gap(now, event)
1536 context["years"] = years
1537 context["clockss_gap"] = clockss_gap
1539 return context
1541 def calculate_articles_and_pages(self, pid, years):
1542 data_by_year = []
1543 total_articles = [0] * len(years)
1544 total_pages = [0] * len(years)
1546 for year in years:
1547 articles = self.get_articles_for_year(pid, year)
1548 articles_count = articles.count()
1549 page_count = sum(article.get_article_page_count() for article in articles)
1551 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1552 total_articles[year - years[0]] += articles_count
1553 total_pages[year - years[0]] += page_count
1555 return data_by_year, total_articles, total_pages
1557 def get_articles_for_year(self, pid, year):
1558 return Article.objects.filter(
1559 Q(my_container__my_collection__pid=pid)
1560 & (
1561 Q(date_published__year=year, date_online_first__isnull=True)
1562 | Q(date_online_first__year=year)
1563 )
1564 ).prefetch_related("resourcecount_set")
1567class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1568 template_name = "dashboard/published_articles.html"
1570 def get_context_data(self, **kwargs):
1571 context = self.get_common_context_data(**kwargs)
1572 years = context["years"]
1574 published_articles = []
1575 total_published_articles = [
1576 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1577 ]
1579 for pid in settings.MERSENNE_COLLECTIONS:
1580 if pid != "MERSENNE":
1581 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1582 pid, years
1583 )
1584 published_articles.append({"pid": pid, "years": articles_data})
1586 for i, year in enumerate(years):
1587 total_published_articles[i]["total_articles"] += total_articles[i]
1588 total_published_articles[i]["total_pages"] += total_pages[i]
1590 context["published_articles"] = published_articles
1591 context["total_published_articles"] = total_published_articles
1593 return context
1596class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1597 template_name = "dashboard/created_volumes.html"
1599 def get_context_data(self, **kwargs):
1600 context = self.get_common_context_data(**kwargs)
1601 years = context["years"]
1603 created_volumes = []
1604 total_created_volumes = [
1605 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1606 ]
1608 for pid in settings.MERSENNE_COLLECTIONS:
1609 if pid != "MERSENNE":
1610 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1611 pid, years
1612 )
1613 created_volumes.append({"pid": pid, "years": volumes_data})
1615 for i, _ in enumerate(years):
1616 total_created_volumes[i]["total_articles"] += total_articles[i]
1617 total_created_volumes[i]["total_pages"] += total_pages[i]
1619 context["created_volumes"] = created_volumes
1620 context["total_created_volumes"] = total_created_volumes
1622 return context
1624 def calculate_volumes_and_pages(self, pid, years):
1625 data_by_year = []
1626 total_articles = [0] * len(years)
1627 total_pages = [0] * len(years)
1629 for year in years:
1630 issues = Container.objects.filter(my_collection__pid=pid, fyear=year)
1631 articles_count = 0
1632 page_count = 0
1634 for issue in issues:
1635 articles = issue.article_set.filter(
1636 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1637 ).prefetch_related("resourcecount_set")
1639 articles_count += articles.count()
1640 page_count += sum(article.get_article_page_count() for article in articles)
1642 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1643 total_articles[year - years[0]] += articles_count
1644 total_pages[year - years[0]] += page_count
1646 return data_by_year, total_articles, total_pages
1649class ReferencingChoice(View):
1650 def post(self, request, *args, **kwargs):
1651 if request.POST.get("optSite") == "ads":
1652 return redirect(
1653 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1654 )
1655 elif request.POST.get("optSite") == "wos":
1656 comp = ReferencingCheckerWos()
1657 journal = comp.make_journal(request.POST.get("selectCol"))
1658 if journal is None:
1659 return render(
1660 request,
1661 "dashboard/referencing.html",
1662 {
1663 "error": "Collection not found",
1664 "colid": request.POST.get("selectCol"),
1665 "optSite": request.POST.get("optSite"),
1666 },
1667 )
1668 return render(
1669 request,
1670 "dashboard/referencing.html",
1671 {
1672 "journal": journal,
1673 "colid": request.POST.get("selectCol"),
1674 "optSite": request.POST.get("optSite"),
1675 },
1676 )
1679class ReferencingWosFileView(View):
1680 template_name = "dashboard/referencing.html"
1682 def post(self, request, *args, **kwargs):
1683 colid = request.POST["colid"]
1684 if request.FILES.get("risfile") is None:
1685 message = "No file uploaded"
1686 return render(
1687 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1688 )
1689 uploaded_file = request.FILES["risfile"]
1690 comp = ReferencingCheckerWos()
1691 journal = comp.check_references(colid, uploaded_file)
1692 return render(request, self.template_name, {"journal": journal})
1695class ReferencingDashboardView(BaseMersenneDashboardView):
1696 template_name = "dashboard/referencing.html"
1698 def get(self, request, *args, **kwargs):
1699 colid = self.kwargs.get("colid", None)
1700 comp = ReferencingCheckerAds()
1701 journal = comp.check_references(colid)
1702 return render(request, self.template_name, {"journal": journal})
1705class BaseCollectionView(TemplateView):
1706 def get_context_data(self, **kwargs):
1707 context = super().get_context_data(**kwargs)
1708 aid = context.get("aid")
1709 year = context.get("year")
1711 if aid and year:
1712 context["collection"] = self.get_collection(aid, year)
1714 return context
1716 def get_collection(self, aid, year):
1717 """Method to be overridden by subclasses to fetch the appropriate collection"""
1718 raise NotImplementedError("Subclasses must implement get_collection method")
1721class ArticleListView(BaseCollectionView):
1722 template_name = "collection-list.html"
1724 def get_collection(self, aid, year):
1725 return Article.objects.filter(
1726 Q(my_container__my_collection__pid=aid)
1727 & (
1728 Q(date_published__year=year, date_online_first__isnull=True)
1729 | Q(date_online_first__year=year)
1730 )
1731 ).prefetch_related("resourcecount_set")
1734class VolumeListView(BaseCollectionView):
1735 template_name = "collection-list.html"
1737 def get_collection(self, aid, year):
1738 return Article.objects.filter(
1739 Q(my_container__my_collection__pid=aid, my_container__fyear=year)
1740 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1741 ).prefetch_related("resourcecount_set")
1744class DOAJResourceRegisterView(View):
1745 def get(self, request, *args, **kwargs):
1746 pid = kwargs.get("pid", None)
1747 resource = model_helpers.get_resource(pid)
1748 if resource is None:
1749 raise Http404
1750 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1751 resource.colid, None
1752 ):
1753 raise RuntimeError("Pending publications should not be deployed")
1755 try:
1756 data = {}
1757 doaj_meta, response = doaj_pid_register(pid)
1758 if response is None:
1759 return HttpResponse(status=204)
1760 elif doaj_meta and 200 <= response.status_code <= 299:
1761 data.update(doaj_meta)
1762 else:
1763 return HttpResponse(status=response.status_code, reason=response.text)
1764 except Timeout as exception:
1765 return HttpResponse(exception, status=408)
1766 except Exception as exception:
1767 return HttpResponseServerError(exception)
1768 return JsonResponse(data)
1771class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View):
1772 """
1773 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml
1774 """
1776 def get(self, request, *args, **kwargs):
1777 pid = kwargs.get("pid")
1778 if not pid:
1779 raise Http404("Missing pid")
1781 article = Article.objects.filter(pid=pid).first()
1782 if not article:
1783 raise Http404(f"Article not found: {pid}")
1785 colid = article.get_collection().pid
1786 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
1787 return JsonResponse(
1788 {"status": 403, "message": f"Tex conversions are disabled in {colid}"}
1789 )
1791 if is_tex_conversion_locked(pid):
1792 logger.warning("Conversion rejected (lock exists) for %s", pid)
1793 return JsonResponse(
1794 {"status": 409, "message": f"A conversion is already running for {pid}"}
1795 )
1797 logger.info("No lock → scheduling conversion for %s", pid)
1799 try:
1800 convert_article_tex.delay(pid=pid, user_pk=request.user.pk)
1801 except Exception:
1802 logger.exception("Failed to enqueue task for %s", pid)
1803 release_tex_conversion_lock(pid)
1804 raise
1806 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"})
1809class CROSSREFResourceRegisterView(View):
1810 def get(self, request, *args, **kwargs):
1811 pid = kwargs.get("pid", None)
1812 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1813 force = kwargs.get("force", None)
1814 if not request.user.is_superuser:
1815 force = None
1817 resource = model_helpers.get_resource(pid)
1818 if resource is None:
1819 raise Http404
1821 resource = resource.cast()
1822 meth = getattr(self, "recordDOI" + resource.classname)
1823 try:
1824 data = meth(resource, force)
1825 except Timeout as exception:
1826 return HttpResponse(exception, status=408)
1827 except Exception as exception:
1828 return HttpResponseServerError(exception)
1829 return JsonResponse(data)
1831 def recordDOIArticle(self, article: "Article", force=None):
1832 result = {"status": 404}
1833 if (
1834 article.doi
1835 and not article.do_not_publish
1836 and (article.date_published or article.date_online_first or force == "force")
1837 ):
1838 if article.my_container.fyear == 0:
1839 article.my_container.fyear = datetime.now().year
1840 result = recordDOI(article)
1841 return result
1843 def recordDOICollection(self, collection, force=None):
1844 return recordDOI(collection)
1846 def recordDOIContainer(self, container, force=None):
1847 data = {"status": 200, "message": "All DOI successfully checked"}
1849 if container.ctype == "issue":
1850 if container.doi:
1851 result = recordDOI(container)
1852 if result["status"] != 200:
1853 return result
1854 if force == "force":
1855 articles = container.article_set.exclude(
1856 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1857 )
1858 else:
1859 articles = container.article_set.exclude(
1860 doi__isnull=True,
1861 do_not_publish=True,
1862 date_published__isnull=True,
1863 date_online_first__isnull=True,
1864 )
1866 for article in articles:
1867 result = self.recordDOIArticle(article, force)
1868 if result["status"] != 200:
1869 data = result
1870 else:
1871 return recordDOI(container)
1872 return data
1875class CROSSREFResourceCheckStatusView(View):
1876 def get(self, request, *args, **kwargs):
1877 pid = kwargs.get("pid", None)
1878 resource = model_helpers.get_resource(pid)
1879 if resource is None:
1880 raise Http404
1881 resource = resource.cast()
1882 meth = getattr(self, "checkDOI" + resource.classname)
1883 try:
1884 meth(resource)
1885 except Timeout as exception:
1886 return HttpResponse(exception, status=408)
1887 except Exception as exception:
1888 return HttpResponseServerError(exception)
1890 data = {"status": 200, "message": "DOI successfully checked"}
1891 return JsonResponse(data)
1893 def checkDOIArticle(self, article: "Article"):
1894 if article.my_container.fyear == 0:
1895 article.my_container.fyear = datetime.now().year
1896 checkDOI(article)
1898 def checkDOICollection(self, collection):
1899 checkDOI(collection)
1901 def checkDOIContainer(self, container):
1902 if container.doi is not None:
1903 checkDOI(container)
1904 for article in container.article_set.all():
1905 self.checkDOIArticle(article)
1908class CROSSREFResourcePendingPublicationRegisterView(View):
1909 def get(self, request, *args, **kwargs):
1910 pid = kwargs.get("pid", None)
1911 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1913 resource = model_helpers.get_resource(pid)
1914 if resource is None:
1915 raise Http404
1917 resource = resource.cast()
1918 meth = getattr(self, "recordPendingPublication" + resource.classname)
1919 try:
1920 data = meth(resource)
1921 except Timeout as exception:
1922 return HttpResponse(exception, status=408)
1923 except Exception as exception:
1924 return HttpResponseServerError(exception)
1925 return JsonResponse(data)
1927 def recordPendingPublicationArticle(self, article):
1928 result = {"status": 404}
1929 if article.doi and not article.date_published and not article.date_online_first:
1930 if article.my_container.fyear is None or article.my_container.fyear == "0":
1931 article.my_container.fyear = datetime.now().year
1932 result = recordPendingPublication(article)
1933 return result
1936class RegisterPubmedFormView(FormView):
1937 template_name = "record_pubmed_dialog.html"
1938 form_class = RegisterPubmedForm
1940 def get_context_data(self, **kwargs):
1941 context = super().get_context_data(**kwargs)
1942 context["pid"] = self.kwargs["pid"]
1943 context["helper"] = PtfLargeModalFormHelper
1944 return context
1947class RegisterPubmedView(View):
1948 def get(self, request, *args, **kwargs):
1949 pid = kwargs.get("pid", None)
1950 update_article = self.request.GET.get("update_article", "on") == "on"
1952 article = model_helpers.get_article(pid)
1953 if article is None:
1954 raise Http404
1955 try:
1956 recordPubmed(article, update_article)
1957 except Exception as exception:
1958 messages.error("Unable to register the article in PubMed")
1959 return HttpResponseServerError(exception)
1961 return HttpResponseRedirect(
1962 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1963 )
1966class PTFToolsArticleView(ArticleView):
1967 def get_context_data(self, **kwargs):
1968 context = super().get_context_data(**kwargs)
1970 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(self.obj)
1972 qs = self.obj.get_top_collection().extlink_set.filter(rel="test_website")
1973 if qs:
1974 test_website = qs.first().location
1975 context["test_website"] = test_website
1976 qs = self.obj.get_top_collection().extlink_set.filter(rel="website")
1977 if qs:
1978 prod_location = qs.first().location
1979 context["prod_website"] = prod_location
1981 return context
1984ItemViewClassFactory.views["article"] = PTFToolsArticleView
1987class PTFToolsContainerView(TemplateView):
1988 template_name = ""
1990 def get_context_data(self, **kwargs):
1991 context = super().get_context_data(**kwargs)
1993 container = model_helpers.get_container(self.kwargs.get("pid"))
1994 if container is None:
1995 raise Http404
1996 citing_articles = container.citations()
1997 source = self.request.GET.get("source", None)
1998 if container.ctype.startswith("book"):
1999 book_parts = (
2000 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
2001 )
2002 references = False
2003 if container.ctype == "book-monograph":
2004 # on regarde si il y a au moins une bibliographie
2005 for art in container.article_set.all():
2006 if art.bibitem_set.count() > 0:
2007 references = True
2008 context.update(
2009 {
2010 "book": container,
2011 "book_parts": list(book_parts),
2012 "source": source,
2013 "citing_articles": citing_articles,
2014 "references": references,
2015 "test_website": container.get_top_collection()
2016 .extlink_set.get(rel="test_website")
2017 .location,
2018 "prod_website": container.get_top_collection()
2019 .extlink_set.get(rel="website")
2020 .location,
2021 }
2022 )
2023 self.template_name = "book-toc.html"
2024 else:
2025 articles = container.article_set.all().order_by("seq")
2026 for article in articles:
2027 try:
2028 last_match = (
2029 history_models.HistoryEvent.objects.filter(
2030 pid=article.pid,
2031 type="matching",
2032 )
2033 .only("created_on")
2034 .latest("created_on")
2035 )
2036 except history_models.HistoryEvent.DoesNotExist as _:
2037 article.last_match = None
2038 else:
2039 article.last_match = last_match.created_on
2041 # article1 = articles.first()
2042 # date = article1.deployed_date()
2043 # TODO next_issue, previous_issue
2045 # check DOI est maintenant une commande à part
2046 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
2047 # articlesWithStatus = []
2048 # for article in articles:
2049 # checkDOIExistence(article)
2050 # articlesWithStatus.append(article)
2052 test_location = prod_location = ""
2053 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
2054 if qs:
2055 test_location = qs.first().location
2056 qs = container.get_top_collection().extlink_set.filter(rel="website")
2057 if qs:
2058 prod_location = qs.first().location
2059 context.update(
2060 {
2061 "issue": container,
2062 "articles": articles,
2063 "source": source,
2064 "citing_articles": citing_articles,
2065 "test_website": test_location,
2066 "prod_website": prod_location,
2067 }
2068 )
2070 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
2071 context["is_issue_pending_publication"] = True
2072 if container.get_top_collection().pid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
2073 context["is_excluded_from_tex_conversion"] = True
2074 self.template_name = "issue-items.html"
2076 context["allow_crossref"] = container.allow_crossref()
2077 context["coltype"] = container.my_collection.coltype
2078 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(container)
2079 return context
2082class ExtLinkInline(InlineFormSetFactory):
2083 model = ExtLink
2084 form_class = ExtLinkForm
2085 factory_kwargs = {"extra": 0}
2088class ResourceIdInline(InlineFormSetFactory):
2089 model = ResourceId
2090 form_class = ResourceIdForm
2091 factory_kwargs = {"extra": 0}
2094class IssueDetailAPIView(View):
2095 def get(self, request, *args, **kwargs):
2096 issue = get_object_or_404(Container, pid=kwargs["pid"])
2097 deployed_date = issue.deployed_date()
2098 result = {
2099 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2100 if deployed_date
2101 else None,
2102 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2103 "all_doi_are_registered": issue.all_doi_are_registered(),
2104 "registered_in_doaj": issue.registered_in_doaj(),
2105 "doi": issue.my_collection.doi,
2106 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2107 }
2108 try:
2109 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2110 except history_models.HistoryEvent.DoesNotExist as _:
2111 pass
2112 else:
2113 result["latest"] = latest.message
2114 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2115 "%Y-%m-%d %H:%M"
2116 )
2118 result["latest_type"] = latest.type.capitalize()
2119 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2120 try:
2121 result[event_type] = timezone.localtime(
2122 history_models.HistoryEvent.objects.filter(
2123 type=event_type,
2124 status="OK",
2125 pid__startswith=issue.pid,
2126 )
2127 .latest("created_on")
2128 .created_on
2129 ).strftime("%Y-%m-%d %H:%M")
2130 except history_models.HistoryEvent.DoesNotExist as _:
2131 result[event_type] = ""
2132 return JsonResponse(result)
2135class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2136 model = Collection
2137 form_class = CollectionForm
2138 inlines = [ResourceIdInline, ExtLinkInline]
2139 inlines_names = ["resource_ids_form", "ext_links_form"]
2141 def get_context_data(self, **kwargs):
2142 context = super().get_context_data(**kwargs)
2143 context["helper"] = PtfFormHelper
2144 context["formset_helper"] = FormSetHelper
2145 return context
2147 def add_description(self, collection, description, lang, seq):
2148 if description:
2149 la = Abstract(
2150 resource=collection,
2151 tag="description",
2152 lang=lang,
2153 seq=seq,
2154 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2155 value_html=description,
2156 value_tex=description,
2157 )
2158 la.save()
2160 def form_valid(self, form):
2161 if form.instance.abbrev:
2162 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2163 else:
2164 form.instance.title_xml = (
2165 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2166 )
2168 form.instance.title_html = form.instance.title_tex
2169 form.instance.title_sort = form.instance.title_tex
2170 result = super().form_valid(form)
2172 collection = self.object
2173 collection.abstract_set.all().delete()
2175 seq = 1
2176 description = form.cleaned_data["description_en"]
2177 if description:
2178 self.add_description(collection, description, "en", seq)
2179 seq += 1
2180 description = form.cleaned_data["description_fr"]
2181 if description:
2182 self.add_description(collection, description, "fr", seq)
2184 return result
2186 def get_success_url(self):
2187 messages.success(
2188 self.request, f'The collection "{self.object.pid}" has been successfully updated'
2189 )
2190 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2193class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2194 """
2195 Warning : Not yet finished
2196 Automatic site membership creation is still missing
2197 """
2200class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2201 slug_field = "pid"
2202 slug_url_kwarg = "pid"
2205def suggest_load_journal_dois(colid):
2206 articles = (
2207 Article.objects.filter(my_container__my_collection__pid=colid)
2208 .filter(doi__isnull=False)
2209 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2210 .values_list("doi", flat=True)
2211 )
2213 try:
2214 articles = sorted(
2215 articles,
2216 key=lambda d: (
2217 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2218 int(re.search(r".(\d+)$", d).group(1)),
2219 ),
2220 )
2221 except: # noqa: E722 (we'll look later)
2222 pass
2223 return [f'<option value="{doi}">' for doi in articles]
2226def get_context_with_volumes(journal):
2227 result = model_helpers.get_volumes_in_collection(journal)
2228 volume_count = result["volume_count"]
2229 collections = []
2230 for ancestor in journal.ancestors.all():
2231 item = model_helpers.get_volumes_in_collection(ancestor)
2232 volume_count = max(0, volume_count)
2233 item.update({"journal": ancestor})
2234 collections.append(item)
2236 # add the parent collection to its children list and sort it by date
2237 result.update({"journal": journal})
2238 collections.append(result)
2240 collections = [c for c in collections if c["sorted_issues"]]
2241 collections.sort(
2242 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2243 reverse=True,
2244 )
2246 context = {
2247 "journal": journal,
2248 "sorted_issues": result["sorted_issues"],
2249 "volume_count": volume_count,
2250 "max_width": result["max_width"],
2251 "collections": collections,
2252 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2253 }
2254 return context
2257class CollectionDetail(
2258 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2259):
2260 model = Collection
2261 slug_field = "pid"
2262 slug_url_kwarg = "pid"
2263 template_name = "ptf/collection_detail.html"
2265 def test_func(self):
2266 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2268 def get(self, request, *args, **kwargs):
2269 self.object = self.get_object(queryset=Collection.objects.all())
2270 return super().get(request, *args, **kwargs)
2272 def get_context_data(self, **kwargs):
2273 context = super().get_context_data(**kwargs)
2274 context["object_list"] = context["object_list"].filter(
2275 Q(ctype="issue") | Q(ctype="book-lecture-notes") | Q(ctype="book-monograph")
2276 )
2277 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2278 context.update(get_context_with_volumes(self.object))
2280 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2281 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2282 context["issue_to_appear"] = Container.objects.filter(
2283 pid=context["issue_to_appear_pid"]
2284 ).exists()
2285 try:
2286 latest_error = history_models.HistoryEvent.objects.filter(
2287 status="ERROR", col=self.object
2288 ).latest("created_on")
2289 except history_models.HistoryEvent.DoesNotExist as _:
2290 pass
2291 else:
2292 message = latest_error.message
2293 if message:
2294 i = message.find(" - ")
2295 latest_exception = message[:i]
2296 latest_error_message = message[i + 3 :]
2297 context["latest_exception"] = latest_exception
2298 context["latest_exception_date"] = latest_error.created_on
2299 context["latest_exception_type"] = latest_error.type
2300 context["latest_error_message"] = latest_error_message
2302 archive_in_error = history_models.HistoryEvent.objects.filter(
2303 status="ERROR", col=self.object, type="archive"
2304 ).exists()
2306 context["archive_in_error"] = archive_in_error
2308 return context
2310 def get_queryset(self):
2311 query = self.object.content.all()
2313 for ancestor in self.object.ancestors.all():
2314 query |= ancestor.content.all()
2316 return query.order_by("-fyear", "-vseries", "-volume", "-volume_int", "-number_int")
2319class ContainerEditView(FormView):
2320 template_name = "container_form.html"
2321 form_class = ContainerForm
2323 def get_success_url(self):
2324 if self.kwargs["pid"]:
2325 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2326 return reverse("mersenne_dashboard/published_articles")
2328 def set_success_message(self): # pylint: disable=no-self-use
2329 messages.success(self.request, "Booklet updated")
2331 def get_form_kwargs(self):
2332 kwargs = super().get_form_kwargs()
2333 if "pid" not in self.kwargs:
2334 self.kwargs["pid"] = None
2335 if "colid" not in self.kwargs:
2336 self.kwargs["colid"] = None
2337 if "data" in kwargs and "colid" in kwargs["data"]:
2338 # colid is passed as a hidden param in the form.
2339 # It is used when you submit a new container
2340 self.kwargs["colid"] = kwargs["data"]["colid"]
2342 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2343 self.kwargs["pid"]
2344 )
2345 return kwargs
2347 def get_context_data(self, **kwargs):
2348 context = super().get_context_data(**kwargs)
2350 context["pid"] = self.kwargs["pid"]
2351 context["colid"] = self.kwargs["colid"]
2352 context["container"] = self.kwargs["container"]
2354 context["edit_container"] = context["pid"] is not None
2355 context["name"] = resolve(self.request.path_info).url_name
2357 return context
2359 def form_valid(self, form):
2360 new_pid = form.cleaned_data.get("pid")
2361 new_title = form.cleaned_data.get("title")
2362 new_trans_title = form.cleaned_data.get("trans_title")
2363 new_publisher = form.cleaned_data.get("publisher")
2364 new_year = form.cleaned_data.get("year")
2365 new_volume = form.cleaned_data.get("volume")
2366 new_number = form.cleaned_data.get("number")
2368 collection = None
2369 issue = self.kwargs["container"]
2370 if issue is not None:
2371 collection = issue.my_collection
2372 elif self.kwargs["colid"] is not None:
2373 if "CR" in self.kwargs["colid"]:
2374 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2375 else:
2376 collection = model_helpers.get_collection(self.kwargs["colid"])
2378 if collection is None:
2379 raise ValueError("Collection for " + new_pid + " does not exist")
2381 # Icon
2382 new_icon_location = ""
2383 if "icon" in self.request.FILES:
2384 filename = os.path.basename(self.request.FILES["icon"].name)
2385 file_extension = filename.split(".")[1]
2387 icon_filename = resolver.get_disk_location(
2388 settings.MERSENNE_TEST_DATA_FOLDER,
2389 collection.pid,
2390 file_extension,
2391 new_pid,
2392 None,
2393 True,
2394 )
2396 with open(icon_filename, "wb+") as destination:
2397 destination.writelines(self.request.FILES["icon"].chunks())
2399 folder = resolver.get_relative_folder(collection.pid, new_pid)
2400 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2401 name = resolve(self.request.path_info).url_name
2402 if name == "special_issue_create":
2403 self.kwargs["name"] = name
2404 if self.kwargs["container"]:
2405 # Edit Issue
2406 issue = self.kwargs["container"]
2407 if issue is None:
2408 raise ValueError(self.kwargs["pid"] + " does not exist")
2410 issue.pid = new_pid
2411 issue.title_tex = issue.title_html = new_title
2412 issue.title_xml = build_title_xml(
2413 title=new_title,
2414 lang=issue.lang,
2415 title_type="issue-title",
2416 )
2418 trans_lang = ""
2419 if new_trans_title != "":
2420 trans_lang = "fr" if issue.lang == "en" else "en"
2422 if trans_lang != "" and new_trans_title != "":
2423 title_xml = build_title_xml(
2424 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2425 )
2427 issue.title_set.update_or_create(
2428 lang=trans_lang,
2429 type="main",
2430 defaults={"title_html": new_trans_title, "title_xml": title_xml},
2431 )
2433 issue.fyear = new_year
2434 issue.volume = new_volume
2435 issue.volume_int = make_int(new_volume)
2436 issue.number = new_number
2437 issue.number_int = make_int(new_number)
2438 issue.save()
2439 else:
2440 xissue = create_issuedata()
2442 xissue.ctype = "issue"
2443 xissue.pid = new_pid
2444 xissue.lang = "en"
2445 xissue.title_tex = new_title
2446 xissue.title_html = new_title
2447 xissue.title_xml = build_title_xml(
2448 title=new_title, lang=xissue.lang, title_type="issue-title"
2449 )
2451 if new_trans_title != "":
2452 trans_lang = "fr"
2453 title_xml = build_title_xml(
2454 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2455 )
2456 title = create_titledata(
2457 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2458 )
2459 issue.titles = [title]
2461 xissue.fyear = new_year
2462 xissue.volume = new_volume
2463 xissue.number = new_number
2464 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2466 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2467 cmd.add_collection(collection)
2468 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2469 issue = cmd.do()
2471 self.kwargs["pid"] = new_pid
2473 # Add objects related to the article: contribs, datastream, counts...
2474 params = {
2475 "icon_location": new_icon_location,
2476 }
2477 cmd = ptf_cmds.updateContainerPtfCmd(params)
2478 cmd.set_resource(issue)
2479 cmd.do()
2481 publisher = model_helpers.get_publisher(new_publisher)
2482 if not publisher:
2483 xpub = create_publisherdata()
2484 xpub.name = new_publisher
2485 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2486 issue.my_publisher = publisher
2487 issue.save()
2489 self.set_success_message()
2491 return super().form_valid(form)
2494# class ArticleEditView(FormView):
2495# template_name = 'article_form.html'
2496# form_class = ArticleForm
2497#
2498# def get_success_url(self):
2499# if self.kwargs['pid']:
2500# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2501# return reverse('mersenne_dashboard/published_articles')
2502#
2503# def set_success_message(self): # pylint: disable=no-self-use
2504# messages.success(self.request, "L'article a été modifié")
2505#
2506# def get_form_kwargs(self):
2507# kwargs = super(ArticleEditView, self).get_form_kwargs()
2508#
2509# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2510# # Article creation: pid is None
2511# self.kwargs['pid'] = None
2512# if 'issue_id' not in self.kwargs:
2513# # Article edit: issue_id is not passed
2514# self.kwargs['issue_id'] = None
2515# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2516# # colid is passed as a hidden param in the form.
2517# # It is used when you submit a new container
2518# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2519#
2520# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2521# return kwargs
2522#
2523# def get_context_data(self, **kwargs):
2524# context = super(ArticleEditView, self).get_context_data(**kwargs)
2525#
2526# context['pid'] = self.kwargs['pid']
2527# context['issue_id'] = self.kwargs['issue_id']
2528# context['article'] = self.kwargs['article']
2529#
2530# context['edit_article'] = context['pid'] is not None
2531#
2532# article = context['article']
2533# if article:
2534# context['author_contributions'] = article.get_author_contributions()
2535# context['kwds_fr'] = None
2536# context['kwds_en'] = None
2537# kwd_gps = article.get_non_msc_kwds()
2538# for kwd_gp in kwd_gps:
2539# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2540# if kwd_gp.value_xml:
2541# kwd_ = types.SimpleNamespace()
2542# kwd_.value = kwd_gp.value_tex
2543# context['kwd_unstructured_fr'] = kwd_
2544# context['kwds_fr'] = kwd_gp.kwd_set.all()
2545# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2546# if kwd_gp.value_xml:
2547# kwd_ = types.SimpleNamespace()
2548# kwd_.value = kwd_gp.value_tex
2549# context['kwd_unstructured_en'] = kwd_
2550# context['kwds_en'] = kwd_gp.kwd_set.all()
2551#
2552# # Article creation: init pid
2553# if context['issue_id'] and context['pid'] is None:
2554# issue = model_helpers.get_container(context['issue_id'])
2555# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2556#
2557# return context
2558#
2559# def form_valid(self, form):
2560#
2561# new_pid = form.cleaned_data.get('pid')
2562# new_title = form.cleaned_data.get('title')
2563# new_fpage = form.cleaned_data.get('fpage')
2564# new_lpage = form.cleaned_data.get('lpage')
2565# new_page_range = form.cleaned_data.get('page_range')
2566# new_page_count = form.cleaned_data.get('page_count')
2567# new_coi_statement = form.cleaned_data.get('coi_statement')
2568# new_show_body = form.cleaned_data.get('show_body')
2569# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2570#
2571# # TODO support MathML
2572# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2573# # We need to pass trans_title to get_title_xml
2574# # Meanwhile, ignore new_title_xml
2575# new_title_xml = jats_parser.get_title_xml(new_title)
2576# new_title_html = new_title
2577#
2578# authors_count = int(self.request.POST.get('authors_count', "0"))
2579# i = 1
2580# new_authors = []
2581# old_author_contributions = []
2582# if self.kwargs['article']:
2583# old_author_contributions = self.kwargs['article'].get_author_contributions()
2584#
2585# while authors_count > 0:
2586# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2587#
2588# if prefix is not None:
2589# addresses = []
2590# if len(old_author_contributions) >= i:
2591# old_author_contribution = old_author_contributions[i - 1]
2592# addresses = [contrib_address.address for contrib_address in
2593# old_author_contribution.get_addresses()]
2594#
2595# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2596# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2597# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2598# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2599# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2600# deceased_before_publication = deceased == 'on'
2601# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2602# equal_contrib = equal_contrib == 'on'
2603# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2604# corresponding = corresponding == 'on'
2605# email = self.request.POST.get('email-' + str(i), None)
2606#
2607# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2608# params['deceased_before_publication'] = deceased_before_publication
2609# params['equal_contrib'] = equal_contrib
2610# params['corresponding'] = corresponding
2611# params['addresses'] = addresses
2612# params['email'] = email
2613#
2614# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2615#
2616# new_authors.append(params)
2617#
2618# authors_count -= 1
2619# i += 1
2620#
2621# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2622# i = 1
2623# new_kwds_fr = []
2624# while kwds_fr_count > 0:
2625# value = self.request.POST.get('kwd-fr-' + str(i), None)
2626# new_kwds_fr.append(value)
2627# kwds_fr_count -= 1
2628# i += 1
2629# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2630#
2631# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2632# i = 1
2633# new_kwds_en = []
2634# while kwds_en_count > 0:
2635# value = self.request.POST.get('kwd-en-' + str(i), None)
2636# new_kwds_en.append(value)
2637# kwds_en_count -= 1
2638# i += 1
2639# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2640#
2641# if self.kwargs['article']:
2642# # Edit article
2643# container = self.kwargs['article'].my_container
2644# else:
2645# # New article
2646# container = model_helpers.get_container(self.kwargs['issue_id'])
2647#
2648# if container is None:
2649# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2650#
2651# collection = container.my_collection
2652#
2653# # Copy PDF file & extract full text
2654# body = ''
2655# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2656# collection.pid,
2657# "pdf",
2658# container.pid,
2659# new_pid,
2660# True)
2661# if 'pdf' in self.request.FILES:
2662# with open(pdf_filename, 'wb+') as destination:
2663# for chunk in self.request.FILES['pdf'].chunks():
2664# destination.write(chunk)
2665#
2666# # Extract full text from the PDF
2667# body = utils.pdf_to_text(pdf_filename)
2668#
2669# # Icon
2670# new_icon_location = ''
2671# if 'icon' in self.request.FILES:
2672# filename = os.path.basename(self.request.FILES['icon'].name)
2673# file_extension = filename.split('.')[1]
2674#
2675# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2676# collection.pid,
2677# file_extension,
2678# container.pid,
2679# new_pid,
2680# True)
2681#
2682# with open(icon_filename, 'wb+') as destination:
2683# for chunk in self.request.FILES['icon'].chunks():
2684# destination.write(chunk)
2685#
2686# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2687# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2688#
2689# if self.kwargs['article']:
2690# # Edit article
2691# article = self.kwargs['article']
2692# article.fpage = new_fpage
2693# article.lpage = new_lpage
2694# article.page_range = new_page_range
2695# article.coi_statement = new_coi_statement
2696# article.show_body = new_show_body
2697# article.do_not_publish = new_do_not_publish
2698# article.save()
2699#
2700# else:
2701# # New article
2702# params = {
2703# 'pid': new_pid,
2704# 'title_xml': new_title_xml,
2705# 'title_html': new_title_html,
2706# 'title_tex': new_title,
2707# 'fpage': new_fpage,
2708# 'lpage': new_lpage,
2709# 'page_range': new_page_range,
2710# 'seq': container.article_set.count() + 1,
2711# 'body': body,
2712# 'coi_statement': new_coi_statement,
2713# 'show_body': new_show_body,
2714# 'do_not_publish': new_do_not_publish
2715# }
2716#
2717# xarticle = create_articledata()
2718# xarticle.pid = new_pid
2719# xarticle.title_xml = new_title_xml
2720# xarticle.title_html = new_title_html
2721# xarticle.title_tex = new_title
2722# xarticle.fpage = new_fpage
2723# xarticle.lpage = new_lpage
2724# xarticle.page_range = new_page_range
2725# xarticle.seq = container.article_set.count() + 1
2726# xarticle.body = body
2727# xarticle.coi_statement = new_coi_statement
2728# params['xobj'] = xarticle
2729#
2730# cmd = ptf_cmds.addArticlePtfCmd(params)
2731# cmd.set_container(container)
2732# cmd.add_collection(container.my_collection)
2733# article = cmd.do()
2734#
2735# self.kwargs['pid'] = new_pid
2736#
2737# # Add objects related to the article: contribs, datastream, counts...
2738# params = {
2739# # 'title_xml': new_title_xml,
2740# # 'title_html': new_title_html,
2741# # 'title_tex': new_title,
2742# 'authors': new_authors,
2743# 'page_count': new_page_count,
2744# 'icon_location': new_icon_location,
2745# 'body': body,
2746# 'use_kwds': True,
2747# 'kwds_fr': new_kwds_fr,
2748# 'kwds_en': new_kwds_en,
2749# 'kwd_uns_fr': new_kwd_uns_fr,
2750# 'kwd_uns_en': new_kwd_uns_en
2751# }
2752# cmd = ptf_cmds.updateArticlePtfCmd(params)
2753# cmd.set_article(article)
2754# cmd.do()
2755#
2756# self.set_success_message()
2757#
2758# return super(ArticleEditView, self).form_valid(form)
2761@require_http_methods(["POST"])
2762def do_not_publish_article(request, *args, **kwargs):
2763 next = request.headers.get("referer")
2765 pid = kwargs.get("pid", "")
2767 article = model_helpers.get_article(pid)
2768 if article:
2769 article.do_not_publish = not article.do_not_publish
2770 article.save()
2771 else:
2772 raise Http404
2774 return HttpResponseRedirect(next)
2777@require_http_methods(["POST"])
2778def show_article_body(request, *args, **kwargs):
2779 next = request.headers.get("referer")
2781 pid = kwargs.get("pid", "")
2783 article = model_helpers.get_article(pid)
2784 if article:
2785 article.show_body = not article.show_body
2786 article.save()
2787 else:
2788 raise Http404
2790 return HttpResponseRedirect(next)
2793class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2794 """
2795 API to get/post article metadata
2796 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2797 """
2799 def __init__(self, *args, **kwargs):
2800 """
2801 we define here what fields we want in the form
2802 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2803 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2804 super().__init__(*args, **kwargs)
2805 self.fields_to_update = [
2806 "lang",
2807 "atype",
2808 "contributors",
2809 "abstracts",
2810 "kwds",
2811 "titles",
2812 "title_html",
2813 "title_xml",
2814 "title_tex",
2815 "streams",
2816 "ext_links",
2817 "date_accepted",
2818 "history_dates",
2819 "subjs",
2820 "bibitems",
2821 "references",
2822 ]
2823 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2824 self.additional_fields = [
2825 "doi",
2826 "pid",
2827 "container_pid",
2828 "pdf",
2829 "illustration",
2830 "dates",
2831 "msc_keywords",
2832 ]
2833 self.editorial_tools = [
2834 "translation",
2835 "sidebar",
2836 "lang_selection",
2837 "back_to_article_option",
2838 "msc_keywords",
2839 ]
2840 self.validation = {
2841 "required_checks": [],
2842 "blocking_checks": [],
2843 }
2844 self.article_container_pid = ""
2845 self.back_url = "trammel"
2847 def save_data(self, data_article):
2848 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2849 # The icons are not preserved since we can add/edit/delete them in VueJs
2850 params = {
2851 "pid": data_article.pid,
2852 "export_folder": settings.MERSENNE_TMP_FOLDER,
2853 "export_all": True,
2854 "with_binary_files": False,
2855 }
2856 ptf_cmds.exportExtraDataPtfCmd(params).do()
2858 def restore_data(self, article):
2859 ptf_cmds.importExtraDataPtfCmd(
2860 {
2861 "pid": article.pid,
2862 "import_folder": settings.MERSENNE_TMP_FOLDER,
2863 "import_bibitemid": False,
2864 }
2865 ).do()
2867 @method_decorator(csrf_exempt)
2868 def dispatch(self, request, *args, **kwargs):
2869 user_role = request.user.roles.all()
2870 if user_role:
2871 new_fields_to_update = []
2872 new_additional_fields = []
2873 new_editorial_tools = []
2874 # if a user have > 1 role, fields accessible shall be cumulative
2875 for role in user_role:
2876 role_fields_to_updates, role_additional_fields = role.get_real_fields_to_update()
2877 new_fields_to_update += role_fields_to_updates
2878 new_additional_fields += role_additional_fields
2879 new_editorial_tools += role.editorial_tools
2880 # in case two roles have some fields in common
2881 self.fields_to_update = list(set(new_fields_to_update))
2882 self.additional_fields = list(set(new_additional_fields))
2883 self.editorial_tools = list(set(new_editorial_tools))
2885 self.additional_fields += ["doi", "pid", "container_pid"]
2886 return super().dispatch(request, *args, **kwargs)
2888 def post(self, request, *args, **kwargs):
2889 response = super().post(request, *args, **kwargs)
2890 if response.status_code in [200, 302]: 2890 ↛ 2898line 2890 didn't jump to line 2898 because the condition on line 2890 was always true
2891 return redirect(
2892 "api-edit-article",
2893 colid=kwargs.get("colid", ""),
2894 containerPid=kwargs.get("containerPid"),
2895 doi=kwargs.get("doi", ""),
2896 )
2897 else:
2898 raise Http404
2901class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2902 template_name = "article_form.html"
2904 def get_success_url(self):
2905 if self.kwargs["doi"]:
2906 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2907 return reverse("mersenne_dashboard/published_articles")
2909 def get_context_data(self, **kwargs):
2910 context = super().get_context_data(**kwargs)
2911 if "doi" in self.kwargs:
2912 article = model_helpers.get_article_by_doi(self.kwargs["doi"])
2913 context["article"] = article
2914 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(article)
2915 context["pid"] = context["article"].pid
2917 context["container_pid"] = kwargs.get("container_pid", "")
2918 return context
2921class ArticleDeleteView(View):
2922 def get(self, request, *args, **kwargs):
2923 pid = self.kwargs.get("pid", None)
2924 article = get_object_or_404(Article, pid=pid)
2925 site = self.kwargs.get("site", None)
2926 message = f"Article successfully removed from {site}"
2927 status = 200
2929 try:
2930 if check_lock():
2931 message = "Trammel is under maintenance. Please try again later."
2932 messages.error(self.request, message)
2933 raise ServerUnderMaintenance(message)
2935 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2937 if site == "trammel":
2938 if article.is_deployed(mersenneSite):
2939 article.undeploy(mersenneSite)
2941 cmd = ptf_cmds.addArticlePtfCmd(
2942 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2943 )
2944 cmd.set_container(article.my_container)
2945 cmd.set_object_to_be_deleted(article)
2946 cmd.undo()
2948 elif site in ["website", "test_website"]:
2949 collection = article.get_collection()
2950 server_url = getattr(collection, site)()
2951 if not server_url:
2952 raise Http404(f"The collection has no {site}")
2954 # Delete on site
2955 url = server_url + reverse("article_delete", kwargs={"pid": pid})
2956 response = requests.delete(url, verify=False)
2957 status = response.status_code
2959 if status == 404:
2960 raise Http404(
2961 f"Article not found on {site}. Please make sure the article exists."
2962 )
2963 elif status > 204:
2964 exception = HTTPError(
2965 response.text.encode("utf8")[:1000],
2966 response=response,
2967 )
2968 raise exception
2969 else:
2970 if site == "website" and article.is_deployed(mersenneSite):
2971 article.undeploy(mersenneSite)
2972 # Delete article binary files
2973 folder = article.get_relative_folder()
2974 resolver.delete_object_folder(
2975 folder,
2976 to_folder=settings.MERSENNE_PROD_DATA_FOLDER,
2977 )
2978 else:
2979 raise ValueError(f"{site} is not a supported site")
2981 except ValueError as exception:
2982 if site in {"trammel", "website", "test_website"}:
2983 raise
2984 message, status = str(exception), 400
2985 except Http404 as exception:
2986 message, status = str(exception), 404
2987 except HTTPError as exception:
2988 message, status = str(exception), 502
2989 except ServerUnderMaintenance as exception:
2990 message, status = str(exception), 503
2991 except requests.exceptions.ConnectionError:
2992 message, status = (
2993 f"{site} is under maintenance. Please try again later.",
2994 503,
2995 )
2996 except Exception as exception:
2997 logger.exception("Unexpected error when deleting article %s from %s", pid, site)
2998 message, status = str(exception), 500
3000 return JsonResponse({"message": message, "status": status}, status=status)
3003def get_messages_in_queue():
3004 app = Celery("ptf-tools")
3005 # tasks = list(current_app.tasks)
3006 tasks = sorted(name for name in current_app.tasks if name.startswith("celery"))
3007 print(tasks)
3008 # i = app.control.inspect()
3010 with app.connection_or_acquire() as conn:
3011 remaining = conn.default_channel.queue_declare(
3012 queue="coordinator", passive=True
3013 ).message_count
3014 return remaining
3017class NumdamView(TemplateView, history_views.HistoryContextMixin):
3018 template_name = "numdam.html"
3020 def get_context_data(self, **kwargs):
3021 context = super().get_context_data(**kwargs)
3023 context["objs"] = ResourceInNumdam.objects.all()
3025 pre_issues = []
3026 prod_issues = []
3027 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
3028 try:
3029 response = requests.get(url)
3030 if response.status_code == 200:
3031 data = response.json()
3032 if "issues" in data:
3033 pre_issues = data["issues"]
3034 except Exception:
3035 pass
3037 url = f"{settings.NUMDAM_URL}/api-all-issues/"
3038 response = requests.get(url)
3039 if response.status_code == 200:
3040 data = response.json()
3041 if "issues" in data:
3042 prod_issues = data["issues"]
3044 new = sorted(set(pre_issues).difference(prod_issues))
3045 removed = sorted(set(prod_issues).difference(pre_issues))
3046 grouped = [
3047 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
3048 ]
3049 grouped_removed = [
3050 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
3051 ]
3052 context["added_issues"] = grouped
3053 context["removed_issues"] = grouped_removed
3055 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
3056 return context
3059class NumdamArchiveView(RedirectView):
3060 @staticmethod
3061 def reset_task_results():
3062 TaskResult.objects.all().delete()
3064 def get_redirect_url(self, *args, **kwargs):
3065 self.colid = kwargs["colid"]
3067 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
3068 return Http404
3070 # we make sure archiving is not already running
3071 # if not get_messages_in_queue():
3072 # self.reset_task_results()
3074 if self.colid == "ALL":
3075 archive_numdam_collections.delay()
3076 else:
3077 archive_numdam_collection.s(self.colid).delay()
3079 return reverse("numdam")
3082class DeployAllNumdamAPIView(View):
3083 def internal_do(self, *args, **kwargs):
3084 pids = []
3086 for obj in ResourceInNumdam.objects.all():
3087 pids.append(obj.pid)
3089 return pids
3091 def get(self, request, *args, **kwargs):
3092 try:
3093 pids, status, message = history_views.execute_and_record_func(
3094 "deploy", "numdam", "ALL", self.internal_do, "numdam"
3095 )
3096 except Exception as exception:
3097 return HttpResponseServerError(exception)
3099 data = {"message": message, "ids": pids, "status": status}
3100 return JsonResponse(data)
3103class NumdamDeleteAPIView(View):
3104 def get(self, request, *args, **kwargs):
3105 pid = self.kwargs.get("pid", None)
3107 try:
3108 obj = ResourceInNumdam.objects.get(pid=pid)
3109 obj.delete()
3110 except Exception as exception:
3111 return HttpResponseServerError(exception)
3113 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
3114 return JsonResponse(data)
3117class ExtIdApiDetail(View):
3118 def get(self, request, *args, **kwargs):
3119 extid = get_object_or_404(
3120 ExtId,
3121 resource__pid=kwargs["pid"],
3122 id_type=kwargs["what"],
3123 )
3124 return JsonResponse(
3125 {
3126 "pk": extid.pk,
3127 "href": extid.get_href(),
3128 "fetch": reverse(
3129 "api-fetch-id",
3130 args=(
3131 extid.resource.pk,
3132 extid.id_value,
3133 extid.id_type,
3134 "extid",
3135 ),
3136 ),
3137 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3138 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3139 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3140 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3141 "is_valid": extid.checked,
3142 }
3143 )
3146class ExtIdFormTemplate(TemplateView):
3147 template_name = "common/externalid_form.html"
3149 def get_context_data(self, **kwargs):
3150 context = super().get_context_data(**kwargs)
3151 context["sequence"] = kwargs["sequence"]
3152 return context
3155class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3156 def get_context_data(self, **kwargs):
3157 context = super().get_context_data(**kwargs)
3158 context["helper"] = PtfFormHelper
3159 return context
3161 def get_success_url(self):
3162 self.post_process()
3163 return self.object.resource.get_absolute_url()
3165 def post_process(self):
3166 model_helpers.post_resource_updated(self.object.resource)
3169class ExtIdCreate(ExtIdFormView, CreateView):
3170 model = ExtId
3171 form_class = ExtIdForm
3173 def get_context_data(self, **kwargs):
3174 context = super().get_context_data(**kwargs)
3175 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3176 return context
3178 def get_initial(self):
3179 initial = super().get_initial()
3180 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3181 return initial
3183 def form_valid(self, form):
3184 form.instance.checked = False
3185 return super().form_valid(form)
3188class ExtIdUpdate(ExtIdFormView, UpdateView):
3189 model = ExtId
3190 form_class = ExtIdForm
3192 def get_context_data(self, **kwargs):
3193 context = super().get_context_data(**kwargs)
3194 context["resource"] = self.object.resource
3195 return context
3198class UpdateTexmfZipAPIView(View):
3199 def get(self, request, *args, **kwargs):
3200 def copy_zip_files(src_folder, dest_folder):
3201 os.makedirs(dest_folder, exist_ok=True)
3203 zip_files = [
3204 os.path.join(src_folder, f)
3205 for f in os.listdir(src_folder)
3206 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3207 ]
3208 for zip_file in zip_files:
3209 resolver.copy_file(zip_file, dest_folder)
3211 # Exceptions: specific zip/gz files
3212 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3213 resolver.copy_file(zip_file, dest_folder)
3215 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3216 resolver.copy_file(zip_file, dest_folder)
3218 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3219 resolver.copy_file(gz_file, dest_folder)
3221 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3223 dest_folder = os.path.join(
3224 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3225 )
3227 try:
3228 copy_zip_files(src_folder, dest_folder)
3229 except Exception as exception:
3230 return HttpResponseServerError(exception)
3232 try:
3233 dest_folder = os.path.join(
3234 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3235 )
3236 copy_zip_files(src_folder, dest_folder)
3237 except Exception as exception:
3238 return HttpResponseServerError(exception)
3240 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3241 return JsonResponse(data)
3244class TrammelTasksProgressView(View):
3245 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3246 """
3247 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3248 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3249 """
3250 task_name = task
3252 def get_event_data():
3253 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3255 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3256 remaining_messages = get_messages_in_queue()
3258 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3259 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3260 failed_tasks = all_tasks.filter(status="FAILURE")
3262 all_tasks_count = all_tasks.count()
3263 success_count = successed_tasks.count()
3264 fail_count = failed_tasks.count()
3266 all_count = all_tasks_count + remaining_messages
3267 remaining_count = all_count - success_count - fail_count
3269 success_rate = int(success_count * 100 / all_count) if all_count else 0
3270 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3271 status = "consuming_queue" if remaining_count != 0 else "polling"
3273 last_task = successed_tasks.first()
3274 last_task = (
3275 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3276 if last_task
3277 else ""
3278 )
3280 # SSE event format
3281 event_data = {
3282 "status": status,
3283 "success_rate": success_rate,
3284 "error_rate": error_rate,
3285 "all_count": all_count,
3286 "remaining_count": remaining_count,
3287 "success_count": success_count,
3288 "fail_count": fail_count,
3289 "last_task": last_task,
3290 }
3292 return event_data
3294 def stream_response(data):
3295 # Send initial response headers
3296 yield f"data: {json.dumps(data)}\n\n"
3298 data = get_event_data()
3299 format = request.GET.get("format", "stream")
3300 if format == "json":
3301 response = JsonResponse(data)
3302 else:
3303 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3304 return response
3307user_signed_up.connect(update_user_from_invite)