Coverage for src / ptf_tools / views / base_views.py: 18%
1632 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-09-07 13:13 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-09-07 13:13 +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_success_url(self):
441 if self.colid:
442 return reverse("collection-detail", kwargs={"pid": self.colid})
443 return "/"
445 def get_context_data(self, **kwargs):
446 context = super().get_context_data(**kwargs)
447 context["colid"] = self.colid
448 context["helper"] = PtfLargeModalFormHelper
449 return context
451 def get_form_kwargs(self):
452 kwargs = super().get_form_kwargs()
453 kwargs["colid"] = self.colid
454 return kwargs
456 def form_valid(self, form):
457 try:
458 if not self.colid:
459 raise ValueError("Missing collection id")
461 issue_name = settings.ISSUE_PENDING_PUBLICATION_PIDS.get(self.colid)
462 if not issue_name:
463 raise ValueError(
464 "Issue not found in Pending Publications PIDs. Did you forget to add it?"
465 )
467 issue = model_helpers.get_container(issue_name)
468 if not issue:
469 raise ValueError("No issue found")
471 editflow_xml_file = form.cleaned_data["editflow_xml_file"]
472 body = editflow_xml_file.read().decode("utf-8")
474 cmd = xml_cmds.addArticleXmlCmd(
475 {
476 "body": body,
477 "issue": issue,
478 "assign_doi": True,
479 "standalone": True,
480 "from_folder": settings.RESOURCES_ROOT,
481 }
482 )
483 cmd.set_collection(issue.get_collection())
484 cmd.do()
486 messages.success(
487 self.request,
488 f'Editflow article successfully imported into issue "{issue_name}"',
489 )
491 except Exception as exception:
492 messages.error(
493 self.request,
494 f"Import failed: {exception}",
495 )
496 return super().form_invalid(form)
498 return super().form_valid(form)
501class MatchingAPIView(View):
502 def get(self, request, *args, **kwargs):
503 pid = self.kwargs.get("pid", None)
505 url = settings.MATCHING_URL
506 headers = {"Content-Type": "application/xml"}
508 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
510 if settings.DEBUG:
511 print("Issue exported to /tmp/issue.xml")
512 f = open("/tmp/issue.xml", "w")
513 f.write(body.encode("utf8"))
514 f.close()
516 r = requests.post(url, data=body.encode("utf8"), headers=headers)
517 body = r.text.encode("utf8")
518 data = {"status": r.status_code, "message": body[:1000]}
520 if settings.DEBUG:
521 print("Matching received, new issue exported to /tmp/issue1.xml")
522 f = open("/tmp/issue1.xml", "w")
523 text = body
524 f.write(text)
525 f.close()
527 resource = model_helpers.get_resource(pid)
528 obj = resource.cast()
529 colid = obj.get_collection().pid
531 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
533 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
534 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
535 )
536 cmd.do()
538 print("Matching finished")
539 return JsonResponse(data)
542class ImportAllAPIView(View):
543 def internal_do(self, *args, **kwargs):
544 pid = self.kwargs.get("pid", None)
546 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
547 if not os.path.isdir(root_folder):
548 raise ValueError(root_folder + " does not exist")
550 resource = model_helpers.get_resource(pid)
551 if not resource:
552 file = os.path.join(root_folder, pid + ".xml")
553 body = utils.get_file_content_in_utf8(file)
554 journals = xml_cmds.addCollectionsXmlCmd(
555 {
556 "body": body,
557 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
558 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
559 }
560 ).do()
561 if not journals:
562 raise ValueError(file + " does not contain a collection")
563 resource = journals[0]
564 # resolver.copy_binary_files(
565 # resource,
566 # settings.MATHDOC_ARCHIVE_FOLDER,
567 # settings.MERSENNE_TEST_DATA_FOLDER)
569 obj = resource.cast()
571 if obj.classname != "Collection":
572 raise ValueError(pid + " does not contain a collection")
574 cmd = xml_cmds.collectEntireCollectionXmlCmd(
575 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
576 )
577 pids = cmd.do()
579 return pids
581 def get(self, request, *args, **kwargs):
582 pid = self.kwargs.get("pid", None)
584 try:
585 pids, status, message = history_views.execute_and_record_func(
586 "import", pid, pid, self.internal_do
587 )
588 except Timeout as exception:
589 return HttpResponse(exception, status=408)
590 except Exception as exception:
591 return HttpResponseServerError(exception)
593 data = {"message": message, "ids": pids, "status": status}
594 return JsonResponse(data)
597class DeployAllAPIView(View):
598 def internal_do(self, *args, **kwargs):
599 pid = self.kwargs.get("pid", None)
600 site = self.kwargs.get("site", None)
602 pids = []
604 collection = model_helpers.get_collection(pid)
605 if not collection:
606 raise RuntimeError(pid + " does not exist")
608 if site == "numdam":
609 server_url = settings.NUMDAM_PRE_URL
610 elif site != "ptf_tools":
611 server_url = getattr(collection, site)()
612 if not server_url:
613 raise RuntimeError("The collection has no " + site)
615 if site != "ptf_tools":
616 # check if the collection exists on the server
617 # if not, check_collection will upload the collection (XML,
618 # image...)
619 check_collection(collection, server_url, site)
621 for issue in collection.content.all():
622 if site != "website" or (site == "website" and issue.are_all_articles_published()):
623 pids.append(issue.pid)
625 return pids
627 def get(self, request, *args, **kwargs):
628 pid = self.kwargs.get("pid", None)
629 site = self.kwargs.get("site", None)
631 try:
632 pids, status, message = history_views.execute_and_record_func(
633 "deploy", pid, pid, self.internal_do, site
634 )
635 except Timeout as exception:
636 return HttpResponse(exception, status=408)
637 except Exception as exception:
638 return HttpResponseServerError(exception)
640 data = {"message": message, "ids": pids, "status": status}
641 return JsonResponse(data)
644class AddIssuePDFView(View):
645 def __init(self, *args, **kwargs):
646 super().__init__(*args, **kwargs)
647 self.pid = None
648 self.issue = None
649 self.collection = None
650 self.site = "test_website"
652 def post_to_site(self, url):
653 response = requests.post(url, verify=False)
654 status = response.status_code
655 if not (199 < status < 205):
656 messages.error(self.request, response.text)
657 if status == 503:
658 raise ServerUnderMaintenance(response.text)
659 else:
660 raise RuntimeError(response.text)
662 def internal_do(self, *args, **kwargs):
663 """
664 Called by history_views.execute_and_record_func to do the actual job.
665 """
667 issue_pid = self.issue.pid
668 colid = self.collection.pid
670 if self.site == "website":
671 # Copy the PDF from the test to the production folder
672 resolver.copy_binary_files(
673 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
674 )
675 else:
676 # Copy the PDF from the cedram to the test folder
677 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
678 from_path = os.path.join(from_folder, issue_pid + ".pdf")
679 if not os.path.isfile(from_path):
680 raise Http404(f"{from_path} does not exist")
682 to_path = resolver.get_disk_location(
683 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
684 )
685 resolver.copy_file(from_path, to_path)
687 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
689 if self.site == "test_website":
690 # Post to ptf-tools: it will add a Datastream to the issue
691 absolute_url = self.request.build_absolute_uri(url)
692 self.post_to_site(absolute_url)
694 server_url = getattr(self.collection, self.site)()
695 absolute_url = server_url + url
696 # Post to the test or production website
697 self.post_to_site(absolute_url)
699 def get(self, request, *args, **kwargs):
700 """
701 Send an issue PDF to the test or production website
702 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
703 :param args:
704 :param kwargs:
705 :return:
706 """
707 if check_lock():
708 m = "Trammel is under maintenance. Please try again later."
709 messages.error(self.request, m)
710 return JsonResponse({"message": m, "status": 503})
712 self.pid = self.kwargs.get("pid", None)
713 self.site = self.kwargs.get("site", "test_website")
715 self.issue = model_helpers.get_container(self.pid)
716 if not self.issue:
717 raise Http404(f"{self.pid} does not exist")
718 self.collection = self.issue.get_top_collection()
720 try:
721 pids, status, message = history_views.execute_and_record_func(
722 "deploy",
723 self.pid,
724 self.collection.pid,
725 self.internal_do,
726 f"add issue PDF to {self.site}",
727 )
729 except Timeout as exception:
730 return HttpResponse(exception, status=408)
731 except Exception as exception:
732 return HttpResponseServerError(exception)
734 data = {"message": message, "status": status}
735 return JsonResponse(data)
738class ArchiveAllAPIView(View):
739 """
740 - archive le xml de la collection ainsi que les binaires liés
741 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
742 @return array of issues pid
743 """
745 def internal_do(self, *args, **kwargs):
746 collection = kwargs["collection"]
747 pids = []
748 colid = collection.pid
750 logfile = os.path.join(settings.LOG_DIR, "archive.log")
751 if os.path.isfile(logfile):
752 os.remove(logfile)
754 ptf_cmds.exportPtfCmd(
755 {
756 "pid": colid,
757 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
758 "with_binary_files": True,
759 "for_archive": True,
760 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
761 }
762 ).do()
764 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
765 if os.path.isfile(cedramcls):
766 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
767 resolver.create_folder(dest_folder)
768 resolver.copy_file(cedramcls, dest_folder)
770 for issue in collection.content.all():
771 qs = issue.article_set.filter(
772 date_online_first__isnull=True, date_published__isnull=True
773 )
774 if qs.count() == 0:
775 pids.append(issue.pid)
777 return pids
779 def get(self, request, *args, **kwargs):
780 pid = self.kwargs.get("pid", None)
782 collection = model_helpers.get_collection(pid)
783 if not collection:
784 return HttpResponse(f"{pid} does not exist", status=400)
786 dict_ = {"collection": collection}
787 args_ = [self]
789 try:
790 pids, status, message = history_views.execute_and_record_func(
791 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_
792 )
793 except Timeout as exception:
794 return HttpResponse(exception, status=408)
795 except Exception as exception:
796 return HttpResponseServerError(exception)
798 data = {"message": message, "ids": pids, "status": status}
799 return JsonResponse(data)
802class CreateAllDjvuAPIView(View):
803 def internal_do(self, *args, **kwargs):
804 issue = kwargs["issue"]
805 pids = [issue.pid]
807 for article in issue.article_set.all():
808 pids.append(article.pid)
810 return pids
812 def get(self, request, *args, **kwargs):
813 pid = self.kwargs.get("pid", None)
814 issue = model_helpers.get_container(pid)
815 if not issue:
816 raise Http404(f"{pid} does not exist")
818 try:
819 dict_ = {"issue": issue}
820 args_ = [self]
822 pids, status, message = history_views.execute_and_record_func(
823 "numdam",
824 pid,
825 issue.get_collection().pid,
826 self.internal_do,
827 "",
828 False,
829 None,
830 None,
831 *args_,
832 **dict_,
833 )
834 except Exception as exception:
835 return HttpResponseServerError(exception)
837 data = {"message": message, "ids": pids, "status": status}
838 return JsonResponse(data)
841class ImportJatsContainerAPIView(View):
842 def internal_do(self, *args, **kwargs):
843 pid = self.kwargs.get("pid", None)
844 colid = self.kwargs.get("colid", None)
846 if pid and colid:
847 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
849 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
850 {
851 "body": body,
852 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
853 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
854 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
855 }
856 )
857 container = cmd.do()
858 if len(cmd.warnings) > 0:
859 messages.warning(
860 self.request,
861 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
862 )
864 if not container:
865 raise RuntimeError("Error: the container " + pid + " was not imported")
867 # resolver.copy_binary_files(
868 # container,
869 # settings.MATHDOC_ARCHIVE_FOLDER,
870 # settings.MERSENNE_TEST_DATA_FOLDER)
871 #
872 # for article in container.article_set.all():
873 # resolver.copy_binary_files(
874 # article,
875 # settings.MATHDOC_ARCHIVE_FOLDER,
876 # settings.MERSENNE_TEST_DATA_FOLDER)
877 else:
878 raise RuntimeError("colid or pid are not defined")
880 def get(self, request, *args, **kwargs):
881 pid = self.kwargs.get("pid", None)
882 colid = self.kwargs.get("colid", None)
884 try:
885 _, status, message = history_views.execute_and_record_func(
886 "import", pid, colid, self.internal_do
887 )
888 except Timeout as exception:
889 return HttpResponse(exception, status=408)
890 except Exception as exception:
891 return HttpResponseServerError(exception)
893 data = {"message": message, "status": status}
894 return JsonResponse(data)
897class DeployCollectionAPIView(View):
898 # Update collection.xml on a site (with its images)
900 def internal_do(self, *args, **kwargs):
901 colid = self.kwargs.get("colid", None)
902 site = self.kwargs.get("site", None)
904 collection = model_helpers.get_collection(colid)
905 if not collection:
906 raise RuntimeError(f"{colid} does not exist")
908 if site == "numdam":
909 server_url = settings.NUMDAM_PRE_URL
910 else:
911 server_url = getattr(collection, site)()
912 if not server_url:
913 raise RuntimeError(f"The collection has no {site}")
915 # check_collection creates or updates the collection (XML, image...)
916 check_collection(collection, server_url, site)
918 def get(self, request, *args, **kwargs):
919 colid = self.kwargs.get("colid", None)
920 site = self.kwargs.get("site", None)
922 try:
923 _, status, message = history_views.execute_and_record_func(
924 "deploy", colid, colid, self.internal_do, site
925 )
926 except Timeout as exception:
927 return HttpResponse(exception, status=408)
928 except Exception as exception:
929 return HttpResponseServerError(exception)
931 data = {"message": message, "status": status}
932 return JsonResponse(data)
935class DeployJatsResourceAPIView(View):
936 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
938 def internal_do(self, *args, **kwargs):
939 pid = self.kwargs.get("pid", None)
940 colid = self.kwargs.get("colid", None)
941 site = self.kwargs.get("site", None)
943 if site == "ptf_tools":
944 raise RuntimeError("Do not choose to deploy on PTF Tools")
945 if check_lock():
946 msg = "Trammel is under maintenance. Please try again later."
947 messages.error(self.request, msg)
948 return JsonResponse({"messages": msg, "status": 503})
950 resource = model_helpers.get_resource(pid)
951 if not resource:
952 raise RuntimeError(f"{pid} does not exist")
954 obj = resource.cast()
955 article = None
956 if obj.classname == "Article":
957 article = obj
958 container = article.my_container
959 articles_to_deploy = [article]
960 else:
961 container = obj
962 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
964 if container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(colid, None):
965 raise RuntimeError("Pending publications should not be deployed")
966 if site == "website" and article is not None and article.do_not_publish:
967 raise RuntimeError(f"{pid} is marked as Do not publish")
968 if site == "numdam" and article is not None:
969 raise RuntimeError("You can only deploy issues to Numdam")
971 collection = container.get_top_collection()
972 colid = collection.pid
973 djvu_exception = None
975 if site == "numdam":
976 server_url = settings.NUMDAM_PRE_URL
977 ResourceInNumdam.objects.get_or_create(pid=container.pid)
979 # 06/12/2022: DjVu are no longer added with Mersenne articles
980 # Add Djvu (before exporting the XML)
981 if False and int(container.fyear) < 2020:
982 for art in container.article_set.all():
983 try:
984 cmd = ptf_cmds.addDjvuPtfCmd()
985 cmd.set_resource(art)
986 cmd.do()
987 except Exception as e:
988 # Djvu are optional.
989 # Allow the deployment, but record the exception in the history
990 djvu_exception = e
991 else:
992 server_url = getattr(collection, site)()
993 if not server_url:
994 raise RuntimeError(f"The collection has no {site}")
996 # check if the collection exists on the server
997 # if not, check_collection will upload the collection (XML,
998 # image...)
999 if article is None:
1000 check_collection(collection, server_url, site)
1002 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
1003 # Create/update deployed date and published date on all container articles
1004 if site == "website":
1005 file_.write(
1006 "Create/Update deployed_date and date_published on all articles for {}\n".format(
1007 pid
1008 )
1009 )
1011 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
1012 cmd = ptf_cmds.publishResourcePtfCmd()
1013 cmd.set_resource(resource)
1014 updated_articles = cmd.do()
1016 create_frontpage(colid, container, updated_articles, test=False)
1018 mersenneSite = model_helpers.get_site_mersenne(colid)
1019 # create or update deployed_date on container and articles
1020 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
1022 for art in articles_to_deploy:
1023 if art.doi and (art.date_published or art.date_online_first):
1024 if art.my_container.fyear is None:
1025 art.my_container.fyear = datetime.now().year
1026 # BUG ? update the container but no save() ?
1028 file_.write(
1029 "Publication date of {} : Online First: {}, Published: {}\n".format(
1030 art.pid, art.date_online_first, art.date_published
1031 )
1032 )
1034 if article is None:
1035 resolver.copy_binary_files(
1036 container,
1037 settings.MERSENNE_TEST_DATA_FOLDER,
1038 settings.MERSENNE_PROD_DATA_FOLDER,
1039 )
1041 for art in articles_to_deploy:
1042 resolver.copy_binary_files(
1043 art,
1044 settings.MERSENNE_TEST_DATA_FOLDER,
1045 settings.MERSENNE_PROD_DATA_FOLDER,
1046 )
1048 elif site == "test_website":
1049 # create date_pre_published on articles without date_pre_published
1050 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
1051 cmd.set_resource(resource)
1052 updated_articles = cmd.do()
1054 create_frontpage(colid, container, updated_articles)
1056 export_to_website = site == "website"
1058 if article is None:
1059 with_djvu = site == "numdam"
1060 xml = ptf_cmds.exportPtfCmd(
1061 {
1062 "pid": pid,
1063 "with_djvu": with_djvu,
1064 "export_to_website": export_to_website,
1065 }
1066 ).do()
1067 body = xml.encode("utf8")
1069 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1070 url = server_url + reverse("issue_upload")
1071 else:
1072 url = server_url + reverse("book_upload")
1074 # verify=False: ignore TLS certificate
1075 response = requests.post(url, data=body, verify=False)
1076 # response = requests.post(url, files=files, verify=False)
1077 else:
1078 xml = ptf_cmds.exportPtfCmd(
1079 {
1080 "pid": pid,
1081 "with_djvu": False,
1082 "article_standalone": True,
1083 "collection_pid": collection.pid,
1084 "export_to_website": export_to_website,
1085 "export_folder": settings.LOG_DIR,
1086 }
1087 ).do()
1088 # Unlike containers that send their XML as the body of the POST request,
1089 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1090 xml_file = io.StringIO(xml)
1091 files = {"xml": xml_file}
1093 url = server_url + reverse(
1094 "article_in_issue_upload", kwargs={"pid": container.pid}
1095 )
1096 # verify=False: ignore TLS certificate
1097 header = {}
1098 response = requests.post(url, headers=header, files=files, verify=False)
1100 status = response.status_code
1102 if 199 < status < 205:
1103 # There is no need to copy files for the test server
1104 # Files were already copied in /mersenne_test_data during the ptf_tools import
1105 # We only need to copy files from /mersenne_test_data to
1106 # /mersenne_prod_data during an upload to prod
1107 if site == "website":
1108 # TODO mettre ici le record doi pour un issue publié
1109 if container.doi:
1110 recordDOI(container)
1112 for art in articles_to_deploy:
1113 # record DOI automatically when deploying in prod
1115 if art.doi and art.allow_crossref():
1116 recordDOI(art)
1118 if colid == "CRBIOL":
1119 recordPubmed(
1120 art, force_update=False, updated_articles=updated_articles
1121 )
1123 if colid == "PCJ":
1124 self.update_pcj_editor(updated_articles)
1126 # Archive the container or the article
1127 if article is None:
1128 archive_resource.delay(
1129 pid,
1130 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1131 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1132 )
1134 else:
1135 archive_resource.delay(
1136 pid,
1137 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1138 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1139 article_doi=article.doi,
1140 )
1141 # cmd = ptf_cmds.archiveIssuePtfCmd({
1142 # "pid": pid,
1143 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1144 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1145 # cmd.set_article(article) # set_article allows archiving only the article
1146 # cmd.do()
1148 elif site == "numdam":
1149 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1150 if colid in settings.NUMDAM_COLLECTIONS:
1151 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1153 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1154 for article in container.article_set.all():
1155 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1157 elif status == 503:
1158 raise ServerUnderMaintenance(response.text)
1159 else:
1160 raise RuntimeError(response.text)
1162 if djvu_exception:
1163 raise djvu_exception
1165 def get(self, request, *args, **kwargs):
1166 pid = self.kwargs.get("pid", None)
1167 colid = self.kwargs.get("colid", None)
1168 site = self.kwargs.get("site", None)
1170 try:
1171 _, status, message = history_views.execute_and_record_func(
1172 "deploy", pid, colid, self.internal_do, site
1173 )
1174 except Timeout as exception:
1175 return HttpResponse(exception, status=408)
1176 except Exception as exception:
1177 return HttpResponseServerError(exception)
1179 data = {"message": message, "status": status}
1180 return JsonResponse(data)
1182 def update_pcj_editor(self, updated_articles):
1183 for article in updated_articles:
1184 data = {
1185 "date_published": article.date_published.strftime("%Y-%m-%d"),
1186 "article_number": article.article_number,
1187 }
1188 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1189 requests.post(url, json=data, verify=False)
1192class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1193 article = None
1195 def internal_do(self, *args, **kwargs):
1196 lang = self.kwargs.get("lang", None)
1198 translation = None
1199 for trans_article in self.article.translations.all():
1200 if trans_article.lang == lang:
1201 translation = trans_article
1203 if translation is None:
1204 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1206 collection = self.article.get_top_collection()
1207 colid = collection.pid
1208 container = self.article.my_container
1210 if translation.date_published is None:
1211 # Add date posted
1212 cmd = ptf_cmds.publishResourcePtfCmd()
1213 cmd.set_resource(translation)
1214 cmd.do()
1215 # updated_articles = cmd.do()
1217 # # Recompile PDF to add the date posted
1218 # try:
1219 # create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1220 # except Exception:
1221 # raise PDFException(
1222 # "Unable to compile the article PDF. Please contact the centre Mersenne"
1223 # )
1225 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1226 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1227 resolver.copy_binary_files(
1228 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1229 )
1231 # Deploy in prod
1232 xml = ptf_cmds.exportPtfCmd(
1233 {
1234 "pid": self.article.pid,
1235 "with_djvu": False,
1236 "article_standalone": True,
1237 "collection_pid": colid,
1238 "export_to_website": True,
1239 "export_folder": settings.LOG_DIR,
1240 }
1241 ).do()
1242 xml_file = io.StringIO(xml)
1243 files = {"xml": xml_file}
1245 server_url = getattr(collection, "website")()
1246 if not server_url:
1247 raise RuntimeError("The collection has no website")
1248 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1249 header = {}
1251 try:
1252 response = requests.post(
1253 url, headers=header, files=files, verify=False
1254 ) # verify: ignore TLS certificate
1255 status = response.status_code
1256 except requests.exceptions.ConnectionError:
1257 raise ServerUnderMaintenance(
1258 "The journal is under maintenance. Please try again later."
1259 )
1261 # Register translation in Crossref
1262 if 199 < status < 205:
1263 if self.article.allow_crossref():
1264 try:
1265 recordDOI(translation)
1266 except Exception:
1267 raise DOIException(
1268 "Error while recording the DOI. Please contact the centre Mersenne"
1269 )
1271 def get(self, request, *args, **kwargs):
1272 doi = kwargs.get("doi", None)
1273 self.article = model_helpers.get_article_by_doi(doi)
1274 if self.article is None:
1275 raise Http404(f"{doi} does not exist")
1277 try:
1278 _, status, message = history_views.execute_and_record_func(
1279 "deploy",
1280 self.article.pid,
1281 self.article.get_top_collection().pid,
1282 self.internal_do,
1283 "website",
1284 )
1285 except Timeout as exception:
1286 return HttpResponse(exception, status=408)
1287 except Exception as exception:
1288 logger.exception(f"Failed to post translation for {self.article.pid}")
1289 return HttpResponseServerError(exception)
1291 data = {"message": message, "status": status}
1292 return JsonResponse(data)
1295class DeleteJatsIssueAPIView(View):
1296 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1297 def get(self, request, *args, **kwargs):
1298 pid = self.kwargs.get("pid", None)
1299 colid = self.kwargs.get("colid", None)
1300 site = self.kwargs.get("site", None)
1301 message = "Le volume a bien été supprimé"
1302 status = 200
1304 issue = model_helpers.get_container(pid)
1305 if not issue:
1306 raise Http404(f"{pid} does not exist")
1307 try:
1308 mersenneSite = model_helpers.get_site_mersenne(colid)
1310 if site == "ptf_tools":
1311 if issue.is_deployed(mersenneSite):
1312 issue.undeploy(mersenneSite)
1313 for article in issue.article_set.all():
1314 article.undeploy(mersenneSite)
1316 p = model_helpers.get_provider("mathdoc-id")
1318 cmd = ptf_cmds.addContainerPtfCmd(
1319 {
1320 "pid": issue.pid,
1321 "ctype": "issue",
1322 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1323 }
1324 )
1325 cmd.set_provider(p)
1326 cmd.add_collection(issue.get_collection())
1327 cmd.set_object_to_be_deleted(issue)
1328 cmd.undo()
1330 else:
1331 if site == "numdam":
1332 server_url = settings.NUMDAM_PRE_URL
1333 else:
1334 collection = issue.get_collection()
1335 server_url = getattr(collection, site)()
1337 if not server_url:
1338 message = "The collection has no " + site
1339 status = 500
1340 else:
1341 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1342 response = requests.delete(url, verify=False)
1343 status = response.status_code
1345 if status == 404:
1346 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1347 elif status > 204:
1348 body = response.text.encode("utf8")
1349 message = body[:1000]
1350 else:
1351 status = 200
1352 # unpublish issue in collection site (site_register.json)
1353 if site == "website":
1354 if issue.is_deployed(mersenneSite):
1355 issue.undeploy(mersenneSite)
1356 for article in issue.article_set.all():
1357 article.undeploy(mersenneSite)
1358 # delete article binary files
1359 folder = article.get_relative_folder()
1360 resolver.delete_object_folder(
1361 folder,
1362 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1363 )
1364 # delete issue binary files
1365 folder = issue.get_relative_folder()
1366 resolver.delete_object_folder(
1367 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1368 )
1370 except Timeout as exception:
1371 return HttpResponse(exception, status=408)
1372 except Exception as exception:
1373 return HttpResponseServerError(exception)
1375 data = {"message": message, "status": status}
1376 return JsonResponse(data)
1379class ArchiveIssueAPIView(View):
1380 def get(self, request, *args, **kwargs):
1381 try:
1382 pid = kwargs["pid"]
1383 colid = kwargs["colid"]
1384 except IndexError:
1385 raise Http404
1387 try:
1388 cmd = ptf_cmds.archiveIssuePtfCmd(
1389 {
1390 "pid": pid,
1391 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1392 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1393 "needs_publication_date": True,
1394 }
1395 )
1396 result_, status, message = history_views.execute_and_record_func(
1397 "archive", pid, colid, cmd.do
1398 )
1399 except Exception as exception:
1400 return HttpResponseServerError(exception)
1402 data = {"message": message, "status": 200}
1403 return JsonResponse(data)
1406class CreateDjvuAPIView(View):
1407 def internal_do(self, *args, **kwargs):
1408 pid = self.kwargs.get("pid", None)
1410 resource = model_helpers.get_resource(pid)
1411 cmd = ptf_cmds.addDjvuPtfCmd()
1412 cmd.set_resource(resource)
1413 cmd.do()
1415 def get(self, request, *args, **kwargs):
1416 pid = self.kwargs.get("pid", None)
1417 colid = pid.split("_")[0]
1419 try:
1420 _, status, message = history_views.execute_and_record_func(
1421 "numdam", pid, colid, self.internal_do
1422 )
1423 except Exception as exception:
1424 return HttpResponseServerError(exception)
1426 data = {"message": message, "status": status}
1427 return JsonResponse(data)
1430class PTFToolsHomeView(LoginRequiredMixin, View):
1431 """
1432 Home Page.
1433 - Admin & staff -> Render blank home.html
1434 - User with unique authorized collection -> Redirect to collection details page
1435 - User with multiple authorized collections -> Render home.html with data
1436 - Comment moderator -> Comments dashboard
1437 - Others -> 404 response
1438 """
1440 def get(self, request, *args, **kwargs) -> HttpResponse:
1441 # Staff or user with authorized collections
1442 if request.user.is_staff or request.user.is_superuser:
1443 return render(request, "home.html")
1445 colids = get_authorized_collections(request.user)
1446 is_mod = is_comment_moderator(request.user)
1448 # The user has no rights
1449 if not (colids or is_mod):
1450 raise Http404("No collections associated with your account.")
1451 # Comment moderator only
1452 elif not colids:
1453 return HttpResponseRedirect(reverse("comment_list"))
1455 # User with unique collection -> Redirect to collection detail page
1456 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1457 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1459 # User with multiple authorized collections - Special home
1460 context = {}
1461 context["overview"] = True
1463 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1464 all_collections = {c["pid"]: c for c in all_collections}
1466 # Comments summary
1467 try:
1468 error, comments_data = get_comments_for_home(request.user)
1469 except AttributeError:
1470 error, comments_data = True, {}
1472 context["comment_server_ok"] = False
1474 if not error:
1475 context["comment_server_ok"] = True
1476 if comments_data:
1477 for col_id, comment_nb in comments_data.items():
1478 if col_id.upper() in all_collections: 1478 ↛ 1477line 1478 didn't jump to line 1477 because the condition on line 1478 was always true
1479 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1481 # TODO: Translations summary
1482 context["translation_server_ok"] = False
1484 # Sort the collections according to the number of pending comments
1485 context["collections"] = sorted(
1486 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1487 )
1489 return render(request, "home.html", context)
1492class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1493 columns = 5
1495 def get_common_context_data(self, **kwargs):
1496 context = super().get_context_data(**kwargs)
1497 now = timezone.now()
1498 curyear = now.year
1499 years = range(curyear - self.columns + 1, curyear + 1)
1501 context["collections"] = settings.MERSENNE_COLLECTIONS
1502 context["containers_to_be_published"] = []
1503 context["last_col_events"] = []
1505 event = get_history_last_event_by("clockss", "ALL")
1506 clockss_gap = get_gap(now, event)
1508 context["years"] = years
1509 context["clockss_gap"] = clockss_gap
1511 return context
1513 def calculate_articles_and_pages(self, pid, years):
1514 data_by_year = []
1515 total_articles = [0] * len(years)
1516 total_pages = [0] * len(years)
1518 for year in years:
1519 articles = self.get_articles_for_year(pid, year)
1520 articles_count = articles.count()
1521 page_count = sum(article.get_article_page_count() for article in articles)
1523 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1524 total_articles[year - years[0]] += articles_count
1525 total_pages[year - years[0]] += page_count
1527 return data_by_year, total_articles, total_pages
1529 def get_articles_for_year(self, pid, year):
1530 return Article.objects.filter(
1531 Q(my_container__my_collection__pid=pid)
1532 & (
1533 Q(date_published__year=year, date_online_first__isnull=True)
1534 | Q(date_online_first__year=year)
1535 )
1536 ).prefetch_related("resourcecount_set")
1539class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1540 template_name = "dashboard/published_articles.html"
1542 def get_context_data(self, **kwargs):
1543 context = self.get_common_context_data(**kwargs)
1544 years = context["years"]
1546 published_articles = []
1547 total_published_articles = [
1548 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1549 ]
1551 for pid in settings.MERSENNE_COLLECTIONS:
1552 if pid != "MERSENNE":
1553 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1554 pid, years
1555 )
1556 published_articles.append({"pid": pid, "years": articles_data})
1558 for i, year in enumerate(years):
1559 total_published_articles[i]["total_articles"] += total_articles[i]
1560 total_published_articles[i]["total_pages"] += total_pages[i]
1562 context["published_articles"] = published_articles
1563 context["total_published_articles"] = total_published_articles
1565 return context
1568class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1569 template_name = "dashboard/created_volumes.html"
1571 def get_context_data(self, **kwargs):
1572 context = self.get_common_context_data(**kwargs)
1573 years = context["years"]
1575 created_volumes = []
1576 total_created_volumes = [
1577 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1578 ]
1580 for pid in settings.MERSENNE_COLLECTIONS:
1581 if pid != "MERSENNE":
1582 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1583 pid, years
1584 )
1585 created_volumes.append({"pid": pid, "years": volumes_data})
1587 for i, _ in enumerate(years):
1588 total_created_volumes[i]["total_articles"] += total_articles[i]
1589 total_created_volumes[i]["total_pages"] += total_pages[i]
1591 context["created_volumes"] = created_volumes
1592 context["total_created_volumes"] = total_created_volumes
1594 return context
1596 def calculate_volumes_and_pages(self, pid, years):
1597 data_by_year = []
1598 total_articles = [0] * len(years)
1599 total_pages = [0] * len(years)
1601 for year in years:
1602 issues = Container.objects.filter(my_collection__pid=pid, fyear=year)
1603 articles_count = 0
1604 page_count = 0
1606 for issue in issues:
1607 articles = issue.article_set.filter(
1608 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1609 ).prefetch_related("resourcecount_set")
1611 articles_count += articles.count()
1612 page_count += sum(article.get_article_page_count() for article in articles)
1614 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1615 total_articles[year - years[0]] += articles_count
1616 total_pages[year - years[0]] += page_count
1618 return data_by_year, total_articles, total_pages
1621class ReferencingChoice(View):
1622 def post(self, request, *args, **kwargs):
1623 if request.POST.get("optSite") == "ads":
1624 return redirect(
1625 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1626 )
1627 elif request.POST.get("optSite") == "wos":
1628 comp = ReferencingCheckerWos()
1629 journal = comp.make_journal(request.POST.get("selectCol"))
1630 if journal is None:
1631 return render(
1632 request,
1633 "dashboard/referencing.html",
1634 {
1635 "error": "Collection not found",
1636 "colid": request.POST.get("selectCol"),
1637 "optSite": request.POST.get("optSite"),
1638 },
1639 )
1640 return render(
1641 request,
1642 "dashboard/referencing.html",
1643 {
1644 "journal": journal,
1645 "colid": request.POST.get("selectCol"),
1646 "optSite": request.POST.get("optSite"),
1647 },
1648 )
1651class ReferencingWosFileView(View):
1652 template_name = "dashboard/referencing.html"
1654 def post(self, request, *args, **kwargs):
1655 colid = request.POST["colid"]
1656 if request.FILES.get("risfile") is None:
1657 message = "No file uploaded"
1658 return render(
1659 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1660 )
1661 uploaded_file = request.FILES["risfile"]
1662 comp = ReferencingCheckerWos()
1663 journal = comp.check_references(colid, uploaded_file)
1664 return render(request, self.template_name, {"journal": journal})
1667class ReferencingDashboardView(BaseMersenneDashboardView):
1668 template_name = "dashboard/referencing.html"
1670 def get(self, request, *args, **kwargs):
1671 colid = self.kwargs.get("colid", None)
1672 comp = ReferencingCheckerAds()
1673 journal = comp.check_references(colid)
1674 return render(request, self.template_name, {"journal": journal})
1677class BaseCollectionView(TemplateView):
1678 def get_context_data(self, **kwargs):
1679 context = super().get_context_data(**kwargs)
1680 aid = context.get("aid")
1681 year = context.get("year")
1683 if aid and year:
1684 context["collection"] = self.get_collection(aid, year)
1686 return context
1688 def get_collection(self, aid, year):
1689 """Method to be overridden by subclasses to fetch the appropriate collection"""
1690 raise NotImplementedError("Subclasses must implement get_collection method")
1693class ArticleListView(BaseCollectionView):
1694 template_name = "collection-list.html"
1696 def get_collection(self, aid, year):
1697 return Article.objects.filter(
1698 Q(my_container__my_collection__pid=aid)
1699 & (
1700 Q(date_published__year=year, date_online_first__isnull=True)
1701 | Q(date_online_first__year=year)
1702 )
1703 ).prefetch_related("resourcecount_set")
1706class VolumeListView(BaseCollectionView):
1707 template_name = "collection-list.html"
1709 def get_collection(self, aid, year):
1710 return Article.objects.filter(
1711 Q(my_container__my_collection__pid=aid, my_container__fyear=year)
1712 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1713 ).prefetch_related("resourcecount_set")
1716class DOAJResourceRegisterView(View):
1717 def get(self, request, *args, **kwargs):
1718 pid = kwargs.get("pid", None)
1719 resource = model_helpers.get_resource(pid)
1720 if resource is None:
1721 raise Http404
1722 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1723 resource.colid, None
1724 ):
1725 raise RuntimeError("Pending publications should not be deployed")
1727 try:
1728 data = {}
1729 doaj_meta, response = doaj_pid_register(pid)
1730 if response is None:
1731 return HttpResponse(status=204)
1732 elif doaj_meta and 200 <= response.status_code <= 299:
1733 data.update(doaj_meta)
1734 else:
1735 return HttpResponse(status=response.status_code, reason=response.text)
1736 except Timeout as exception:
1737 return HttpResponse(exception, status=408)
1738 except Exception as exception:
1739 return HttpResponseServerError(exception)
1740 return JsonResponse(data)
1743class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View):
1744 """
1745 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml
1746 """
1748 def get(self, request, *args, **kwargs):
1749 pid = kwargs.get("pid")
1750 if not pid:
1751 raise Http404("Missing pid")
1753 article = Article.objects.filter(pid=pid).first()
1754 if not article:
1755 raise Http404(f"Article not found: {pid}")
1757 colid = article.get_collection().pid
1758 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
1759 return JsonResponse(
1760 {"status": 403, "message": f"Tex conversions are disabled in {colid}"}
1761 )
1763 if is_tex_conversion_locked(pid):
1764 logger.warning("Conversion rejected (lock exists) for %s", pid)
1765 return JsonResponse(
1766 {"status": 409, "message": f"A conversion is already running for {pid}"}
1767 )
1769 logger.info("No lock → scheduling conversion for %s", pid)
1771 try:
1772 convert_article_tex.delay(pid=pid, user_pk=request.user.pk)
1773 except Exception:
1774 logger.exception("Failed to enqueue task for %s", pid)
1775 release_tex_conversion_lock(pid)
1776 raise
1778 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"})
1781class CROSSREFResourceRegisterView(View):
1782 def get(self, request, *args, **kwargs):
1783 pid = kwargs.get("pid", None)
1784 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1785 force = kwargs.get("force", None)
1786 if not request.user.is_superuser:
1787 force = None
1789 resource = model_helpers.get_resource(pid)
1790 if resource is None:
1791 raise Http404
1793 resource = resource.cast()
1794 meth = getattr(self, "recordDOI" + resource.classname)
1795 try:
1796 data = meth(resource, force)
1797 except Timeout as exception:
1798 return HttpResponse(exception, status=408)
1799 except Exception as exception:
1800 return HttpResponseServerError(exception)
1801 return JsonResponse(data)
1803 def recordDOIArticle(self, article: "Article", force=None):
1804 result = {"status": 404}
1805 if (
1806 article.doi
1807 and not article.do_not_publish
1808 and (article.date_published or article.date_online_first or force == "force")
1809 ):
1810 if article.my_container.fyear == 0:
1811 article.my_container.fyear = datetime.now().year
1812 result = recordDOI(article)
1813 return result
1815 def recordDOICollection(self, collection, force=None):
1816 return recordDOI(collection)
1818 def recordDOIContainer(self, container, force=None):
1819 data = {"status": 200, "message": "All DOI successfully checked"}
1821 if container.ctype == "issue":
1822 if container.doi:
1823 result = recordDOI(container)
1824 if result["status"] != 200:
1825 return result
1826 if force == "force":
1827 articles = container.article_set.exclude(
1828 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1829 )
1830 else:
1831 articles = container.article_set.exclude(
1832 doi__isnull=True,
1833 do_not_publish=True,
1834 date_published__isnull=True,
1835 date_online_first__isnull=True,
1836 )
1838 for article in articles:
1839 result = self.recordDOIArticle(article, force)
1840 if result["status"] != 200:
1841 data = result
1842 else:
1843 return recordDOI(container)
1844 return data
1847class CROSSREFResourceCheckStatusView(View):
1848 def get(self, request, *args, **kwargs):
1849 pid = kwargs.get("pid", None)
1850 resource = model_helpers.get_resource(pid)
1851 if resource is None:
1852 raise Http404
1853 resource = resource.cast()
1854 meth = getattr(self, "checkDOI" + resource.classname)
1855 try:
1856 meth(resource)
1857 except Timeout as exception:
1858 return HttpResponse(exception, status=408)
1859 except Exception as exception:
1860 return HttpResponseServerError(exception)
1862 data = {"status": 200, "message": "DOI successfully checked"}
1863 return JsonResponse(data)
1865 def checkDOIArticle(self, article: "Article"):
1866 if article.my_container.fyear == 0:
1867 article.my_container.fyear = datetime.now().year
1868 checkDOI(article)
1870 def checkDOICollection(self, collection):
1871 checkDOI(collection)
1873 def checkDOIContainer(self, container):
1874 if container.doi is not None:
1875 checkDOI(container)
1876 for article in container.article_set.all():
1877 self.checkDOIArticle(article)
1880class CROSSREFResourcePendingPublicationRegisterView(View):
1881 def get(self, request, *args, **kwargs):
1882 pid = kwargs.get("pid", None)
1883 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1885 resource = model_helpers.get_resource(pid)
1886 if resource is None:
1887 raise Http404
1889 resource = resource.cast()
1890 meth = getattr(self, "recordPendingPublication" + resource.classname)
1891 try:
1892 data = meth(resource)
1893 except Timeout as exception:
1894 return HttpResponse(exception, status=408)
1895 except Exception as exception:
1896 return HttpResponseServerError(exception)
1897 return JsonResponse(data)
1899 def recordPendingPublicationArticle(self, article):
1900 result = {"status": 404}
1901 if article.doi and not article.date_published and not article.date_online_first:
1902 if article.my_container.fyear is None or article.my_container.fyear == "0":
1903 article.my_container.fyear = datetime.now().year
1904 result = recordPendingPublication(article)
1905 return result
1908class RegisterPubmedFormView(FormView):
1909 template_name = "record_pubmed_dialog.html"
1910 form_class = RegisterPubmedForm
1912 def get_context_data(self, **kwargs):
1913 context = super().get_context_data(**kwargs)
1914 context["pid"] = self.kwargs["pid"]
1915 context["helper"] = PtfLargeModalFormHelper
1916 return context
1919class RegisterPubmedView(View):
1920 def get(self, request, *args, **kwargs):
1921 pid = kwargs.get("pid", None)
1922 update_article = self.request.GET.get("update_article", "on") == "on"
1924 article = model_helpers.get_article(pid)
1925 if article is None:
1926 raise Http404
1927 try:
1928 recordPubmed(article, update_article)
1929 except Exception as exception:
1930 messages.error("Unable to register the article in PubMed")
1931 return HttpResponseServerError(exception)
1933 return HttpResponseRedirect(
1934 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1935 )
1938class PTFToolsArticleView(ArticleView):
1939 def get_context_data(self, **kwargs):
1940 context = super().get_context_data(**kwargs)
1942 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(self.obj)
1944 qs = self.obj.get_top_collection().extlink_set.filter(rel="test_website")
1945 if qs:
1946 test_website = qs.first().location
1947 context["test_website"] = test_website
1948 qs = self.obj.get_top_collection().extlink_set.filter(rel="website")
1949 if qs:
1950 prod_location = qs.first().location
1951 context["prod_website"] = prod_location
1953 return context
1956ItemViewClassFactory.views["article"] = PTFToolsArticleView
1959class PTFToolsContainerView(TemplateView):
1960 template_name = ""
1962 def get_context_data(self, **kwargs):
1963 context = super().get_context_data(**kwargs)
1965 container = model_helpers.get_container(self.kwargs.get("pid"))
1966 if container is None:
1967 raise Http404
1968 citing_articles = container.citations()
1969 source = self.request.GET.get("source", None)
1970 if container.ctype.startswith("book"):
1971 book_parts = (
1972 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1973 )
1974 references = False
1975 if container.ctype == "book-monograph":
1976 # on regarde si il y a au moins une bibliographie
1977 for art in container.article_set.all():
1978 if art.bibitem_set.count() > 0:
1979 references = True
1980 context.update(
1981 {
1982 "book": container,
1983 "book_parts": list(book_parts),
1984 "source": source,
1985 "citing_articles": citing_articles,
1986 "references": references,
1987 "test_website": container.get_top_collection()
1988 .extlink_set.get(rel="test_website")
1989 .location,
1990 "prod_website": container.get_top_collection()
1991 .extlink_set.get(rel="website")
1992 .location,
1993 }
1994 )
1995 self.template_name = "book-toc.html"
1996 else:
1997 articles = container.article_set.all().order_by("seq")
1998 for article in articles:
1999 try:
2000 last_match = (
2001 history_models.HistoryEvent.objects.filter(
2002 pid=article.pid,
2003 type="matching",
2004 )
2005 .only("created_on")
2006 .latest("created_on")
2007 )
2008 except history_models.HistoryEvent.DoesNotExist as _:
2009 article.last_match = None
2010 else:
2011 article.last_match = last_match.created_on
2013 # article1 = articles.first()
2014 # date = article1.deployed_date()
2015 # TODO next_issue, previous_issue
2017 # check DOI est maintenant une commande à part
2018 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
2019 # articlesWithStatus = []
2020 # for article in articles:
2021 # checkDOIExistence(article)
2022 # articlesWithStatus.append(article)
2024 test_location = prod_location = ""
2025 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
2026 if qs:
2027 test_location = qs.first().location
2028 qs = container.get_top_collection().extlink_set.filter(rel="website")
2029 if qs:
2030 prod_location = qs.first().location
2031 context.update(
2032 {
2033 "issue": container,
2034 "articles": articles,
2035 "source": source,
2036 "citing_articles": citing_articles,
2037 "test_website": test_location,
2038 "prod_website": prod_location,
2039 }
2040 )
2042 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
2043 context["is_issue_pending_publication"] = True
2044 if container.get_top_collection().pid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
2045 context["is_excluded_from_tex_conversion"] = True
2046 self.template_name = "issue-items.html"
2048 context["allow_crossref"] = container.allow_crossref()
2049 context["coltype"] = container.my_collection.coltype
2050 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(container)
2051 return context
2054class ExtLinkInline(InlineFormSetFactory):
2055 model = ExtLink
2056 form_class = ExtLinkForm
2057 factory_kwargs = {"extra": 0}
2060class ResourceIdInline(InlineFormSetFactory):
2061 model = ResourceId
2062 form_class = ResourceIdForm
2063 factory_kwargs = {"extra": 0}
2066class IssueDetailAPIView(View):
2067 def get(self, request, *args, **kwargs):
2068 issue = get_object_or_404(Container, pid=kwargs["pid"])
2069 deployed_date = issue.deployed_date()
2070 result = {
2071 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2072 if deployed_date
2073 else None,
2074 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2075 "all_doi_are_registered": issue.all_doi_are_registered(),
2076 "registered_in_doaj": issue.registered_in_doaj(),
2077 "doi": issue.my_collection.doi,
2078 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2079 }
2080 try:
2081 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2082 except history_models.HistoryEvent.DoesNotExist as _:
2083 pass
2084 else:
2085 result["latest"] = latest.message
2086 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2087 "%Y-%m-%d %H:%M"
2088 )
2090 result["latest_type"] = latest.type.capitalize()
2091 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2092 try:
2093 result[event_type] = timezone.localtime(
2094 history_models.HistoryEvent.objects.filter(
2095 type=event_type,
2096 status="OK",
2097 pid__startswith=issue.pid,
2098 )
2099 .latest("created_on")
2100 .created_on
2101 ).strftime("%Y-%m-%d %H:%M")
2102 except history_models.HistoryEvent.DoesNotExist as _:
2103 result[event_type] = ""
2104 return JsonResponse(result)
2107class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2108 model = Collection
2109 form_class = CollectionForm
2110 inlines = [ResourceIdInline, ExtLinkInline]
2111 inlines_names = ["resource_ids_form", "ext_links_form"]
2113 def get_context_data(self, **kwargs):
2114 context = super().get_context_data(**kwargs)
2115 context["helper"] = PtfFormHelper
2116 context["formset_helper"] = FormSetHelper
2117 return context
2119 def add_description(self, collection, description, lang, seq):
2120 if description:
2121 la = Abstract(
2122 resource=collection,
2123 tag="description",
2124 lang=lang,
2125 seq=seq,
2126 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2127 value_html=description,
2128 value_tex=description,
2129 )
2130 la.save()
2132 def form_valid(self, form):
2133 if form.instance.abbrev:
2134 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2135 else:
2136 form.instance.title_xml = (
2137 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2138 )
2140 form.instance.title_html = form.instance.title_tex
2141 form.instance.title_sort = form.instance.title_tex
2142 result = super().form_valid(form)
2144 collection = self.object
2145 collection.abstract_set.all().delete()
2147 seq = 1
2148 description = form.cleaned_data["description_en"]
2149 if description:
2150 self.add_description(collection, description, "en", seq)
2151 seq += 1
2152 description = form.cleaned_data["description_fr"]
2153 if description:
2154 self.add_description(collection, description, "fr", seq)
2156 return result
2158 def get_success_url(self):
2159 messages.success(
2160 self.request, f'The collection "{self.object.pid}" has been successfully updated'
2161 )
2162 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2165class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2166 """
2167 Warning : Not yet finished
2168 Automatic site membership creation is still missing
2169 """
2172class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2173 slug_field = "pid"
2174 slug_url_kwarg = "pid"
2177def suggest_load_journal_dois(colid):
2178 articles = (
2179 Article.objects.filter(my_container__my_collection__pid=colid)
2180 .filter(doi__isnull=False)
2181 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2182 .values_list("doi", flat=True)
2183 )
2185 try:
2186 articles = sorted(
2187 articles,
2188 key=lambda d: (
2189 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2190 int(re.search(r".(\d+)$", d).group(1)),
2191 ),
2192 )
2193 except: # noqa: E722 (we'll look later)
2194 pass
2195 return [f'<option value="{doi}">' for doi in articles]
2198def get_context_with_volumes(journal):
2199 result = model_helpers.get_volumes_in_collection(journal)
2200 volume_count = result["volume_count"]
2201 collections = []
2202 for ancestor in journal.ancestors.all():
2203 item = model_helpers.get_volumes_in_collection(ancestor)
2204 volume_count = max(0, volume_count)
2205 item.update({"journal": ancestor})
2206 collections.append(item)
2208 # add the parent collection to its children list and sort it by date
2209 result.update({"journal": journal})
2210 collections.append(result)
2212 collections = [c for c in collections if c["sorted_issues"]]
2213 collections.sort(
2214 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2215 reverse=True,
2216 )
2218 context = {
2219 "journal": journal,
2220 "sorted_issues": result["sorted_issues"],
2221 "volume_count": volume_count,
2222 "max_width": result["max_width"],
2223 "collections": collections,
2224 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2225 }
2226 return context
2229class CollectionDetail(
2230 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2231):
2232 model = Collection
2233 slug_field = "pid"
2234 slug_url_kwarg = "pid"
2235 template_name = "ptf/collection_detail.html"
2237 def test_func(self):
2238 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2240 def get(self, request, *args, **kwargs):
2241 self.object = self.get_object(queryset=Collection.objects.all())
2242 return super().get(request, *args, **kwargs)
2244 def get_context_data(self, **kwargs):
2245 context = super().get_context_data(**kwargs)
2246 context["object_list"] = context["object_list"].filter(
2247 Q(ctype="issue") | Q(ctype="book-lecture-notes") | Q(ctype="book-monograph")
2248 )
2249 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2250 context.update(get_context_with_volumes(self.object))
2252 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2253 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2254 context["issue_to_appear"] = Container.objects.filter(
2255 pid=context["issue_to_appear_pid"]
2256 ).exists()
2257 try:
2258 latest_error = history_models.HistoryEvent.objects.filter(
2259 status="ERROR", col=self.object
2260 ).latest("created_on")
2261 except history_models.HistoryEvent.DoesNotExist as _:
2262 pass
2263 else:
2264 message = latest_error.message
2265 if message:
2266 i = message.find(" - ")
2267 latest_exception = message[:i]
2268 latest_error_message = message[i + 3 :]
2269 context["latest_exception"] = latest_exception
2270 context["latest_exception_date"] = latest_error.created_on
2271 context["latest_exception_type"] = latest_error.type
2272 context["latest_error_message"] = latest_error_message
2274 archive_in_error = history_models.HistoryEvent.objects.filter(
2275 status="ERROR", col=self.object, type="archive"
2276 ).exists()
2278 context["archive_in_error"] = archive_in_error
2280 return context
2282 def get_queryset(self):
2283 query = self.object.content.all()
2285 for ancestor in self.object.ancestors.all():
2286 query |= ancestor.content.all()
2288 return query.order_by("-fyear", "-vseries", "-volume", "-volume_int", "-number_int")
2291class ContainerEditView(FormView):
2292 template_name = "container_form.html"
2293 form_class = ContainerForm
2295 def get_success_url(self):
2296 if self.kwargs["pid"]:
2297 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2298 return reverse("mersenne_dashboard/published_articles")
2300 def set_success_message(self): # pylint: disable=no-self-use
2301 messages.success(self.request, "Booklet updated")
2303 def get_form_kwargs(self):
2304 kwargs = super().get_form_kwargs()
2305 if "pid" not in self.kwargs:
2306 self.kwargs["pid"] = None
2307 if "colid" not in self.kwargs:
2308 self.kwargs["colid"] = None
2309 if "data" in kwargs and "colid" in kwargs["data"]:
2310 # colid is passed as a hidden param in the form.
2311 # It is used when you submit a new container
2312 self.kwargs["colid"] = kwargs["data"]["colid"]
2314 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2315 self.kwargs["pid"]
2316 )
2317 return kwargs
2319 def get_context_data(self, **kwargs):
2320 context = super().get_context_data(**kwargs)
2322 context["pid"] = self.kwargs["pid"]
2323 context["colid"] = self.kwargs["colid"]
2324 context["container"] = self.kwargs["container"]
2326 context["edit_container"] = context["pid"] is not None
2327 context["name"] = resolve(self.request.path_info).url_name
2329 return context
2331 def form_valid(self, form):
2332 new_pid = form.cleaned_data.get("pid")
2333 new_title = form.cleaned_data.get("title")
2334 new_trans_title = form.cleaned_data.get("trans_title")
2335 new_publisher = form.cleaned_data.get("publisher")
2336 new_year = form.cleaned_data.get("year")
2337 new_volume = form.cleaned_data.get("volume")
2338 new_number = form.cleaned_data.get("number")
2340 collection = None
2341 issue = self.kwargs["container"]
2342 if issue is not None:
2343 collection = issue.my_collection
2344 elif self.kwargs["colid"] is not None:
2345 if "CR" in self.kwargs["colid"]:
2346 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2347 else:
2348 collection = model_helpers.get_collection(self.kwargs["colid"])
2350 if collection is None:
2351 raise ValueError("Collection for " + new_pid + " does not exist")
2353 # Icon
2354 new_icon_location = ""
2355 if "icon" in self.request.FILES:
2356 filename = os.path.basename(self.request.FILES["icon"].name)
2357 file_extension = filename.split(".")[1]
2359 icon_filename = resolver.get_disk_location(
2360 settings.MERSENNE_TEST_DATA_FOLDER,
2361 collection.pid,
2362 file_extension,
2363 new_pid,
2364 None,
2365 True,
2366 )
2368 with open(icon_filename, "wb+") as destination:
2369 for chunk in self.request.FILES["icon"].chunks():
2370 destination.write(chunk)
2372 folder = resolver.get_relative_folder(collection.pid, new_pid)
2373 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2374 name = resolve(self.request.path_info).url_name
2375 if name == "special_issue_create":
2376 self.kwargs["name"] = name
2377 if self.kwargs["container"]:
2378 # Edit Issue
2379 issue = self.kwargs["container"]
2380 if issue is None:
2381 raise ValueError(self.kwargs["pid"] + " does not exist")
2383 issue.pid = new_pid
2384 issue.title_tex = issue.title_html = new_title
2385 issue.title_xml = build_title_xml(
2386 title=new_title,
2387 lang=issue.lang,
2388 title_type="issue-title",
2389 )
2391 trans_lang = ""
2392 if new_trans_title != "":
2393 trans_lang = "fr" if issue.lang == "en" else "en"
2395 if trans_lang != "" and new_trans_title != "":
2396 title_xml = build_title_xml(
2397 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2398 )
2400 issue.title_set.update_or_create(
2401 lang=trans_lang,
2402 type="main",
2403 defaults={"title_html": new_trans_title, "title_xml": title_xml},
2404 )
2406 issue.fyear = new_year
2407 issue.volume = new_volume
2408 issue.volume_int = make_int(new_volume)
2409 issue.number = new_number
2410 issue.number_int = make_int(new_number)
2411 issue.save()
2412 else:
2413 xissue = create_issuedata()
2415 xissue.ctype = "issue"
2416 xissue.pid = new_pid
2417 xissue.lang = "en"
2418 xissue.title_tex = new_title
2419 xissue.title_html = new_title
2420 xissue.title_xml = build_title_xml(
2421 title=new_title, lang=xissue.lang, title_type="issue-title"
2422 )
2424 if new_trans_title != "":
2425 trans_lang = "fr"
2426 title_xml = build_title_xml(
2427 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2428 )
2429 title = create_titledata(
2430 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2431 )
2432 issue.titles = [title]
2434 xissue.fyear = new_year
2435 xissue.volume = new_volume
2436 xissue.number = new_number
2437 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2439 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2440 cmd.add_collection(collection)
2441 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2442 issue = cmd.do()
2444 self.kwargs["pid"] = new_pid
2446 # Add objects related to the article: contribs, datastream, counts...
2447 params = {
2448 "icon_location": new_icon_location,
2449 }
2450 cmd = ptf_cmds.updateContainerPtfCmd(params)
2451 cmd.set_resource(issue)
2452 cmd.do()
2454 publisher = model_helpers.get_publisher(new_publisher)
2455 if not publisher:
2456 xpub = create_publisherdata()
2457 xpub.name = new_publisher
2458 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2459 issue.my_publisher = publisher
2460 issue.save()
2462 self.set_success_message()
2464 return super().form_valid(form)
2467# class ArticleEditView(FormView):
2468# template_name = 'article_form.html'
2469# form_class = ArticleForm
2470#
2471# def get_success_url(self):
2472# if self.kwargs['pid']:
2473# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2474# return reverse('mersenne_dashboard/published_articles')
2475#
2476# def set_success_message(self): # pylint: disable=no-self-use
2477# messages.success(self.request, "L'article a été modifié")
2478#
2479# def get_form_kwargs(self):
2480# kwargs = super(ArticleEditView, self).get_form_kwargs()
2481#
2482# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2483# # Article creation: pid is None
2484# self.kwargs['pid'] = None
2485# if 'issue_id' not in self.kwargs:
2486# # Article edit: issue_id is not passed
2487# self.kwargs['issue_id'] = None
2488# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2489# # colid is passed as a hidden param in the form.
2490# # It is used when you submit a new container
2491# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2492#
2493# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2494# return kwargs
2495#
2496# def get_context_data(self, **kwargs):
2497# context = super(ArticleEditView, self).get_context_data(**kwargs)
2498#
2499# context['pid'] = self.kwargs['pid']
2500# context['issue_id'] = self.kwargs['issue_id']
2501# context['article'] = self.kwargs['article']
2502#
2503# context['edit_article'] = context['pid'] is not None
2504#
2505# article = context['article']
2506# if article:
2507# context['author_contributions'] = article.get_author_contributions()
2508# context['kwds_fr'] = None
2509# context['kwds_en'] = None
2510# kwd_gps = article.get_non_msc_kwds()
2511# for kwd_gp in kwd_gps:
2512# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2513# if kwd_gp.value_xml:
2514# kwd_ = types.SimpleNamespace()
2515# kwd_.value = kwd_gp.value_tex
2516# context['kwd_unstructured_fr'] = kwd_
2517# context['kwds_fr'] = kwd_gp.kwd_set.all()
2518# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2519# if kwd_gp.value_xml:
2520# kwd_ = types.SimpleNamespace()
2521# kwd_.value = kwd_gp.value_tex
2522# context['kwd_unstructured_en'] = kwd_
2523# context['kwds_en'] = kwd_gp.kwd_set.all()
2524#
2525# # Article creation: init pid
2526# if context['issue_id'] and context['pid'] is None:
2527# issue = model_helpers.get_container(context['issue_id'])
2528# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2529#
2530# return context
2531#
2532# def form_valid(self, form):
2533#
2534# new_pid = form.cleaned_data.get('pid')
2535# new_title = form.cleaned_data.get('title')
2536# new_fpage = form.cleaned_data.get('fpage')
2537# new_lpage = form.cleaned_data.get('lpage')
2538# new_page_range = form.cleaned_data.get('page_range')
2539# new_page_count = form.cleaned_data.get('page_count')
2540# new_coi_statement = form.cleaned_data.get('coi_statement')
2541# new_show_body = form.cleaned_data.get('show_body')
2542# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2543#
2544# # TODO support MathML
2545# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2546# # We need to pass trans_title to get_title_xml
2547# # Meanwhile, ignore new_title_xml
2548# new_title_xml = jats_parser.get_title_xml(new_title)
2549# new_title_html = new_title
2550#
2551# authors_count = int(self.request.POST.get('authors_count', "0"))
2552# i = 1
2553# new_authors = []
2554# old_author_contributions = []
2555# if self.kwargs['article']:
2556# old_author_contributions = self.kwargs['article'].get_author_contributions()
2557#
2558# while authors_count > 0:
2559# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2560#
2561# if prefix is not None:
2562# addresses = []
2563# if len(old_author_contributions) >= i:
2564# old_author_contribution = old_author_contributions[i - 1]
2565# addresses = [contrib_address.address for contrib_address in
2566# old_author_contribution.get_addresses()]
2567#
2568# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2569# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2570# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2571# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2572# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2573# deceased_before_publication = deceased == 'on'
2574# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2575# equal_contrib = equal_contrib == 'on'
2576# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2577# corresponding = corresponding == 'on'
2578# email = self.request.POST.get('email-' + str(i), None)
2579#
2580# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2581# params['deceased_before_publication'] = deceased_before_publication
2582# params['equal_contrib'] = equal_contrib
2583# params['corresponding'] = corresponding
2584# params['addresses'] = addresses
2585# params['email'] = email
2586#
2587# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2588#
2589# new_authors.append(params)
2590#
2591# authors_count -= 1
2592# i += 1
2593#
2594# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2595# i = 1
2596# new_kwds_fr = []
2597# while kwds_fr_count > 0:
2598# value = self.request.POST.get('kwd-fr-' + str(i), None)
2599# new_kwds_fr.append(value)
2600# kwds_fr_count -= 1
2601# i += 1
2602# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2603#
2604# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2605# i = 1
2606# new_kwds_en = []
2607# while kwds_en_count > 0:
2608# value = self.request.POST.get('kwd-en-' + str(i), None)
2609# new_kwds_en.append(value)
2610# kwds_en_count -= 1
2611# i += 1
2612# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2613#
2614# if self.kwargs['article']:
2615# # Edit article
2616# container = self.kwargs['article'].my_container
2617# else:
2618# # New article
2619# container = model_helpers.get_container(self.kwargs['issue_id'])
2620#
2621# if container is None:
2622# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2623#
2624# collection = container.my_collection
2625#
2626# # Copy PDF file & extract full text
2627# body = ''
2628# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2629# collection.pid,
2630# "pdf",
2631# container.pid,
2632# new_pid,
2633# True)
2634# if 'pdf' in self.request.FILES:
2635# with open(pdf_filename, 'wb+') as destination:
2636# for chunk in self.request.FILES['pdf'].chunks():
2637# destination.write(chunk)
2638#
2639# # Extract full text from the PDF
2640# body = utils.pdf_to_text(pdf_filename)
2641#
2642# # Icon
2643# new_icon_location = ''
2644# if 'icon' in self.request.FILES:
2645# filename = os.path.basename(self.request.FILES['icon'].name)
2646# file_extension = filename.split('.')[1]
2647#
2648# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2649# collection.pid,
2650# file_extension,
2651# container.pid,
2652# new_pid,
2653# True)
2654#
2655# with open(icon_filename, 'wb+') as destination:
2656# for chunk in self.request.FILES['icon'].chunks():
2657# destination.write(chunk)
2658#
2659# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2660# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2661#
2662# if self.kwargs['article']:
2663# # Edit article
2664# article = self.kwargs['article']
2665# article.fpage = new_fpage
2666# article.lpage = new_lpage
2667# article.page_range = new_page_range
2668# article.coi_statement = new_coi_statement
2669# article.show_body = new_show_body
2670# article.do_not_publish = new_do_not_publish
2671# article.save()
2672#
2673# else:
2674# # New article
2675# params = {
2676# 'pid': new_pid,
2677# 'title_xml': new_title_xml,
2678# 'title_html': new_title_html,
2679# 'title_tex': new_title,
2680# 'fpage': new_fpage,
2681# 'lpage': new_lpage,
2682# 'page_range': new_page_range,
2683# 'seq': container.article_set.count() + 1,
2684# 'body': body,
2685# 'coi_statement': new_coi_statement,
2686# 'show_body': new_show_body,
2687# 'do_not_publish': new_do_not_publish
2688# }
2689#
2690# xarticle = create_articledata()
2691# xarticle.pid = new_pid
2692# xarticle.title_xml = new_title_xml
2693# xarticle.title_html = new_title_html
2694# xarticle.title_tex = new_title
2695# xarticle.fpage = new_fpage
2696# xarticle.lpage = new_lpage
2697# xarticle.page_range = new_page_range
2698# xarticle.seq = container.article_set.count() + 1
2699# xarticle.body = body
2700# xarticle.coi_statement = new_coi_statement
2701# params['xobj'] = xarticle
2702#
2703# cmd = ptf_cmds.addArticlePtfCmd(params)
2704# cmd.set_container(container)
2705# cmd.add_collection(container.my_collection)
2706# article = cmd.do()
2707#
2708# self.kwargs['pid'] = new_pid
2709#
2710# # Add objects related to the article: contribs, datastream, counts...
2711# params = {
2712# # 'title_xml': new_title_xml,
2713# # 'title_html': new_title_html,
2714# # 'title_tex': new_title,
2715# 'authors': new_authors,
2716# 'page_count': new_page_count,
2717# 'icon_location': new_icon_location,
2718# 'body': body,
2719# 'use_kwds': True,
2720# 'kwds_fr': new_kwds_fr,
2721# 'kwds_en': new_kwds_en,
2722# 'kwd_uns_fr': new_kwd_uns_fr,
2723# 'kwd_uns_en': new_kwd_uns_en
2724# }
2725# cmd = ptf_cmds.updateArticlePtfCmd(params)
2726# cmd.set_article(article)
2727# cmd.do()
2728#
2729# self.set_success_message()
2730#
2731# return super(ArticleEditView, self).form_valid(form)
2734@require_http_methods(["POST"])
2735def do_not_publish_article(request, *args, **kwargs):
2736 next = request.headers.get("referer")
2738 pid = kwargs.get("pid", "")
2740 article = model_helpers.get_article(pid)
2741 if article:
2742 article.do_not_publish = not article.do_not_publish
2743 article.save()
2744 else:
2745 raise Http404
2747 return HttpResponseRedirect(next)
2750@require_http_methods(["POST"])
2751def show_article_body(request, *args, **kwargs):
2752 next = request.headers.get("referer")
2754 pid = kwargs.get("pid", "")
2756 article = model_helpers.get_article(pid)
2757 if article:
2758 article.show_body = not article.show_body
2759 article.save()
2760 else:
2761 raise Http404
2763 return HttpResponseRedirect(next)
2766class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2767 """
2768 API to get/post article metadata
2769 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2770 """
2772 def __init__(self, *args, **kwargs):
2773 """
2774 we define here what fields we want in the form
2775 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2776 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2777 super().__init__(*args, **kwargs)
2778 self.fields_to_update = [
2779 "lang",
2780 "atype",
2781 "contributors",
2782 "abstracts",
2783 "kwds",
2784 "titles",
2785 "title_html",
2786 "title_xml",
2787 "title_tex",
2788 "streams",
2789 "ext_links",
2790 "date_accepted",
2791 "history_dates",
2792 "subjs",
2793 "bibitems",
2794 "references",
2795 ]
2796 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2797 self.additional_fields = [
2798 "doi",
2799 "pid",
2800 "container_pid",
2801 "pdf",
2802 "illustration",
2803 "dates",
2804 "msc_keywords",
2805 ]
2806 self.editorial_tools = [
2807 "translation",
2808 "sidebar",
2809 "lang_selection",
2810 "back_to_article_option",
2811 "msc_keywords",
2812 ]
2813 self.article_container_pid = ""
2814 self.back_url = "trammel"
2816 def save_data(self, data_article):
2817 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2818 # The icons are not preserved since we can add/edit/delete them in VueJs
2819 params = {
2820 "pid": data_article.pid,
2821 "export_folder": settings.MERSENNE_TMP_FOLDER,
2822 "export_all": True,
2823 "with_binary_files": False,
2824 }
2825 ptf_cmds.exportExtraDataPtfCmd(params).do()
2827 def restore_data(self, article):
2828 ptf_cmds.importExtraDataPtfCmd(
2829 {
2830 "pid": article.pid,
2831 "import_folder": settings.MERSENNE_TMP_FOLDER,
2832 "import_bibitemid": False,
2833 }
2834 ).do()
2836 @method_decorator(csrf_exempt)
2837 def dispatch(self, request, *args, **kwargs):
2838 user_role = request.user.roles.all()
2839 if user_role:
2840 new_fields_to_update = []
2841 new_additional_fields = []
2842 new_editorial_tools = []
2843 # if a user have > 1 role, fields accessible shall be cumulative
2844 for role in user_role:
2845 role_fields_to_updates, role_additional_fields = role.get_real_fields_to_update()
2846 new_fields_to_update += role_fields_to_updates
2847 new_additional_fields += role_additional_fields
2848 new_editorial_tools += role.editorial_tools
2849 # in case two roles have some fields in common
2850 self.fields_to_update = list(set(new_fields_to_update))
2851 self.additional_fields = list(set(new_additional_fields))
2852 self.editorial_tools = list(set(new_editorial_tools))
2854 self.additional_fields += ["doi", "pid", "container_pid"]
2855 return super().dispatch(request, *args, **kwargs)
2857 def post(self, request, *args, **kwargs):
2858 response = super().post(request, *args, **kwargs)
2859 if response.status_code in [200, 302]: 2859 ↛ 2867line 2859 didn't jump to line 2867 because the condition on line 2859 was always true
2860 return redirect(
2861 "api-edit-article",
2862 colid=kwargs.get("colid", ""),
2863 containerPid=kwargs.get("containerPid"),
2864 doi=kwargs.get("doi", ""),
2865 )
2866 else:
2867 raise Http404
2870class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2871 template_name = "article_form.html"
2873 def get_success_url(self):
2874 if self.kwargs["doi"]:
2875 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2876 return reverse("mersenne_dashboard/published_articles")
2878 def get_context_data(self, **kwargs):
2879 context = super().get_context_data(**kwargs)
2880 if "doi" in self.kwargs:
2881 article = model_helpers.get_article_by_doi(self.kwargs["doi"])
2882 context["article"] = article
2883 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(article)
2884 context["pid"] = context["article"].pid
2886 context["container_pid"] = kwargs.get("container_pid", "")
2887 return context
2890class ArticleDeleteView(View):
2891 def get(self, request, *args, **kwargs):
2892 pid = self.kwargs.get("pid", None)
2893 article = get_object_or_404(Article, pid=pid)
2895 try:
2896 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2897 article.undeploy(mersenneSite)
2899 cmd = ptf_cmds.addArticlePtfCmd(
2900 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2901 )
2902 cmd.set_container(article.my_container)
2903 cmd.set_object_to_be_deleted(article)
2904 cmd.undo()
2905 except Exception as exception:
2906 return HttpResponseServerError(exception)
2908 data = {"message": "Article successfully removed from Trammel", "status": 200}
2909 return JsonResponse(data)
2912def get_messages_in_queue():
2913 app = Celery("ptf-tools")
2914 # tasks = list(current_app.tasks)
2915 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2916 print(tasks)
2917 # i = app.control.inspect()
2919 with app.connection_or_acquire() as conn:
2920 remaining = conn.default_channel.queue_declare(
2921 queue="coordinator", passive=True
2922 ).message_count
2923 return remaining
2926class NumdamView(TemplateView, history_views.HistoryContextMixin):
2927 template_name = "numdam.html"
2929 def get_context_data(self, **kwargs):
2930 context = super().get_context_data(**kwargs)
2932 context["objs"] = ResourceInNumdam.objects.all()
2934 pre_issues = []
2935 prod_issues = []
2936 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2937 try:
2938 response = requests.get(url)
2939 if response.status_code == 200:
2940 data = response.json()
2941 if "issues" in data:
2942 pre_issues = data["issues"]
2943 except Exception:
2944 pass
2946 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2947 response = requests.get(url)
2948 if response.status_code == 200:
2949 data = response.json()
2950 if "issues" in data:
2951 prod_issues = data["issues"]
2953 new = sorted(list(set(pre_issues).difference(prod_issues)))
2954 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2955 grouped = [
2956 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2957 ]
2958 grouped_removed = [
2959 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2960 ]
2961 context["added_issues"] = grouped
2962 context["removed_issues"] = grouped_removed
2964 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2965 return context
2968class NumdamArchiveView(RedirectView):
2969 @staticmethod
2970 def reset_task_results():
2971 TaskResult.objects.all().delete()
2973 def get_redirect_url(self, *args, **kwargs):
2974 self.colid = kwargs["colid"]
2976 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2977 return Http404
2979 # we make sure archiving is not already running
2980 # if not get_messages_in_queue():
2981 # self.reset_task_results()
2983 if self.colid == "ALL":
2984 archive_numdam_collections.delay()
2985 else:
2986 archive_numdam_collection.s(self.colid).delay()
2988 return reverse("numdam")
2991class DeployAllNumdamAPIView(View):
2992 def internal_do(self, *args, **kwargs):
2993 pids = []
2995 for obj in ResourceInNumdam.objects.all():
2996 pids.append(obj.pid)
2998 return pids
3000 def get(self, request, *args, **kwargs):
3001 try:
3002 pids, status, message = history_views.execute_and_record_func(
3003 "deploy", "numdam", "ALL", self.internal_do, "numdam"
3004 )
3005 except Exception as exception:
3006 return HttpResponseServerError(exception)
3008 data = {"message": message, "ids": pids, "status": status}
3009 return JsonResponse(data)
3012class NumdamDeleteAPIView(View):
3013 def get(self, request, *args, **kwargs):
3014 pid = self.kwargs.get("pid", None)
3016 try:
3017 obj = ResourceInNumdam.objects.get(pid=pid)
3018 obj.delete()
3019 except Exception as exception:
3020 return HttpResponseServerError(exception)
3022 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
3023 return JsonResponse(data)
3026class ExtIdApiDetail(View):
3027 def get(self, request, *args, **kwargs):
3028 extid = get_object_or_404(
3029 ExtId,
3030 resource__pid=kwargs["pid"],
3031 id_type=kwargs["what"],
3032 )
3033 return JsonResponse(
3034 {
3035 "pk": extid.pk,
3036 "href": extid.get_href(),
3037 "fetch": reverse(
3038 "api-fetch-id",
3039 args=(
3040 extid.resource.pk,
3041 extid.id_value,
3042 extid.id_type,
3043 "extid",
3044 ),
3045 ),
3046 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3047 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3048 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3049 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3050 "is_valid": extid.checked,
3051 }
3052 )
3055class ExtIdFormTemplate(TemplateView):
3056 template_name = "common/externalid_form.html"
3058 def get_context_data(self, **kwargs):
3059 context = super().get_context_data(**kwargs)
3060 context["sequence"] = kwargs["sequence"]
3061 return context
3064class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3065 def get_context_data(self, **kwargs):
3066 context = super().get_context_data(**kwargs)
3067 context["helper"] = PtfFormHelper
3068 return context
3070 def get_success_url(self):
3071 self.post_process()
3072 return self.object.resource.get_absolute_url()
3074 def post_process(self):
3075 model_helpers.post_resource_updated(self.object.resource)
3078class ExtIdCreate(ExtIdFormView, CreateView):
3079 model = ExtId
3080 form_class = ExtIdForm
3082 def get_context_data(self, **kwargs):
3083 context = super().get_context_data(**kwargs)
3084 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3085 return context
3087 def get_initial(self):
3088 initial = super().get_initial()
3089 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3090 return initial
3092 def form_valid(self, form):
3093 form.instance.checked = False
3094 return super().form_valid(form)
3097class ExtIdUpdate(ExtIdFormView, UpdateView):
3098 model = ExtId
3099 form_class = ExtIdForm
3101 def get_context_data(self, **kwargs):
3102 context = super().get_context_data(**kwargs)
3103 context["resource"] = self.object.resource
3104 return context
3107class UpdateTexmfZipAPIView(View):
3108 def get(self, request, *args, **kwargs):
3109 def copy_zip_files(src_folder, dest_folder):
3110 os.makedirs(dest_folder, exist_ok=True)
3112 zip_files = [
3113 os.path.join(src_folder, f)
3114 for f in os.listdir(src_folder)
3115 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3116 ]
3117 for zip_file in zip_files:
3118 resolver.copy_file(zip_file, dest_folder)
3120 # Exceptions: specific zip/gz files
3121 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3122 resolver.copy_file(zip_file, dest_folder)
3124 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3125 resolver.copy_file(zip_file, dest_folder)
3127 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3128 resolver.copy_file(gz_file, dest_folder)
3130 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3132 dest_folder = os.path.join(
3133 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3134 )
3136 try:
3137 copy_zip_files(src_folder, dest_folder)
3138 except Exception as exception:
3139 return HttpResponseServerError(exception)
3141 try:
3142 dest_folder = os.path.join(
3143 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3144 )
3145 copy_zip_files(src_folder, dest_folder)
3146 except Exception as exception:
3147 return HttpResponseServerError(exception)
3149 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3150 return JsonResponse(data)
3153class TrammelTasksProgressView(View):
3154 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3155 """
3156 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3157 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3158 """
3159 task_name = task
3161 def get_event_data():
3162 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3164 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3165 remaining_messages = get_messages_in_queue()
3167 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3168 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3169 failed_tasks = all_tasks.filter(status="FAILURE")
3171 all_tasks_count = all_tasks.count()
3172 success_count = successed_tasks.count()
3173 fail_count = failed_tasks.count()
3175 all_count = all_tasks_count + remaining_messages
3176 remaining_count = all_count - success_count - fail_count
3178 success_rate = int(success_count * 100 / all_count) if all_count else 0
3179 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3180 status = "consuming_queue" if remaining_count != 0 else "polling"
3182 last_task = successed_tasks.first()
3183 last_task = (
3184 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3185 if last_task
3186 else ""
3187 )
3189 # SSE event format
3190 event_data = {
3191 "status": status,
3192 "success_rate": success_rate,
3193 "error_rate": error_rate,
3194 "all_count": all_count,
3195 "remaining_count": remaining_count,
3196 "success_count": success_count,
3197 "fail_count": fail_count,
3198 "last_task": last_task,
3199 }
3201 return event_data
3203 def stream_response(data):
3204 # Send initial response headers
3205 yield f"data: {json.dumps(data)}\n\n"
3207 data = get_event_data()
3208 format = request.GET.get("format", "stream")
3209 if format == "json":
3210 response = JsonResponse(data)
3211 else:
3212 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3213 return response
3216user_signed_up.connect(update_user_from_invite)