Coverage for src / ptf_tools / views / base_views.py: 18%
1666 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-07-09 12:21 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-07-09 12:21 +0000
1import io
2import json
3import logging
4import os
5import re
6from datetime import datetime
7from itertools import groupby
9import jsonpickle
10import requests
11from allauth.account.signals import user_signed_up
12from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin
13from celery import Celery, current_app
14from django.conf import settings
15from django.contrib import messages
16from django.contrib.auth.mixins import UserPassesTestMixin
17from django.db.models import Q
18from django.http import (
19 Http404,
20 HttpRequest,
21 HttpResponse,
22 HttpResponseRedirect,
23 HttpResponseServerError,
24 JsonResponse,
25)
26from django.shortcuts import get_object_or_404, redirect, render
27from django.urls import resolve, reverse
28from django.utils import timezone
29from django.views.decorators.http import require_http_methods
30from django.views.generic import ListView, TemplateView, View
31from django.views.generic.base import RedirectView
32from django.views.generic.detail import SingleObjectMixin
33from django.views.generic.edit import CreateView, FormView, UpdateView
34from django_celery_results.models import TaskResult
35from external.back.crossref.doi import checkDOI, recordDOI, recordPendingPublication
36from extra_views import (
37 CreateWithInlinesView,
38 InlineFormSetFactory,
39 NamedFormsetsMixin,
40 UpdateWithInlinesView,
41)
43# from ptf.views import ArticleEditFormWithVueAPIView
44from matching_back.views import ArticleEditFormWithVueAPIView
45from ptf import model_data_converter, model_helpers, utils
46from ptf.cmds import ptf_cmds, xml_cmds
47from ptf.cmds.base_cmds import make_int
48from ptf.cmds.xml.jats.builder.issue import build_title_xml
49from ptf.cmds.xml.xml_utils import replace_html_entities
50from ptf.display import resolver
51from ptf.exceptions import DOIException, PDFException, ServerUnderMaintenance
52from ptf.model_data import create_issuedata, create_publisherdata, create_titledata
53from ptf.models import (
54 Abstract,
55 Article,
56 BibItem,
57 BibItemId,
58 Collection,
59 Container,
60 ExtId,
61 ExtLink,
62 Resource,
63 ResourceId,
64)
65from ptf_back.cmds.xml_cmds import updateBibitemCitationXmlCmd
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 BibItemIdForm,
87 CollectionForm,
88 ContainerForm,
89 DiffContainerForm,
90 ExtIdForm,
91 ExtLinkForm,
92 FormSetHelper,
93 ImportArticleForm,
94 ImportContainerForm,
95 ImportEditflowArticleForm,
96 PtfFormHelper,
97 PtfLargeModalFormHelper,
98 PtfModalFormHelper,
99 RegisterPubmedForm,
100 ResourceIdForm,
101 get_article_choices,
102)
103from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos
104from ptf_tools.models import ResourceInNumdam
105from ptf_tools.signals import update_user_from_invite
106from ptf_tools.tasks import (
107 archive_numdam_collection,
108 archive_numdam_collections,
109)
110from ptf_tools.templatetags.tools_helpers import get_authorized_collections
111from ptf_tools.utils import is_authorized_editor
112from ptf_tools.views.components import breadcrumb
114logger = logging.getLogger(__name__)
117def view_404(request: HttpRequest, *args, **kwargs):
118 """
119 Dummy view raising HTTP 404 exception.
120 """
121 raise Http404
124def check_collection(collection, server_url, server_type):
125 """
126 Check if a collection exists on a serveur (test/prod)
127 and upload the collection (XML, image) if necessary
128 """
130 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
131 response = requests.get(url, verify=False)
132 # First, upload the collection XML
133 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
134 body = xml.encode("utf8")
136 url = server_url + reverse("upload-serials")
137 if response.status_code == 200:
138 # PUT http verb is used for update
139 response = requests.put(url, data=body, verify=False)
140 else:
141 # POST http verb is used for creation
142 response = requests.post(url, data=body, verify=False)
144 # Second, copy the collection images
145 # There is no need to copy files for the test server
146 # Files were already copied in /mersenne_test_data during the ptf_tools import
147 # We only need to copy files from /mersenne_test_data to
148 # /mersenne_prod_data during an upload to prod
149 if server_type == "website":
150 resolver.copy_binary_files(
151 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
152 )
153 elif server_type == "numdam":
154 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
155 if collection.pid in settings.NUMDAM_COLLECTIONS:
156 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
158 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
161def check_lock():
162 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
165def load_cedrics_article_choices(request):
166 colid = request.GET.get("colid")
167 issue = request.GET.get("issue")
168 article_choices = get_article_choices(colid, issue)
169 return render(
170 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
171 )
174class ImportCedricsArticleFormView(FormView):
175 template_name = "import_article.html"
176 form_class = ImportArticleForm
178 def dispatch(self, request, *args, **kwargs):
179 self.colid = self.kwargs["colid"]
180 return super().dispatch(request, *args, **kwargs)
182 def get_success_url(self):
183 if self.colid:
184 return reverse("collection-detail", kwargs={"pid": self.colid})
185 return "/"
187 def get_context_data(self, **kwargs):
188 context = super().get_context_data(**kwargs)
189 context["colid"] = self.colid
190 context["helper"] = PtfModalFormHelper
191 return context
193 def get_form_kwargs(self):
194 kwargs = super().get_form_kwargs()
195 kwargs["colid"] = self.colid
196 return kwargs
198 def form_valid(self, form):
199 self.issue = form.cleaned_data["issue"]
200 self.article = form.cleaned_data["article"]
201 return super().form_valid(form)
203 def import_cedrics_article(self, *args, **kwargs):
204 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
205 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
206 )
207 cmd.do()
209 def post(self, request, *args, **kwargs):
210 self.colid = self.kwargs.get("colid", None)
211 issue = request.POST["issue"]
212 self.article_pid = request.POST["article"]
213 self.issue_pid = os.path.basename(os.path.dirname(issue))
215 import_args = [self]
216 import_kwargs = {}
218 try:
219 _, status, message = history_views.execute_and_record_func(
220 "import",
221 f"{self.issue_pid} / {self.article_pid}",
222 self.colid,
223 self.import_cedrics_article,
224 "",
225 False,
226 None,
227 None,
228 *import_args,
229 **import_kwargs,
230 )
232 messages.success(
233 self.request, f"L'article {self.article_pid} a été importé avec succès"
234 )
236 except Exception as exception:
237 messages.error(
238 self.request,
239 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}",
240 )
242 return redirect(self.get_success_url())
245class ImportCedricsIssueView(FormView):
246 template_name = "import_container.html"
247 form_class = ImportContainerForm
249 def dispatch(self, request, *args, **kwargs):
250 self.colid = self.kwargs["colid"]
251 self.to_appear = self.request.GET.get("to_appear", False)
252 return super().dispatch(request, *args, **kwargs)
254 def get_success_url(self):
255 if self.filename:
256 return reverse(
257 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
258 )
259 return "/"
261 def get_context_data(self, **kwargs):
262 context = super().get_context_data(**kwargs)
263 context["colid"] = self.colid
264 context["helper"] = PtfModalFormHelper
265 return context
267 def get_form_kwargs(self):
268 kwargs = super().get_form_kwargs()
269 kwargs["colid"] = self.colid
270 kwargs["to_appear"] = self.to_appear
271 return kwargs
273 def form_valid(self, form):
274 self.filename = form.cleaned_data["filename"].split("/")[-1]
275 return super().form_valid(form)
278class DiffCedricsIssueView(FormView):
279 template_name = "diff_container_form.html"
280 form_class = DiffContainerForm
281 diffs = None
282 xissue = None
283 xissue_encoded = None
285 def get_success_url(self):
286 return reverse("collection-detail", kwargs={"pid": self.colid})
288 def dispatch(self, request, *args, **kwargs):
289 self.colid = self.kwargs["colid"]
290 # self.filename = self.kwargs['filename']
291 return super().dispatch(request, *args, **kwargs)
293 def get(self, request, *args, **kwargs):
294 self.filename = request.GET["filename"]
295 self.remove_mail = request.GET.get("remove_email", "off")
296 self.remove_date_prod = request.GET.get("remove_date_prod", "off")
297 self.remove_email = self.remove_mail == "on"
298 self.remove_date_prod = self.remove_date_prod == "on"
300 try:
301 result, status, message = history_views.execute_and_record_func(
302 "import",
303 os.path.basename(self.filename),
304 self.colid,
305 self.diff_cedrics_issue,
306 "",
307 True,
308 )
309 except Exception as exception:
310 pid = self.filename.split("/")[-1]
311 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
312 return HttpResponseRedirect(self.get_success_url())
314 no_conflict = result[0]
315 self.diffs = result[1]
316 self.xissue = result[2]
318 if no_conflict:
319 # Proceed with the import
320 self.form_valid(self.get_form())
321 return redirect(self.get_success_url())
322 else:
323 # Display the diff template
324 self.xissue_encoded = jsonpickle.encode(self.xissue)
326 return super().get(request, *args, **kwargs)
328 def post(self, request, *args, **kwargs):
329 self.filename = request.POST["filename"]
330 data = request.POST["xissue_encoded"]
331 self.xissue = jsonpickle.decode(data)
333 return super().post(request, *args, **kwargs)
335 def get_context_data(self, **kwargs):
336 context = super().get_context_data(**kwargs)
337 context["colid"] = self.colid
338 context["diff"] = self.diffs
339 context["filename"] = self.filename
340 context["xissue_encoded"] = self.xissue_encoded
341 return context
343 def get_form_kwargs(self):
344 kwargs = super().get_form_kwargs()
345 kwargs["colid"] = self.colid
346 return kwargs
348 def diff_cedrics_issue(self, *args, **kwargs):
349 params = {
350 "colid": self.colid,
351 "input_file": self.filename,
352 "remove_email": self.remove_mail,
353 "remove_date_prod": self.remove_date_prod,
354 "diff_only": True,
355 }
357 if settings.IMPORT_CEDRICS_DIRECTLY:
358 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
359 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
360 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
361 else:
362 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
364 result = cmd.do()
365 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
366 messages.warning(
367 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings
368 )
370 return result
372 def import_cedrics_issue(self, *args, **kwargs):
373 # modify xissue with data_issue if params to override
374 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
375 issue = model_helpers.get_container(self.xissue.pid)
376 if issue:
377 data_issue = model_data_converter.db_to_issue_data(issue)
378 for xarticle in self.xissue.articles:
379 filter_articles = [
380 article for article in data_issue.articles if article.doi == xarticle.doi
381 ]
382 if len(filter_articles) > 0:
383 db_article = filter_articles[0]
384 xarticle.coi_statement = db_article.coi_statement
385 xarticle.kwds = db_article.kwds
386 xarticle.contrib_groups = db_article.contrib_groups
388 params = {
389 "colid": self.colid,
390 "xissue": self.xissue,
391 "input_file": self.filename,
392 }
394 if settings.IMPORT_CEDRICS_DIRECTLY:
395 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
396 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
397 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
398 else:
399 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
401 cmd.do()
403 def form_valid(self, form):
404 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
405 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
406 else:
407 import_kwargs = {}
408 import_args = [self]
410 try:
411 _, status, message = history_views.execute_and_record_func(
412 "import",
413 self.xissue.pid,
414 self.kwargs["colid"],
415 self.import_cedrics_issue,
416 "",
417 False,
418 None,
419 None,
420 *import_args,
421 **import_kwargs,
422 )
423 except Exception as exception:
424 messages.error(
425 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
426 )
427 return super().form_invalid(form)
429 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
430 return super().form_valid(form)
433class ImportEditflowArticleFormView(FormView):
434 template_name = "import_editflow_article.html"
435 form_class = ImportEditflowArticleForm
437 def dispatch(self, request, *args, **kwargs):
438 self.colid = self.kwargs["colid"]
439 return super().dispatch(request, *args, **kwargs)
441 def get_context_data(self, **kwargs):
442 context = super().get_context_data(**kwargs)
443 context["colid"] = self.kwargs["colid"]
444 context["helper"] = PtfLargeModalFormHelper
445 return context
447 def get_success_url(self):
448 if self.colid:
449 return reverse("collection-detail", kwargs={"pid": self.colid})
450 return "/"
452 def post(self, request, *args, **kwargs):
453 self.colid = self.kwargs.get("colid", None)
454 try:
455 if not self.colid:
456 raise ValueError("Missing collection id")
458 issue_name = settings.ISSUE_PENDING_PUBLICATION_PIDS.get(self.colid)
459 if not issue_name:
460 raise ValueError(
461 "Issue not found in Pending Publications PIDs. Did you forget to add it?"
462 )
464 issue = model_helpers.get_container(issue_name)
465 if not issue:
466 raise ValueError("No issue found")
468 editflow_xml_file = request.FILES.get("editflow_xml_file")
469 if not editflow_xml_file:
470 raise ValueError("The file you specified couldn't be found")
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 request,
488 f'Editflow article successfully imported into issue "{issue_name}"',
489 )
491 except Exception as exception:
492 messages.error(
493 request,
494 f"Import failed: {str(exception)}",
495 )
497 return redirect(self.get_success_url())
500class BibtexAPIView(View):
501 def get(self, request, *args, **kwargs):
502 pid = self.kwargs.get("pid", None)
503 all_bibtex = ""
504 if pid:
505 article = model_helpers.get_article(pid)
506 if article:
507 for bibitem in article.bibitem_set.all():
508 bibtex_array = bibitem.get_bibtex()
509 last = len(bibtex_array)
510 i = 1
511 for bibtex in bibtex_array:
512 if i > 1 and i < last:
513 all_bibtex += " "
514 all_bibtex += bibtex + "\n"
515 i += 1
517 data = {"bibtex": all_bibtex}
518 return JsonResponse(data)
521class MatchingAPIView(View):
522 def get(self, request, *args, **kwargs):
523 pid = self.kwargs.get("pid", None)
525 url = settings.MATCHING_URL
526 headers = {"Content-Type": "application/xml"}
528 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
530 if settings.DEBUG:
531 print("Issue exported to /tmp/issue.xml")
532 f = open("/tmp/issue.xml", "w")
533 f.write(body.encode("utf8"))
534 f.close()
536 r = requests.post(url, data=body.encode("utf8"), headers=headers)
537 body = r.text.encode("utf8")
538 data = {"status": r.status_code, "message": body[:1000]}
540 if settings.DEBUG:
541 print("Matching received, new issue exported to /tmp/issue1.xml")
542 f = open("/tmp/issue1.xml", "w")
543 text = body
544 f.write(text)
545 f.close()
547 resource = model_helpers.get_resource(pid)
548 obj = resource.cast()
549 colid = obj.get_collection().pid
551 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
553 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
554 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
555 )
556 cmd.do()
558 print("Matching finished")
559 return JsonResponse(data)
562class ImportAllAPIView(View):
563 def internal_do(self, *args, **kwargs):
564 pid = self.kwargs.get("pid", None)
566 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
567 if not os.path.isdir(root_folder):
568 raise ValueError(root_folder + " does not exist")
570 resource = model_helpers.get_resource(pid)
571 if not resource:
572 file = os.path.join(root_folder, pid + ".xml")
573 body = utils.get_file_content_in_utf8(file)
574 journals = xml_cmds.addCollectionsXmlCmd(
575 {
576 "body": body,
577 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
578 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
579 }
580 ).do()
581 if not journals:
582 raise ValueError(file + " does not contain a collection")
583 resource = journals[0]
584 # resolver.copy_binary_files(
585 # resource,
586 # settings.MATHDOC_ARCHIVE_FOLDER,
587 # settings.MERSENNE_TEST_DATA_FOLDER)
589 obj = resource.cast()
591 if obj.classname != "Collection":
592 raise ValueError(pid + " does not contain a collection")
594 cmd = xml_cmds.collectEntireCollectionXmlCmd(
595 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
596 )
597 pids = cmd.do()
599 return pids
601 def get(self, request, *args, **kwargs):
602 pid = self.kwargs.get("pid", None)
604 try:
605 pids, status, message = history_views.execute_and_record_func(
606 "import", pid, pid, self.internal_do
607 )
608 except Timeout as exception:
609 return HttpResponse(exception, status=408)
610 except Exception as exception:
611 return HttpResponseServerError(exception)
613 data = {"message": message, "ids": pids, "status": status}
614 return JsonResponse(data)
617class DeployAllAPIView(View):
618 def internal_do(self, *args, **kwargs):
619 pid = self.kwargs.get("pid", None)
620 site = self.kwargs.get("site", None)
622 pids = []
624 collection = model_helpers.get_collection(pid)
625 if not collection:
626 raise RuntimeError(pid + " does not exist")
628 if site == "numdam":
629 server_url = settings.NUMDAM_PRE_URL
630 elif site != "ptf_tools":
631 server_url = getattr(collection, site)()
632 if not server_url:
633 raise RuntimeError("The collection has no " + site)
635 if site != "ptf_tools":
636 # check if the collection exists on the server
637 # if not, check_collection will upload the collection (XML,
638 # image...)
639 check_collection(collection, server_url, site)
641 for issue in collection.content.all():
642 if site != "website" or (site == "website" and issue.are_all_articles_published()):
643 pids.append(issue.pid)
645 return pids
647 def get(self, request, *args, **kwargs):
648 pid = self.kwargs.get("pid", None)
649 site = self.kwargs.get("site", None)
651 try:
652 pids, status, message = history_views.execute_and_record_func(
653 "deploy", pid, pid, self.internal_do, site
654 )
655 except Timeout as exception:
656 return HttpResponse(exception, status=408)
657 except Exception as exception:
658 return HttpResponseServerError(exception)
660 data = {"message": message, "ids": pids, "status": status}
661 return JsonResponse(data)
664class AddIssuePDFView(View):
665 def __init(self, *args, **kwargs):
666 super().__init__(*args, **kwargs)
667 self.pid = None
668 self.issue = None
669 self.collection = None
670 self.site = "test_website"
672 def post_to_site(self, url):
673 response = requests.post(url, verify=False)
674 status = response.status_code
675 if not (199 < status < 205):
676 messages.error(self.request, response.text)
677 if status == 503:
678 raise ServerUnderMaintenance(response.text)
679 else:
680 raise RuntimeError(response.text)
682 def internal_do(self, *args, **kwargs):
683 """
684 Called by history_views.execute_and_record_func to do the actual job.
685 """
687 issue_pid = self.issue.pid
688 colid = self.collection.pid
690 if self.site == "website":
691 # Copy the PDF from the test to the production folder
692 resolver.copy_binary_files(
693 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
694 )
695 else:
696 # Copy the PDF from the cedram to the test folder
697 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
698 from_path = os.path.join(from_folder, issue_pid + ".pdf")
699 if not os.path.isfile(from_path):
700 raise Http404(f"{from_path} does not exist")
702 to_path = resolver.get_disk_location(
703 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
704 )
705 resolver.copy_file(from_path, to_path)
707 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
709 if self.site == "test_website":
710 # Post to ptf-tools: it will add a Datastream to the issue
711 absolute_url = self.request.build_absolute_uri(url)
712 self.post_to_site(absolute_url)
714 server_url = getattr(self.collection, self.site)()
715 absolute_url = server_url + url
716 # Post to the test or production website
717 self.post_to_site(absolute_url)
719 def get(self, request, *args, **kwargs):
720 """
721 Send an issue PDF to the test or production website
722 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
723 :param args:
724 :param kwargs:
725 :return:
726 """
727 if check_lock():
728 m = "Trammel is under maintenance. Please try again later."
729 messages.error(self.request, m)
730 return JsonResponse({"message": m, "status": 503})
732 self.pid = self.kwargs.get("pid", None)
733 self.site = self.kwargs.get("site", "test_website")
735 self.issue = model_helpers.get_container(self.pid)
736 if not self.issue:
737 raise Http404(f"{self.pid} does not exist")
738 self.collection = self.issue.get_top_collection()
740 try:
741 pids, status, message = history_views.execute_and_record_func(
742 "deploy",
743 self.pid,
744 self.collection.pid,
745 self.internal_do,
746 f"add issue PDF to {self.site}",
747 )
749 except Timeout as exception:
750 return HttpResponse(exception, status=408)
751 except Exception as exception:
752 return HttpResponseServerError(exception)
754 data = {"message": message, "status": status}
755 return JsonResponse(data)
758class ArchiveAllAPIView(View):
759 """
760 - archive le xml de la collection ainsi que les binaires liés
761 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
762 @return array of issues pid
763 """
765 def internal_do(self, *args, **kwargs):
766 collection = kwargs["collection"]
767 pids = []
768 colid = collection.pid
770 logfile = os.path.join(settings.LOG_DIR, "archive.log")
771 if os.path.isfile(logfile):
772 os.remove(logfile)
774 ptf_cmds.exportPtfCmd(
775 {
776 "pid": colid,
777 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
778 "with_binary_files": True,
779 "for_archive": True,
780 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
781 }
782 ).do()
784 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
785 if os.path.isfile(cedramcls):
786 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
787 resolver.create_folder(dest_folder)
788 resolver.copy_file(cedramcls, dest_folder)
790 for issue in collection.content.all():
791 qs = issue.article_set.filter(
792 date_online_first__isnull=True, date_published__isnull=True
793 )
794 if qs.count() == 0:
795 pids.append(issue.pid)
797 return pids
799 def get(self, request, *args, **kwargs):
800 pid = self.kwargs.get("pid", None)
802 collection = model_helpers.get_collection(pid)
803 if not collection:
804 return HttpResponse(f"{pid} does not exist", status=400)
806 dict_ = {"collection": collection}
807 args_ = [self]
809 try:
810 pids, status, message = history_views.execute_and_record_func(
811 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_
812 )
813 except Timeout as exception:
814 return HttpResponse(exception, status=408)
815 except Exception as exception:
816 return HttpResponseServerError(exception)
818 data = {"message": message, "ids": pids, "status": status}
819 return JsonResponse(data)
822class CreateAllDjvuAPIView(View):
823 def internal_do(self, *args, **kwargs):
824 issue = kwargs["issue"]
825 pids = [issue.pid]
827 for article in issue.article_set.all():
828 pids.append(article.pid)
830 return pids
832 def get(self, request, *args, **kwargs):
833 pid = self.kwargs.get("pid", None)
834 issue = model_helpers.get_container(pid)
835 if not issue:
836 raise Http404(f"{pid} does not exist")
838 try:
839 dict_ = {"issue": issue}
840 args_ = [self]
842 pids, status, message = history_views.execute_and_record_func(
843 "numdam",
844 pid,
845 issue.get_collection().pid,
846 self.internal_do,
847 "",
848 False,
849 None,
850 None,
851 *args_,
852 **dict_,
853 )
854 except Exception as exception:
855 return HttpResponseServerError(exception)
857 data = {"message": message, "ids": pids, "status": status}
858 return JsonResponse(data)
861class ImportJatsContainerAPIView(View):
862 def internal_do(self, *args, **kwargs):
863 pid = self.kwargs.get("pid", None)
864 colid = self.kwargs.get("colid", None)
866 if pid and colid:
867 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
869 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
870 {
871 "body": body,
872 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
873 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
874 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
875 }
876 )
877 container = cmd.do()
878 if len(cmd.warnings) > 0:
879 messages.warning(
880 self.request,
881 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
882 )
884 if not container:
885 raise RuntimeError("Error: the container " + pid + " was not imported")
887 # resolver.copy_binary_files(
888 # container,
889 # settings.MATHDOC_ARCHIVE_FOLDER,
890 # settings.MERSENNE_TEST_DATA_FOLDER)
891 #
892 # for article in container.article_set.all():
893 # resolver.copy_binary_files(
894 # article,
895 # settings.MATHDOC_ARCHIVE_FOLDER,
896 # settings.MERSENNE_TEST_DATA_FOLDER)
897 else:
898 raise RuntimeError("colid or pid are not defined")
900 def get(self, request, *args, **kwargs):
901 pid = self.kwargs.get("pid", None)
902 colid = self.kwargs.get("colid", None)
904 try:
905 _, status, message = history_views.execute_and_record_func(
906 "import", pid, colid, self.internal_do
907 )
908 except Timeout as exception:
909 return HttpResponse(exception, status=408)
910 except Exception as exception:
911 return HttpResponseServerError(exception)
913 data = {"message": message, "status": status}
914 return JsonResponse(data)
917class DeployCollectionAPIView(View):
918 # Update collection.xml on a site (with its images)
920 def internal_do(self, *args, **kwargs):
921 colid = self.kwargs.get("colid", None)
922 site = self.kwargs.get("site", None)
924 collection = model_helpers.get_collection(colid)
925 if not collection:
926 raise RuntimeError(f"{colid} does not exist")
928 if site == "numdam":
929 server_url = settings.NUMDAM_PRE_URL
930 else:
931 server_url = getattr(collection, site)()
932 if not server_url:
933 raise RuntimeError(f"The collection has no {site}")
935 # check_collection creates or updates the collection (XML, image...)
936 check_collection(collection, server_url, site)
938 def get(self, request, *args, **kwargs):
939 colid = self.kwargs.get("colid", None)
940 site = self.kwargs.get("site", None)
942 try:
943 _, status, message = history_views.execute_and_record_func(
944 "deploy", colid, colid, self.internal_do, site
945 )
946 except Timeout as exception:
947 return HttpResponse(exception, status=408)
948 except Exception as exception:
949 return HttpResponseServerError(exception)
951 data = {"message": message, "status": status}
952 return JsonResponse(data)
955class DeployJatsResourceAPIView(View):
956 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
958 def internal_do(self, *args, **kwargs):
959 pid = self.kwargs.get("pid", None)
960 colid = self.kwargs.get("colid", None)
961 site = self.kwargs.get("site", None)
963 if site == "ptf_tools":
964 raise RuntimeError("Do not choose to deploy on PTF Tools")
965 if check_lock():
966 msg = "Trammel is under maintenance. Please try again later."
967 messages.error(self.request, msg)
968 return JsonResponse({"messages": msg, "status": 503})
970 resource = model_helpers.get_resource(pid)
971 if not resource:
972 raise RuntimeError(f"{pid} does not exist")
974 obj = resource.cast()
975 article = None
976 if obj.classname == "Article":
977 article = obj
978 container = article.my_container
979 articles_to_deploy = [article]
980 else:
981 container = obj
982 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
984 if container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(colid, None):
985 raise RuntimeError("Pending publications should not be deployed")
986 if site == "website" and article is not None and article.do_not_publish:
987 raise RuntimeError(f"{pid} is marked as Do not publish")
988 if site == "numdam" and article is not None:
989 raise RuntimeError("You can only deploy issues to Numdam")
991 collection = container.get_top_collection()
992 colid = collection.pid
993 djvu_exception = None
995 if site == "numdam":
996 server_url = settings.NUMDAM_PRE_URL
997 ResourceInNumdam.objects.get_or_create(pid=container.pid)
999 # 06/12/2022: DjVu are no longer added with Mersenne articles
1000 # Add Djvu (before exporting the XML)
1001 if False and int(container.fyear) < 2020:
1002 for art in container.article_set.all():
1003 try:
1004 cmd = ptf_cmds.addDjvuPtfCmd()
1005 cmd.set_resource(art)
1006 cmd.do()
1007 except Exception as e:
1008 # Djvu are optional.
1009 # Allow the deployment, but record the exception in the history
1010 djvu_exception = e
1011 else:
1012 server_url = getattr(collection, site)()
1013 if not server_url:
1014 raise RuntimeError(f"The collection has no {site}")
1016 # check if the collection exists on the server
1017 # if not, check_collection will upload the collection (XML,
1018 # image...)
1019 if article is None:
1020 check_collection(collection, server_url, site)
1022 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
1023 # Create/update deployed date and published date on all container articles
1024 if site == "website":
1025 file_.write(
1026 "Create/Update deployed_date and date_published on all articles for {}\n".format(
1027 pid
1028 )
1029 )
1031 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
1032 cmd = ptf_cmds.publishResourcePtfCmd()
1033 cmd.set_resource(resource)
1034 updated_articles = cmd.do()
1036 create_frontpage(colid, container, updated_articles, test=False)
1038 mersenneSite = model_helpers.get_site_mersenne(colid)
1039 # create or update deployed_date on container and articles
1040 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
1042 for art in articles_to_deploy:
1043 if art.doi and (art.date_published or art.date_online_first):
1044 if art.my_container.fyear is None:
1045 art.my_container.fyear = datetime.now().year
1046 # BUG ? update the container but no save() ?
1048 file_.write(
1049 "Publication date of {} : Online First: {}, Published: {}\n".format(
1050 art.pid, art.date_online_first, art.date_published
1051 )
1052 )
1054 if article is None:
1055 resolver.copy_binary_files(
1056 container,
1057 settings.MERSENNE_TEST_DATA_FOLDER,
1058 settings.MERSENNE_PROD_DATA_FOLDER,
1059 )
1061 for art in articles_to_deploy:
1062 resolver.copy_binary_files(
1063 art,
1064 settings.MERSENNE_TEST_DATA_FOLDER,
1065 settings.MERSENNE_PROD_DATA_FOLDER,
1066 )
1068 elif site == "test_website":
1069 # create date_pre_published on articles without date_pre_published
1070 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
1071 cmd.set_resource(resource)
1072 updated_articles = cmd.do()
1074 create_frontpage(colid, container, updated_articles)
1076 export_to_website = site == "website"
1078 if article is None:
1079 with_djvu = site == "numdam"
1080 xml = ptf_cmds.exportPtfCmd(
1081 {
1082 "pid": pid,
1083 "with_djvu": with_djvu,
1084 "export_to_website": export_to_website,
1085 }
1086 ).do()
1087 body = xml.encode("utf8")
1089 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1090 url = server_url + reverse("issue_upload")
1091 else:
1092 url = server_url + reverse("book_upload")
1094 # verify=False: ignore TLS certificate
1095 response = requests.post(url, data=body, verify=False)
1096 # response = requests.post(url, files=files, verify=False)
1097 else:
1098 xml = ptf_cmds.exportPtfCmd(
1099 {
1100 "pid": pid,
1101 "with_djvu": False,
1102 "article_standalone": True,
1103 "collection_pid": collection.pid,
1104 "export_to_website": export_to_website,
1105 "export_folder": settings.LOG_DIR,
1106 }
1107 ).do()
1108 # Unlike containers that send their XML as the body of the POST request,
1109 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1110 xml_file = io.StringIO(xml)
1111 files = {"xml": xml_file}
1113 url = server_url + reverse(
1114 "article_in_issue_upload", kwargs={"pid": container.pid}
1115 )
1116 # verify=False: ignore TLS certificate
1117 header = {}
1118 response = requests.post(url, headers=header, files=files, verify=False)
1120 status = response.status_code
1122 if 199 < status < 205:
1123 # There is no need to copy files for the test server
1124 # Files were already copied in /mersenne_test_data during the ptf_tools import
1125 # We only need to copy files from /mersenne_test_data to
1126 # /mersenne_prod_data during an upload to prod
1127 if site == "website":
1128 # TODO mettre ici le record doi pour un issue publié
1129 if container.doi:
1130 recordDOI(container)
1132 for art in articles_to_deploy:
1133 # record DOI automatically when deploying in prod
1135 if art.doi and art.allow_crossref():
1136 recordDOI(art)
1138 if colid == "CRBIOL":
1139 recordPubmed(
1140 art, force_update=False, updated_articles=updated_articles
1141 )
1143 if colid == "PCJ":
1144 self.update_pcj_editor(updated_articles)
1146 # Archive the container or the article
1147 if article is None:
1148 archive_resource.delay(
1149 pid,
1150 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1151 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1152 )
1154 else:
1155 archive_resource.delay(
1156 pid,
1157 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1158 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1159 article_doi=article.doi,
1160 )
1161 # cmd = ptf_cmds.archiveIssuePtfCmd({
1162 # "pid": pid,
1163 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1164 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1165 # cmd.set_article(article) # set_article allows archiving only the article
1166 # cmd.do()
1168 elif site == "numdam":
1169 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1170 if colid in settings.NUMDAM_COLLECTIONS:
1171 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1173 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1174 for article in container.article_set.all():
1175 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1177 elif status == 503:
1178 raise ServerUnderMaintenance(response.text)
1179 else:
1180 raise RuntimeError(response.text)
1182 if djvu_exception:
1183 raise djvu_exception
1185 def get(self, request, *args, **kwargs):
1186 pid = self.kwargs.get("pid", None)
1187 colid = self.kwargs.get("colid", None)
1188 site = self.kwargs.get("site", None)
1190 try:
1191 _, status, message = history_views.execute_and_record_func(
1192 "deploy", pid, colid, self.internal_do, site
1193 )
1194 except Timeout as exception:
1195 return HttpResponse(exception, status=408)
1196 except Exception as exception:
1197 return HttpResponseServerError(exception)
1199 data = {"message": message, "status": status}
1200 return JsonResponse(data)
1202 def update_pcj_editor(self, updated_articles):
1203 for article in updated_articles:
1204 data = {
1205 "date_published": article.date_published.strftime("%Y-%m-%d"),
1206 "article_number": article.article_number,
1207 }
1208 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1209 requests.post(url, json=data, verify=False)
1212class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1213 article = None
1215 def internal_do(self, *args, **kwargs):
1216 lang = self.kwargs.get("lang", None)
1218 translation = None
1219 for trans_article in self.article.translations.all():
1220 if trans_article.lang == lang:
1221 translation = trans_article
1223 if translation is None:
1224 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1226 collection = self.article.get_top_collection()
1227 colid = collection.pid
1228 container = self.article.my_container
1230 if translation.date_published is None:
1231 # Add date posted
1232 cmd = ptf_cmds.publishResourcePtfCmd()
1233 cmd.set_resource(translation)
1234 updated_articles = cmd.do()
1236 # Recompile PDF to add the date posted
1237 try:
1238 create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1239 except Exception:
1240 raise PDFException(
1241 "Unable to compile the article PDF. Please contact the centre Mersenne"
1242 )
1244 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1245 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1246 resolver.copy_binary_files(
1247 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1248 )
1250 # Deploy in prod
1251 xml = ptf_cmds.exportPtfCmd(
1252 {
1253 "pid": self.article.pid,
1254 "with_djvu": False,
1255 "article_standalone": True,
1256 "collection_pid": colid,
1257 "export_to_website": True,
1258 "export_folder": settings.LOG_DIR,
1259 }
1260 ).do()
1261 xml_file = io.StringIO(xml)
1262 files = {"xml": xml_file}
1264 server_url = getattr(collection, "website")()
1265 if not server_url:
1266 raise RuntimeError("The collection has no website")
1267 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1268 header = {}
1270 try:
1271 response = requests.post(
1272 url, headers=header, files=files, verify=False
1273 ) # verify: ignore TLS certificate
1274 status = response.status_code
1275 except requests.exceptions.ConnectionError:
1276 raise ServerUnderMaintenance(
1277 "The journal is under maintenance. Please try again later."
1278 )
1280 # Register translation in Crossref
1281 if 199 < status < 205:
1282 if self.article.allow_crossref():
1283 try:
1284 recordDOI(translation)
1285 except Exception:
1286 raise DOIException(
1287 "Error while recording the DOI. Please contact the centre Mersenne"
1288 )
1290 def get(self, request, *args, **kwargs):
1291 doi = kwargs.get("doi", None)
1292 self.article = model_helpers.get_article_by_doi(doi)
1293 if self.article is None:
1294 raise Http404(f"{doi} does not exist")
1296 try:
1297 _, status, message = history_views.execute_and_record_func(
1298 "deploy",
1299 self.article.pid,
1300 self.article.get_top_collection().pid,
1301 self.internal_do,
1302 "website",
1303 )
1304 except Timeout as exception:
1305 return HttpResponse(exception, status=408)
1306 except Exception as exception:
1307 return HttpResponseServerError(exception)
1309 data = {"message": message, "status": status}
1310 return JsonResponse(data)
1313class DeleteJatsIssueAPIView(View):
1314 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1315 def get(self, request, *args, **kwargs):
1316 pid = self.kwargs.get("pid", None)
1317 colid = self.kwargs.get("colid", None)
1318 site = self.kwargs.get("site", None)
1319 message = "Le volume a bien été supprimé"
1320 status = 200
1322 issue = model_helpers.get_container(pid)
1323 if not issue:
1324 raise Http404(f"{pid} does not exist")
1325 try:
1326 mersenneSite = model_helpers.get_site_mersenne(colid)
1328 if site == "ptf_tools":
1329 if issue.is_deployed(mersenneSite):
1330 issue.undeploy(mersenneSite)
1331 for article in issue.article_set.all():
1332 article.undeploy(mersenneSite)
1334 p = model_helpers.get_provider("mathdoc-id")
1336 cmd = ptf_cmds.addContainerPtfCmd(
1337 {
1338 "pid": issue.pid,
1339 "ctype": "issue",
1340 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1341 }
1342 )
1343 cmd.set_provider(p)
1344 cmd.add_collection(issue.get_collection())
1345 cmd.set_object_to_be_deleted(issue)
1346 cmd.undo()
1348 else:
1349 if site == "numdam":
1350 server_url = settings.NUMDAM_PRE_URL
1351 else:
1352 collection = issue.get_collection()
1353 server_url = getattr(collection, site)()
1355 if not server_url:
1356 message = "The collection has no " + site
1357 status = 500
1358 else:
1359 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1360 response = requests.delete(url, verify=False)
1361 status = response.status_code
1363 if status == 404:
1364 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1365 elif status > 204:
1366 body = response.text.encode("utf8")
1367 message = body[:1000]
1368 else:
1369 status = 200
1370 # unpublish issue in collection site (site_register.json)
1371 if site == "website":
1372 if issue.is_deployed(mersenneSite):
1373 issue.undeploy(mersenneSite)
1374 for article in issue.article_set.all():
1375 article.undeploy(mersenneSite)
1376 # delete article binary files
1377 folder = article.get_relative_folder()
1378 resolver.delete_object_folder(
1379 folder,
1380 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1381 )
1382 # delete issue binary files
1383 folder = issue.get_relative_folder()
1384 resolver.delete_object_folder(
1385 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1386 )
1388 except Timeout as exception:
1389 return HttpResponse(exception, status=408)
1390 except Exception as exception:
1391 return HttpResponseServerError(exception)
1393 data = {"message": message, "status": status}
1394 return JsonResponse(data)
1397class ArchiveIssueAPIView(View):
1398 def get(self, request, *args, **kwargs):
1399 try:
1400 pid = kwargs["pid"]
1401 colid = kwargs["colid"]
1402 except IndexError:
1403 raise Http404
1405 try:
1406 cmd = ptf_cmds.archiveIssuePtfCmd(
1407 {
1408 "pid": pid,
1409 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1410 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1411 "needs_publication_date": True,
1412 }
1413 )
1414 result_, status, message = history_views.execute_and_record_func(
1415 "archive", pid, colid, cmd.do
1416 )
1417 except Exception as exception:
1418 return HttpResponseServerError(exception)
1420 data = {"message": message, "status": 200}
1421 return JsonResponse(data)
1424class CreateDjvuAPIView(View):
1425 def internal_do(self, *args, **kwargs):
1426 pid = self.kwargs.get("pid", None)
1428 resource = model_helpers.get_resource(pid)
1429 cmd = ptf_cmds.addDjvuPtfCmd()
1430 cmd.set_resource(resource)
1431 cmd.do()
1433 def get(self, request, *args, **kwargs):
1434 pid = self.kwargs.get("pid", None)
1435 colid = pid.split("_")[0]
1437 try:
1438 _, status, message = history_views.execute_and_record_func(
1439 "numdam", pid, colid, self.internal_do
1440 )
1441 except Exception as exception:
1442 return HttpResponseServerError(exception)
1444 data = {"message": message, "status": status}
1445 return JsonResponse(data)
1448class PTFToolsHomeView(LoginRequiredMixin, View):
1449 """
1450 Home Page.
1451 - Admin & staff -> Render blank home.html
1452 - User with unique authorized collection -> Redirect to collection details page
1453 - User with multiple authorized collections -> Render home.html with data
1454 - Comment moderator -> Comments dashboard
1455 - Others -> 404 response
1456 """
1458 def get(self, request, *args, **kwargs) -> HttpResponse:
1459 # Staff or user with authorized collections
1460 if request.user.is_staff or request.user.is_superuser:
1461 return render(request, "home.html")
1463 colids = get_authorized_collections(request.user)
1464 is_mod = is_comment_moderator(request.user)
1466 # The user has no rights
1467 if not (colids or is_mod):
1468 raise Http404("No collections associated with your account.")
1469 # Comment moderator only
1470 elif not colids:
1471 return HttpResponseRedirect(reverse("comment_list"))
1473 # User with unique collection -> Redirect to collection detail page
1474 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1475 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1477 # User with multiple authorized collections - Special home
1478 context = {}
1479 context["overview"] = True
1481 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1482 all_collections = {c["pid"]: c for c in all_collections}
1484 # Comments summary
1485 try:
1486 error, comments_data = get_comments_for_home(request.user)
1487 except AttributeError:
1488 error, comments_data = True, {}
1490 context["comment_server_ok"] = False
1492 if not error:
1493 context["comment_server_ok"] = True
1494 if comments_data:
1495 for col_id, comment_nb in comments_data.items():
1496 if col_id.upper() in all_collections: 1496 ↛ 1495line 1496 didn't jump to line 1495 because the condition on line 1496 was always true
1497 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1499 # TODO: Translations summary
1500 context["translation_server_ok"] = False
1502 # Sort the collections according to the number of pending comments
1503 context["collections"] = sorted(
1504 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1505 )
1507 return render(request, "home.html", context)
1510class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1511 columns = 5
1513 def get_common_context_data(self, **kwargs):
1514 context = super().get_context_data(**kwargs)
1515 now = timezone.now()
1516 curyear = now.year
1517 years = range(curyear - self.columns + 1, curyear + 1)
1519 context["collections"] = settings.MERSENNE_COLLECTIONS
1520 context["containers_to_be_published"] = []
1521 context["last_col_events"] = []
1523 event = get_history_last_event_by("clockss", "ALL")
1524 clockss_gap = get_gap(now, event)
1526 context["years"] = years
1527 context["clockss_gap"] = clockss_gap
1529 return context
1531 def calculate_articles_and_pages(self, pid, years):
1532 data_by_year = []
1533 total_articles = [0] * len(years)
1534 total_pages = [0] * len(years)
1536 for year in years:
1537 articles = self.get_articles_for_year(pid, year)
1538 articles_count = articles.count()
1539 page_count = sum(article.get_article_page_count() for article in articles)
1541 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1542 total_articles[year - years[0]] += articles_count
1543 total_pages[year - years[0]] += page_count
1545 return data_by_year, total_articles, total_pages
1547 def get_articles_for_year(self, pid, year):
1548 return Article.objects.filter(
1549 Q(my_container__my_collection__pid=pid)
1550 & (
1551 Q(date_published__year=year, date_online_first__isnull=True)
1552 | Q(date_online_first__year=year)
1553 )
1554 ).prefetch_related("resourcecount_set")
1557class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1558 template_name = "dashboard/published_articles.html"
1560 def get_context_data(self, **kwargs):
1561 context = self.get_common_context_data(**kwargs)
1562 years = context["years"]
1564 published_articles = []
1565 total_published_articles = [
1566 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1567 ]
1569 for pid in settings.MERSENNE_COLLECTIONS:
1570 if pid != "MERSENNE":
1571 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1572 pid, years
1573 )
1574 published_articles.append({"pid": pid, "years": articles_data})
1576 for i, year in enumerate(years):
1577 total_published_articles[i]["total_articles"] += total_articles[i]
1578 total_published_articles[i]["total_pages"] += total_pages[i]
1580 context["published_articles"] = published_articles
1581 context["total_published_articles"] = total_published_articles
1583 return context
1586class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1587 template_name = "dashboard/created_volumes.html"
1589 def get_context_data(self, **kwargs):
1590 context = self.get_common_context_data(**kwargs)
1591 years = context["years"]
1593 created_volumes = []
1594 total_created_volumes = [
1595 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1596 ]
1598 for pid in settings.MERSENNE_COLLECTIONS:
1599 if pid != "MERSENNE":
1600 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1601 pid, years
1602 )
1603 created_volumes.append({"pid": pid, "years": volumes_data})
1605 for i, _ in enumerate(years):
1606 total_created_volumes[i]["total_articles"] += total_articles[i]
1607 total_created_volumes[i]["total_pages"] += total_pages[i]
1609 context["created_volumes"] = created_volumes
1610 context["total_created_volumes"] = total_created_volumes
1612 return context
1614 def calculate_volumes_and_pages(self, pid, years):
1615 data_by_year = []
1616 total_articles = [0] * len(years)
1617 total_pages = [0] * len(years)
1619 for year in years:
1620 issues = Container.objects.filter(my_collection__pid=pid, fyear=year)
1621 articles_count = 0
1622 page_count = 0
1624 for issue in issues:
1625 articles = issue.article_set.filter(
1626 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1627 ).prefetch_related("resourcecount_set")
1629 articles_count += articles.count()
1630 page_count += sum(article.get_article_page_count() for article in articles)
1632 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1633 total_articles[year - years[0]] += articles_count
1634 total_pages[year - years[0]] += page_count
1636 return data_by_year, total_articles, total_pages
1639class ReferencingChoice(View):
1640 def post(self, request, *args, **kwargs):
1641 if request.POST.get("optSite") == "ads":
1642 return redirect(
1643 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1644 )
1645 elif request.POST.get("optSite") == "wos":
1646 comp = ReferencingCheckerWos()
1647 journal = comp.make_journal(request.POST.get("selectCol"))
1648 if journal is None:
1649 return render(
1650 request,
1651 "dashboard/referencing.html",
1652 {
1653 "error": "Collection not found",
1654 "colid": request.POST.get("selectCol"),
1655 "optSite": request.POST.get("optSite"),
1656 },
1657 )
1658 return render(
1659 request,
1660 "dashboard/referencing.html",
1661 {
1662 "journal": journal,
1663 "colid": request.POST.get("selectCol"),
1664 "optSite": request.POST.get("optSite"),
1665 },
1666 )
1669class ReferencingWosFileView(View):
1670 template_name = "dashboard/referencing.html"
1672 def post(self, request, *args, **kwargs):
1673 colid = request.POST["colid"]
1674 if request.FILES.get("risfile") is None:
1675 message = "No file uploaded"
1676 return render(
1677 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1678 )
1679 uploaded_file = request.FILES["risfile"]
1680 comp = ReferencingCheckerWos()
1681 journal = comp.check_references(colid, uploaded_file)
1682 return render(request, self.template_name, {"journal": journal})
1685class ReferencingDashboardView(BaseMersenneDashboardView):
1686 template_name = "dashboard/referencing.html"
1688 def get(self, request, *args, **kwargs):
1689 colid = self.kwargs.get("colid", None)
1690 comp = ReferencingCheckerAds()
1691 journal = comp.check_references(colid)
1692 return render(request, self.template_name, {"journal": journal})
1695class BaseCollectionView(TemplateView):
1696 def get_context_data(self, **kwargs):
1697 context = super().get_context_data(**kwargs)
1698 aid = context.get("aid")
1699 year = context.get("year")
1701 if aid and year:
1702 context["collection"] = self.get_collection(aid, year)
1704 return context
1706 def get_collection(self, aid, year):
1707 """Method to be overridden by subclasses to fetch the appropriate collection"""
1708 raise NotImplementedError("Subclasses must implement get_collection method")
1711class ArticleListView(BaseCollectionView):
1712 template_name = "collection-list.html"
1714 def get_collection(self, aid, year):
1715 return Article.objects.filter(
1716 Q(my_container__my_collection__pid=aid)
1717 & (
1718 Q(date_published__year=year, date_online_first__isnull=True)
1719 | Q(date_online_first__year=year)
1720 )
1721 ).prefetch_related("resourcecount_set")
1724class VolumeListView(BaseCollectionView):
1725 template_name = "collection-list.html"
1727 def get_collection(self, aid, year):
1728 return Article.objects.filter(
1729 Q(my_container__my_collection__pid=aid, my_container__fyear=year)
1730 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1731 ).prefetch_related("resourcecount_set")
1734class DOAJResourceRegisterView(View):
1735 def get(self, request, *args, **kwargs):
1736 pid = kwargs.get("pid", None)
1737 resource = model_helpers.get_resource(pid)
1738 if resource is None:
1739 raise Http404
1740 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1741 resource.colid, None
1742 ):
1743 raise RuntimeError("Pending publications should not be deployed")
1745 try:
1746 data = {}
1747 doaj_meta, response = doaj_pid_register(pid)
1748 if response is None:
1749 return HttpResponse(status=204)
1750 elif doaj_meta and 200 <= response.status_code <= 299:
1751 data.update(doaj_meta)
1752 else:
1753 return HttpResponse(status=response.status_code, reason=response.text)
1754 except Timeout as exception:
1755 return HttpResponse(exception, status=408)
1756 except Exception as exception:
1757 return HttpResponseServerError(exception)
1758 return JsonResponse(data)
1761class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View):
1762 """
1763 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml
1764 """
1766 def get(self, request, *args, **kwargs):
1767 pid = kwargs.get("pid")
1768 if not pid:
1769 raise Http404("Missing pid")
1771 article = Article.objects.filter(pid=pid).first()
1772 if not article:
1773 raise Http404(f"Article not found: {pid}")
1775 colid = article.get_collection().pid
1776 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
1777 return JsonResponse(
1778 {"status": 403, "message": f"Tex conversions are disabled in {colid}"}
1779 )
1781 if is_tex_conversion_locked(pid):
1782 logger.warning("Conversion rejected (lock exists) for %s", pid)
1783 return JsonResponse(
1784 {"status": 409, "message": f"A conversion is already running for {pid}"}
1785 )
1787 logger.info("No lock → scheduling conversion for %s", pid)
1789 try:
1790 convert_article_tex.delay(pid=pid, user_pk=request.user.pk)
1791 except Exception:
1792 logger.exception("Failed to enqueue task for %s", pid)
1793 release_tex_conversion_lock(pid)
1794 raise
1796 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"})
1799class CROSSREFResourceRegisterView(View):
1800 def get(self, request, *args, **kwargs):
1801 pid = kwargs.get("pid", None)
1802 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1803 force = kwargs.get("force", None)
1804 if not request.user.is_superuser:
1805 force = None
1807 resource = model_helpers.get_resource(pid)
1808 if resource is None:
1809 raise Http404
1811 resource = resource.cast()
1812 meth = getattr(self, "recordDOI" + resource.classname)
1813 try:
1814 data = meth(resource, force)
1815 except Timeout as exception:
1816 return HttpResponse(exception, status=408)
1817 except Exception as exception:
1818 return HttpResponseServerError(exception)
1819 return JsonResponse(data)
1821 def recordDOIArticle(self, article: "Article", force=None):
1822 result = {"status": 404}
1823 if (
1824 article.doi
1825 and not article.do_not_publish
1826 and (article.date_published or article.date_online_first or force == "force")
1827 ):
1828 if article.my_container.fyear == 0:
1829 article.my_container.fyear = datetime.now().year
1830 result = recordDOI(article)
1831 return result
1833 def recordDOICollection(self, collection, force=None):
1834 return recordDOI(collection)
1836 def recordDOIContainer(self, container, force=None):
1837 data = {"status": 200, "message": "All DOI successfully checked"}
1839 if container.ctype == "issue":
1840 if container.doi:
1841 result = recordDOI(container)
1842 if result["status"] != 200:
1843 return result
1844 if force == "force":
1845 articles = container.article_set.exclude(
1846 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1847 )
1848 else:
1849 articles = container.article_set.exclude(
1850 doi__isnull=True,
1851 do_not_publish=True,
1852 date_published__isnull=True,
1853 date_online_first__isnull=True,
1854 )
1856 for article in articles:
1857 result = self.recordDOIArticle(article, force)
1858 if result["status"] != 200:
1859 data = result
1860 else:
1861 return recordDOI(container)
1862 return data
1865class CROSSREFResourceCheckStatusView(View):
1866 def get(self, request, *args, **kwargs):
1867 pid = kwargs.get("pid", None)
1868 resource = model_helpers.get_resource(pid)
1869 if resource is None:
1870 raise Http404
1871 resource = resource.cast()
1872 meth = getattr(self, "checkDOI" + resource.classname)
1873 try:
1874 meth(resource)
1875 except Timeout as exception:
1876 return HttpResponse(exception, status=408)
1877 except Exception as exception:
1878 return HttpResponseServerError(exception)
1880 data = {"status": 200, "message": "DOI successfully checked"}
1881 return JsonResponse(data)
1883 def checkDOIArticle(self, article: "Article"):
1884 if article.my_container.fyear == 0:
1885 article.my_container.fyear = datetime.now().year
1886 checkDOI(article)
1888 def checkDOICollection(self, collection):
1889 checkDOI(collection)
1891 def checkDOIContainer(self, container):
1892 if container.doi is not None:
1893 checkDOI(container)
1894 for article in container.article_set.all():
1895 self.checkDOIArticle(article)
1898class CROSSREFResourcePendingPublicationRegisterView(View):
1899 def get(self, request, *args, **kwargs):
1900 pid = kwargs.get("pid", None)
1901 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1903 resource = model_helpers.get_resource(pid)
1904 if resource is None:
1905 raise Http404
1907 resource = resource.cast()
1908 meth = getattr(self, "recordPendingPublication" + resource.classname)
1909 try:
1910 data = meth(resource)
1911 except Timeout as exception:
1912 return HttpResponse(exception, status=408)
1913 except Exception as exception:
1914 return HttpResponseServerError(exception)
1915 return JsonResponse(data)
1917 def recordPendingPublicationArticle(self, article):
1918 result = {"status": 404}
1919 if article.doi and not article.date_published and not article.date_online_first:
1920 if article.my_container.fyear is None or article.my_container.fyear == "0":
1921 article.my_container.fyear = datetime.now().year
1922 result = recordPendingPublication(article)
1923 return result
1926class RegisterPubmedFormView(FormView):
1927 template_name = "record_pubmed_dialog.html"
1928 form_class = RegisterPubmedForm
1930 def get_context_data(self, **kwargs):
1931 context = super().get_context_data(**kwargs)
1932 context["pid"] = self.kwargs["pid"]
1933 context["helper"] = PtfLargeModalFormHelper
1934 return context
1937class RegisterPubmedView(View):
1938 def get(self, request, *args, **kwargs):
1939 pid = kwargs.get("pid", None)
1940 update_article = self.request.GET.get("update_article", "on") == "on"
1942 article = model_helpers.get_article(pid)
1943 if article is None:
1944 raise Http404
1945 try:
1946 recordPubmed(article, update_article)
1947 except Exception as exception:
1948 messages.error("Unable to register the article in PubMed")
1949 return HttpResponseServerError(exception)
1951 return HttpResponseRedirect(
1952 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1953 )
1956class PTFToolsContainerView(TemplateView):
1957 template_name = ""
1959 def get_context_data(self, **kwargs):
1960 context = super().get_context_data(**kwargs)
1962 container = model_helpers.get_container(self.kwargs.get("pid"))
1963 if container is None:
1964 raise Http404
1965 citing_articles = container.citations()
1966 source = self.request.GET.get("source", None)
1967 if container.ctype.startswith("book"):
1968 book_parts = (
1969 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1970 )
1971 references = False
1972 if container.ctype == "book-monograph":
1973 # on regarde si il y a au moins une bibliographie
1974 for art in container.article_set.all():
1975 if art.bibitem_set.count() > 0:
1976 references = True
1977 context.update(
1978 {
1979 "book": container,
1980 "book_parts": list(book_parts),
1981 "source": source,
1982 "citing_articles": citing_articles,
1983 "references": references,
1984 "test_website": container.get_top_collection()
1985 .extlink_set.get(rel="test_website")
1986 .location,
1987 "prod_website": container.get_top_collection()
1988 .extlink_set.get(rel="website")
1989 .location,
1990 }
1991 )
1992 self.template_name = "book-toc.html"
1993 else:
1994 articles = container.article_set.all().order_by("seq")
1995 for article in articles:
1996 try:
1997 last_match = (
1998 history_models.HistoryEvent.objects.filter(
1999 pid=article.pid,
2000 type="matching",
2001 )
2002 .only("created_on")
2003 .latest("created_on")
2004 )
2005 except history_models.HistoryEvent.DoesNotExist as _:
2006 article.last_match = None
2007 else:
2008 article.last_match = last_match.created_on
2010 # article1 = articles.first()
2011 # date = article1.deployed_date()
2012 # TODO next_issue, previous_issue
2014 # check DOI est maintenant une commande à part
2015 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
2016 # articlesWithStatus = []
2017 # for article in articles:
2018 # checkDOIExistence(article)
2019 # articlesWithStatus.append(article)
2021 test_location = prod_location = ""
2022 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
2023 if qs:
2024 test_location = qs.first().location
2025 qs = container.get_top_collection().extlink_set.filter(rel="website")
2026 if qs:
2027 prod_location = qs.first().location
2028 context.update(
2029 {
2030 "issue": container,
2031 "articles": articles,
2032 "source": source,
2033 "citing_articles": citing_articles,
2034 "test_website": test_location,
2035 "prod_website": prod_location,
2036 }
2037 )
2039 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
2040 context["is_issue_pending_publication"] = True
2041 if container.get_top_collection().pid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
2042 context["is_excluded_from_tex_conversion"] = True
2043 self.template_name = "issue-items.html"
2045 context["allow_crossref"] = container.allow_crossref()
2046 context["coltype"] = container.my_collection.coltype
2047 context["breadcrumb"] = breadcrumb.get_trammel_breadcrumb(container)
2048 return context
2051class ExtLinkInline(InlineFormSetFactory):
2052 model = ExtLink
2053 form_class = ExtLinkForm
2054 factory_kwargs = {"extra": 0}
2057class ResourceIdInline(InlineFormSetFactory):
2058 model = ResourceId
2059 form_class = ResourceIdForm
2060 factory_kwargs = {"extra": 0}
2063class IssueDetailAPIView(View):
2064 def get(self, request, *args, **kwargs):
2065 issue = get_object_or_404(Container, pid=kwargs["pid"])
2066 deployed_date = issue.deployed_date()
2067 result = {
2068 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2069 if deployed_date
2070 else None,
2071 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2072 "all_doi_are_registered": issue.all_doi_are_registered(),
2073 "registered_in_doaj": issue.registered_in_doaj(),
2074 "doi": issue.my_collection.doi,
2075 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2076 }
2077 try:
2078 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2079 except history_models.HistoryEvent.DoesNotExist as _:
2080 pass
2081 else:
2082 result["latest"] = latest.message
2083 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2084 "%Y-%m-%d %H:%M"
2085 )
2087 result["latest_type"] = latest.type.capitalize()
2088 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2089 try:
2090 result[event_type] = timezone.localtime(
2091 history_models.HistoryEvent.objects.filter(
2092 type=event_type,
2093 status="OK",
2094 pid__startswith=issue.pid,
2095 )
2096 .latest("created_on")
2097 .created_on
2098 ).strftime("%Y-%m-%d %H:%M")
2099 except history_models.HistoryEvent.DoesNotExist as _:
2100 result[event_type] = ""
2101 return JsonResponse(result)
2104class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2105 model = Collection
2106 form_class = CollectionForm
2107 inlines = [ResourceIdInline, ExtLinkInline]
2108 inlines_names = ["resource_ids_form", "ext_links_form"]
2110 def get_context_data(self, **kwargs):
2111 context = super().get_context_data(**kwargs)
2112 context["helper"] = PtfFormHelper
2113 context["formset_helper"] = FormSetHelper
2114 return context
2116 def add_description(self, collection, description, lang, seq):
2117 if description:
2118 la = Abstract(
2119 resource=collection,
2120 tag="description",
2121 lang=lang,
2122 seq=seq,
2123 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2124 value_html=description,
2125 value_tex=description,
2126 )
2127 la.save()
2129 def form_valid(self, form):
2130 if form.instance.abbrev:
2131 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2132 else:
2133 form.instance.title_xml = (
2134 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2135 )
2137 form.instance.title_html = form.instance.title_tex
2138 form.instance.title_sort = form.instance.title_tex
2139 result = super().form_valid(form)
2141 collection = self.object
2142 collection.abstract_set.all().delete()
2144 seq = 1
2145 description = form.cleaned_data["description_en"]
2146 if description:
2147 self.add_description(collection, description, "en", seq)
2148 seq += 1
2149 description = form.cleaned_data["description_fr"]
2150 if description:
2151 self.add_description(collection, description, "fr", seq)
2153 return result
2155 def get_success_url(self):
2156 messages.success(
2157 self.request, f'The collection "{self.object.pid}" has been successfully updated'
2158 )
2159 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2162class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2163 """
2164 Warning : Not yet finished
2165 Automatic site membership creation is still missing
2166 """
2169class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2170 slug_field = "pid"
2171 slug_url_kwarg = "pid"
2174def suggest_load_journal_dois(colid):
2175 articles = (
2176 Article.objects.filter(my_container__my_collection__pid=colid)
2177 .filter(doi__isnull=False)
2178 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2179 .values_list("doi", flat=True)
2180 )
2182 try:
2183 articles = sorted(
2184 articles,
2185 key=lambda d: (
2186 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2187 int(re.search(r".(\d+)$", d).group(1)),
2188 ),
2189 )
2190 except: # noqa: E722 (we'll look later)
2191 pass
2192 return [f'<option value="{doi}">' for doi in articles]
2195def get_context_with_volumes(journal):
2196 result = model_helpers.get_volumes_in_collection(journal)
2197 volume_count = result["volume_count"]
2198 collections = []
2199 for ancestor in journal.ancestors.all():
2200 item = model_helpers.get_volumes_in_collection(ancestor)
2201 volume_count = max(0, volume_count)
2202 item.update({"journal": ancestor})
2203 collections.append(item)
2205 # add the parent collection to its children list and sort it by date
2206 result.update({"journal": journal})
2207 collections.append(result)
2209 collections = [c for c in collections if c["sorted_issues"]]
2210 collections.sort(
2211 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2212 reverse=True,
2213 )
2215 context = {
2216 "journal": journal,
2217 "sorted_issues": result["sorted_issues"],
2218 "volume_count": volume_count,
2219 "max_width": result["max_width"],
2220 "collections": collections,
2221 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2222 }
2223 return context
2226class CollectionDetail(
2227 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2228):
2229 model = Collection
2230 slug_field = "pid"
2231 slug_url_kwarg = "pid"
2232 template_name = "ptf/collection_detail.html"
2234 def test_func(self):
2235 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2237 def get(self, request, *args, **kwargs):
2238 self.object = self.get_object(queryset=Collection.objects.all())
2239 return super().get(request, *args, **kwargs)
2241 def get_context_data(self, **kwargs):
2242 context = super().get_context_data(**kwargs)
2243 context["object_list"] = context["object_list"].filter(
2244 Q(ctype="issue") | Q(ctype="book-lecture-notes")
2245 )
2246 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2247 context.update(get_context_with_volumes(self.object))
2249 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2250 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2251 context["issue_to_appear"] = Container.objects.filter(
2252 pid=context["issue_to_appear_pid"]
2253 ).exists()
2254 try:
2255 latest_error = history_models.HistoryEvent.objects.filter(
2256 status="ERROR", col=self.object
2257 ).latest("created_on")
2258 except history_models.HistoryEvent.DoesNotExist as _:
2259 pass
2260 else:
2261 message = latest_error.message
2262 if message:
2263 i = message.find(" - ")
2264 latest_exception = message[:i]
2265 latest_error_message = message[i + 3 :]
2266 context["latest_exception"] = latest_exception
2267 context["latest_exception_date"] = latest_error.created_on
2268 context["latest_exception_type"] = latest_error.type
2269 context["latest_error_message"] = latest_error_message
2271 archive_in_error = history_models.HistoryEvent.objects.filter(
2272 status="ERROR", col=self.object, type="archive"
2273 ).exists()
2275 context["archive_in_error"] = archive_in_error
2277 return context
2279 def get_queryset(self):
2280 query = self.object.content.all()
2282 for ancestor in self.object.ancestors.all():
2283 query |= ancestor.content.all()
2285 return query.order_by("-fyear", "-vseries", "-volume", "-volume_int", "-number_int")
2288class ContainerEditView(FormView):
2289 template_name = "container_form.html"
2290 form_class = ContainerForm
2292 def get_success_url(self):
2293 if self.kwargs["pid"]:
2294 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2295 return reverse("mersenne_dashboard/published_articles")
2297 def set_success_message(self): # pylint: disable=no-self-use
2298 messages.success(self.request, "Booklet updated")
2300 def get_form_kwargs(self):
2301 kwargs = super().get_form_kwargs()
2302 if "pid" not in self.kwargs:
2303 self.kwargs["pid"] = None
2304 if "colid" not in self.kwargs:
2305 self.kwargs["colid"] = None
2306 if "data" in kwargs and "colid" in kwargs["data"]:
2307 # colid is passed as a hidden param in the form.
2308 # It is used when you submit a new container
2309 self.kwargs["colid"] = kwargs["data"]["colid"]
2311 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2312 self.kwargs["pid"]
2313 )
2314 return kwargs
2316 def get_context_data(self, **kwargs):
2317 context = super().get_context_data(**kwargs)
2319 context["pid"] = self.kwargs["pid"]
2320 context["colid"] = self.kwargs["colid"]
2321 context["container"] = self.kwargs["container"]
2323 context["edit_container"] = context["pid"] is not None
2324 context["name"] = resolve(self.request.path_info).url_name
2326 return context
2328 def form_valid(self, form):
2329 new_pid = form.cleaned_data.get("pid")
2330 new_title = form.cleaned_data.get("title")
2331 new_trans_title = form.cleaned_data.get("trans_title")
2332 new_publisher = form.cleaned_data.get("publisher")
2333 new_year = form.cleaned_data.get("year")
2334 new_volume = form.cleaned_data.get("volume")
2335 new_number = form.cleaned_data.get("number")
2337 collection = None
2338 issue = self.kwargs["container"]
2339 if issue is not None:
2340 collection = issue.my_collection
2341 elif self.kwargs["colid"] is not None:
2342 if "CR" in self.kwargs["colid"]:
2343 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2344 else:
2345 collection = model_helpers.get_collection(self.kwargs["colid"])
2347 if collection is None:
2348 raise ValueError("Collection for " + new_pid + " does not exist")
2350 # Icon
2351 new_icon_location = ""
2352 if "icon" in self.request.FILES:
2353 filename = os.path.basename(self.request.FILES["icon"].name)
2354 file_extension = filename.split(".")[1]
2356 icon_filename = resolver.get_disk_location(
2357 settings.MERSENNE_TEST_DATA_FOLDER,
2358 collection.pid,
2359 file_extension,
2360 new_pid,
2361 None,
2362 True,
2363 )
2365 with open(icon_filename, "wb+") as destination:
2366 for chunk in self.request.FILES["icon"].chunks():
2367 destination.write(chunk)
2369 folder = resolver.get_relative_folder(collection.pid, new_pid)
2370 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2371 name = resolve(self.request.path_info).url_name
2372 if name == "special_issue_create":
2373 self.kwargs["name"] = name
2374 if self.kwargs["container"]:
2375 # Edit Issue
2376 issue = self.kwargs["container"]
2377 if issue is None:
2378 raise ValueError(self.kwargs["pid"] + " does not exist")
2380 issue.pid = new_pid
2381 issue.title_tex = issue.title_html = new_title
2382 issue.title_xml = build_title_xml(
2383 title=new_title,
2384 lang=issue.lang,
2385 title_type="issue-title",
2386 )
2388 trans_lang = ""
2389 if new_trans_title != "":
2390 trans_lang = "fr" if issue.lang == "en" else "en"
2392 if trans_lang != "" and new_trans_title != "":
2393 title_xml = build_title_xml(
2394 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2395 )
2397 issue.title_set.update_or_create(
2398 lang=trans_lang,
2399 type="main",
2400 defaults={"title_html": new_trans_title, "title_xml": title_xml},
2401 )
2403 issue.fyear = new_year
2404 issue.volume = new_volume
2405 issue.volume_int = make_int(new_volume)
2406 issue.number = new_number
2407 issue.number_int = make_int(new_number)
2408 issue.save()
2409 else:
2410 xissue = create_issuedata()
2412 xissue.ctype = "issue"
2413 xissue.pid = new_pid
2414 xissue.lang = "en"
2415 xissue.title_tex = new_title
2416 xissue.title_html = new_title
2417 xissue.title_xml = build_title_xml(
2418 title=new_title, lang=xissue.lang, title_type="issue-title"
2419 )
2421 if new_trans_title != "":
2422 trans_lang = "fr"
2423 title_xml = build_title_xml(
2424 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2425 )
2426 title = create_titledata(
2427 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2428 )
2429 issue.titles = [title]
2431 xissue.fyear = new_year
2432 xissue.volume = new_volume
2433 xissue.number = new_number
2434 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2436 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2437 cmd.add_collection(collection)
2438 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2439 issue = cmd.do()
2441 self.kwargs["pid"] = new_pid
2443 # Add objects related to the article: contribs, datastream, counts...
2444 params = {
2445 "icon_location": new_icon_location,
2446 }
2447 cmd = ptf_cmds.updateContainerPtfCmd(params)
2448 cmd.set_resource(issue)
2449 cmd.do()
2451 publisher = model_helpers.get_publisher(new_publisher)
2452 if not publisher:
2453 xpub = create_publisherdata()
2454 xpub.name = new_publisher
2455 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2456 issue.my_publisher = publisher
2457 issue.save()
2459 self.set_success_message()
2461 return super().form_valid(form)
2464# class ArticleEditView(FormView):
2465# template_name = 'article_form.html'
2466# form_class = ArticleForm
2467#
2468# def get_success_url(self):
2469# if self.kwargs['pid']:
2470# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2471# return reverse('mersenne_dashboard/published_articles')
2472#
2473# def set_success_message(self): # pylint: disable=no-self-use
2474# messages.success(self.request, "L'article a été modifié")
2475#
2476# def get_form_kwargs(self):
2477# kwargs = super(ArticleEditView, self).get_form_kwargs()
2478#
2479# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2480# # Article creation: pid is None
2481# self.kwargs['pid'] = None
2482# if 'issue_id' not in self.kwargs:
2483# # Article edit: issue_id is not passed
2484# self.kwargs['issue_id'] = None
2485# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2486# # colid is passed as a hidden param in the form.
2487# # It is used when you submit a new container
2488# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2489#
2490# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2491# return kwargs
2492#
2493# def get_context_data(self, **kwargs):
2494# context = super(ArticleEditView, self).get_context_data(**kwargs)
2495#
2496# context['pid'] = self.kwargs['pid']
2497# context['issue_id'] = self.kwargs['issue_id']
2498# context['article'] = self.kwargs['article']
2499#
2500# context['edit_article'] = context['pid'] is not None
2501#
2502# article = context['article']
2503# if article:
2504# context['author_contributions'] = article.get_author_contributions()
2505# context['kwds_fr'] = None
2506# context['kwds_en'] = None
2507# kwd_gps = article.get_non_msc_kwds()
2508# for kwd_gp in kwd_gps:
2509# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2510# if kwd_gp.value_xml:
2511# kwd_ = types.SimpleNamespace()
2512# kwd_.value = kwd_gp.value_tex
2513# context['kwd_unstructured_fr'] = kwd_
2514# context['kwds_fr'] = kwd_gp.kwd_set.all()
2515# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2516# if kwd_gp.value_xml:
2517# kwd_ = types.SimpleNamespace()
2518# kwd_.value = kwd_gp.value_tex
2519# context['kwd_unstructured_en'] = kwd_
2520# context['kwds_en'] = kwd_gp.kwd_set.all()
2521#
2522# # Article creation: init pid
2523# if context['issue_id'] and context['pid'] is None:
2524# issue = model_helpers.get_container(context['issue_id'])
2525# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2526#
2527# return context
2528#
2529# def form_valid(self, form):
2530#
2531# new_pid = form.cleaned_data.get('pid')
2532# new_title = form.cleaned_data.get('title')
2533# new_fpage = form.cleaned_data.get('fpage')
2534# new_lpage = form.cleaned_data.get('lpage')
2535# new_page_range = form.cleaned_data.get('page_range')
2536# new_page_count = form.cleaned_data.get('page_count')
2537# new_coi_statement = form.cleaned_data.get('coi_statement')
2538# new_show_body = form.cleaned_data.get('show_body')
2539# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2540#
2541# # TODO support MathML
2542# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2543# # We need to pass trans_title to get_title_xml
2544# # Meanwhile, ignore new_title_xml
2545# new_title_xml = jats_parser.get_title_xml(new_title)
2546# new_title_html = new_title
2547#
2548# authors_count = int(self.request.POST.get('authors_count', "0"))
2549# i = 1
2550# new_authors = []
2551# old_author_contributions = []
2552# if self.kwargs['article']:
2553# old_author_contributions = self.kwargs['article'].get_author_contributions()
2554#
2555# while authors_count > 0:
2556# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2557#
2558# if prefix is not None:
2559# addresses = []
2560# if len(old_author_contributions) >= i:
2561# old_author_contribution = old_author_contributions[i - 1]
2562# addresses = [contrib_address.address for contrib_address in
2563# old_author_contribution.get_addresses()]
2564#
2565# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2566# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2567# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2568# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2569# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2570# deceased_before_publication = deceased == 'on'
2571# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2572# equal_contrib = equal_contrib == 'on'
2573# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2574# corresponding = corresponding == 'on'
2575# email = self.request.POST.get('email-' + str(i), None)
2576#
2577# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2578# params['deceased_before_publication'] = deceased_before_publication
2579# params['equal_contrib'] = equal_contrib
2580# params['corresponding'] = corresponding
2581# params['addresses'] = addresses
2582# params['email'] = email
2583#
2584# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2585#
2586# new_authors.append(params)
2587#
2588# authors_count -= 1
2589# i += 1
2590#
2591# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2592# i = 1
2593# new_kwds_fr = []
2594# while kwds_fr_count > 0:
2595# value = self.request.POST.get('kwd-fr-' + str(i), None)
2596# new_kwds_fr.append(value)
2597# kwds_fr_count -= 1
2598# i += 1
2599# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2600#
2601# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2602# i = 1
2603# new_kwds_en = []
2604# while kwds_en_count > 0:
2605# value = self.request.POST.get('kwd-en-' + str(i), None)
2606# new_kwds_en.append(value)
2607# kwds_en_count -= 1
2608# i += 1
2609# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2610#
2611# if self.kwargs['article']:
2612# # Edit article
2613# container = self.kwargs['article'].my_container
2614# else:
2615# # New article
2616# container = model_helpers.get_container(self.kwargs['issue_id'])
2617#
2618# if container is None:
2619# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2620#
2621# collection = container.my_collection
2622#
2623# # Copy PDF file & extract full text
2624# body = ''
2625# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2626# collection.pid,
2627# "pdf",
2628# container.pid,
2629# new_pid,
2630# True)
2631# if 'pdf' in self.request.FILES:
2632# with open(pdf_filename, 'wb+') as destination:
2633# for chunk in self.request.FILES['pdf'].chunks():
2634# destination.write(chunk)
2635#
2636# # Extract full text from the PDF
2637# body = utils.pdf_to_text(pdf_filename)
2638#
2639# # Icon
2640# new_icon_location = ''
2641# if 'icon' in self.request.FILES:
2642# filename = os.path.basename(self.request.FILES['icon'].name)
2643# file_extension = filename.split('.')[1]
2644#
2645# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2646# collection.pid,
2647# file_extension,
2648# container.pid,
2649# new_pid,
2650# True)
2651#
2652# with open(icon_filename, 'wb+') as destination:
2653# for chunk in self.request.FILES['icon'].chunks():
2654# destination.write(chunk)
2655#
2656# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2657# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2658#
2659# if self.kwargs['article']:
2660# # Edit article
2661# article = self.kwargs['article']
2662# article.fpage = new_fpage
2663# article.lpage = new_lpage
2664# article.page_range = new_page_range
2665# article.coi_statement = new_coi_statement
2666# article.show_body = new_show_body
2667# article.do_not_publish = new_do_not_publish
2668# article.save()
2669#
2670# else:
2671# # New article
2672# params = {
2673# 'pid': new_pid,
2674# 'title_xml': new_title_xml,
2675# 'title_html': new_title_html,
2676# 'title_tex': new_title,
2677# 'fpage': new_fpage,
2678# 'lpage': new_lpage,
2679# 'page_range': new_page_range,
2680# 'seq': container.article_set.count() + 1,
2681# 'body': body,
2682# 'coi_statement': new_coi_statement,
2683# 'show_body': new_show_body,
2684# 'do_not_publish': new_do_not_publish
2685# }
2686#
2687# xarticle = create_articledata()
2688# xarticle.pid = new_pid
2689# xarticle.title_xml = new_title_xml
2690# xarticle.title_html = new_title_html
2691# xarticle.title_tex = new_title
2692# xarticle.fpage = new_fpage
2693# xarticle.lpage = new_lpage
2694# xarticle.page_range = new_page_range
2695# xarticle.seq = container.article_set.count() + 1
2696# xarticle.body = body
2697# xarticle.coi_statement = new_coi_statement
2698# params['xobj'] = xarticle
2699#
2700# cmd = ptf_cmds.addArticlePtfCmd(params)
2701# cmd.set_container(container)
2702# cmd.add_collection(container.my_collection)
2703# article = cmd.do()
2704#
2705# self.kwargs['pid'] = new_pid
2706#
2707# # Add objects related to the article: contribs, datastream, counts...
2708# params = {
2709# # 'title_xml': new_title_xml,
2710# # 'title_html': new_title_html,
2711# # 'title_tex': new_title,
2712# 'authors': new_authors,
2713# 'page_count': new_page_count,
2714# 'icon_location': new_icon_location,
2715# 'body': body,
2716# 'use_kwds': True,
2717# 'kwds_fr': new_kwds_fr,
2718# 'kwds_en': new_kwds_en,
2719# 'kwd_uns_fr': new_kwd_uns_fr,
2720# 'kwd_uns_en': new_kwd_uns_en
2721# }
2722# cmd = ptf_cmds.updateArticlePtfCmd(params)
2723# cmd.set_article(article)
2724# cmd.do()
2725#
2726# self.set_success_message()
2727#
2728# return super(ArticleEditView, self).form_valid(form)
2731@require_http_methods(["POST"])
2732def do_not_publish_article(request, *args, **kwargs):
2733 next = request.headers.get("referer")
2735 pid = kwargs.get("pid", "")
2737 article = model_helpers.get_article(pid)
2738 if article:
2739 article.do_not_publish = not article.do_not_publish
2740 article.save()
2741 else:
2742 raise Http404
2744 return HttpResponseRedirect(next)
2747@require_http_methods(["POST"])
2748def show_article_body(request, *args, **kwargs):
2749 next = request.headers.get("referer")
2751 pid = kwargs.get("pid", "")
2753 article = model_helpers.get_article(pid)
2754 if article:
2755 article.show_body = not article.show_body
2756 article.save()
2757 else:
2758 raise Http404
2760 return HttpResponseRedirect(next)
2763class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2764 """
2765 API to get/post article metadata
2766 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2767 """
2769 def __init__(self, *args, **kwargs):
2770 """
2771 we define here what fields we want in the form
2772 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2773 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2774 super().__init__(*args, **kwargs)
2775 self.fields_to_update = [
2776 "lang",
2777 "atype",
2778 "contributors",
2779 "abstracts",
2780 "kwds",
2781 "titles",
2782 "title_html",
2783 "title_xml",
2784 "title_tex",
2785 "streams",
2786 "ext_links",
2787 "date_accepted",
2788 "history_dates",
2789 "subjs",
2790 "bibitems",
2791 "references",
2792 ]
2793 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2794 self.additional_fields = [
2795 "doi",
2796 "pid",
2797 "container_pid",
2798 "pdf",
2799 "illustration",
2800 "dates",
2801 "msc_keywords",
2802 ]
2803 self.editorial_tools = [
2804 "translation",
2805 "sidebar",
2806 "lang_selection",
2807 "back_to_article_option",
2808 "msc_keywords",
2809 ]
2810 self.article_container_pid = ""
2811 self.back_url = "trammel"
2813 def save_data(self, data_article):
2814 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2815 # The icons are not preserved since we can add/edit/delete them in VueJs
2816 params = {
2817 "pid": data_article.pid,
2818 "export_folder": settings.MERSENNE_TMP_FOLDER,
2819 "export_all": True,
2820 "with_binary_files": False,
2821 }
2822 ptf_cmds.exportExtraDataPtfCmd(params).do()
2824 def restore_data(self, article):
2825 ptf_cmds.importExtraDataPtfCmd(
2826 {
2827 "pid": article.pid,
2828 "import_folder": settings.MERSENNE_TMP_FOLDER,
2829 "import_bibitemid": False,
2830 }
2831 ).do()
2833 def get(self, request, *args, **kwargs):
2834 try:
2835 request.user.groups.get(name="maquettiste")
2836 self.fields_to_update = [
2837 "bibitems",
2838 "references",
2839 "titles",
2840 "title_html",
2841 "title_xml",
2842 "title_tex",
2843 ]
2844 self.additional_fields = [
2845 "doi",
2846 "pid",
2847 "container_pid",
2848 ]
2849 self.editorial_tools = ["sidebar", "is_maquettiste"]
2850 except Exception:
2851 pass
2852 data = super().get(request, *args, **kwargs)
2853 return data
2855 def post(self, request, *args, **kwargs):
2856 response = super().post(request, *args, **kwargs)
2857 if response["message"] == "OK": 2857 ↛ 2865line 2857 didn't jump to line 2865 because the condition on line 2857 was always true
2858 return redirect(
2859 "api-edit-article",
2860 colid=kwargs.get("colid", ""),
2861 containerPid=kwargs.get("containerPid"),
2862 doi=kwargs.get("doi", ""),
2863 )
2864 else:
2865 raise Http404
2868class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2869 template_name = "article_form.html"
2871 def get_success_url(self):
2872 if self.kwargs["doi"]:
2873 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2874 return reverse("mersenne_dashboard/published_articles")
2876 def get_context_data(self, **kwargs):
2877 context = super().get_context_data(**kwargs)
2878 if "doi" in self.kwargs:
2879 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"])
2880 context["pid"] = context["article"].pid
2882 context["container_pid"] = kwargs.get("container_pid", "")
2883 return context
2886class ArticleDeleteView(View):
2887 def get(self, request, *args, **kwargs):
2888 pid = self.kwargs.get("pid", None)
2889 article = get_object_or_404(Article, pid=pid)
2891 try:
2892 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2893 article.undeploy(mersenneSite)
2895 cmd = ptf_cmds.addArticlePtfCmd(
2896 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2897 )
2898 cmd.set_container(article.my_container)
2899 cmd.set_object_to_be_deleted(article)
2900 cmd.undo()
2901 except Exception as exception:
2902 return HttpResponseServerError(exception)
2904 data = {"message": "Article successfully removed from Trammel", "status": 200}
2905 return JsonResponse(data)
2908def get_messages_in_queue():
2909 app = Celery("ptf-tools")
2910 # tasks = list(current_app.tasks)
2911 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2912 print(tasks)
2913 # i = app.control.inspect()
2915 with app.connection_or_acquire() as conn:
2916 remaining = conn.default_channel.queue_declare(
2917 queue="coordinator", passive=True
2918 ).message_count
2919 return remaining
2922class NumdamView(TemplateView, history_views.HistoryContextMixin):
2923 template_name = "numdam.html"
2925 def get_context_data(self, **kwargs):
2926 context = super().get_context_data(**kwargs)
2928 context["objs"] = ResourceInNumdam.objects.all()
2930 pre_issues = []
2931 prod_issues = []
2932 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2933 try:
2934 response = requests.get(url)
2935 if response.status_code == 200:
2936 data = response.json()
2937 if "issues" in data:
2938 pre_issues = data["issues"]
2939 except Exception:
2940 pass
2942 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2943 response = requests.get(url)
2944 if response.status_code == 200:
2945 data = response.json()
2946 if "issues" in data:
2947 prod_issues = data["issues"]
2949 new = sorted(list(set(pre_issues).difference(prod_issues)))
2950 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2951 grouped = [
2952 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2953 ]
2954 grouped_removed = [
2955 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2956 ]
2957 context["added_issues"] = grouped
2958 context["removed_issues"] = grouped_removed
2960 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2961 return context
2964class NumdamArchiveView(RedirectView):
2965 @staticmethod
2966 def reset_task_results():
2967 TaskResult.objects.all().delete()
2969 def get_redirect_url(self, *args, **kwargs):
2970 self.colid = kwargs["colid"]
2972 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2973 return Http404
2975 # we make sure archiving is not already running
2976 # if not get_messages_in_queue():
2977 # self.reset_task_results()
2979 if self.colid == "ALL":
2980 archive_numdam_collections.delay()
2981 else:
2982 archive_numdam_collection.s(self.colid).delay()
2984 return reverse("numdam")
2987class DeployAllNumdamAPIView(View):
2988 def internal_do(self, *args, **kwargs):
2989 pids = []
2991 for obj in ResourceInNumdam.objects.all():
2992 pids.append(obj.pid)
2994 return pids
2996 def get(self, request, *args, **kwargs):
2997 try:
2998 pids, status, message = history_views.execute_and_record_func(
2999 "deploy", "numdam", "ALL", self.internal_do, "numdam"
3000 )
3001 except Exception as exception:
3002 return HttpResponseServerError(exception)
3004 data = {"message": message, "ids": pids, "status": status}
3005 return JsonResponse(data)
3008class NumdamDeleteAPIView(View):
3009 def get(self, request, *args, **kwargs):
3010 pid = self.kwargs.get("pid", None)
3012 try:
3013 obj = ResourceInNumdam.objects.get(pid=pid)
3014 obj.delete()
3015 except Exception as exception:
3016 return HttpResponseServerError(exception)
3018 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
3019 return JsonResponse(data)
3022class ExtIdApiDetail(View):
3023 def get(self, request, *args, **kwargs):
3024 extid = get_object_or_404(
3025 ExtId,
3026 resource__pid=kwargs["pid"],
3027 id_type=kwargs["what"],
3028 )
3029 return JsonResponse(
3030 {
3031 "pk": extid.pk,
3032 "href": extid.get_href(),
3033 "fetch": reverse(
3034 "api-fetch-id",
3035 args=(
3036 extid.resource.pk,
3037 extid.id_value,
3038 extid.id_type,
3039 "extid",
3040 ),
3041 ),
3042 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3043 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3044 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3045 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3046 "is_valid": extid.checked,
3047 }
3048 )
3051class ExtIdFormTemplate(TemplateView):
3052 template_name = "common/externalid_form.html"
3054 def get_context_data(self, **kwargs):
3055 context = super().get_context_data(**kwargs)
3056 context["sequence"] = kwargs["sequence"]
3057 return context
3060class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3061 def get_context_data(self, **kwargs):
3062 context = super().get_context_data(**kwargs)
3063 context["helper"] = PtfFormHelper
3064 return context
3066 def get_success_url(self):
3067 self.post_process()
3068 return self.object.bibitem.resource.get_absolute_url()
3070 def post_process(self):
3071 cmd = updateBibitemCitationXmlCmd()
3072 cmd.set_bibitem(self.object.bibitem)
3073 cmd.do()
3074 model_helpers.post_resource_updated(self.object.bibitem.resource)
3077class BibItemIdCreate(BibItemIdFormView, CreateView):
3078 model = BibItemId
3079 form_class = BibItemIdForm
3081 def get_context_data(self, **kwargs):
3082 context = super().get_context_data(**kwargs)
3083 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3084 return context
3086 def get_initial(self):
3087 initial = super().get_initial()
3088 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3089 return initial
3091 def form_valid(self, form):
3092 form.instance.checked = False
3093 return super().form_valid(form)
3096class BibItemIdUpdate(BibItemIdFormView, UpdateView):
3097 model = BibItemId
3098 form_class = BibItemIdForm
3100 def get_context_data(self, **kwargs):
3101 context = super().get_context_data(**kwargs)
3102 context["bibitem"] = self.object.bibitem
3103 return context
3106class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3107 def get_context_data(self, **kwargs):
3108 context = super().get_context_data(**kwargs)
3109 context["helper"] = PtfFormHelper
3110 return context
3112 def get_success_url(self):
3113 self.post_process()
3114 return self.object.resource.get_absolute_url()
3116 def post_process(self):
3117 model_helpers.post_resource_updated(self.object.resource)
3120class ExtIdCreate(ExtIdFormView, CreateView):
3121 model = ExtId
3122 form_class = ExtIdForm
3124 def get_context_data(self, **kwargs):
3125 context = super().get_context_data(**kwargs)
3126 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3127 return context
3129 def get_initial(self):
3130 initial = super().get_initial()
3131 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3132 return initial
3134 def form_valid(self, form):
3135 form.instance.checked = False
3136 return super().form_valid(form)
3139class ExtIdUpdate(ExtIdFormView, UpdateView):
3140 model = ExtId
3141 form_class = ExtIdForm
3143 def get_context_data(self, **kwargs):
3144 context = super().get_context_data(**kwargs)
3145 context["resource"] = self.object.resource
3146 return context
3149class BibItemIdApiDetail(View):
3150 def get(self, request, *args, **kwargs):
3151 bibitemid = get_object_or_404(
3152 BibItemId,
3153 bibitem__resource__pid=kwargs["pid"],
3154 bibitem__sequence=kwargs["seq"],
3155 id_type=kwargs["what"],
3156 )
3157 return JsonResponse(
3158 {
3159 "pk": bibitemid.pk,
3160 "href": bibitemid.get_href(),
3161 "fetch": reverse(
3162 "api-fetch-id",
3163 args=(
3164 bibitemid.bibitem.pk,
3165 bibitemid.id_value,
3166 bibitemid.id_type,
3167 "bibitemid",
3168 ),
3169 ),
3170 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
3171 "uncheck": reverse(
3172 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
3173 ),
3174 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
3175 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
3176 "is_valid": bibitemid.checked,
3177 }
3178 )
3181class UpdateTexmfZipAPIView(View):
3182 def get(self, request, *args, **kwargs):
3183 def copy_zip_files(src_folder, dest_folder):
3184 os.makedirs(dest_folder, exist_ok=True)
3186 zip_files = [
3187 os.path.join(src_folder, f)
3188 for f in os.listdir(src_folder)
3189 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3190 ]
3191 for zip_file in zip_files:
3192 resolver.copy_file(zip_file, dest_folder)
3194 # Exceptions: specific zip/gz files
3195 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3196 resolver.copy_file(zip_file, dest_folder)
3198 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3199 resolver.copy_file(zip_file, dest_folder)
3201 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3202 resolver.copy_file(gz_file, dest_folder)
3204 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3206 dest_folder = os.path.join(
3207 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3208 )
3210 try:
3211 copy_zip_files(src_folder, dest_folder)
3212 except Exception as exception:
3213 return HttpResponseServerError(exception)
3215 try:
3216 dest_folder = os.path.join(
3217 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3218 )
3219 copy_zip_files(src_folder, dest_folder)
3220 except Exception as exception:
3221 return HttpResponseServerError(exception)
3223 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3224 return JsonResponse(data)
3227class TrammelTasksProgressView(View):
3228 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3229 """
3230 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3231 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3232 """
3233 task_name = task
3235 def get_event_data():
3236 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3238 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3239 remaining_messages = get_messages_in_queue()
3241 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3242 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3243 failed_tasks = all_tasks.filter(status="FAILURE")
3245 all_tasks_count = all_tasks.count()
3246 success_count = successed_tasks.count()
3247 fail_count = failed_tasks.count()
3249 all_count = all_tasks_count + remaining_messages
3250 remaining_count = all_count - success_count - fail_count
3252 success_rate = int(success_count * 100 / all_count) if all_count else 0
3253 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3254 status = "consuming_queue" if remaining_count != 0 else "polling"
3256 last_task = successed_tasks.first()
3257 last_task = (
3258 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3259 if last_task
3260 else ""
3261 )
3263 # SSE event format
3264 event_data = {
3265 "status": status,
3266 "success_rate": success_rate,
3267 "error_rate": error_rate,
3268 "all_count": all_count,
3269 "remaining_count": remaining_count,
3270 "success_count": success_count,
3271 "fail_count": fail_count,
3272 "last_task": last_task,
3273 }
3275 return event_data
3277 def stream_response(data):
3278 # Send initial response headers
3279 yield f"data: {json.dumps(data)}\n\n"
3281 data = get_event_data()
3282 format = request.GET.get("format", "stream")
3283 if format == "json":
3284 response = JsonResponse(data)
3285 else:
3286 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3287 return response
3290user_signed_up.connect(update_user_from_invite)