Coverage for src / ptf_tools / views / base_views.py: 18%
1613 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-08-14 10:08 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-08-14 10:08 +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.views.decorators.http import require_http_methods
30from django.views.generic import ListView, TemplateView, View
31from django.views.generic.base import RedirectView
32from django.views.generic.detail import SingleObjectMixin
33from django.views.generic.edit import CreateView, FormView, UpdateView
34from django_celery_results.models import TaskResult
35from external.back.crossref.doi import checkDOI, recordDOI, recordPendingPublication
36from extra_views import (
37 CreateWithInlinesView,
38 InlineFormSetFactory,
39 NamedFormsetsMixin,
40 UpdateWithInlinesView,
41)
43# from ptf.views import ArticleEditFormWithVueAPIView
44from matching_back.views import ArticleEditFormWithVueAPIView
45from ptf import model_data_converter, model_helpers, utils
46from ptf.cmds import ptf_cmds, xml_cmds
47from ptf.cmds.base_cmds import make_int
48from ptf.cmds.xml.jats.builder.issue import build_title_xml
49from ptf.cmds.xml.xml_utils import replace_html_entities
50from ptf.display import resolver
51from ptf.exceptions import DOIException, ServerUnderMaintenance
52from ptf.model_data import create_issuedata, create_publisherdata, create_titledata
53from ptf.models import (
54 Abstract,
55 Article,
56 Collection,
57 Container,
58 ExtId,
59 ExtLink,
60 Resource,
61 ResourceId,
62)
63from ptf_back.locks import (
64 is_tex_conversion_locked,
65 release_tex_conversion_lock,
66)
67from ptf_back.tex import create_frontpage
68from ptf_back.tex.tex_tasks import convert_article_tex
69from pubmed.views import recordPubmed
70from requests import Timeout
71from task.tasks.archiving_tasks import archive_resource
73from comments_moderation.utils import get_comments_for_home, is_comment_moderator
74from history import models as history_models
75from history import views as history_views
76from history.utils import (
77 get_gap,
78 get_history_last_event_by,
79 get_last_unsolved_error,
80)
81from ptf_tools.doaj import doaj_pid_register
82from ptf_tools.forms import (
83 CollectionForm,
84 ContainerForm,
85 DiffContainerForm,
86 ExtIdForm,
87 ExtLinkForm,
88 FormSetHelper,
89 ImportArticleForm,
90 ImportContainerForm,
91 ImportEditflowArticleForm,
92 PtfFormHelper,
93 PtfLargeModalFormHelper,
94 PtfModalFormHelper,
95 RegisterPubmedForm,
96 ResourceIdForm,
97 get_article_choices,
98)
99from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos
100from ptf_tools.models import ResourceInNumdam
101from ptf_tools.signals import update_user_from_invite
102from ptf_tools.tasks import (
103 archive_numdam_collection,
104 archive_numdam_collections,
105)
106from ptf_tools.templatetags.tools_helpers import get_authorized_collections
107from ptf_tools.utils import is_authorized_editor
108from ptf_tools.views.components import breadcrumb
110logger = logging.getLogger(__name__)
113def view_404(request: HttpRequest, *args, **kwargs):
114 """
115 Dummy view raising HTTP 404 exception.
116 """
117 raise Http404
120def check_collection(collection, server_url, server_type):
121 """
122 Check if a collection exists on a serveur (test/prod)
123 and upload the collection (XML, image) if necessary
124 """
126 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
127 response = requests.get(url, verify=False)
128 # First, upload the collection XML
129 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
130 body = xml.encode("utf8")
132 url = server_url + reverse("upload-serials")
133 if response.status_code == 200:
134 # PUT http verb is used for update
135 response = requests.put(url, data=body, verify=False)
136 else:
137 # POST http verb is used for creation
138 response = requests.post(url, data=body, verify=False)
140 # Second, copy the collection images
141 # There is no need to copy files for the test server
142 # Files were already copied in /mersenne_test_data during the ptf_tools import
143 # We only need to copy files from /mersenne_test_data to
144 # /mersenne_prod_data during an upload to prod
145 if server_type == "website":
146 resolver.copy_binary_files(
147 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
148 )
149 elif server_type == "numdam":
150 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
151 if collection.pid in settings.NUMDAM_COLLECTIONS:
152 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
154 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
157def check_lock():
158 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
161def load_cedrics_article_choices(request):
162 colid = request.GET.get("colid")
163 issue = request.GET.get("issue")
164 article_choices = get_article_choices(colid, issue)
165 return render(
166 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
167 )
170class ImportCedricsArticleFormView(FormView):
171 template_name = "import_article.html"
172 form_class = ImportArticleForm
174 def dispatch(self, request, *args, **kwargs):
175 self.colid = self.kwargs["colid"]
176 return super().dispatch(request, *args, **kwargs)
178 def get_success_url(self):
179 if self.colid:
180 return reverse("collection-detail", kwargs={"pid": self.colid})
181 return "/"
183 def get_context_data(self, **kwargs):
184 context = super().get_context_data(**kwargs)
185 context["colid"] = self.colid
186 context["helper"] = PtfModalFormHelper
187 return context
189 def get_form_kwargs(self):
190 kwargs = super().get_form_kwargs()
191 kwargs["colid"] = self.colid
192 return kwargs
194 def form_valid(self, form):
195 self.issue = form.cleaned_data["issue"]
196 self.article = form.cleaned_data["article"]
197 return super().form_valid(form)
199 def import_cedrics_article(self, *args, **kwargs):
200 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
201 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
202 )
203 cmd.do()
205 def post(self, request, *args, **kwargs):
206 self.colid = self.kwargs.get("colid", None)
207 issue = request.POST["issue"]
208 self.article_pid = request.POST["article"]
209 self.issue_pid = os.path.basename(os.path.dirname(issue))
211 import_args = [self]
212 import_kwargs = {}
214 try:
215 _, status, message = history_views.execute_and_record_func(
216 "import",
217 f"{self.issue_pid} / {self.article_pid}",
218 self.colid,
219 self.import_cedrics_article,
220 "",
221 False,
222 None,
223 None,
224 *import_args,
225 **import_kwargs,
226 )
228 messages.success(
229 self.request, f"L'article {self.article_pid} a été importé avec succès"
230 )
232 except Exception as exception:
233 messages.error(
234 self.request,
235 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}",
236 )
238 return redirect(self.get_success_url())
241class ImportCedricsIssueView(FormView):
242 template_name = "import_container.html"
243 form_class = ImportContainerForm
245 def dispatch(self, request, *args, **kwargs):
246 self.colid = self.kwargs["colid"]
247 self.to_appear = self.request.GET.get("to_appear", False)
248 return super().dispatch(request, *args, **kwargs)
250 def get_success_url(self):
251 if self.filename:
252 return reverse(
253 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
254 )
255 return "/"
257 def get_context_data(self, **kwargs):
258 context = super().get_context_data(**kwargs)
259 context["colid"] = self.colid
260 context["helper"] = PtfModalFormHelper
261 return context
263 def get_form_kwargs(self):
264 kwargs = super().get_form_kwargs()
265 kwargs["colid"] = self.colid
266 kwargs["to_appear"] = self.to_appear
267 return kwargs
269 def form_valid(self, form):
270 self.filename = form.cleaned_data["filename"].split("/")[-1]
271 return super().form_valid(form)
274class DiffCedricsIssueView(FormView):
275 template_name = "diff_container_form.html"
276 form_class = DiffContainerForm
277 diffs = None
278 xissue = None
279 xissue_encoded = None
281 def get_success_url(self):
282 return reverse("collection-detail", kwargs={"pid": self.colid})
284 def dispatch(self, request, *args, **kwargs):
285 self.colid = self.kwargs["colid"]
286 # self.filename = self.kwargs['filename']
287 return super().dispatch(request, *args, **kwargs)
289 def get(self, request, *args, **kwargs):
290 self.filename = request.GET["filename"]
291 self.remove_mail = request.GET.get("remove_email", "off")
292 self.remove_date_prod = request.GET.get("remove_date_prod", "off")
293 self.remove_email = self.remove_mail == "on"
294 self.remove_date_prod = self.remove_date_prod == "on"
296 try:
297 result, status, message = history_views.execute_and_record_func(
298 "import",
299 os.path.basename(self.filename),
300 self.colid,
301 self.diff_cedrics_issue,
302 "",
303 True,
304 )
305 except Exception as exception:
306 pid = self.filename.split("/")[-1]
307 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
308 return HttpResponseRedirect(self.get_success_url())
310 no_conflict = result[0]
311 self.diffs = result[1]
312 self.xissue = result[2]
314 if True or no_conflict:
315 # Proceed with the import
316 self.form_valid(self.get_form())
317 return redirect(self.get_success_url())
318 else:
319 # Display the diff template
320 self.xissue_encoded = jsonpickle.encode(self.xissue)
322 return super().get(request, *args, **kwargs)
324 def post(self, request, *args, **kwargs):
325 self.filename = request.POST["filename"]
326 data = request.POST["xissue_encoded"]
327 self.xissue = jsonpickle.decode(data)
329 return super().post(request, *args, **kwargs)
331 def get_context_data(self, **kwargs):
332 context = super().get_context_data(**kwargs)
333 context["colid"] = self.colid
334 context["diff"] = self.diffs
335 context["filename"] = self.filename
336 context["xissue_encoded"] = self.xissue_encoded
337 return context
339 def get_form_kwargs(self):
340 kwargs = super().get_form_kwargs()
341 kwargs["colid"] = self.colid
342 return kwargs
344 def diff_cedrics_issue(self, *args, **kwargs):
345 params = {
346 "colid": self.colid,
347 "input_file": self.filename,
348 "remove_email": self.remove_mail,
349 "remove_date_prod": self.remove_date_prod,
350 "diff_only": True,
351 }
353 if settings.IMPORT_CEDRICS_DIRECTLY:
354 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
355 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
356 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
357 else:
358 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
360 result = cmd.do()
361 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
362 messages.warning(
363 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings
364 )
366 return result
368 def import_cedrics_issue(self, *args, **kwargs):
369 # modify xissue with data_issue if params to override
370 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
371 issue = model_helpers.get_container(self.xissue.pid)
372 if issue:
373 data_issue = model_data_converter.db_to_issue_data(issue)
374 for xarticle in self.xissue.articles:
375 filter_articles = [
376 article for article in data_issue.articles if article.doi == xarticle.doi
377 ]
378 if len(filter_articles) > 0:
379 db_article = filter_articles[0]
380 xarticle.coi_statement = db_article.coi_statement
381 xarticle.kwds = db_article.kwds
382 xarticle.contrib_groups = db_article.contrib_groups
384 params = {
385 "colid": self.colid,
386 "xissue": self.xissue,
387 "input_file": self.filename,
388 }
390 if settings.IMPORT_CEDRICS_DIRECTLY:
391 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
392 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
393 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
394 else:
395 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
397 cmd.do()
399 def form_valid(self, form):
400 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
401 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
402 else:
403 import_kwargs = {}
404 import_args = [self]
406 try:
407 _, status, message = history_views.execute_and_record_func(
408 "import",
409 self.xissue.pid,
410 self.kwargs["colid"],
411 self.import_cedrics_issue,
412 "",
413 False,
414 None,
415 None,
416 *import_args,
417 **import_kwargs,
418 )
419 except Exception as exception:
420 messages.error(
421 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
422 )
423 return super().form_invalid(form)
425 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
426 return super().form_valid(form)
429class ImportEditflowArticleFormView(FormView):
430 template_name = "import_editflow_article.html"
431 form_class = ImportEditflowArticleForm
433 def dispatch(self, request, *args, **kwargs):
434 self.colid = self.kwargs["colid"]
435 return super().dispatch(request, *args, **kwargs)
437 def get_context_data(self, **kwargs):
438 context = super().get_context_data(**kwargs)
439 context["colid"] = self.kwargs["colid"]
440 context["helper"] = PtfLargeModalFormHelper
441 return context
443 def get_success_url(self):
444 if self.colid:
445 return reverse("collection-detail", kwargs={"pid": self.colid})
446 return "/"
448 def post(self, request, *args, **kwargs):
449 self.colid = self.kwargs.get("colid", None)
450 try:
451 if not self.colid:
452 raise ValueError("Missing collection id")
454 issue_name = settings.ISSUE_PENDING_PUBLICATION_PIDS.get(self.colid)
455 if not issue_name:
456 raise ValueError(
457 "Issue not found in Pending Publications PIDs. Did you forget to add it?"
458 )
460 issue = model_helpers.get_container(issue_name)
461 if not issue:
462 raise ValueError("No issue found")
464 editflow_xml_file = request.FILES.get("editflow_xml_file")
465 if not editflow_xml_file:
466 raise ValueError("The file you specified couldn't be found")
468 body = editflow_xml_file.read().decode("utf-8")
470 cmd = xml_cmds.addArticleXmlCmd(
471 {
472 "body": body,
473 "issue": issue,
474 "assign_doi": True,
475 "standalone": True,
476 "from_folder": settings.RESOURCES_ROOT,
477 }
478 )
479 cmd.set_collection(issue.get_collection())
480 cmd.do()
482 messages.success(
483 request,
484 f'Editflow article successfully imported into issue "{issue_name}"',
485 )
487 except Exception as exception:
488 messages.error(
489 request,
490 f"Import failed: {str(exception)}",
491 )
493 return redirect(self.get_success_url())
496class MatchingAPIView(View):
497 def get(self, request, *args, **kwargs):
498 pid = self.kwargs.get("pid", None)
500 url = settings.MATCHING_URL
501 headers = {"Content-Type": "application/xml"}
503 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
505 if settings.DEBUG:
506 print("Issue exported to /tmp/issue.xml")
507 f = open("/tmp/issue.xml", "w")
508 f.write(body.encode("utf8"))
509 f.close()
511 r = requests.post(url, data=body.encode("utf8"), headers=headers)
512 body = r.text.encode("utf8")
513 data = {"status": r.status_code, "message": body[:1000]}
515 if settings.DEBUG:
516 print("Matching received, new issue exported to /tmp/issue1.xml")
517 f = open("/tmp/issue1.xml", "w")
518 text = body
519 f.write(text)
520 f.close()
522 resource = model_helpers.get_resource(pid)
523 obj = resource.cast()
524 colid = obj.get_collection().pid
526 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
528 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
529 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
530 )
531 cmd.do()
533 print("Matching finished")
534 return JsonResponse(data)
537class ImportAllAPIView(View):
538 def internal_do(self, *args, **kwargs):
539 pid = self.kwargs.get("pid", None)
541 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
542 if not os.path.isdir(root_folder):
543 raise ValueError(root_folder + " does not exist")
545 resource = model_helpers.get_resource(pid)
546 if not resource:
547 file = os.path.join(root_folder, pid + ".xml")
548 body = utils.get_file_content_in_utf8(file)
549 journals = xml_cmds.addCollectionsXmlCmd(
550 {
551 "body": body,
552 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
553 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
554 }
555 ).do()
556 if not journals:
557 raise ValueError(file + " does not contain a collection")
558 resource = journals[0]
559 # resolver.copy_binary_files(
560 # resource,
561 # settings.MATHDOC_ARCHIVE_FOLDER,
562 # settings.MERSENNE_TEST_DATA_FOLDER)
564 obj = resource.cast()
566 if obj.classname != "Collection":
567 raise ValueError(pid + " does not contain a collection")
569 cmd = xml_cmds.collectEntireCollectionXmlCmd(
570 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
571 )
572 pids = cmd.do()
574 return pids
576 def get(self, request, *args, **kwargs):
577 pid = self.kwargs.get("pid", None)
579 try:
580 pids, status, message = history_views.execute_and_record_func(
581 "import", pid, pid, self.internal_do
582 )
583 except Timeout as exception:
584 return HttpResponse(exception, status=408)
585 except Exception as exception:
586 return HttpResponseServerError(exception)
588 data = {"message": message, "ids": pids, "status": status}
589 return JsonResponse(data)
592class DeployAllAPIView(View):
593 def internal_do(self, *args, **kwargs):
594 pid = self.kwargs.get("pid", None)
595 site = self.kwargs.get("site", None)
597 pids = []
599 collection = model_helpers.get_collection(pid)
600 if not collection:
601 raise RuntimeError(pid + " does not exist")
603 if site == "numdam":
604 server_url = settings.NUMDAM_PRE_URL
605 elif site != "ptf_tools":
606 server_url = getattr(collection, site)()
607 if not server_url:
608 raise RuntimeError("The collection has no " + site)
610 if site != "ptf_tools":
611 # check if the collection exists on the server
612 # if not, check_collection will upload the collection (XML,
613 # image...)
614 check_collection(collection, server_url, site)
616 for issue in collection.content.all():
617 if site != "website" or (site == "website" and issue.are_all_articles_published()):
618 pids.append(issue.pid)
620 return pids
622 def get(self, request, *args, **kwargs):
623 pid = self.kwargs.get("pid", None)
624 site = self.kwargs.get("site", None)
626 try:
627 pids, status, message = history_views.execute_and_record_func(
628 "deploy", pid, pid, self.internal_do, site
629 )
630 except Timeout as exception:
631 return HttpResponse(exception, status=408)
632 except Exception as exception:
633 return HttpResponseServerError(exception)
635 data = {"message": message, "ids": pids, "status": status}
636 return JsonResponse(data)
639class AddIssuePDFView(View):
640 def __init(self, *args, **kwargs):
641 super().__init__(*args, **kwargs)
642 self.pid = None
643 self.issue = None
644 self.collection = None
645 self.site = "test_website"
647 def post_to_site(self, url):
648 response = requests.post(url, verify=False)
649 status = response.status_code
650 if not (199 < status < 205):
651 messages.error(self.request, response.text)
652 if status == 503:
653 raise ServerUnderMaintenance(response.text)
654 else:
655 raise RuntimeError(response.text)
657 def internal_do(self, *args, **kwargs):
658 """
659 Called by history_views.execute_and_record_func to do the actual job.
660 """
662 issue_pid = self.issue.pid
663 colid = self.collection.pid
665 if self.site == "website":
666 # Copy the PDF from the test to the production folder
667 resolver.copy_binary_files(
668 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
669 )
670 else:
671 # Copy the PDF from the cedram to the test folder
672 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
673 from_path = os.path.join(from_folder, issue_pid + ".pdf")
674 if not os.path.isfile(from_path):
675 raise Http404(f"{from_path} does not exist")
677 to_path = resolver.get_disk_location(
678 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
679 )
680 resolver.copy_file(from_path, to_path)
682 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
684 if self.site == "test_website":
685 # Post to ptf-tools: it will add a Datastream to the issue
686 absolute_url = self.request.build_absolute_uri(url)
687 self.post_to_site(absolute_url)
689 server_url = getattr(self.collection, self.site)()
690 absolute_url = server_url + url
691 # Post to the test or production website
692 self.post_to_site(absolute_url)
694 def get(self, request, *args, **kwargs):
695 """
696 Send an issue PDF to the test or production website
697 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
698 :param args:
699 :param kwargs:
700 :return:
701 """
702 if check_lock():
703 m = "Trammel is under maintenance. Please try again later."
704 messages.error(self.request, m)
705 return JsonResponse({"message": m, "status": 503})
707 self.pid = self.kwargs.get("pid", None)
708 self.site = self.kwargs.get("site", "test_website")
710 self.issue = model_helpers.get_container(self.pid)
711 if not self.issue:
712 raise Http404(f"{self.pid} does not exist")
713 self.collection = self.issue.get_top_collection()
715 try:
716 pids, status, message = history_views.execute_and_record_func(
717 "deploy",
718 self.pid,
719 self.collection.pid,
720 self.internal_do,
721 f"add issue PDF to {self.site}",
722 )
724 except Timeout as exception:
725 return HttpResponse(exception, status=408)
726 except Exception as exception:
727 return HttpResponseServerError(exception)
729 data = {"message": message, "status": status}
730 return JsonResponse(data)
733class ArchiveAllAPIView(View):
734 """
735 - archive le xml de la collection ainsi que les binaires liés
736 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
737 @return array of issues pid
738 """
740 def internal_do(self, *args, **kwargs):
741 collection = kwargs["collection"]
742 pids = []
743 colid = collection.pid
745 logfile = os.path.join(settings.LOG_DIR, "archive.log")
746 if os.path.isfile(logfile):
747 os.remove(logfile)
749 ptf_cmds.exportPtfCmd(
750 {
751 "pid": colid,
752 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
753 "with_binary_files": True,
754 "for_archive": True,
755 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
756 }
757 ).do()
759 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
760 if os.path.isfile(cedramcls):
761 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
762 resolver.create_folder(dest_folder)
763 resolver.copy_file(cedramcls, dest_folder)
765 for issue in collection.content.all():
766 qs = issue.article_set.filter(
767 date_online_first__isnull=True, date_published__isnull=True
768 )
769 if qs.count() == 0:
770 pids.append(issue.pid)
772 return pids
774 def get(self, request, *args, **kwargs):
775 pid = self.kwargs.get("pid", None)
777 collection = model_helpers.get_collection(pid)
778 if not collection:
779 return HttpResponse(f"{pid} does not exist", status=400)
781 dict_ = {"collection": collection}
782 args_ = [self]
784 try:
785 pids, status, message = history_views.execute_and_record_func(
786 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_
787 )
788 except Timeout as exception:
789 return HttpResponse(exception, status=408)
790 except Exception as exception:
791 return HttpResponseServerError(exception)
793 data = {"message": message, "ids": pids, "status": status}
794 return JsonResponse(data)
797class CreateAllDjvuAPIView(View):
798 def internal_do(self, *args, **kwargs):
799 issue = kwargs["issue"]
800 pids = [issue.pid]
802 for article in issue.article_set.all():
803 pids.append(article.pid)
805 return pids
807 def get(self, request, *args, **kwargs):
808 pid = self.kwargs.get("pid", None)
809 issue = model_helpers.get_container(pid)
810 if not issue:
811 raise Http404(f"{pid} does not exist")
813 try:
814 dict_ = {"issue": issue}
815 args_ = [self]
817 pids, status, message = history_views.execute_and_record_func(
818 "numdam",
819 pid,
820 issue.get_collection().pid,
821 self.internal_do,
822 "",
823 False,
824 None,
825 None,
826 *args_,
827 **dict_,
828 )
829 except Exception as exception:
830 return HttpResponseServerError(exception)
832 data = {"message": message, "ids": pids, "status": status}
833 return JsonResponse(data)
836class ImportJatsContainerAPIView(View):
837 def internal_do(self, *args, **kwargs):
838 pid = self.kwargs.get("pid", None)
839 colid = self.kwargs.get("colid", None)
841 if pid and colid:
842 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
844 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
845 {
846 "body": body,
847 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
848 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
849 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
850 }
851 )
852 container = cmd.do()
853 if len(cmd.warnings) > 0:
854 messages.warning(
855 self.request,
856 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
857 )
859 if not container:
860 raise RuntimeError("Error: the container " + pid + " was not imported")
862 # resolver.copy_binary_files(
863 # container,
864 # settings.MATHDOC_ARCHIVE_FOLDER,
865 # settings.MERSENNE_TEST_DATA_FOLDER)
866 #
867 # for article in container.article_set.all():
868 # resolver.copy_binary_files(
869 # article,
870 # settings.MATHDOC_ARCHIVE_FOLDER,
871 # settings.MERSENNE_TEST_DATA_FOLDER)
872 else:
873 raise RuntimeError("colid or pid are not defined")
875 def get(self, request, *args, **kwargs):
876 pid = self.kwargs.get("pid", None)
877 colid = self.kwargs.get("colid", None)
879 try:
880 _, status, message = history_views.execute_and_record_func(
881 "import", pid, colid, self.internal_do
882 )
883 except Timeout as exception:
884 return HttpResponse(exception, status=408)
885 except Exception as exception:
886 return HttpResponseServerError(exception)
888 data = {"message": message, "status": status}
889 return JsonResponse(data)
892class DeployCollectionAPIView(View):
893 # Update collection.xml on a site (with its images)
895 def internal_do(self, *args, **kwargs):
896 colid = self.kwargs.get("colid", None)
897 site = self.kwargs.get("site", None)
899 collection = model_helpers.get_collection(colid)
900 if not collection:
901 raise RuntimeError(f"{colid} does not exist")
903 if site == "numdam":
904 server_url = settings.NUMDAM_PRE_URL
905 else:
906 server_url = getattr(collection, site)()
907 if not server_url:
908 raise RuntimeError(f"The collection has no {site}")
910 # check_collection creates or updates the collection (XML, image...)
911 check_collection(collection, server_url, site)
913 def get(self, request, *args, **kwargs):
914 colid = self.kwargs.get("colid", None)
915 site = self.kwargs.get("site", None)
917 try:
918 _, status, message = history_views.execute_and_record_func(
919 "deploy", colid, colid, self.internal_do, site
920 )
921 except Timeout as exception:
922 return HttpResponse(exception, status=408)
923 except Exception as exception:
924 return HttpResponseServerError(exception)
926 data = {"message": message, "status": status}
927 return JsonResponse(data)
930class DeployJatsResourceAPIView(View):
931 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
933 def internal_do(self, *args, **kwargs):
934 pid = self.kwargs.get("pid", None)
935 colid = self.kwargs.get("colid", None)
936 site = self.kwargs.get("site", None)
938 if site == "ptf_tools":
939 raise RuntimeError("Do not choose to deploy on PTF Tools")
940 if check_lock():
941 msg = "Trammel is under maintenance. Please try again later."
942 messages.error(self.request, msg)
943 return JsonResponse({"messages": msg, "status": 503})
945 resource = model_helpers.get_resource(pid)
946 if not resource:
947 raise RuntimeError(f"{pid} does not exist")
949 obj = resource.cast()
950 article = None
951 if obj.classname == "Article":
952 article = obj
953 container = article.my_container
954 articles_to_deploy = [article]
955 else:
956 container = obj
957 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
959 if container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(colid, None):
960 raise RuntimeError("Pending publications should not be deployed")
961 if site == "website" and article is not None and article.do_not_publish:
962 raise RuntimeError(f"{pid} is marked as Do not publish")
963 if site == "numdam" and article is not None:
964 raise RuntimeError("You can only deploy issues to Numdam")
966 collection = container.get_top_collection()
967 colid = collection.pid
968 djvu_exception = None
970 if site == "numdam":
971 server_url = settings.NUMDAM_PRE_URL
972 ResourceInNumdam.objects.get_or_create(pid=container.pid)
974 # 06/12/2022: DjVu are no longer added with Mersenne articles
975 # Add Djvu (before exporting the XML)
976 if False and int(container.fyear) < 2020:
977 for art in container.article_set.all():
978 try:
979 cmd = ptf_cmds.addDjvuPtfCmd()
980 cmd.set_resource(art)
981 cmd.do()
982 except Exception as e:
983 # Djvu are optional.
984 # Allow the deployment, but record the exception in the history
985 djvu_exception = e
986 else:
987 server_url = getattr(collection, site)()
988 if not server_url:
989 raise RuntimeError(f"The collection has no {site}")
991 # check if the collection exists on the server
992 # if not, check_collection will upload the collection (XML,
993 # image...)
994 if article is None:
995 check_collection(collection, server_url, site)
997 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
998 # Create/update deployed date and published date on all container articles
999 if site == "website":
1000 file_.write(
1001 "Create/Update deployed_date and date_published on all articles for {}\n".format(
1002 pid
1003 )
1004 )
1006 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
1007 cmd = ptf_cmds.publishResourcePtfCmd()
1008 cmd.set_resource(resource)
1009 updated_articles = cmd.do()
1011 create_frontpage(colid, container, updated_articles, test=False)
1013 mersenneSite = model_helpers.get_site_mersenne(colid)
1014 # create or update deployed_date on container and articles
1015 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
1017 for art in articles_to_deploy:
1018 if art.doi and (art.date_published or art.date_online_first):
1019 if art.my_container.fyear is None:
1020 art.my_container.fyear = datetime.now().year
1021 # BUG ? update the container but no save() ?
1023 file_.write(
1024 "Publication date of {} : Online First: {}, Published: {}\n".format(
1025 art.pid, art.date_online_first, art.date_published
1026 )
1027 )
1029 if article is None:
1030 resolver.copy_binary_files(
1031 container,
1032 settings.MERSENNE_TEST_DATA_FOLDER,
1033 settings.MERSENNE_PROD_DATA_FOLDER,
1034 )
1036 for art in articles_to_deploy:
1037 resolver.copy_binary_files(
1038 art,
1039 settings.MERSENNE_TEST_DATA_FOLDER,
1040 settings.MERSENNE_PROD_DATA_FOLDER,
1041 )
1043 elif site == "test_website":
1044 # create date_pre_published on articles without date_pre_published
1045 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
1046 cmd.set_resource(resource)
1047 updated_articles = cmd.do()
1049 create_frontpage(colid, container, updated_articles)
1051 export_to_website = site == "website"
1053 if article is None:
1054 with_djvu = site == "numdam"
1055 xml = ptf_cmds.exportPtfCmd(
1056 {
1057 "pid": pid,
1058 "with_djvu": with_djvu,
1059 "export_to_website": export_to_website,
1060 }
1061 ).do()
1062 body = xml.encode("utf8")
1064 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1065 url = server_url + reverse("issue_upload")
1066 else:
1067 url = server_url + reverse("book_upload")
1069 # verify=False: ignore TLS certificate
1070 response = requests.post(url, data=body, verify=False)
1071 # response = requests.post(url, files=files, verify=False)
1072 else:
1073 xml = ptf_cmds.exportPtfCmd(
1074 {
1075 "pid": pid,
1076 "with_djvu": False,
1077 "article_standalone": True,
1078 "collection_pid": collection.pid,
1079 "export_to_website": export_to_website,
1080 "export_folder": settings.LOG_DIR,
1081 }
1082 ).do()
1083 # Unlike containers that send their XML as the body of the POST request,
1084 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1085 xml_file = io.StringIO(xml)
1086 files = {"xml": xml_file}
1088 url = server_url + reverse(
1089 "article_in_issue_upload", kwargs={"pid": container.pid}
1090 )
1091 # verify=False: ignore TLS certificate
1092 header = {}
1093 response = requests.post(url, headers=header, files=files, verify=False)
1095 status = response.status_code
1097 if 199 < status < 205:
1098 # There is no need to copy files for the test server
1099 # Files were already copied in /mersenne_test_data during the ptf_tools import
1100 # We only need to copy files from /mersenne_test_data to
1101 # /mersenne_prod_data during an upload to prod
1102 if site == "website":
1103 # TODO mettre ici le record doi pour un issue publié
1104 if container.doi:
1105 recordDOI(container)
1107 for art in articles_to_deploy:
1108 # record DOI automatically when deploying in prod
1110 if art.doi and art.allow_crossref():
1111 recordDOI(art)
1113 if colid == "CRBIOL":
1114 recordPubmed(
1115 art, force_update=False, updated_articles=updated_articles
1116 )
1118 if colid == "PCJ":
1119 self.update_pcj_editor(updated_articles)
1121 # Archive the container or the article
1122 if article is None:
1123 archive_resource.delay(
1124 pid,
1125 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1126 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1127 )
1129 else:
1130 archive_resource.delay(
1131 pid,
1132 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1133 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1134 article_doi=article.doi,
1135 )
1136 # cmd = ptf_cmds.archiveIssuePtfCmd({
1137 # "pid": pid,
1138 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1139 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1140 # cmd.set_article(article) # set_article allows archiving only the article
1141 # cmd.do()
1143 elif site == "numdam":
1144 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1145 if colid in settings.NUMDAM_COLLECTIONS:
1146 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1148 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1149 for article in container.article_set.all():
1150 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1152 elif status == 503:
1153 raise ServerUnderMaintenance(response.text)
1154 else:
1155 raise RuntimeError(response.text)
1157 if djvu_exception:
1158 raise djvu_exception
1160 def get(self, request, *args, **kwargs):
1161 pid = self.kwargs.get("pid", None)
1162 colid = self.kwargs.get("colid", None)
1163 site = self.kwargs.get("site", None)
1165 try:
1166 _, status, message = history_views.execute_and_record_func(
1167 "deploy", pid, colid, self.internal_do, site
1168 )
1169 except Timeout as exception:
1170 return HttpResponse(exception, status=408)
1171 except Exception as exception:
1172 return HttpResponseServerError(exception)
1174 data = {"message": message, "status": status}
1175 return JsonResponse(data)
1177 def update_pcj_editor(self, updated_articles):
1178 for article in updated_articles:
1179 data = {
1180 "date_published": article.date_published.strftime("%Y-%m-%d"),
1181 "article_number": article.article_number,
1182 }
1183 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1184 requests.post(url, json=data, verify=False)
1187class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1188 article = None
1190 def internal_do(self, *args, **kwargs):
1191 lang = self.kwargs.get("lang", None)
1193 translation = None
1194 for trans_article in self.article.translations.all():
1195 if trans_article.lang == lang:
1196 translation = trans_article
1198 if translation is None:
1199 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1201 collection = self.article.get_top_collection()
1202 colid = collection.pid
1203 container = self.article.my_container
1205 if translation.date_published is None:
1206 # Add date posted
1207 cmd = ptf_cmds.publishResourcePtfCmd()
1208 cmd.set_resource(translation)
1209 cmd.do()
1210 # updated_articles = cmd.do()
1212 # # Recompile PDF to add the date posted
1213 # try:
1214 # create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1215 # except Exception:
1216 # raise PDFException(
1217 # "Unable to compile the article PDF. Please contact the centre Mersenne"
1218 # )
1220 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1221 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1222 resolver.copy_binary_files(
1223 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1224 )
1226 # Deploy in prod
1227 xml = ptf_cmds.exportPtfCmd(
1228 {
1229 "pid": self.article.pid,
1230 "with_djvu": False,
1231 "article_standalone": True,
1232 "collection_pid": colid,
1233 "export_to_website": True,
1234 "export_folder": settings.LOG_DIR,
1235 }
1236 ).do()
1237 xml_file = io.StringIO(xml)
1238 files = {"xml": xml_file}
1240 server_url = getattr(collection, "website")()
1241 if not server_url:
1242 raise RuntimeError("The collection has no website")
1243 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1244 header = {}
1246 try:
1247 response = requests.post(
1248 url, headers=header, files=files, verify=False
1249 ) # verify: ignore TLS certificate
1250 status = response.status_code
1251 except requests.exceptions.ConnectionError:
1252 raise ServerUnderMaintenance(
1253 "The journal is under maintenance. Please try again later."
1254 )
1256 # Register translation in Crossref
1257 if 199 < status < 205:
1258 if self.article.allow_crossref():
1259 try:
1260 recordDOI(translation)
1261 except Exception:
1262 raise DOIException(
1263 "Error while recording the DOI. Please contact the centre Mersenne"
1264 )
1266 def get(self, request, *args, **kwargs):
1267 doi = kwargs.get("doi", None)
1268 self.article = model_helpers.get_article_by_doi(doi)
1269 if self.article is None:
1270 raise Http404(f"{doi} does not exist")
1272 try:
1273 _, status, message = history_views.execute_and_record_func(
1274 "deploy",
1275 self.article.pid,
1276 self.article.get_top_collection().pid,
1277 self.internal_do,
1278 "website",
1279 )
1280 except Timeout as exception:
1281 return HttpResponse(exception, status=408)
1282 except Exception as exception:
1283 logger.exception(f"Failed to post translation for {self.article.pid}")
1284 return HttpResponseServerError(exception)
1286 data = {"message": message, "status": status}
1287 return JsonResponse(data)
1290class DeleteJatsIssueAPIView(View):
1291 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1292 def get(self, request, *args, **kwargs):
1293 pid = self.kwargs.get("pid", None)
1294 colid = self.kwargs.get("colid", None)
1295 site = self.kwargs.get("site", None)
1296 message = "Le volume a bien été supprimé"
1297 status = 200
1299 issue = model_helpers.get_container(pid)
1300 if not issue:
1301 raise Http404(f"{pid} does not exist")
1302 try:
1303 mersenneSite = model_helpers.get_site_mersenne(colid)
1305 if site == "ptf_tools":
1306 if issue.is_deployed(mersenneSite):
1307 issue.undeploy(mersenneSite)
1308 for article in issue.article_set.all():
1309 article.undeploy(mersenneSite)
1311 p = model_helpers.get_provider("mathdoc-id")
1313 cmd = ptf_cmds.addContainerPtfCmd(
1314 {
1315 "pid": issue.pid,
1316 "ctype": "issue",
1317 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1318 }
1319 )
1320 cmd.set_provider(p)
1321 cmd.add_collection(issue.get_collection())
1322 cmd.set_object_to_be_deleted(issue)
1323 cmd.undo()
1325 else:
1326 if site == "numdam":
1327 server_url = settings.NUMDAM_PRE_URL
1328 else:
1329 collection = issue.get_collection()
1330 server_url = getattr(collection, site)()
1332 if not server_url:
1333 message = "The collection has no " + site
1334 status = 500
1335 else:
1336 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1337 response = requests.delete(url, verify=False)
1338 status = response.status_code
1340 if status == 404:
1341 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1342 elif status > 204:
1343 body = response.text.encode("utf8")
1344 message = body[:1000]
1345 else:
1346 status = 200
1347 # unpublish issue in collection site (site_register.json)
1348 if site == "website":
1349 if issue.is_deployed(mersenneSite):
1350 issue.undeploy(mersenneSite)
1351 for article in issue.article_set.all():
1352 article.undeploy(mersenneSite)
1353 # delete article binary files
1354 folder = article.get_relative_folder()
1355 resolver.delete_object_folder(
1356 folder,
1357 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1358 )
1359 # delete issue binary files
1360 folder = issue.get_relative_folder()
1361 resolver.delete_object_folder(
1362 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1363 )
1365 except Timeout as exception:
1366 return HttpResponse(exception, status=408)
1367 except Exception as exception:
1368 return HttpResponseServerError(exception)
1370 data = {"message": message, "status": status}
1371 return JsonResponse(data)
1374class ArchiveIssueAPIView(View):
1375 def get(self, request, *args, **kwargs):
1376 try:
1377 pid = kwargs["pid"]
1378 colid = kwargs["colid"]
1379 except IndexError:
1380 raise Http404
1382 try:
1383 cmd = ptf_cmds.archiveIssuePtfCmd(
1384 {
1385 "pid": pid,
1386 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1387 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1388 "needs_publication_date": True,
1389 }
1390 )
1391 result_, status, message = history_views.execute_and_record_func(
1392 "archive", pid, colid, cmd.do
1393 )
1394 except Exception as exception:
1395 return HttpResponseServerError(exception)
1397 data = {"message": message, "status": 200}
1398 return JsonResponse(data)
1401class CreateDjvuAPIView(View):
1402 def internal_do(self, *args, **kwargs):
1403 pid = self.kwargs.get("pid", None)
1405 resource = model_helpers.get_resource(pid)
1406 cmd = ptf_cmds.addDjvuPtfCmd()
1407 cmd.set_resource(resource)
1408 cmd.do()
1410 def get(self, request, *args, **kwargs):
1411 pid = self.kwargs.get("pid", None)
1412 colid = pid.split("_")[0]
1414 try:
1415 _, status, message = history_views.execute_and_record_func(
1416 "numdam", pid, colid, self.internal_do
1417 )
1418 except Exception as exception:
1419 return HttpResponseServerError(exception)
1421 data = {"message": message, "status": status}
1422 return JsonResponse(data)
1425class PTFToolsHomeView(LoginRequiredMixin, View):
1426 """
1427 Home Page.
1428 - Admin & staff -> Render blank home.html
1429 - User with unique authorized collection -> Redirect to collection details page
1430 - User with multiple authorized collections -> Render home.html with data
1431 - Comment moderator -> Comments dashboard
1432 - Others -> 404 response
1433 """
1435 def get(self, request, *args, **kwargs) -> HttpResponse:
1436 # Staff or user with authorized collections
1437 if request.user.is_staff or request.user.is_superuser:
1438 return render(request, "home.html")
1440 colids = get_authorized_collections(request.user)
1441 is_mod = is_comment_moderator(request.user)
1443 # The user has no rights
1444 if not (colids or is_mod):
1445 raise Http404("No collections associated with your account.")
1446 # Comment moderator only
1447 elif not colids:
1448 return HttpResponseRedirect(reverse("comment_list"))
1450 # User with unique collection -> Redirect to collection detail page
1451 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1452 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1454 # User with multiple authorized collections - Special home
1455 context = {}
1456 context["overview"] = True
1458 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1459 all_collections = {c["pid"]: c for c in all_collections}
1461 # Comments summary
1462 try:
1463 error, comments_data = get_comments_for_home(request.user)
1464 except AttributeError:
1465 error, comments_data = True, {}
1467 context["comment_server_ok"] = False
1469 if not error:
1470 context["comment_server_ok"] = True
1471 if comments_data:
1472 for col_id, comment_nb in comments_data.items():
1473 if col_id.upper() in all_collections: 1473 ↛ 1472line 1473 didn't jump to line 1472 because the condition on line 1473 was always true
1474 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1476 # TODO: Translations summary
1477 context["translation_server_ok"] = False
1479 # Sort the collections according to the number of pending comments
1480 context["collections"] = sorted(
1481 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1482 )
1484 return render(request, "home.html", context)
1487class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1488 columns = 5
1490 def get_common_context_data(self, **kwargs):
1491 context = super().get_context_data(**kwargs)
1492 now = timezone.now()
1493 curyear = now.year
1494 years = range(curyear - self.columns + 1, curyear + 1)
1496 context["collections"] = settings.MERSENNE_COLLECTIONS
1497 context["containers_to_be_published"] = []
1498 context["last_col_events"] = []
1500 event = get_history_last_event_by("clockss", "ALL")
1501 clockss_gap = get_gap(now, event)
1503 context["years"] = years
1504 context["clockss_gap"] = clockss_gap
1506 return context
1508 def calculate_articles_and_pages(self, pid, years):
1509 data_by_year = []
1510 total_articles = [0] * len(years)
1511 total_pages = [0] * len(years)
1513 for year in years:
1514 articles = self.get_articles_for_year(pid, year)
1515 articles_count = articles.count()
1516 page_count = sum(article.get_article_page_count() for article in articles)
1518 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1519 total_articles[year - years[0]] += articles_count
1520 total_pages[year - years[0]] += page_count
1522 return data_by_year, total_articles, total_pages
1524 def get_articles_for_year(self, pid, year):
1525 return Article.objects.filter(
1526 Q(my_container__my_collection__pid=pid)
1527 & (
1528 Q(date_published__year=year, date_online_first__isnull=True)
1529 | Q(date_online_first__year=year)
1530 )
1531 ).prefetch_related("resourcecount_set")
1534class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1535 template_name = "dashboard/published_articles.html"
1537 def get_context_data(self, **kwargs):
1538 context = self.get_common_context_data(**kwargs)
1539 years = context["years"]
1541 published_articles = []
1542 total_published_articles = [
1543 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1544 ]
1546 for pid in settings.MERSENNE_COLLECTIONS:
1547 if pid != "MERSENNE":
1548 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1549 pid, years
1550 )
1551 published_articles.append({"pid": pid, "years": articles_data})
1553 for i, year in enumerate(years):
1554 total_published_articles[i]["total_articles"] += total_articles[i]
1555 total_published_articles[i]["total_pages"] += total_pages[i]
1557 context["published_articles"] = published_articles
1558 context["total_published_articles"] = total_published_articles
1560 return context
1563class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1564 template_name = "dashboard/created_volumes.html"
1566 def get_context_data(self, **kwargs):
1567 context = self.get_common_context_data(**kwargs)
1568 years = context["years"]
1570 created_volumes = []
1571 total_created_volumes = [
1572 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1573 ]
1575 for pid in settings.MERSENNE_COLLECTIONS:
1576 if pid != "MERSENNE":
1577 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1578 pid, years
1579 )
1580 created_volumes.append({"pid": pid, "years": volumes_data})
1582 for i, _ in enumerate(years):
1583 total_created_volumes[i]["total_articles"] += total_articles[i]
1584 total_created_volumes[i]["total_pages"] += total_pages[i]
1586 context["created_volumes"] = created_volumes
1587 context["total_created_volumes"] = total_created_volumes
1589 return context
1591 def calculate_volumes_and_pages(self, pid, years):
1592 data_by_year = []
1593 total_articles = [0] * len(years)
1594 total_pages = [0] * len(years)
1596 for year in years:
1597 issues = Container.objects.filter(my_collection__pid=pid, fyear=year)
1598 articles_count = 0
1599 page_count = 0
1601 for issue in issues:
1602 articles = issue.article_set.filter(
1603 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1604 ).prefetch_related("resourcecount_set")
1606 articles_count += articles.count()
1607 page_count += sum(article.get_article_page_count() for article in articles)
1609 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1610 total_articles[year - years[0]] += articles_count
1611 total_pages[year - years[0]] += page_count
1613 return data_by_year, total_articles, total_pages
1616class ReferencingChoice(View):
1617 def post(self, request, *args, **kwargs):
1618 if request.POST.get("optSite") == "ads":
1619 return redirect(
1620 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1621 )
1622 elif request.POST.get("optSite") == "wos":
1623 comp = ReferencingCheckerWos()
1624 journal = comp.make_journal(request.POST.get("selectCol"))
1625 if journal is None:
1626 return render(
1627 request,
1628 "dashboard/referencing.html",
1629 {
1630 "error": "Collection not found",
1631 "colid": request.POST.get("selectCol"),
1632 "optSite": request.POST.get("optSite"),
1633 },
1634 )
1635 return render(
1636 request,
1637 "dashboard/referencing.html",
1638 {
1639 "journal": journal,
1640 "colid": request.POST.get("selectCol"),
1641 "optSite": request.POST.get("optSite"),
1642 },
1643 )
1646class ReferencingWosFileView(View):
1647 template_name = "dashboard/referencing.html"
1649 def post(self, request, *args, **kwargs):
1650 colid = request.POST["colid"]
1651 if request.FILES.get("risfile") is None:
1652 message = "No file uploaded"
1653 return render(
1654 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1655 )
1656 uploaded_file = request.FILES["risfile"]
1657 comp = ReferencingCheckerWos()
1658 journal = comp.check_references(colid, uploaded_file)
1659 return render(request, self.template_name, {"journal": journal})
1662class ReferencingDashboardView(BaseMersenneDashboardView):
1663 template_name = "dashboard/referencing.html"
1665 def get(self, request, *args, **kwargs):
1666 colid = self.kwargs.get("colid", None)
1667 comp = ReferencingCheckerAds()
1668 journal = comp.check_references(colid)
1669 return render(request, self.template_name, {"journal": journal})
1672class BaseCollectionView(TemplateView):
1673 def get_context_data(self, **kwargs):
1674 context = super().get_context_data(**kwargs)
1675 aid = context.get("aid")
1676 year = context.get("year")
1678 if aid and year:
1679 context["collection"] = self.get_collection(aid, year)
1681 return context
1683 def get_collection(self, aid, year):
1684 """Method to be overridden by subclasses to fetch the appropriate collection"""
1685 raise NotImplementedError("Subclasses must implement get_collection method")
1688class ArticleListView(BaseCollectionView):
1689 template_name = "collection-list.html"
1691 def get_collection(self, aid, year):
1692 return Article.objects.filter(
1693 Q(my_container__my_collection__pid=aid)
1694 & (
1695 Q(date_published__year=year, date_online_first__isnull=True)
1696 | Q(date_online_first__year=year)
1697 )
1698 ).prefetch_related("resourcecount_set")
1701class VolumeListView(BaseCollectionView):
1702 template_name = "collection-list.html"
1704 def get_collection(self, aid, year):
1705 return Article.objects.filter(
1706 Q(my_container__my_collection__pid=aid, my_container__fyear=year)
1707 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1708 ).prefetch_related("resourcecount_set")
1711class DOAJResourceRegisterView(View):
1712 def get(self, request, *args, **kwargs):
1713 pid = kwargs.get("pid", None)
1714 resource = model_helpers.get_resource(pid)
1715 if resource is None:
1716 raise Http404
1717 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1718 resource.colid, None
1719 ):
1720 raise RuntimeError("Pending publications should not be deployed")
1722 try:
1723 data = {}
1724 doaj_meta, response = doaj_pid_register(pid)
1725 if response is None:
1726 return HttpResponse(status=204)
1727 elif doaj_meta and 200 <= response.status_code <= 299:
1728 data.update(doaj_meta)
1729 else:
1730 return HttpResponse(status=response.status_code, reason=response.text)
1731 except Timeout as exception:
1732 return HttpResponse(exception, status=408)
1733 except Exception as exception:
1734 return HttpResponseServerError(exception)
1735 return JsonResponse(data)
1738class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View):
1739 """
1740 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml
1741 """
1743 def get(self, request, *args, **kwargs):
1744 pid = kwargs.get("pid")
1745 if not pid:
1746 raise Http404("Missing pid")
1748 article = Article.objects.filter(pid=pid).first()
1749 if not article:
1750 raise Http404(f"Article not found: {pid}")
1752 colid = article.get_collection().pid
1753 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
1754 return JsonResponse(
1755 {"status": 403, "message": f"Tex conversions are disabled in {colid}"}
1756 )
1758 if is_tex_conversion_locked(pid):
1759 logger.warning("Conversion rejected (lock exists) for %s", pid)
1760 return JsonResponse(
1761 {"status": 409, "message": f"A conversion is already running for {pid}"}
1762 )
1764 logger.info("No lock → scheduling conversion for %s", pid)
1766 try:
1767 convert_article_tex.delay(pid=pid, user_pk=request.user.pk)
1768 except Exception:
1769 logger.exception("Failed to enqueue task for %s", pid)
1770 release_tex_conversion_lock(pid)
1771 raise
1773 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"})
1776class CROSSREFResourceRegisterView(View):
1777 def get(self, request, *args, **kwargs):
1778 pid = kwargs.get("pid", None)
1779 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1780 force = kwargs.get("force", None)
1781 if not request.user.is_superuser:
1782 force = None
1784 resource = model_helpers.get_resource(pid)
1785 if resource is None:
1786 raise Http404
1788 resource = resource.cast()
1789 meth = getattr(self, "recordDOI" + resource.classname)
1790 try:
1791 data = meth(resource, force)
1792 except Timeout as exception:
1793 return HttpResponse(exception, status=408)
1794 except Exception as exception:
1795 return HttpResponseServerError(exception)
1796 return JsonResponse(data)
1798 def recordDOIArticle(self, article: "Article", force=None):
1799 result = {"status": 404}
1800 if (
1801 article.doi
1802 and not article.do_not_publish
1803 and (article.date_published or article.date_online_first or force == "force")
1804 ):
1805 if article.my_container.fyear == 0:
1806 article.my_container.fyear = datetime.now().year
1807 result = recordDOI(article)
1808 return result
1810 def recordDOICollection(self, collection, force=None):
1811 return recordDOI(collection)
1813 def recordDOIContainer(self, container, force=None):
1814 data = {"status": 200, "message": "All DOI successfully checked"}
1816 if container.ctype == "issue":
1817 if container.doi:
1818 result = recordDOI(container)
1819 if result["status"] != 200:
1820 return result
1821 if force == "force":
1822 articles = container.article_set.exclude(
1823 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1824 )
1825 else:
1826 articles = container.article_set.exclude(
1827 doi__isnull=True,
1828 do_not_publish=True,
1829 date_published__isnull=True,
1830 date_online_first__isnull=True,
1831 )
1833 for article in articles:
1834 result = self.recordDOIArticle(article, force)
1835 if result["status"] != 200:
1836 data = result
1837 else:
1838 return recordDOI(container)
1839 return data
1842class CROSSREFResourceCheckStatusView(View):
1843 def get(self, request, *args, **kwargs):
1844 pid = kwargs.get("pid", None)
1845 resource = model_helpers.get_resource(pid)
1846 if resource is None:
1847 raise Http404
1848 resource = resource.cast()
1849 meth = getattr(self, "checkDOI" + resource.classname)
1850 try:
1851 meth(resource)
1852 except Timeout as exception:
1853 return HttpResponse(exception, status=408)
1854 except Exception as exception:
1855 return HttpResponseServerError(exception)
1857 data = {"status": 200, "message": "DOI successfully checked"}
1858 return JsonResponse(data)
1860 def checkDOIArticle(self, article: "Article"):
1861 if article.my_container.fyear == 0:
1862 article.my_container.fyear = datetime.now().year
1863 checkDOI(article)
1865 def checkDOICollection(self, collection):
1866 checkDOI(collection)
1868 def checkDOIContainer(self, container):
1869 if container.doi is not None:
1870 checkDOI(container)
1871 for article in container.article_set.all():
1872 self.checkDOIArticle(article)
1875class CROSSREFResourcePendingPublicationRegisterView(View):
1876 def get(self, request, *args, **kwargs):
1877 pid = kwargs.get("pid", None)
1878 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1880 resource = model_helpers.get_resource(pid)
1881 if resource is None:
1882 raise Http404
1884 resource = resource.cast()
1885 meth = getattr(self, "recordPendingPublication" + resource.classname)
1886 try:
1887 data = meth(resource)
1888 except Timeout as exception:
1889 return HttpResponse(exception, status=408)
1890 except Exception as exception:
1891 return HttpResponseServerError(exception)
1892 return JsonResponse(data)
1894 def recordPendingPublicationArticle(self, article):
1895 result = {"status": 404}
1896 if article.doi and not article.date_published and not article.date_online_first:
1897 if article.my_container.fyear is None or article.my_container.fyear == "0":
1898 article.my_container.fyear = datetime.now().year
1899 result = recordPendingPublication(article)
1900 return result
1903class RegisterPubmedFormView(FormView):
1904 template_name = "record_pubmed_dialog.html"
1905 form_class = RegisterPubmedForm
1907 def get_context_data(self, **kwargs):
1908 context = super().get_context_data(**kwargs)
1909 context["pid"] = self.kwargs["pid"]
1910 context["helper"] = PtfLargeModalFormHelper
1911 return context
1914class RegisterPubmedView(View):
1915 def get(self, request, *args, **kwargs):
1916 pid = kwargs.get("pid", None)
1917 update_article = self.request.GET.get("update_article", "on") == "on"
1919 article = model_helpers.get_article(pid)
1920 if article is None:
1921 raise Http404
1922 try:
1923 recordPubmed(article, update_article)
1924 except Exception as exception:
1925 messages.error("Unable to register the article in PubMed")
1926 return HttpResponseServerError(exception)
1928 return HttpResponseRedirect(
1929 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1930 )
1933class PTFToolsContainerView(TemplateView):
1934 template_name = ""
1936 def get_context_data(self, **kwargs):
1937 context = super().get_context_data(**kwargs)
1939 container = model_helpers.get_container(self.kwargs.get("pid"))
1940 if container is None:
1941 raise Http404
1942 citing_articles = container.citations()
1943 source = self.request.GET.get("source", None)
1944 if container.ctype.startswith("book"):
1945 book_parts = (
1946 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1947 )
1948 references = False
1949 if container.ctype == "book-monograph":
1950 # on regarde si il y a au moins une bibliographie
1951 for art in container.article_set.all():
1952 if art.bibitem_set.count() > 0:
1953 references = True
1954 context.update(
1955 {
1956 "book": container,
1957 "book_parts": list(book_parts),
1958 "source": source,
1959 "citing_articles": citing_articles,
1960 "references": references,
1961 "test_website": container.get_top_collection()
1962 .extlink_set.get(rel="test_website")
1963 .location,
1964 "prod_website": container.get_top_collection()
1965 .extlink_set.get(rel="website")
1966 .location,
1967 }
1968 )
1969 self.template_name = "book-toc.html"
1970 else:
1971 articles = container.article_set.all().order_by("seq")
1972 for article in articles:
1973 try:
1974 last_match = (
1975 history_models.HistoryEvent.objects.filter(
1976 pid=article.pid,
1977 type="matching",
1978 )
1979 .only("created_on")
1980 .latest("created_on")
1981 )
1982 except history_models.HistoryEvent.DoesNotExist as _:
1983 article.last_match = None
1984 else:
1985 article.last_match = last_match.created_on
1987 # article1 = articles.first()
1988 # date = article1.deployed_date()
1989 # TODO next_issue, previous_issue
1991 # check DOI est maintenant une commande à part
1992 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
1993 # articlesWithStatus = []
1994 # for article in articles:
1995 # checkDOIExistence(article)
1996 # articlesWithStatus.append(article)
1998 test_location = prod_location = ""
1999 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
2000 if qs:
2001 test_location = qs.first().location
2002 qs = container.get_top_collection().extlink_set.filter(rel="website")
2003 if qs:
2004 prod_location = qs.first().location
2005 context.update(
2006 {
2007 "issue": container,
2008 "articles": articles,
2009 "source": source,
2010 "citing_articles": citing_articles,
2011 "test_website": test_location,
2012 "prod_website": prod_location,
2013 }
2014 )
2016 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
2017 context["is_issue_pending_publication"] = True
2018 if container.get_top_collection().pid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
2019 context["is_excluded_from_tex_conversion"] = True
2020 self.template_name = "issue-items.html"
2022 context["allow_crossref"] = container.allow_crossref()
2023 context["coltype"] = container.my_collection.coltype
2024 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(container)
2025 return context
2028class ExtLinkInline(InlineFormSetFactory):
2029 model = ExtLink
2030 form_class = ExtLinkForm
2031 factory_kwargs = {"extra": 0}
2034class ResourceIdInline(InlineFormSetFactory):
2035 model = ResourceId
2036 form_class = ResourceIdForm
2037 factory_kwargs = {"extra": 0}
2040class IssueDetailAPIView(View):
2041 def get(self, request, *args, **kwargs):
2042 issue = get_object_or_404(Container, pid=kwargs["pid"])
2043 deployed_date = issue.deployed_date()
2044 result = {
2045 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2046 if deployed_date
2047 else None,
2048 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2049 "all_doi_are_registered": issue.all_doi_are_registered(),
2050 "registered_in_doaj": issue.registered_in_doaj(),
2051 "doi": issue.my_collection.doi,
2052 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2053 }
2054 try:
2055 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2056 except history_models.HistoryEvent.DoesNotExist as _:
2057 pass
2058 else:
2059 result["latest"] = latest.message
2060 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2061 "%Y-%m-%d %H:%M"
2062 )
2064 result["latest_type"] = latest.type.capitalize()
2065 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2066 try:
2067 result[event_type] = timezone.localtime(
2068 history_models.HistoryEvent.objects.filter(
2069 type=event_type,
2070 status="OK",
2071 pid__startswith=issue.pid,
2072 )
2073 .latest("created_on")
2074 .created_on
2075 ).strftime("%Y-%m-%d %H:%M")
2076 except history_models.HistoryEvent.DoesNotExist as _:
2077 result[event_type] = ""
2078 return JsonResponse(result)
2081class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2082 model = Collection
2083 form_class = CollectionForm
2084 inlines = [ResourceIdInline, ExtLinkInline]
2085 inlines_names = ["resource_ids_form", "ext_links_form"]
2087 def get_context_data(self, **kwargs):
2088 context = super().get_context_data(**kwargs)
2089 context["helper"] = PtfFormHelper
2090 context["formset_helper"] = FormSetHelper
2091 return context
2093 def add_description(self, collection, description, lang, seq):
2094 if description:
2095 la = Abstract(
2096 resource=collection,
2097 tag="description",
2098 lang=lang,
2099 seq=seq,
2100 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2101 value_html=description,
2102 value_tex=description,
2103 )
2104 la.save()
2106 def form_valid(self, form):
2107 if form.instance.abbrev:
2108 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2109 else:
2110 form.instance.title_xml = (
2111 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2112 )
2114 form.instance.title_html = form.instance.title_tex
2115 form.instance.title_sort = form.instance.title_tex
2116 result = super().form_valid(form)
2118 collection = self.object
2119 collection.abstract_set.all().delete()
2121 seq = 1
2122 description = form.cleaned_data["description_en"]
2123 if description:
2124 self.add_description(collection, description, "en", seq)
2125 seq += 1
2126 description = form.cleaned_data["description_fr"]
2127 if description:
2128 self.add_description(collection, description, "fr", seq)
2130 return result
2132 def get_success_url(self):
2133 messages.success(
2134 self.request, f'The collection "{self.object.pid}" has been successfully updated'
2135 )
2136 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2139class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2140 """
2141 Warning : Not yet finished
2142 Automatic site membership creation is still missing
2143 """
2146class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2147 slug_field = "pid"
2148 slug_url_kwarg = "pid"
2151def suggest_load_journal_dois(colid):
2152 articles = (
2153 Article.objects.filter(my_container__my_collection__pid=colid)
2154 .filter(doi__isnull=False)
2155 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2156 .values_list("doi", flat=True)
2157 )
2159 try:
2160 articles = sorted(
2161 articles,
2162 key=lambda d: (
2163 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2164 int(re.search(r".(\d+)$", d).group(1)),
2165 ),
2166 )
2167 except: # noqa: E722 (we'll look later)
2168 pass
2169 return [f'<option value="{doi}">' for doi in articles]
2172def get_context_with_volumes(journal):
2173 result = model_helpers.get_volumes_in_collection(journal)
2174 volume_count = result["volume_count"]
2175 collections = []
2176 for ancestor in journal.ancestors.all():
2177 item = model_helpers.get_volumes_in_collection(ancestor)
2178 volume_count = max(0, volume_count)
2179 item.update({"journal": ancestor})
2180 collections.append(item)
2182 # add the parent collection to its children list and sort it by date
2183 result.update({"journal": journal})
2184 collections.append(result)
2186 collections = [c for c in collections if c["sorted_issues"]]
2187 collections.sort(
2188 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2189 reverse=True,
2190 )
2192 context = {
2193 "journal": journal,
2194 "sorted_issues": result["sorted_issues"],
2195 "volume_count": volume_count,
2196 "max_width": result["max_width"],
2197 "collections": collections,
2198 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2199 }
2200 return context
2203class CollectionDetail(
2204 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2205):
2206 model = Collection
2207 slug_field = "pid"
2208 slug_url_kwarg = "pid"
2209 template_name = "ptf/collection_detail.html"
2211 def test_func(self):
2212 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2214 def get(self, request, *args, **kwargs):
2215 self.object = self.get_object(queryset=Collection.objects.all())
2216 return super().get(request, *args, **kwargs)
2218 def get_context_data(self, **kwargs):
2219 context = super().get_context_data(**kwargs)
2220 context["object_list"] = context["object_list"].filter(
2221 Q(ctype="issue") | Q(ctype="book-lecture-notes") | Q(ctype="book-monograph")
2222 )
2223 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2224 context.update(get_context_with_volumes(self.object))
2226 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2227 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2228 context["issue_to_appear"] = Container.objects.filter(
2229 pid=context["issue_to_appear_pid"]
2230 ).exists()
2231 try:
2232 latest_error = history_models.HistoryEvent.objects.filter(
2233 status="ERROR", col=self.object
2234 ).latest("created_on")
2235 except history_models.HistoryEvent.DoesNotExist as _:
2236 pass
2237 else:
2238 message = latest_error.message
2239 if message:
2240 i = message.find(" - ")
2241 latest_exception = message[:i]
2242 latest_error_message = message[i + 3 :]
2243 context["latest_exception"] = latest_exception
2244 context["latest_exception_date"] = latest_error.created_on
2245 context["latest_exception_type"] = latest_error.type
2246 context["latest_error_message"] = latest_error_message
2248 archive_in_error = history_models.HistoryEvent.objects.filter(
2249 status="ERROR", col=self.object, type="archive"
2250 ).exists()
2252 context["archive_in_error"] = archive_in_error
2254 return context
2256 def get_queryset(self):
2257 query = self.object.content.all()
2259 for ancestor in self.object.ancestors.all():
2260 query |= ancestor.content.all()
2262 return query.order_by("-fyear", "-vseries", "-volume", "-volume_int", "-number_int")
2265class ContainerEditView(FormView):
2266 template_name = "container_form.html"
2267 form_class = ContainerForm
2269 def get_success_url(self):
2270 if self.kwargs["pid"]:
2271 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2272 return reverse("mersenne_dashboard/published_articles")
2274 def set_success_message(self): # pylint: disable=no-self-use
2275 messages.success(self.request, "Booklet updated")
2277 def get_form_kwargs(self):
2278 kwargs = super().get_form_kwargs()
2279 if "pid" not in self.kwargs:
2280 self.kwargs["pid"] = None
2281 if "colid" not in self.kwargs:
2282 self.kwargs["colid"] = None
2283 if "data" in kwargs and "colid" in kwargs["data"]:
2284 # colid is passed as a hidden param in the form.
2285 # It is used when you submit a new container
2286 self.kwargs["colid"] = kwargs["data"]["colid"]
2288 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2289 self.kwargs["pid"]
2290 )
2291 return kwargs
2293 def get_context_data(self, **kwargs):
2294 context = super().get_context_data(**kwargs)
2296 context["pid"] = self.kwargs["pid"]
2297 context["colid"] = self.kwargs["colid"]
2298 context["container"] = self.kwargs["container"]
2300 context["edit_container"] = context["pid"] is not None
2301 context["name"] = resolve(self.request.path_info).url_name
2303 return context
2305 def form_valid(self, form):
2306 new_pid = form.cleaned_data.get("pid")
2307 new_title = form.cleaned_data.get("title")
2308 new_trans_title = form.cleaned_data.get("trans_title")
2309 new_publisher = form.cleaned_data.get("publisher")
2310 new_year = form.cleaned_data.get("year")
2311 new_volume = form.cleaned_data.get("volume")
2312 new_number = form.cleaned_data.get("number")
2314 collection = None
2315 issue = self.kwargs["container"]
2316 if issue is not None:
2317 collection = issue.my_collection
2318 elif self.kwargs["colid"] is not None:
2319 if "CR" in self.kwargs["colid"]:
2320 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2321 else:
2322 collection = model_helpers.get_collection(self.kwargs["colid"])
2324 if collection is None:
2325 raise ValueError("Collection for " + new_pid + " does not exist")
2327 # Icon
2328 new_icon_location = ""
2329 if "icon" in self.request.FILES:
2330 filename = os.path.basename(self.request.FILES["icon"].name)
2331 file_extension = filename.split(".")[1]
2333 icon_filename = resolver.get_disk_location(
2334 settings.MERSENNE_TEST_DATA_FOLDER,
2335 collection.pid,
2336 file_extension,
2337 new_pid,
2338 None,
2339 True,
2340 )
2342 with open(icon_filename, "wb+") as destination:
2343 for chunk in self.request.FILES["icon"].chunks():
2344 destination.write(chunk)
2346 folder = resolver.get_relative_folder(collection.pid, new_pid)
2347 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2348 name = resolve(self.request.path_info).url_name
2349 if name == "special_issue_create":
2350 self.kwargs["name"] = name
2351 if self.kwargs["container"]:
2352 # Edit Issue
2353 issue = self.kwargs["container"]
2354 if issue is None:
2355 raise ValueError(self.kwargs["pid"] + " does not exist")
2357 issue.pid = new_pid
2358 issue.title_tex = issue.title_html = new_title
2359 issue.title_xml = build_title_xml(
2360 title=new_title,
2361 lang=issue.lang,
2362 title_type="issue-title",
2363 )
2365 trans_lang = ""
2366 if new_trans_title != "":
2367 trans_lang = "fr" if issue.lang == "en" else "en"
2369 if trans_lang != "" and new_trans_title != "":
2370 title_xml = build_title_xml(
2371 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2372 )
2374 issue.title_set.update_or_create(
2375 lang=trans_lang,
2376 type="main",
2377 defaults={"title_html": new_trans_title, "title_xml": title_xml},
2378 )
2380 issue.fyear = new_year
2381 issue.volume = new_volume
2382 issue.volume_int = make_int(new_volume)
2383 issue.number = new_number
2384 issue.number_int = make_int(new_number)
2385 issue.save()
2386 else:
2387 xissue = create_issuedata()
2389 xissue.ctype = "issue"
2390 xissue.pid = new_pid
2391 xissue.lang = "en"
2392 xissue.title_tex = new_title
2393 xissue.title_html = new_title
2394 xissue.title_xml = build_title_xml(
2395 title=new_title, lang=xissue.lang, title_type="issue-title"
2396 )
2398 if new_trans_title != "":
2399 trans_lang = "fr"
2400 title_xml = build_title_xml(
2401 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2402 )
2403 title = create_titledata(
2404 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2405 )
2406 issue.titles = [title]
2408 xissue.fyear = new_year
2409 xissue.volume = new_volume
2410 xissue.number = new_number
2411 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2413 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2414 cmd.add_collection(collection)
2415 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2416 issue = cmd.do()
2418 self.kwargs["pid"] = new_pid
2420 # Add objects related to the article: contribs, datastream, counts...
2421 params = {
2422 "icon_location": new_icon_location,
2423 }
2424 cmd = ptf_cmds.updateContainerPtfCmd(params)
2425 cmd.set_resource(issue)
2426 cmd.do()
2428 publisher = model_helpers.get_publisher(new_publisher)
2429 if not publisher:
2430 xpub = create_publisherdata()
2431 xpub.name = new_publisher
2432 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2433 issue.my_publisher = publisher
2434 issue.save()
2436 self.set_success_message()
2438 return super().form_valid(form)
2441# class ArticleEditView(FormView):
2442# template_name = 'article_form.html'
2443# form_class = ArticleForm
2444#
2445# def get_success_url(self):
2446# if self.kwargs['pid']:
2447# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2448# return reverse('mersenne_dashboard/published_articles')
2449#
2450# def set_success_message(self): # pylint: disable=no-self-use
2451# messages.success(self.request, "L'article a été modifié")
2452#
2453# def get_form_kwargs(self):
2454# kwargs = super(ArticleEditView, self).get_form_kwargs()
2455#
2456# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2457# # Article creation: pid is None
2458# self.kwargs['pid'] = None
2459# if 'issue_id' not in self.kwargs:
2460# # Article edit: issue_id is not passed
2461# self.kwargs['issue_id'] = None
2462# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2463# # colid is passed as a hidden param in the form.
2464# # It is used when you submit a new container
2465# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2466#
2467# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2468# return kwargs
2469#
2470# def get_context_data(self, **kwargs):
2471# context = super(ArticleEditView, self).get_context_data(**kwargs)
2472#
2473# context['pid'] = self.kwargs['pid']
2474# context['issue_id'] = self.kwargs['issue_id']
2475# context['article'] = self.kwargs['article']
2476#
2477# context['edit_article'] = context['pid'] is not None
2478#
2479# article = context['article']
2480# if article:
2481# context['author_contributions'] = article.get_author_contributions()
2482# context['kwds_fr'] = None
2483# context['kwds_en'] = None
2484# kwd_gps = article.get_non_msc_kwds()
2485# for kwd_gp in kwd_gps:
2486# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2487# if kwd_gp.value_xml:
2488# kwd_ = types.SimpleNamespace()
2489# kwd_.value = kwd_gp.value_tex
2490# context['kwd_unstructured_fr'] = kwd_
2491# context['kwds_fr'] = kwd_gp.kwd_set.all()
2492# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2493# if kwd_gp.value_xml:
2494# kwd_ = types.SimpleNamespace()
2495# kwd_.value = kwd_gp.value_tex
2496# context['kwd_unstructured_en'] = kwd_
2497# context['kwds_en'] = kwd_gp.kwd_set.all()
2498#
2499# # Article creation: init pid
2500# if context['issue_id'] and context['pid'] is None:
2501# issue = model_helpers.get_container(context['issue_id'])
2502# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2503#
2504# return context
2505#
2506# def form_valid(self, form):
2507#
2508# new_pid = form.cleaned_data.get('pid')
2509# new_title = form.cleaned_data.get('title')
2510# new_fpage = form.cleaned_data.get('fpage')
2511# new_lpage = form.cleaned_data.get('lpage')
2512# new_page_range = form.cleaned_data.get('page_range')
2513# new_page_count = form.cleaned_data.get('page_count')
2514# new_coi_statement = form.cleaned_data.get('coi_statement')
2515# new_show_body = form.cleaned_data.get('show_body')
2516# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2517#
2518# # TODO support MathML
2519# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2520# # We need to pass trans_title to get_title_xml
2521# # Meanwhile, ignore new_title_xml
2522# new_title_xml = jats_parser.get_title_xml(new_title)
2523# new_title_html = new_title
2524#
2525# authors_count = int(self.request.POST.get('authors_count', "0"))
2526# i = 1
2527# new_authors = []
2528# old_author_contributions = []
2529# if self.kwargs['article']:
2530# old_author_contributions = self.kwargs['article'].get_author_contributions()
2531#
2532# while authors_count > 0:
2533# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2534#
2535# if prefix is not None:
2536# addresses = []
2537# if len(old_author_contributions) >= i:
2538# old_author_contribution = old_author_contributions[i - 1]
2539# addresses = [contrib_address.address for contrib_address in
2540# old_author_contribution.get_addresses()]
2541#
2542# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2543# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2544# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2545# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2546# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2547# deceased_before_publication = deceased == 'on'
2548# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2549# equal_contrib = equal_contrib == 'on'
2550# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2551# corresponding = corresponding == 'on'
2552# email = self.request.POST.get('email-' + str(i), None)
2553#
2554# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2555# params['deceased_before_publication'] = deceased_before_publication
2556# params['equal_contrib'] = equal_contrib
2557# params['corresponding'] = corresponding
2558# params['addresses'] = addresses
2559# params['email'] = email
2560#
2561# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2562#
2563# new_authors.append(params)
2564#
2565# authors_count -= 1
2566# i += 1
2567#
2568# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2569# i = 1
2570# new_kwds_fr = []
2571# while kwds_fr_count > 0:
2572# value = self.request.POST.get('kwd-fr-' + str(i), None)
2573# new_kwds_fr.append(value)
2574# kwds_fr_count -= 1
2575# i += 1
2576# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2577#
2578# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2579# i = 1
2580# new_kwds_en = []
2581# while kwds_en_count > 0:
2582# value = self.request.POST.get('kwd-en-' + str(i), None)
2583# new_kwds_en.append(value)
2584# kwds_en_count -= 1
2585# i += 1
2586# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2587#
2588# if self.kwargs['article']:
2589# # Edit article
2590# container = self.kwargs['article'].my_container
2591# else:
2592# # New article
2593# container = model_helpers.get_container(self.kwargs['issue_id'])
2594#
2595# if container is None:
2596# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2597#
2598# collection = container.my_collection
2599#
2600# # Copy PDF file & extract full text
2601# body = ''
2602# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2603# collection.pid,
2604# "pdf",
2605# container.pid,
2606# new_pid,
2607# True)
2608# if 'pdf' in self.request.FILES:
2609# with open(pdf_filename, 'wb+') as destination:
2610# for chunk in self.request.FILES['pdf'].chunks():
2611# destination.write(chunk)
2612#
2613# # Extract full text from the PDF
2614# body = utils.pdf_to_text(pdf_filename)
2615#
2616# # Icon
2617# new_icon_location = ''
2618# if 'icon' in self.request.FILES:
2619# filename = os.path.basename(self.request.FILES['icon'].name)
2620# file_extension = filename.split('.')[1]
2621#
2622# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2623# collection.pid,
2624# file_extension,
2625# container.pid,
2626# new_pid,
2627# True)
2628#
2629# with open(icon_filename, 'wb+') as destination:
2630# for chunk in self.request.FILES['icon'].chunks():
2631# destination.write(chunk)
2632#
2633# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2634# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2635#
2636# if self.kwargs['article']:
2637# # Edit article
2638# article = self.kwargs['article']
2639# article.fpage = new_fpage
2640# article.lpage = new_lpage
2641# article.page_range = new_page_range
2642# article.coi_statement = new_coi_statement
2643# article.show_body = new_show_body
2644# article.do_not_publish = new_do_not_publish
2645# article.save()
2646#
2647# else:
2648# # New article
2649# params = {
2650# 'pid': new_pid,
2651# 'title_xml': new_title_xml,
2652# 'title_html': new_title_html,
2653# 'title_tex': new_title,
2654# 'fpage': new_fpage,
2655# 'lpage': new_lpage,
2656# 'page_range': new_page_range,
2657# 'seq': container.article_set.count() + 1,
2658# 'body': body,
2659# 'coi_statement': new_coi_statement,
2660# 'show_body': new_show_body,
2661# 'do_not_publish': new_do_not_publish
2662# }
2663#
2664# xarticle = create_articledata()
2665# xarticle.pid = new_pid
2666# xarticle.title_xml = new_title_xml
2667# xarticle.title_html = new_title_html
2668# xarticle.title_tex = new_title
2669# xarticle.fpage = new_fpage
2670# xarticle.lpage = new_lpage
2671# xarticle.page_range = new_page_range
2672# xarticle.seq = container.article_set.count() + 1
2673# xarticle.body = body
2674# xarticle.coi_statement = new_coi_statement
2675# params['xobj'] = xarticle
2676#
2677# cmd = ptf_cmds.addArticlePtfCmd(params)
2678# cmd.set_container(container)
2679# cmd.add_collection(container.my_collection)
2680# article = cmd.do()
2681#
2682# self.kwargs['pid'] = new_pid
2683#
2684# # Add objects related to the article: contribs, datastream, counts...
2685# params = {
2686# # 'title_xml': new_title_xml,
2687# # 'title_html': new_title_html,
2688# # 'title_tex': new_title,
2689# 'authors': new_authors,
2690# 'page_count': new_page_count,
2691# 'icon_location': new_icon_location,
2692# 'body': body,
2693# 'use_kwds': True,
2694# 'kwds_fr': new_kwds_fr,
2695# 'kwds_en': new_kwds_en,
2696# 'kwd_uns_fr': new_kwd_uns_fr,
2697# 'kwd_uns_en': new_kwd_uns_en
2698# }
2699# cmd = ptf_cmds.updateArticlePtfCmd(params)
2700# cmd.set_article(article)
2701# cmd.do()
2702#
2703# self.set_success_message()
2704#
2705# return super(ArticleEditView, self).form_valid(form)
2708@require_http_methods(["POST"])
2709def do_not_publish_article(request, *args, **kwargs):
2710 next = request.headers.get("referer")
2712 pid = kwargs.get("pid", "")
2714 article = model_helpers.get_article(pid)
2715 if article:
2716 article.do_not_publish = not article.do_not_publish
2717 article.save()
2718 else:
2719 raise Http404
2721 return HttpResponseRedirect(next)
2724@require_http_methods(["POST"])
2725def show_article_body(request, *args, **kwargs):
2726 next = request.headers.get("referer")
2728 pid = kwargs.get("pid", "")
2730 article = model_helpers.get_article(pid)
2731 if article:
2732 article.show_body = not article.show_body
2733 article.save()
2734 else:
2735 raise Http404
2737 return HttpResponseRedirect(next)
2740class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2741 """
2742 API to get/post article metadata
2743 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2744 """
2746 def __init__(self, *args, **kwargs):
2747 """
2748 we define here what fields we want in the form
2749 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2750 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2751 super().__init__(*args, **kwargs)
2752 self.fields_to_update = [
2753 "lang",
2754 "atype",
2755 "contributors",
2756 "abstracts",
2757 "kwds",
2758 "titles",
2759 "title_html",
2760 "title_xml",
2761 "title_tex",
2762 "streams",
2763 "ext_links",
2764 "date_accepted",
2765 "history_dates",
2766 "subjs",
2767 "bibitems",
2768 "references",
2769 ]
2770 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2771 self.additional_fields = [
2772 "doi",
2773 "pid",
2774 "container_pid",
2775 "pdf",
2776 "illustration",
2777 "dates",
2778 "msc_keywords",
2779 ]
2780 self.editorial_tools = [
2781 "translation",
2782 "sidebar",
2783 "lang_selection",
2784 "back_to_article_option",
2785 "msc_keywords",
2786 ]
2787 self.article_container_pid = ""
2788 self.back_url = "trammel"
2790 def save_data(self, data_article):
2791 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2792 # The icons are not preserved since we can add/edit/delete them in VueJs
2793 params = {
2794 "pid": data_article.pid,
2795 "export_folder": settings.MERSENNE_TMP_FOLDER,
2796 "export_all": True,
2797 "with_binary_files": False,
2798 }
2799 ptf_cmds.exportExtraDataPtfCmd(params).do()
2801 def restore_data(self, article):
2802 ptf_cmds.importExtraDataPtfCmd(
2803 {
2804 "pid": article.pid,
2805 "import_folder": settings.MERSENNE_TMP_FOLDER,
2806 "import_bibitemid": False,
2807 }
2808 ).do()
2810 def get(self, request, *args, **kwargs):
2811 user_role = request.user.roles.all()
2812 if user_role:
2813 new_fields_to_update = []
2814 new_additional_fields = []
2815 new_editorial_tools = []
2816 # if a user have > 1 role, fields accessible shall be cumulative
2817 for role in user_role:
2818 role_fields_to_updates, role_additional_fields = role.get_real_fields_to_update()
2819 new_fields_to_update += role_fields_to_updates
2820 new_additional_fields += role_additional_fields
2821 new_editorial_tools += role.editorial_tools
2822 # in case two roles have some fields in common
2823 self.fields_to_update = list(set(new_fields_to_update))
2824 self.additional_fields = list(set(new_additional_fields))
2825 self.editorial_tools = list(set(new_editorial_tools))
2827 self.additional_fields += ["doi", "pid", "container_pid"]
2828 response = super().get(request, *args, **kwargs)
2829 return response
2831 def post(self, request, *args, **kwargs):
2832 response = super().post(request, *args, **kwargs)
2833 if response.status_code in [200, 302]: 2833 ↛ 2841line 2833 didn't jump to line 2841 because the condition on line 2833 was always true
2834 return redirect(
2835 "api-edit-article",
2836 colid=kwargs.get("colid", ""),
2837 containerPid=kwargs.get("containerPid"),
2838 doi=kwargs.get("doi", ""),
2839 )
2840 else:
2841 raise Http404
2844class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2845 template_name = "article_form.html"
2847 def get_success_url(self):
2848 if self.kwargs["doi"]:
2849 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2850 return reverse("mersenne_dashboard/published_articles")
2852 def get_context_data(self, **kwargs):
2853 context = super().get_context_data(**kwargs)
2854 if "doi" in self.kwargs:
2855 article = model_helpers.get_article_by_doi(self.kwargs["doi"])
2856 context["article"] = article
2857 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(article)
2858 context["pid"] = context["article"].pid
2860 context["container_pid"] = kwargs.get("container_pid", "")
2861 return context
2864class ArticleDeleteView(View):
2865 def get(self, request, *args, **kwargs):
2866 pid = self.kwargs.get("pid", None)
2867 article = get_object_or_404(Article, pid=pid)
2869 try:
2870 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2871 article.undeploy(mersenneSite)
2873 cmd = ptf_cmds.addArticlePtfCmd(
2874 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2875 )
2876 cmd.set_container(article.my_container)
2877 cmd.set_object_to_be_deleted(article)
2878 cmd.undo()
2879 except Exception as exception:
2880 return HttpResponseServerError(exception)
2882 data = {"message": "Article successfully removed from Trammel", "status": 200}
2883 return JsonResponse(data)
2886def get_messages_in_queue():
2887 app = Celery("ptf-tools")
2888 # tasks = list(current_app.tasks)
2889 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2890 print(tasks)
2891 # i = app.control.inspect()
2893 with app.connection_or_acquire() as conn:
2894 remaining = conn.default_channel.queue_declare(
2895 queue="coordinator", passive=True
2896 ).message_count
2897 return remaining
2900class NumdamView(TemplateView, history_views.HistoryContextMixin):
2901 template_name = "numdam.html"
2903 def get_context_data(self, **kwargs):
2904 context = super().get_context_data(**kwargs)
2906 context["objs"] = ResourceInNumdam.objects.all()
2908 pre_issues = []
2909 prod_issues = []
2910 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2911 try:
2912 response = requests.get(url)
2913 if response.status_code == 200:
2914 data = response.json()
2915 if "issues" in data:
2916 pre_issues = data["issues"]
2917 except Exception:
2918 pass
2920 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2921 response = requests.get(url)
2922 if response.status_code == 200:
2923 data = response.json()
2924 if "issues" in data:
2925 prod_issues = data["issues"]
2927 new = sorted(list(set(pre_issues).difference(prod_issues)))
2928 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2929 grouped = [
2930 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2931 ]
2932 grouped_removed = [
2933 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2934 ]
2935 context["added_issues"] = grouped
2936 context["removed_issues"] = grouped_removed
2938 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2939 return context
2942class NumdamArchiveView(RedirectView):
2943 @staticmethod
2944 def reset_task_results():
2945 TaskResult.objects.all().delete()
2947 def get_redirect_url(self, *args, **kwargs):
2948 self.colid = kwargs["colid"]
2950 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2951 return Http404
2953 # we make sure archiving is not already running
2954 # if not get_messages_in_queue():
2955 # self.reset_task_results()
2957 if self.colid == "ALL":
2958 archive_numdam_collections.delay()
2959 else:
2960 archive_numdam_collection.s(self.colid).delay()
2962 return reverse("numdam")
2965class DeployAllNumdamAPIView(View):
2966 def internal_do(self, *args, **kwargs):
2967 pids = []
2969 for obj in ResourceInNumdam.objects.all():
2970 pids.append(obj.pid)
2972 return pids
2974 def get(self, request, *args, **kwargs):
2975 try:
2976 pids, status, message = history_views.execute_and_record_func(
2977 "deploy", "numdam", "ALL", self.internal_do, "numdam"
2978 )
2979 except Exception as exception:
2980 return HttpResponseServerError(exception)
2982 data = {"message": message, "ids": pids, "status": status}
2983 return JsonResponse(data)
2986class NumdamDeleteAPIView(View):
2987 def get(self, request, *args, **kwargs):
2988 pid = self.kwargs.get("pid", None)
2990 try:
2991 obj = ResourceInNumdam.objects.get(pid=pid)
2992 obj.delete()
2993 except Exception as exception:
2994 return HttpResponseServerError(exception)
2996 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
2997 return JsonResponse(data)
3000class ExtIdApiDetail(View):
3001 def get(self, request, *args, **kwargs):
3002 extid = get_object_or_404(
3003 ExtId,
3004 resource__pid=kwargs["pid"],
3005 id_type=kwargs["what"],
3006 )
3007 return JsonResponse(
3008 {
3009 "pk": extid.pk,
3010 "href": extid.get_href(),
3011 "fetch": reverse(
3012 "api-fetch-id",
3013 args=(
3014 extid.resource.pk,
3015 extid.id_value,
3016 extid.id_type,
3017 "extid",
3018 ),
3019 ),
3020 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3021 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3022 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3023 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3024 "is_valid": extid.checked,
3025 }
3026 )
3029class ExtIdFormTemplate(TemplateView):
3030 template_name = "common/externalid_form.html"
3032 def get_context_data(self, **kwargs):
3033 context = super().get_context_data(**kwargs)
3034 context["sequence"] = kwargs["sequence"]
3035 return context
3038class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3039 def get_context_data(self, **kwargs):
3040 context = super().get_context_data(**kwargs)
3041 context["helper"] = PtfFormHelper
3042 return context
3044 def get_success_url(self):
3045 self.post_process()
3046 return self.object.resource.get_absolute_url()
3048 def post_process(self):
3049 model_helpers.post_resource_updated(self.object.resource)
3052class ExtIdCreate(ExtIdFormView, CreateView):
3053 model = ExtId
3054 form_class = ExtIdForm
3056 def get_context_data(self, **kwargs):
3057 context = super().get_context_data(**kwargs)
3058 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3059 return context
3061 def get_initial(self):
3062 initial = super().get_initial()
3063 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3064 return initial
3066 def form_valid(self, form):
3067 form.instance.checked = False
3068 return super().form_valid(form)
3071class ExtIdUpdate(ExtIdFormView, UpdateView):
3072 model = ExtId
3073 form_class = ExtIdForm
3075 def get_context_data(self, **kwargs):
3076 context = super().get_context_data(**kwargs)
3077 context["resource"] = self.object.resource
3078 return context
3081class UpdateTexmfZipAPIView(View):
3082 def get(self, request, *args, **kwargs):
3083 def copy_zip_files(src_folder, dest_folder):
3084 os.makedirs(dest_folder, exist_ok=True)
3086 zip_files = [
3087 os.path.join(src_folder, f)
3088 for f in os.listdir(src_folder)
3089 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3090 ]
3091 for zip_file in zip_files:
3092 resolver.copy_file(zip_file, dest_folder)
3094 # Exceptions: specific zip/gz files
3095 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3096 resolver.copy_file(zip_file, dest_folder)
3098 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3099 resolver.copy_file(zip_file, dest_folder)
3101 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3102 resolver.copy_file(gz_file, dest_folder)
3104 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3106 dest_folder = os.path.join(
3107 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3108 )
3110 try:
3111 copy_zip_files(src_folder, dest_folder)
3112 except Exception as exception:
3113 return HttpResponseServerError(exception)
3115 try:
3116 dest_folder = os.path.join(
3117 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3118 )
3119 copy_zip_files(src_folder, dest_folder)
3120 except Exception as exception:
3121 return HttpResponseServerError(exception)
3123 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3124 return JsonResponse(data)
3127class TrammelTasksProgressView(View):
3128 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3129 """
3130 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3131 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3132 """
3133 task_name = task
3135 def get_event_data():
3136 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3138 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3139 remaining_messages = get_messages_in_queue()
3141 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3142 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3143 failed_tasks = all_tasks.filter(status="FAILURE")
3145 all_tasks_count = all_tasks.count()
3146 success_count = successed_tasks.count()
3147 fail_count = failed_tasks.count()
3149 all_count = all_tasks_count + remaining_messages
3150 remaining_count = all_count - success_count - fail_count
3152 success_rate = int(success_count * 100 / all_count) if all_count else 0
3153 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3154 status = "consuming_queue" if remaining_count != 0 else "polling"
3156 last_task = successed_tasks.first()
3157 last_task = (
3158 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3159 if last_task
3160 else ""
3161 )
3163 # SSE event format
3164 event_data = {
3165 "status": status,
3166 "success_rate": success_rate,
3167 "error_rate": error_rate,
3168 "all_count": all_count,
3169 "remaining_count": remaining_count,
3170 "success_count": success_count,
3171 "fail_count": fail_count,
3172 "last_task": last_task,
3173 }
3175 return event_data
3177 def stream_response(data):
3178 # Send initial response headers
3179 yield f"data: {json.dumps(data)}\n\n"
3181 data = get_event_data()
3182 format = request.GET.get("format", "stream")
3183 if format == "json":
3184 response = JsonResponse(data)
3185 else:
3186 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3187 return response
3190user_signed_up.connect(update_user_from_invite)