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