Coverage for src / ptf_tools / views / base_views.py: 17%
1666 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-04-20 09:25 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-04-20 09:25 +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
113logger = logging.getLogger(__name__)
116def view_404(request: HttpRequest):
117 """
118 Dummy view raising HTTP 404 exception.
119 """
120 raise Http404
123def check_collection(collection, server_url, server_type):
124 """
125 Check if a collection exists on a serveur (test/prod)
126 and upload the collection (XML, image) if necessary
127 """
129 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
130 response = requests.get(url, verify=False)
131 # First, upload the collection XML
132 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
133 body = xml.encode("utf8")
135 url = server_url + reverse("upload-serials")
136 if response.status_code == 200:
137 # PUT http verb is used for update
138 response = requests.put(url, data=body, verify=False)
139 else:
140 # POST http verb is used for creation
141 response = requests.post(url, data=body, verify=False)
143 # Second, copy the collection images
144 # There is no need to copy files for the test server
145 # Files were already copied in /mersenne_test_data during the ptf_tools import
146 # We only need to copy files from /mersenne_test_data to
147 # /mersenne_prod_data during an upload to prod
148 if server_type == "website":
149 resolver.copy_binary_files(
150 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
151 )
152 elif server_type == "numdam":
153 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
154 if collection.pid in settings.NUMDAM_COLLECTIONS:
155 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
157 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
160def check_lock():
161 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
164def load_cedrics_article_choices(request):
165 colid = request.GET.get("colid")
166 issue = request.GET.get("issue")
167 article_choices = get_article_choices(colid, issue)
168 return render(
169 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
170 )
173class ImportCedricsArticleFormView(FormView):
174 template_name = "import_article.html"
175 form_class = ImportArticleForm
177 def dispatch(self, request, *args, **kwargs):
178 self.colid = self.kwargs["colid"]
179 return super().dispatch(request, *args, **kwargs)
181 def get_success_url(self):
182 if self.colid:
183 return reverse("collection-detail", kwargs={"pid": self.colid})
184 return "/"
186 def get_context_data(self, **kwargs):
187 context = super().get_context_data(**kwargs)
188 context["colid"] = self.colid
189 context["helper"] = PtfModalFormHelper
190 return context
192 def get_form_kwargs(self):
193 kwargs = super().get_form_kwargs()
194 kwargs["colid"] = self.colid
195 return kwargs
197 def form_valid(self, form):
198 self.issue = form.cleaned_data["issue"]
199 self.article = form.cleaned_data["article"]
200 return super().form_valid(form)
202 def import_cedrics_article(self, *args, **kwargs):
203 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
204 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
205 )
206 cmd.do()
208 def post(self, request, *args, **kwargs):
209 self.colid = self.kwargs.get("colid", None)
210 issue = request.POST["issue"]
211 self.article_pid = request.POST["article"]
212 self.issue_pid = os.path.basename(os.path.dirname(issue))
214 import_args = [self]
215 import_kwargs = {}
217 try:
218 _, status, message = history_views.execute_and_record_func(
219 "import",
220 f"{self.issue_pid} / {self.article_pid}",
221 self.colid,
222 self.import_cedrics_article,
223 "",
224 False,
225 None,
226 None,
227 *import_args,
228 **import_kwargs,
229 )
231 messages.success(
232 self.request, f"L'article {self.article_pid} a été importé avec succès"
233 )
235 except Exception as exception:
236 messages.error(
237 self.request,
238 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}",
239 )
241 return redirect(self.get_success_url())
244class ImportCedricsIssueView(FormView):
245 template_name = "import_container.html"
246 form_class = ImportContainerForm
248 def dispatch(self, request, *args, **kwargs):
249 self.colid = self.kwargs["colid"]
250 self.to_appear = self.request.GET.get("to_appear", False)
251 return super().dispatch(request, *args, **kwargs)
253 def get_success_url(self):
254 if self.filename:
255 return reverse(
256 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
257 )
258 return "/"
260 def get_context_data(self, **kwargs):
261 context = super().get_context_data(**kwargs)
262 context["colid"] = self.colid
263 context["helper"] = PtfModalFormHelper
264 return context
266 def get_form_kwargs(self):
267 kwargs = super().get_form_kwargs()
268 kwargs["colid"] = self.colid
269 kwargs["to_appear"] = self.to_appear
270 return kwargs
272 def form_valid(self, form):
273 self.filename = form.cleaned_data["filename"].split("/")[-1]
274 return super().form_valid(form)
277class DiffCedricsIssueView(FormView):
278 template_name = "diff_container_form.html"
279 form_class = DiffContainerForm
280 diffs = None
281 xissue = None
282 xissue_encoded = None
284 def get_success_url(self):
285 return reverse("collection-detail", kwargs={"pid": self.colid})
287 def dispatch(self, request, *args, **kwargs):
288 self.colid = self.kwargs["colid"]
289 # self.filename = self.kwargs['filename']
290 return super().dispatch(request, *args, **kwargs)
292 def get(self, request, *args, **kwargs):
293 self.filename = request.GET["filename"]
294 self.remove_mail = request.GET.get("remove_email", "off")
295 self.remove_date_prod = request.GET.get("remove_date_prod", "off")
296 self.remove_email = self.remove_mail == "on"
297 self.remove_date_prod = self.remove_date_prod == "on"
299 try:
300 result, status, message = history_views.execute_and_record_func(
301 "import",
302 os.path.basename(self.filename),
303 self.colid,
304 self.diff_cedrics_issue,
305 "",
306 True,
307 )
308 except Exception as exception:
309 pid = self.filename.split("/")[-1]
310 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
311 return HttpResponseRedirect(self.get_success_url())
313 no_conflict = result[0]
314 self.diffs = result[1]
315 self.xissue = result[2]
317 if no_conflict:
318 # Proceed with the import
319 self.form_valid(self.get_form())
320 return redirect(self.get_success_url())
321 else:
322 # Display the diff template
323 self.xissue_encoded = jsonpickle.encode(self.xissue)
325 return super().get(request, *args, **kwargs)
327 def post(self, request, *args, **kwargs):
328 self.filename = request.POST["filename"]
329 data = request.POST["xissue_encoded"]
330 self.xissue = jsonpickle.decode(data)
332 return super().post(request, *args, **kwargs)
334 def get_context_data(self, **kwargs):
335 context = super().get_context_data(**kwargs)
336 context["colid"] = self.colid
337 context["diff"] = self.diffs
338 context["filename"] = self.filename
339 context["xissue_encoded"] = self.xissue_encoded
340 return context
342 def get_form_kwargs(self):
343 kwargs = super().get_form_kwargs()
344 kwargs["colid"] = self.colid
345 return kwargs
347 def diff_cedrics_issue(self, *args, **kwargs):
348 params = {
349 "colid": self.colid,
350 "input_file": self.filename,
351 "remove_email": self.remove_mail,
352 "remove_date_prod": self.remove_date_prod,
353 "diff_only": True,
354 }
356 if settings.IMPORT_CEDRICS_DIRECTLY:
357 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
358 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
359 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
360 else:
361 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
363 result = cmd.do()
364 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
365 messages.warning(
366 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings
367 )
369 return result
371 def import_cedrics_issue(self, *args, **kwargs):
372 # modify xissue with data_issue if params to override
373 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
374 issue = model_helpers.get_container(self.xissue.pid)
375 if issue:
376 data_issue = model_data_converter.db_to_issue_data(issue)
377 for xarticle in self.xissue.articles:
378 filter_articles = [
379 article for article in data_issue.articles if article.doi == xarticle.doi
380 ]
381 if len(filter_articles) > 0:
382 db_article = filter_articles[0]
383 xarticle.coi_statement = db_article.coi_statement
384 xarticle.kwds = db_article.kwds
385 xarticle.contrib_groups = db_article.contrib_groups
387 params = {
388 "colid": self.colid,
389 "xissue": self.xissue,
390 "input_file": self.filename,
391 }
393 if settings.IMPORT_CEDRICS_DIRECTLY:
394 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
395 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
396 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
397 else:
398 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
400 cmd.do()
402 def form_valid(self, form):
403 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
404 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
405 else:
406 import_kwargs = {}
407 import_args = [self]
409 try:
410 _, status, message = history_views.execute_and_record_func(
411 "import",
412 self.xissue.pid,
413 self.kwargs["colid"],
414 self.import_cedrics_issue,
415 "",
416 False,
417 None,
418 None,
419 *import_args,
420 **import_kwargs,
421 )
422 except Exception as exception:
423 messages.error(
424 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
425 )
426 return super().form_invalid(form)
428 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
429 return super().form_valid(form)
432class ImportEditflowArticleFormView(FormView):
433 template_name = "import_editflow_article.html"
434 form_class = ImportEditflowArticleForm
436 def dispatch(self, request, *args, **kwargs):
437 self.colid = self.kwargs["colid"]
438 return super().dispatch(request, *args, **kwargs)
440 def get_context_data(self, **kwargs):
441 context = super().get_context_data(**kwargs)
442 context["colid"] = self.kwargs["colid"]
443 context["helper"] = PtfLargeModalFormHelper
444 return context
446 def get_success_url(self):
447 if self.colid:
448 return reverse("collection-detail", kwargs={"pid": self.colid})
449 return "/"
451 def post(self, request, *args, **kwargs):
452 self.colid = self.kwargs.get("colid", None)
453 try:
454 if not self.colid:
455 raise ValueError("Missing collection id")
457 issue_name = settings.ISSUE_PENDING_PUBLICATION_PIDS.get(self.colid)
458 if not issue_name:
459 raise ValueError(
460 "Issue not found in Pending Publications PIDs. Did you forget to add it?"
461 )
463 issue = model_helpers.get_container(issue_name)
464 if not issue:
465 raise ValueError("No issue found")
467 editflow_xml_file = request.FILES.get("editflow_xml_file")
468 if not editflow_xml_file:
469 raise ValueError("The file you specified couldn't be found")
471 body = editflow_xml_file.read().decode("utf-8")
473 cmd = xml_cmds.addArticleXmlCmd(
474 {
475 "body": body,
476 "issue": issue,
477 "assign_doi": True,
478 "standalone": True,
479 "from_folder": settings.RESOURCES_ROOT,
480 }
481 )
482 cmd.set_collection(issue.get_collection())
483 cmd.do()
485 messages.success(
486 request,
487 f'Editflow article successfully imported into issue "{issue_name}"',
488 )
490 except Exception as exception:
491 messages.error(
492 request,
493 f"Import failed: {str(exception)}",
494 )
496 return redirect(self.get_success_url())
499class BibtexAPIView(View):
500 def get(self, request, *args, **kwargs):
501 pid = self.kwargs.get("pid", None)
502 all_bibtex = ""
503 if pid:
504 article = model_helpers.get_article(pid)
505 if article:
506 for bibitem in article.bibitem_set.all():
507 bibtex_array = bibitem.get_bibtex()
508 last = len(bibtex_array)
509 i = 1
510 for bibtex in bibtex_array:
511 if i > 1 and i < last:
512 all_bibtex += " "
513 all_bibtex += bibtex + "\n"
514 i += 1
516 data = {"bibtex": all_bibtex}
517 return JsonResponse(data)
520class MatchingAPIView(View):
521 def get(self, request, *args, **kwargs):
522 pid = self.kwargs.get("pid", None)
524 url = settings.MATCHING_URL
525 headers = {"Content-Type": "application/xml"}
527 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
529 if settings.DEBUG:
530 print("Issue exported to /tmp/issue.xml")
531 f = open("/tmp/issue.xml", "w")
532 f.write(body.encode("utf8"))
533 f.close()
535 r = requests.post(url, data=body.encode("utf8"), headers=headers)
536 body = r.text.encode("utf8")
537 data = {"status": r.status_code, "message": body[:1000]}
539 if settings.DEBUG:
540 print("Matching received, new issue exported to /tmp/issue1.xml")
541 f = open("/tmp/issue1.xml", "w")
542 text = body
543 f.write(text)
544 f.close()
546 resource = model_helpers.get_resource(pid)
547 obj = resource.cast()
548 colid = obj.get_collection().pid
550 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
552 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
553 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
554 )
555 cmd.do()
557 print("Matching finished")
558 return JsonResponse(data)
561class ImportAllAPIView(View):
562 def internal_do(self, *args, **kwargs):
563 pid = self.kwargs.get("pid", None)
565 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
566 if not os.path.isdir(root_folder):
567 raise ValueError(root_folder + " does not exist")
569 resource = model_helpers.get_resource(pid)
570 if not resource:
571 file = os.path.join(root_folder, pid + ".xml")
572 body = utils.get_file_content_in_utf8(file)
573 journals = xml_cmds.addCollectionsXmlCmd(
574 {
575 "body": body,
576 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
577 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
578 }
579 ).do()
580 if not journals:
581 raise ValueError(file + " does not contain a collection")
582 resource = journals[0]
583 # resolver.copy_binary_files(
584 # resource,
585 # settings.MATHDOC_ARCHIVE_FOLDER,
586 # settings.MERSENNE_TEST_DATA_FOLDER)
588 obj = resource.cast()
590 if obj.classname != "Collection":
591 raise ValueError(pid + " does not contain a collection")
593 cmd = xml_cmds.collectEntireCollectionXmlCmd(
594 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
595 )
596 pids = cmd.do()
598 return pids
600 def get(self, request, *args, **kwargs):
601 pid = self.kwargs.get("pid", None)
603 try:
604 pids, status, message = history_views.execute_and_record_func(
605 "import", pid, pid, self.internal_do
606 )
607 except Timeout as exception:
608 return HttpResponse(exception, status=408)
609 except Exception as exception:
610 return HttpResponseServerError(exception)
612 data = {"message": message, "ids": pids, "status": status}
613 return JsonResponse(data)
616class DeployAllAPIView(View):
617 def internal_do(self, *args, **kwargs):
618 pid = self.kwargs.get("pid", None)
619 site = self.kwargs.get("site", None)
621 pids = []
623 collection = model_helpers.get_collection(pid)
624 if not collection:
625 raise RuntimeError(pid + " does not exist")
627 if site == "numdam":
628 server_url = settings.NUMDAM_PRE_URL
629 elif site != "ptf_tools":
630 server_url = getattr(collection, site)()
631 if not server_url:
632 raise RuntimeError("The collection has no " + site)
634 if site != "ptf_tools":
635 # check if the collection exists on the server
636 # if not, check_collection will upload the collection (XML,
637 # image...)
638 check_collection(collection, server_url, site)
640 for issue in collection.content.all():
641 if site != "website" or (site == "website" and issue.are_all_articles_published()):
642 pids.append(issue.pid)
644 return pids
646 def get(self, request, *args, **kwargs):
647 pid = self.kwargs.get("pid", None)
648 site = self.kwargs.get("site", None)
650 try:
651 pids, status, message = history_views.execute_and_record_func(
652 "deploy", pid, pid, self.internal_do, site
653 )
654 except Timeout as exception:
655 return HttpResponse(exception, status=408)
656 except Exception as exception:
657 return HttpResponseServerError(exception)
659 data = {"message": message, "ids": pids, "status": status}
660 return JsonResponse(data)
663class AddIssuePDFView(View):
664 def __init(self, *args, **kwargs):
665 super().__init__(*args, **kwargs)
666 self.pid = None
667 self.issue = None
668 self.collection = None
669 self.site = "test_website"
671 def post_to_site(self, url):
672 response = requests.post(url, verify=False)
673 status = response.status_code
674 if not (199 < status < 205):
675 messages.error(self.request, response.text)
676 if status == 503:
677 raise ServerUnderMaintenance(response.text)
678 else:
679 raise RuntimeError(response.text)
681 def internal_do(self, *args, **kwargs):
682 """
683 Called by history_views.execute_and_record_func to do the actual job.
684 """
686 issue_pid = self.issue.pid
687 colid = self.collection.pid
689 if self.site == "website":
690 # Copy the PDF from the test to the production folder
691 resolver.copy_binary_files(
692 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
693 )
694 else:
695 # Copy the PDF from the cedram to the test folder
696 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
697 from_path = os.path.join(from_folder, issue_pid + ".pdf")
698 if not os.path.isfile(from_path):
699 raise Http404(f"{from_path} does not exist")
701 to_path = resolver.get_disk_location(
702 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
703 )
704 resolver.copy_file(from_path, to_path)
706 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
708 if self.site == "test_website":
709 # Post to ptf-tools: it will add a Datastream to the issue
710 absolute_url = self.request.build_absolute_uri(url)
711 self.post_to_site(absolute_url)
713 server_url = getattr(self.collection, self.site)()
714 absolute_url = server_url + url
715 # Post to the test or production website
716 self.post_to_site(absolute_url)
718 def get(self, request, *args, **kwargs):
719 """
720 Send an issue PDF to the test or production website
721 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
722 :param args:
723 :param kwargs:
724 :return:
725 """
726 if check_lock():
727 m = "Trammel is under maintenance. Please try again later."
728 messages.error(self.request, m)
729 return JsonResponse({"message": m, "status": 503})
731 self.pid = self.kwargs.get("pid", None)
732 self.site = self.kwargs.get("site", "test_website")
734 self.issue = model_helpers.get_container(self.pid)
735 if not self.issue:
736 raise Http404(f"{self.pid} does not exist")
737 self.collection = self.issue.get_top_collection()
739 try:
740 pids, status, message = history_views.execute_and_record_func(
741 "deploy",
742 self.pid,
743 self.collection.pid,
744 self.internal_do,
745 f"add issue PDF to {self.site}",
746 )
748 except Timeout as exception:
749 return HttpResponse(exception, status=408)
750 except Exception as exception:
751 return HttpResponseServerError(exception)
753 data = {"message": message, "status": status}
754 return JsonResponse(data)
757class ArchiveAllAPIView(View):
758 """
759 - archive le xml de la collection ainsi que les binaires liés
760 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
761 @return array of issues pid
762 """
764 def internal_do(self, *args, **kwargs):
765 collection = kwargs["collection"]
766 pids = []
767 colid = collection.pid
769 logfile = os.path.join(settings.LOG_DIR, "archive.log")
770 if os.path.isfile(logfile):
771 os.remove(logfile)
773 ptf_cmds.exportPtfCmd(
774 {
775 "pid": colid,
776 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
777 "with_binary_files": True,
778 "for_archive": True,
779 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
780 }
781 ).do()
783 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
784 if os.path.isfile(cedramcls):
785 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
786 resolver.create_folder(dest_folder)
787 resolver.copy_file(cedramcls, dest_folder)
789 for issue in collection.content.all():
790 qs = issue.article_set.filter(
791 date_online_first__isnull=True, date_published__isnull=True
792 )
793 if qs.count() == 0:
794 pids.append(issue.pid)
796 return pids
798 def get(self, request, *args, **kwargs):
799 pid = self.kwargs.get("pid", None)
801 collection = model_helpers.get_collection(pid)
802 if not collection:
803 return HttpResponse(f"{pid} does not exist", status=400)
805 dict_ = {"collection": collection}
806 args_ = [self]
808 try:
809 pids, status, message = history_views.execute_and_record_func(
810 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_
811 )
812 except Timeout as exception:
813 return HttpResponse(exception, status=408)
814 except Exception as exception:
815 return HttpResponseServerError(exception)
817 data = {"message": message, "ids": pids, "status": status}
818 return JsonResponse(data)
821class CreateAllDjvuAPIView(View):
822 def internal_do(self, *args, **kwargs):
823 issue = kwargs["issue"]
824 pids = [issue.pid]
826 for article in issue.article_set.all():
827 pids.append(article.pid)
829 return pids
831 def get(self, request, *args, **kwargs):
832 pid = self.kwargs.get("pid", None)
833 issue = model_helpers.get_container(pid)
834 if not issue:
835 raise Http404(f"{pid} does not exist")
837 try:
838 dict_ = {"issue": issue}
839 args_ = [self]
841 pids, status, message = history_views.execute_and_record_func(
842 "numdam",
843 pid,
844 issue.get_collection().pid,
845 self.internal_do,
846 "",
847 False,
848 None,
849 None,
850 *args_,
851 **dict_,
852 )
853 except Exception as exception:
854 return HttpResponseServerError(exception)
856 data = {"message": message, "ids": pids, "status": status}
857 return JsonResponse(data)
860class ImportJatsContainerAPIView(View):
861 def internal_do(self, *args, **kwargs):
862 pid = self.kwargs.get("pid", None)
863 colid = self.kwargs.get("colid", None)
865 if pid and colid:
866 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
868 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
869 {
870 "body": body,
871 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
872 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
873 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
874 }
875 )
876 container = cmd.do()
877 if len(cmd.warnings) > 0:
878 messages.warning(
879 self.request,
880 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
881 )
883 if not container:
884 raise RuntimeError("Error: the container " + pid + " was not imported")
886 # resolver.copy_binary_files(
887 # container,
888 # settings.MATHDOC_ARCHIVE_FOLDER,
889 # settings.MERSENNE_TEST_DATA_FOLDER)
890 #
891 # for article in container.article_set.all():
892 # resolver.copy_binary_files(
893 # article,
894 # settings.MATHDOC_ARCHIVE_FOLDER,
895 # settings.MERSENNE_TEST_DATA_FOLDER)
896 else:
897 raise RuntimeError("colid or pid are not defined")
899 def get(self, request, *args, **kwargs):
900 pid = self.kwargs.get("pid", None)
901 colid = self.kwargs.get("colid", None)
903 try:
904 _, status, message = history_views.execute_and_record_func(
905 "import", pid, colid, self.internal_do
906 )
907 except Timeout as exception:
908 return HttpResponse(exception, status=408)
909 except Exception as exception:
910 return HttpResponseServerError(exception)
912 data = {"message": message, "status": status}
913 return JsonResponse(data)
916class DeployCollectionAPIView(View):
917 # Update collection.xml on a site (with its images)
919 def internal_do(self, *args, **kwargs):
920 colid = self.kwargs.get("colid", None)
921 site = self.kwargs.get("site", None)
923 collection = model_helpers.get_collection(colid)
924 if not collection:
925 raise RuntimeError(f"{colid} does not exist")
927 if site == "numdam":
928 server_url = settings.NUMDAM_PRE_URL
929 else:
930 server_url = getattr(collection, site)()
931 if not server_url:
932 raise RuntimeError(f"The collection has no {site}")
934 # check_collection creates or updates the collection (XML, image...)
935 check_collection(collection, server_url, site)
937 def get(self, request, *args, **kwargs):
938 colid = self.kwargs.get("colid", None)
939 site = self.kwargs.get("site", None)
941 try:
942 _, status, message = history_views.execute_and_record_func(
943 "deploy", colid, colid, self.internal_do, site
944 )
945 except Timeout as exception:
946 return HttpResponse(exception, status=408)
947 except Exception as exception:
948 return HttpResponseServerError(exception)
950 data = {"message": message, "status": status}
951 return JsonResponse(data)
954class DeployJatsResourceAPIView(View):
955 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
957 def internal_do(self, *args, **kwargs):
958 pid = self.kwargs.get("pid", None)
959 colid = self.kwargs.get("colid", None)
960 site = self.kwargs.get("site", None)
962 if site == "ptf_tools":
963 raise RuntimeError("Do not choose to deploy on PTF Tools")
964 if check_lock():
965 msg = "Trammel is under maintenance. Please try again later."
966 messages.error(self.request, msg)
967 return JsonResponse({"messages": msg, "status": 503})
969 resource = model_helpers.get_resource(pid)
970 if not resource:
971 raise RuntimeError(f"{pid} does not exist")
973 obj = resource.cast()
974 article = None
975 if obj.classname == "Article":
976 article = obj
977 container = article.my_container
978 articles_to_deploy = [article]
979 else:
980 container = obj
981 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
983 if container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(colid, None):
984 raise RuntimeError("Pending publications should not be deployed")
985 if site == "website" and article is not None and article.do_not_publish:
986 raise RuntimeError(f"{pid} is marked as Do not publish")
987 if site == "numdam" and article is not None:
988 raise RuntimeError("You can only deploy issues to Numdam")
990 collection = container.get_top_collection()
991 colid = collection.pid
992 djvu_exception = None
994 if site == "numdam":
995 server_url = settings.NUMDAM_PRE_URL
996 ResourceInNumdam.objects.get_or_create(pid=container.pid)
998 # 06/12/2022: DjVu are no longer added with Mersenne articles
999 # Add Djvu (before exporting the XML)
1000 if False and int(container.year) < 2020:
1001 for art in container.article_set.all():
1002 try:
1003 cmd = ptf_cmds.addDjvuPtfCmd()
1004 cmd.set_resource(art)
1005 cmd.do()
1006 except Exception as e:
1007 # Djvu are optional.
1008 # Allow the deployment, but record the exception in the history
1009 djvu_exception = e
1010 else:
1011 server_url = getattr(collection, site)()
1012 if not server_url:
1013 raise RuntimeError(f"The collection has no {site}")
1015 # check if the collection exists on the server
1016 # if not, check_collection will upload the collection (XML,
1017 # image...)
1018 if article is None:
1019 check_collection(collection, server_url, site)
1021 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
1022 # Create/update deployed date and published date on all container articles
1023 if site == "website":
1024 file_.write(
1025 "Create/Update deployed_date and date_published on all articles for {}\n".format(
1026 pid
1027 )
1028 )
1030 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
1031 cmd = ptf_cmds.publishResourcePtfCmd()
1032 cmd.set_resource(resource)
1033 updated_articles = cmd.do()
1035 try:
1036 create_frontpage(colid, container, updated_articles, test=False)
1037 except Exception as exc:
1038 return JsonResponse({"status": 500, "message": str(exc)})
1040 mersenneSite = model_helpers.get_site_mersenne(colid)
1041 # create or update deployed_date on container and articles
1042 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
1044 for art in articles_to_deploy:
1045 if art.doi and (art.date_published or art.date_online_first):
1046 if art.my_container.year is None:
1047 art.my_container.year = datetime.now().strftime("%Y")
1048 # BUG ? update the container but no save() ?
1050 file_.write(
1051 "Publication date of {} : Online First: {}, Published: {}\n".format(
1052 art.pid, art.date_online_first, art.date_published
1053 )
1054 )
1056 if article is None:
1057 resolver.copy_binary_files(
1058 container,
1059 settings.MERSENNE_TEST_DATA_FOLDER,
1060 settings.MERSENNE_PROD_DATA_FOLDER,
1061 )
1063 for art in articles_to_deploy:
1064 resolver.copy_binary_files(
1065 art,
1066 settings.MERSENNE_TEST_DATA_FOLDER,
1067 settings.MERSENNE_PROD_DATA_FOLDER,
1068 )
1070 elif site == "test_website":
1071 # create date_pre_published on articles without date_pre_published
1072 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
1073 cmd.set_resource(resource)
1074 updated_articles = cmd.do()
1076 create_frontpage(colid, container, updated_articles)
1078 export_to_website = site == "website"
1080 if article is None:
1081 with_djvu = site == "numdam"
1082 xml = ptf_cmds.exportPtfCmd(
1083 {
1084 "pid": pid,
1085 "with_djvu": with_djvu,
1086 "export_to_website": export_to_website,
1087 }
1088 ).do()
1089 body = xml.encode("utf8")
1091 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1092 url = server_url + reverse("issue_upload")
1093 else:
1094 url = server_url + reverse("book_upload")
1096 # verify=False: ignore TLS certificate
1097 response = requests.post(url, data=body, verify=False)
1098 # response = requests.post(url, files=files, verify=False)
1099 else:
1100 xml = ptf_cmds.exportPtfCmd(
1101 {
1102 "pid": pid,
1103 "with_djvu": False,
1104 "article_standalone": True,
1105 "collection_pid": collection.pid,
1106 "export_to_website": export_to_website,
1107 "export_folder": settings.LOG_DIR,
1108 }
1109 ).do()
1110 # Unlike containers that send their XML as the body of the POST request,
1111 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1112 xml_file = io.StringIO(xml)
1113 files = {"xml": xml_file}
1115 url = server_url + reverse(
1116 "article_in_issue_upload", kwargs={"pid": container.pid}
1117 )
1118 # verify=False: ignore TLS certificate
1119 header = {}
1120 response = requests.post(url, headers=header, files=files, verify=False)
1122 status = response.status_code
1124 if 199 < status < 205:
1125 # There is no need to copy files for the test server
1126 # Files were already copied in /mersenne_test_data during the ptf_tools import
1127 # We only need to copy files from /mersenne_test_data to
1128 # /mersenne_prod_data during an upload to prod
1129 if site == "website":
1130 # TODO mettre ici le record doi pour un issue publié
1131 if container.doi:
1132 recordDOI(container)
1134 for art in articles_to_deploy:
1135 # record DOI automatically when deploying in prod
1137 if art.doi and art.allow_crossref():
1138 recordDOI(art)
1140 if colid == "CRBIOL":
1141 recordPubmed(
1142 art, force_update=False, updated_articles=updated_articles
1143 )
1145 if colid == "PCJ":
1146 self.update_pcj_editor(updated_articles)
1148 # Archive the container or the article
1149 if article is None:
1150 archive_resource.delay(
1151 pid,
1152 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1153 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1154 )
1156 else:
1157 archive_resource.delay(
1158 pid,
1159 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1160 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1161 article_doi=article.doi,
1162 )
1163 # cmd = ptf_cmds.archiveIssuePtfCmd({
1164 # "pid": pid,
1165 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1166 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1167 # cmd.set_article(article) # set_article allows archiving only the article
1168 # cmd.do()
1170 elif site == "numdam":
1171 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1172 if colid in settings.NUMDAM_COLLECTIONS:
1173 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1175 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1176 for article in container.article_set.all():
1177 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1179 elif status == 503:
1180 raise ServerUnderMaintenance(response.text)
1181 else:
1182 raise RuntimeError(response.text)
1184 if djvu_exception:
1185 raise djvu_exception
1187 def get(self, request, *args, **kwargs):
1188 pid = self.kwargs.get("pid", None)
1189 colid = self.kwargs.get("colid", None)
1190 site = self.kwargs.get("site", None)
1192 try:
1193 _, status, message = history_views.execute_and_record_func(
1194 "deploy", pid, colid, self.internal_do, site
1195 )
1196 except Timeout as exception:
1197 return HttpResponse(exception, status=408)
1198 except Exception as exception:
1199 return HttpResponseServerError(exception)
1201 data = {"message": message, "status": status}
1202 return JsonResponse(data)
1204 def update_pcj_editor(self, updated_articles):
1205 for article in updated_articles:
1206 data = {
1207 "date_published": article.date_published.strftime("%Y-%m-%d"),
1208 "article_number": article.article_number,
1209 }
1210 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1211 requests.post(url, json=data, verify=False)
1214class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1215 article = None
1217 def internal_do(self, *args, **kwargs):
1218 lang = self.kwargs.get("lang", None)
1220 translation = None
1221 for trans_article in self.article.translations.all():
1222 if trans_article.lang == lang:
1223 translation = trans_article
1225 if translation is None:
1226 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1228 collection = self.article.get_top_collection()
1229 colid = collection.pid
1230 container = self.article.my_container
1232 if translation.date_published is None:
1233 # Add date posted
1234 cmd = ptf_cmds.publishResourcePtfCmd()
1235 cmd.set_resource(translation)
1236 updated_articles = cmd.do()
1238 # Recompile PDF to add the date posted
1239 try:
1240 create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1241 except Exception:
1242 raise PDFException(
1243 "Unable to compile the article PDF. Please contact the centre Mersenne"
1244 )
1246 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1247 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1248 resolver.copy_binary_files(
1249 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1250 )
1252 # Deploy in prod
1253 xml = ptf_cmds.exportPtfCmd(
1254 {
1255 "pid": self.article.pid,
1256 "with_djvu": False,
1257 "article_standalone": True,
1258 "collection_pid": colid,
1259 "export_to_website": True,
1260 "export_folder": settings.LOG_DIR,
1261 }
1262 ).do()
1263 xml_file = io.StringIO(xml)
1264 files = {"xml": xml_file}
1266 server_url = getattr(collection, "website")()
1267 if not server_url:
1268 raise RuntimeError("The collection has no website")
1269 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1270 header = {}
1272 try:
1273 response = requests.post(
1274 url, headers=header, files=files, verify=False
1275 ) # verify: ignore TLS certificate
1276 status = response.status_code
1277 except requests.exceptions.ConnectionError:
1278 raise ServerUnderMaintenance(
1279 "The journal is under maintenance. Please try again later."
1280 )
1282 # Register translation in Crossref
1283 if 199 < status < 205:
1284 if self.article.allow_crossref():
1285 try:
1286 recordDOI(translation)
1287 except Exception:
1288 raise DOIException(
1289 "Error while recording the DOI. Please contact the centre Mersenne"
1290 )
1292 def get(self, request, *args, **kwargs):
1293 doi = kwargs.get("doi", None)
1294 self.article = model_helpers.get_article_by_doi(doi)
1295 if self.article is None:
1296 raise Http404(f"{doi} does not exist")
1298 try:
1299 _, status, message = history_views.execute_and_record_func(
1300 "deploy",
1301 self.article.pid,
1302 self.article.get_top_collection().pid,
1303 self.internal_do,
1304 "website",
1305 )
1306 except Timeout as exception:
1307 return HttpResponse(exception, status=408)
1308 except Exception as exception:
1309 return HttpResponseServerError(exception)
1311 data = {"message": message, "status": status}
1312 return JsonResponse(data)
1315class DeleteJatsIssueAPIView(View):
1316 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1317 def get(self, request, *args, **kwargs):
1318 pid = self.kwargs.get("pid", None)
1319 colid = self.kwargs.get("colid", None)
1320 site = self.kwargs.get("site", None)
1321 message = "Le volume a bien été supprimé"
1322 status = 200
1324 issue = model_helpers.get_container(pid)
1325 if not issue:
1326 raise Http404(f"{pid} does not exist")
1327 try:
1328 mersenneSite = model_helpers.get_site_mersenne(colid)
1330 if site == "ptf_tools":
1331 if issue.is_deployed(mersenneSite):
1332 issue.undeploy(mersenneSite)
1333 for article in issue.article_set.all():
1334 article.undeploy(mersenneSite)
1336 p = model_helpers.get_provider("mathdoc-id")
1338 cmd = ptf_cmds.addContainerPtfCmd(
1339 {
1340 "pid": issue.pid,
1341 "ctype": "issue",
1342 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1343 }
1344 )
1345 cmd.set_provider(p)
1346 cmd.add_collection(issue.get_collection())
1347 cmd.set_object_to_be_deleted(issue)
1348 cmd.undo()
1350 else:
1351 if site == "numdam":
1352 server_url = settings.NUMDAM_PRE_URL
1353 else:
1354 collection = issue.get_collection()
1355 server_url = getattr(collection, site)()
1357 if not server_url:
1358 message = "The collection has no " + site
1359 status = 500
1360 else:
1361 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1362 response = requests.delete(url, verify=False)
1363 status = response.status_code
1365 if status == 404:
1366 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1367 elif status > 204:
1368 body = response.text.encode("utf8")
1369 message = body[:1000]
1370 else:
1371 status = 200
1372 # unpublish issue in collection site (site_register.json)
1373 if site == "website":
1374 if issue.is_deployed(mersenneSite):
1375 issue.undeploy(mersenneSite)
1376 for article in issue.article_set.all():
1377 article.undeploy(mersenneSite)
1378 # delete article binary files
1379 folder = article.get_relative_folder()
1380 resolver.delete_object_folder(
1381 folder,
1382 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1383 )
1384 # delete issue binary files
1385 folder = issue.get_relative_folder()
1386 resolver.delete_object_folder(
1387 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1388 )
1390 except Timeout as exception:
1391 return HttpResponse(exception, status=408)
1392 except Exception as exception:
1393 return HttpResponseServerError(exception)
1395 data = {"message": message, "status": status}
1396 return JsonResponse(data)
1399class ArchiveIssueAPIView(View):
1400 def get(self, request, *args, **kwargs):
1401 try:
1402 pid = kwargs["pid"]
1403 colid = kwargs["colid"]
1404 except IndexError:
1405 raise Http404
1407 try:
1408 cmd = ptf_cmds.archiveIssuePtfCmd(
1409 {
1410 "pid": pid,
1411 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1412 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1413 "needs_publication_date": True,
1414 }
1415 )
1416 result_, status, message = history_views.execute_and_record_func(
1417 "archive", pid, colid, cmd.do
1418 )
1419 except Exception as exception:
1420 return HttpResponseServerError(exception)
1422 data = {"message": message, "status": 200}
1423 return JsonResponse(data)
1426class CreateDjvuAPIView(View):
1427 def internal_do(self, *args, **kwargs):
1428 pid = self.kwargs.get("pid", None)
1430 resource = model_helpers.get_resource(pid)
1431 cmd = ptf_cmds.addDjvuPtfCmd()
1432 cmd.set_resource(resource)
1433 cmd.do()
1435 def get(self, request, *args, **kwargs):
1436 pid = self.kwargs.get("pid", None)
1437 colid = pid.split("_")[0]
1439 try:
1440 _, status, message = history_views.execute_and_record_func(
1441 "numdam", pid, colid, self.internal_do
1442 )
1443 except Exception as exception:
1444 return HttpResponseServerError(exception)
1446 data = {"message": message, "status": status}
1447 return JsonResponse(data)
1450class PTFToolsHomeView(LoginRequiredMixin, View):
1451 """
1452 Home Page.
1453 - Admin & staff -> Render blank home.html
1454 - User with unique authorized collection -> Redirect to collection details page
1455 - User with multiple authorized collections -> Render home.html with data
1456 - Comment moderator -> Comments dashboard
1457 - Others -> 404 response
1458 """
1460 def get(self, request, *args, **kwargs) -> HttpResponse:
1461 # Staff or user with authorized collections
1462 if request.user.is_staff or request.user.is_superuser:
1463 return render(request, "home.html")
1465 colids = get_authorized_collections(request.user)
1466 is_mod = is_comment_moderator(request.user)
1468 # The user has no rights
1469 if not (colids or is_mod):
1470 raise Http404("No collections associated with your account.")
1471 # Comment moderator only
1472 elif not colids:
1473 return HttpResponseRedirect(reverse("comment_list"))
1475 # User with unique collection -> Redirect to collection detail page
1476 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1477 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1479 # User with multiple authorized collections - Special home
1480 context = {}
1481 context["overview"] = True
1483 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1484 all_collections = {c["pid"]: c for c in all_collections}
1486 # Comments summary
1487 try:
1488 error, comments_data = get_comments_for_home(request.user)
1489 except AttributeError:
1490 error, comments_data = True, {}
1492 context["comment_server_ok"] = False
1494 if not error:
1495 context["comment_server_ok"] = True
1496 if comments_data:
1497 for col_id, comment_nb in comments_data.items():
1498 if col_id.upper() in all_collections: 1498 ↛ 1497line 1498 didn't jump to line 1497 because the condition on line 1498 was always true
1499 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1501 # TODO: Translations summary
1502 context["translation_server_ok"] = False
1504 # Sort the collections according to the number of pending comments
1505 context["collections"] = sorted(
1506 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1507 )
1509 return render(request, "home.html", context)
1512class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1513 columns = 5
1515 def get_common_context_data(self, **kwargs):
1516 context = super().get_context_data(**kwargs)
1517 now = timezone.now()
1518 curyear = now.year
1519 years = range(curyear - self.columns + 1, curyear + 1)
1521 context["collections"] = settings.MERSENNE_COLLECTIONS
1522 context["containers_to_be_published"] = []
1523 context["last_col_events"] = []
1525 event = get_history_last_event_by("clockss", "ALL")
1526 clockss_gap = get_gap(now, event)
1528 context["years"] = years
1529 context["clockss_gap"] = clockss_gap
1531 return context
1533 def calculate_articles_and_pages(self, pid, years):
1534 data_by_year = []
1535 total_articles = [0] * len(years)
1536 total_pages = [0] * len(years)
1538 for year in years:
1539 articles = self.get_articles_for_year(pid, year)
1540 articles_count = articles.count()
1541 page_count = sum(article.get_article_page_count() for article in articles)
1543 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1544 total_articles[year - years[0]] += articles_count
1545 total_pages[year - years[0]] += page_count
1547 return data_by_year, total_articles, total_pages
1549 def get_articles_for_year(self, pid, year):
1550 return Article.objects.filter(
1551 Q(my_container__my_collection__pid=pid)
1552 & (
1553 Q(date_published__year=year, date_online_first__isnull=True)
1554 | Q(date_online_first__year=year)
1555 )
1556 ).prefetch_related("resourcecount_set")
1559class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1560 template_name = "dashboard/published_articles.html"
1562 def get_context_data(self, **kwargs):
1563 context = self.get_common_context_data(**kwargs)
1564 years = context["years"]
1566 published_articles = []
1567 total_published_articles = [
1568 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1569 ]
1571 for pid in settings.MERSENNE_COLLECTIONS:
1572 if pid != "MERSENNE":
1573 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1574 pid, years
1575 )
1576 published_articles.append({"pid": pid, "years": articles_data})
1578 for i, year in enumerate(years):
1579 total_published_articles[i]["total_articles"] += total_articles[i]
1580 total_published_articles[i]["total_pages"] += total_pages[i]
1582 context["published_articles"] = published_articles
1583 context["total_published_articles"] = total_published_articles
1585 return context
1588class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1589 template_name = "dashboard/created_volumes.html"
1591 def get_context_data(self, **kwargs):
1592 context = self.get_common_context_data(**kwargs)
1593 years = context["years"]
1595 created_volumes = []
1596 total_created_volumes = [
1597 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1598 ]
1600 for pid in settings.MERSENNE_COLLECTIONS:
1601 if pid != "MERSENNE":
1602 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1603 pid, years
1604 )
1605 created_volumes.append({"pid": pid, "years": volumes_data})
1607 for i, year in enumerate(years):
1608 total_created_volumes[i]["total_articles"] += total_articles[i]
1609 total_created_volumes[i]["total_pages"] += total_pages[i]
1611 context["created_volumes"] = created_volumes
1612 context["total_created_volumes"] = total_created_volumes
1614 return context
1616 def calculate_volumes_and_pages(self, pid, years):
1617 data_by_year = []
1618 total_articles = [0] * len(years)
1619 total_pages = [0] * len(years)
1621 for year in years:
1622 issues = Container.objects.filter(my_collection__pid=pid, year=year)
1623 articles_count = 0
1624 page_count = 0
1626 for issue in issues:
1627 articles = issue.article_set.filter(
1628 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1629 ).prefetch_related("resourcecount_set")
1631 articles_count += articles.count()
1632 page_count += sum(article.get_article_page_count() for article in articles)
1634 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1635 total_articles[year - years[0]] += articles_count
1636 total_pages[year - years[0]] += page_count
1638 return data_by_year, total_articles, total_pages
1641class ReferencingChoice(View):
1642 def post(self, request, *args, **kwargs):
1643 if request.POST.get("optSite") == "ads":
1644 return redirect(
1645 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1646 )
1647 elif request.POST.get("optSite") == "wos":
1648 comp = ReferencingCheckerWos()
1649 journal = comp.make_journal(request.POST.get("selectCol"))
1650 if journal is None:
1651 return render(
1652 request,
1653 "dashboard/referencing.html",
1654 {
1655 "error": "Collection not found",
1656 "colid": request.POST.get("selectCol"),
1657 "optSite": request.POST.get("optSite"),
1658 },
1659 )
1660 return render(
1661 request,
1662 "dashboard/referencing.html",
1663 {
1664 "journal": journal,
1665 "colid": request.POST.get("selectCol"),
1666 "optSite": request.POST.get("optSite"),
1667 },
1668 )
1671class ReferencingWosFileView(View):
1672 template_name = "dashboard/referencing.html"
1674 def post(self, request, *args, **kwargs):
1675 colid = request.POST["colid"]
1676 if request.FILES.get("risfile") is None:
1677 message = "No file uploaded"
1678 return render(
1679 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1680 )
1681 uploaded_file = request.FILES["risfile"]
1682 comp = ReferencingCheckerWos()
1683 journal = comp.check_references(colid, uploaded_file)
1684 return render(request, self.template_name, {"journal": journal})
1687class ReferencingDashboardView(BaseMersenneDashboardView):
1688 template_name = "dashboard/referencing.html"
1690 def get(self, request, *args, **kwargs):
1691 colid = self.kwargs.get("colid", None)
1692 comp = ReferencingCheckerAds()
1693 journal = comp.check_references(colid)
1694 return render(request, self.template_name, {"journal": journal})
1697class BaseCollectionView(TemplateView):
1698 def get_context_data(self, **kwargs):
1699 context = super().get_context_data(**kwargs)
1700 aid = context.get("aid")
1701 year = context.get("year")
1703 if aid and year:
1704 context["collection"] = self.get_collection(aid, year)
1706 return context
1708 def get_collection(self, aid, year):
1709 """Method to be overridden by subclasses to fetch the appropriate collection"""
1710 raise NotImplementedError("Subclasses must implement get_collection method")
1713class ArticleListView(BaseCollectionView):
1714 template_name = "collection-list.html"
1716 def get_collection(self, aid, year):
1717 return Article.objects.filter(
1718 Q(my_container__my_collection__pid=aid)
1719 & (
1720 Q(date_published__year=year, date_online_first__isnull=True)
1721 | Q(date_online_first__year=year)
1722 )
1723 ).prefetch_related("resourcecount_set")
1726class VolumeListView(BaseCollectionView):
1727 template_name = "collection-list.html"
1729 def get_collection(self, aid, year):
1730 return Article.objects.filter(
1731 Q(my_container__my_collection__pid=aid, my_container__year=year)
1732 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1733 ).prefetch_related("resourcecount_set")
1736class DOAJResourceRegisterView(View):
1737 def get(self, request, *args, **kwargs):
1738 pid = kwargs.get("pid", None)
1739 resource = model_helpers.get_resource(pid)
1740 if resource is None:
1741 raise Http404
1742 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1743 resource.colid, None
1744 ):
1745 raise RuntimeError("Pending publications should not be deployed")
1747 try:
1748 data = {}
1749 doaj_meta, response = doaj_pid_register(pid)
1750 if response is None:
1751 return HttpResponse(status=204)
1752 elif doaj_meta and 200 <= response.status_code <= 299:
1753 data.update(doaj_meta)
1754 else:
1755 return HttpResponse(status=response.status_code, reason=response.text)
1756 except Timeout as exception:
1757 return HttpResponse(exception, status=408)
1758 except Exception as exception:
1759 return HttpResponseServerError(exception)
1760 return JsonResponse(data)
1763class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View):
1764 """
1765 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml
1766 """
1768 def get(self, request, *args, **kwargs):
1769 pid = kwargs.get("pid")
1770 if not pid:
1771 raise Http404("Missing pid")
1773 article = Article.objects.filter(pid=pid).first()
1774 if not article:
1775 raise Http404(f"Article not found: {pid}")
1777 colid = article.get_collection().pid
1778 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
1779 return JsonResponse(
1780 {"status": 403, "message": f"Tex conversions are disabled in {colid}"}
1781 )
1783 if is_tex_conversion_locked(pid):
1784 logger.warning("Conversion rejected (lock exists) for %s", pid)
1785 return JsonResponse(
1786 {"status": 409, "message": f"A conversion is already running for {pid}"}
1787 )
1789 logger.info("No lock → scheduling conversion for %s", pid)
1791 try:
1792 convert_article_tex.delay(pid=pid, user_pk=request.user.pk)
1793 except Exception:
1794 logger.exception("Failed to enqueue task for %s", pid)
1795 release_tex_conversion_lock(pid)
1796 raise
1798 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"})
1801class CROSSREFResourceRegisterView(View):
1802 def get(self, request, *args, **kwargs):
1803 pid = kwargs.get("pid", None)
1804 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1805 force = kwargs.get("force", None)
1806 if not request.user.is_superuser:
1807 force = None
1809 resource = model_helpers.get_resource(pid)
1810 if resource is None:
1811 raise Http404
1813 resource = resource.cast()
1814 meth = getattr(self, "recordDOI" + resource.classname)
1815 try:
1816 data = meth(resource, force)
1817 except Timeout as exception:
1818 return HttpResponse(exception, status=408)
1819 except Exception as exception:
1820 return HttpResponseServerError(exception)
1821 return JsonResponse(data)
1823 def recordDOIArticle(self, article, force=None):
1824 result = {"status": 404}
1825 if (
1826 article.doi
1827 and not article.do_not_publish
1828 and (article.date_published or article.date_online_first or force == "force")
1829 ):
1830 if article.my_container.year is None: # or article.my_container.year == '0':
1831 article.my_container.year = datetime.now().strftime("%Y")
1832 result = recordDOI(article)
1833 return result
1835 def recordDOICollection(self, collection, force=None):
1836 return recordDOI(collection)
1838 def recordDOIContainer(self, container, force=None):
1839 data = {"status": 200, "message": "tout va bien"}
1841 if container.ctype == "issue":
1842 if container.doi:
1843 result = recordDOI(container)
1844 if result["status"] != 200:
1845 return result
1846 if force == "force":
1847 articles = container.article_set.exclude(
1848 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1849 )
1850 else:
1851 articles = container.article_set.exclude(
1852 doi__isnull=True,
1853 do_not_publish=True,
1854 date_published__isnull=True,
1855 date_online_first__isnull=True,
1856 )
1858 for article in articles:
1859 result = self.recordDOIArticle(article, force)
1860 if result["status"] != 200:
1861 data = result
1862 else:
1863 return recordDOI(container)
1864 return data
1867class CROSSREFResourceCheckStatusView(View):
1868 def get(self, request, *args, **kwargs):
1869 pid = kwargs.get("pid", None)
1870 resource = model_helpers.get_resource(pid)
1871 if resource is None:
1872 raise Http404
1873 resource = resource.cast()
1874 meth = getattr(self, "checkDOI" + resource.classname)
1875 try:
1876 meth(resource)
1877 except Timeout as exception:
1878 return HttpResponse(exception, status=408)
1879 except Exception as exception:
1880 return HttpResponseServerError(exception)
1882 data = {"status": 200, "message": "tout va bien"}
1883 return JsonResponse(data)
1885 def checkDOIArticle(self, article):
1886 if article.my_container.year is None or article.my_container.year == "0":
1887 article.my_container.year = datetime.now().strftime("%Y")
1888 checkDOI(article)
1890 def checkDOICollection(self, collection):
1891 checkDOI(collection)
1893 def checkDOIContainer(self, container):
1894 if container.doi is not None:
1895 checkDOI(container)
1896 for article in container.article_set.all():
1897 self.checkDOIArticle(article)
1900class CROSSREFResourcePendingPublicationRegisterView(View):
1901 def get(self, request, *args, **kwargs):
1902 pid = kwargs.get("pid", None)
1903 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1905 resource = model_helpers.get_resource(pid)
1906 if resource is None:
1907 raise Http404
1909 resource = resource.cast()
1910 meth = getattr(self, "recordPendingPublication" + resource.classname)
1911 try:
1912 data = meth(resource)
1913 except Timeout as exception:
1914 return HttpResponse(exception, status=408)
1915 except Exception as exception:
1916 return HttpResponseServerError(exception)
1917 return JsonResponse(data)
1919 def recordPendingPublicationArticle(self, article):
1920 result = {"status": 404}
1921 if article.doi and not article.date_published and not article.date_online_first:
1922 if article.my_container.year is None: # or article.my_container.year == '0':
1923 article.my_container.year = datetime.now().strftime("%Y")
1924 result = recordPendingPublication(article)
1925 return result
1928class RegisterPubmedFormView(FormView):
1929 template_name = "record_pubmed_dialog.html"
1930 form_class = RegisterPubmedForm
1932 def get_context_data(self, **kwargs):
1933 context = super().get_context_data(**kwargs)
1934 context["pid"] = self.kwargs["pid"]
1935 context["helper"] = PtfLargeModalFormHelper
1936 return context
1939class RegisterPubmedView(View):
1940 def get(self, request, *args, **kwargs):
1941 pid = kwargs.get("pid", None)
1942 update_article = self.request.GET.get("update_article", "on") == "on"
1944 article = model_helpers.get_article(pid)
1945 if article is None:
1946 raise Http404
1947 try:
1948 recordPubmed(article, update_article)
1949 except Exception as exception:
1950 messages.error("Unable to register the article in PubMed")
1951 return HttpResponseServerError(exception)
1953 return HttpResponseRedirect(
1954 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1955 )
1958class PTFToolsContainerView(TemplateView):
1959 template_name = ""
1961 def get_context_data(self, **kwargs):
1962 context = super().get_context_data(**kwargs)
1964 container = model_helpers.get_container(self.kwargs.get("pid"))
1965 if container is None:
1966 raise Http404
1967 citing_articles = container.citations()
1968 source = self.request.GET.get("source", None)
1969 if container.ctype.startswith("book"):
1970 book_parts = (
1971 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1972 )
1973 references = False
1974 if container.ctype == "book-monograph":
1975 # on regarde si il y a au moins une bibliographie
1976 for art in container.article_set.all():
1977 if art.bibitem_set.count() > 0:
1978 references = True
1979 context.update(
1980 {
1981 "book": container,
1982 "book_parts": list(book_parts),
1983 "source": source,
1984 "citing_articles": citing_articles,
1985 "references": references,
1986 "test_website": container.get_top_collection()
1987 .extlink_set.get(rel="test_website")
1988 .location,
1989 "prod_website": container.get_top_collection()
1990 .extlink_set.get(rel="website")
1991 .location,
1992 }
1993 )
1994 self.template_name = "book-toc.html"
1995 else:
1996 articles = container.article_set.all().order_by("seq")
1997 for article in articles:
1998 try:
1999 last_match = (
2000 history_models.HistoryEvent.objects.filter(
2001 pid=article.pid,
2002 type="matching",
2003 )
2004 .only("created_on")
2005 .latest("created_on")
2006 )
2007 except history_models.HistoryEvent.DoesNotExist as _:
2008 article.last_match = None
2009 else:
2010 article.last_match = last_match.created_on
2012 # article1 = articles.first()
2013 # date = article1.deployed_date()
2014 # TODO next_issue, previous_issue
2016 # check DOI est maintenant une commande à part
2017 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
2018 # articlesWithStatus = []
2019 # for article in articles:
2020 # checkDOIExistence(article)
2021 # articlesWithStatus.append(article)
2023 test_location = prod_location = ""
2024 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
2025 if qs:
2026 test_location = qs.first().location
2027 qs = container.get_top_collection().extlink_set.filter(rel="website")
2028 if qs:
2029 prod_location = qs.first().location
2030 context.update(
2031 {
2032 "issue": container,
2033 "articles": articles,
2034 "source": source,
2035 "citing_articles": citing_articles,
2036 "test_website": test_location,
2037 "prod_website": prod_location,
2038 }
2039 )
2041 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
2042 context["is_issue_pending_publication"] = True
2043 if container.get_top_collection().pid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
2044 context["is_excluded_from_tex_conversion"] = True
2045 self.template_name = "issue-items.html"
2047 context["allow_crossref"] = container.allow_crossref()
2048 context["coltype"] = container.my_collection.coltype
2049 return context
2052class ExtLinkInline(InlineFormSetFactory):
2053 model = ExtLink
2054 form_class = ExtLinkForm
2055 factory_kwargs = {"extra": 0}
2058class ResourceIdInline(InlineFormSetFactory):
2059 model = ResourceId
2060 form_class = ResourceIdForm
2061 factory_kwargs = {"extra": 0}
2064class IssueDetailAPIView(View):
2065 def get(self, request, *args, **kwargs):
2066 issue = get_object_or_404(Container, pid=kwargs["pid"])
2067 deployed_date = issue.deployed_date()
2068 result = {
2069 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2070 if deployed_date
2071 else None,
2072 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2073 "all_doi_are_registered": issue.all_doi_are_registered(),
2074 "registered_in_doaj": issue.registered_in_doaj(),
2075 "doi": issue.my_collection.doi,
2076 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2077 }
2078 try:
2079 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2080 except history_models.HistoryEvent.DoesNotExist as _:
2081 pass
2082 else:
2083 result["latest"] = latest.message
2084 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2085 "%Y-%m-%d %H:%M"
2086 )
2088 result["latest_type"] = latest.type.capitalize()
2089 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2090 try:
2091 result[event_type] = timezone.localtime(
2092 history_models.HistoryEvent.objects.filter(
2093 type=event_type,
2094 status="OK",
2095 pid__startswith=issue.pid,
2096 )
2097 .latest("created_on")
2098 .created_on
2099 ).strftime("%Y-%m-%d %H:%M")
2100 except history_models.HistoryEvent.DoesNotExist as _:
2101 result[event_type] = ""
2102 return JsonResponse(result)
2105class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2106 model = Collection
2107 form_class = CollectionForm
2108 inlines = [ResourceIdInline, ExtLinkInline]
2109 inlines_names = ["resource_ids_form", "ext_links_form"]
2111 def get_context_data(self, **kwargs):
2112 context = super().get_context_data(**kwargs)
2113 context["helper"] = PtfFormHelper
2114 context["formset_helper"] = FormSetHelper
2115 return context
2117 def add_description(self, collection, description, lang, seq):
2118 if description:
2119 la = Abstract(
2120 resource=collection,
2121 tag="description",
2122 lang=lang,
2123 seq=seq,
2124 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2125 value_html=description,
2126 value_tex=description,
2127 )
2128 la.save()
2130 def form_valid(self, form):
2131 if form.instance.abbrev:
2132 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2133 else:
2134 form.instance.title_xml = (
2135 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2136 )
2138 form.instance.title_html = form.instance.title_tex
2139 form.instance.title_sort = form.instance.title_tex
2140 result = super().form_valid(form)
2142 collection = self.object
2143 collection.abstract_set.all().delete()
2145 seq = 1
2146 description = form.cleaned_data["description_en"]
2147 if description:
2148 self.add_description(collection, description, "en", seq)
2149 seq += 1
2150 description = form.cleaned_data["description_fr"]
2151 if description:
2152 self.add_description(collection, description, "fr", seq)
2154 return result
2156 def get_success_url(self):
2157 messages.success(self.request, "La Collection a été modifiée avec succès")
2158 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2161class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2162 """
2163 Warning : Not yet finished
2164 Automatic site membership creation is still missing
2165 """
2168class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2169 slug_field = "pid"
2170 slug_url_kwarg = "pid"
2173def suggest_load_journal_dois(colid):
2174 articles = (
2175 Article.objects.filter(my_container__my_collection__pid=colid)
2176 .filter(doi__isnull=False)
2177 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2178 .values_list("doi", flat=True)
2179 )
2181 try:
2182 articles = sorted(
2183 articles,
2184 key=lambda d: (
2185 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2186 int(re.search(r".(\d+)$", d).group(1)),
2187 ),
2188 )
2189 except: # noqa: E722 (we'll look later)
2190 pass
2191 return [f'<option value="{doi}">' for doi in articles]
2194def get_context_with_volumes(journal):
2195 result = model_helpers.get_volumes_in_collection(journal)
2196 volume_count = result["volume_count"]
2197 collections = []
2198 for ancestor in journal.ancestors.all():
2199 item = model_helpers.get_volumes_in_collection(ancestor)
2200 volume_count = max(0, volume_count)
2201 item.update({"journal": ancestor})
2202 collections.append(item)
2204 # add the parent collection to its children list and sort it by date
2205 result.update({"journal": journal})
2206 collections.append(result)
2208 collections = [c for c in collections if c["sorted_issues"]]
2209 collections.sort(
2210 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2211 reverse=True,
2212 )
2214 context = {
2215 "journal": journal,
2216 "sorted_issues": result["sorted_issues"],
2217 "volume_count": volume_count,
2218 "max_width": result["max_width"],
2219 "collections": collections,
2220 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2221 }
2222 return context
2225class CollectionDetail(
2226 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2227):
2228 model = Collection
2229 slug_field = "pid"
2230 slug_url_kwarg = "pid"
2231 template_name = "ptf/collection_detail.html"
2233 def test_func(self):
2234 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2236 def get(self, request, *args, **kwargs):
2237 self.object = self.get_object(queryset=Collection.objects.all())
2238 return super().get(request, *args, **kwargs)
2240 def get_context_data(self, **kwargs):
2241 context = super().get_context_data(**kwargs)
2242 context["object_list"] = context["object_list"].filter(
2243 Q(ctype="issue") | Q(ctype="book-lecture-notes")
2244 )
2245 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2246 context.update(get_context_with_volumes(self.object))
2248 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2249 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2250 context["issue_to_appear"] = Container.objects.filter(
2251 pid=context["issue_to_appear_pid"]
2252 ).exists()
2253 try:
2254 latest_error = history_models.HistoryEvent.objects.filter(
2255 status="ERROR", col=self.object
2256 ).latest("created_on")
2257 except history_models.HistoryEvent.DoesNotExist as _:
2258 pass
2259 else:
2260 message = latest_error.message
2261 if message:
2262 i = message.find(" - ")
2263 latest_exception = message[:i]
2264 latest_error_message = message[i + 3 :]
2265 context["latest_exception"] = latest_exception
2266 context["latest_exception_date"] = latest_error.created_on
2267 context["latest_exception_type"] = latest_error.type
2268 context["latest_error_message"] = latest_error_message
2270 archive_in_error = history_models.HistoryEvent.objects.filter(
2271 status="ERROR", col=self.object, type="archive"
2272 ).exists()
2274 context["archive_in_error"] = archive_in_error
2276 return context
2278 def get_queryset(self):
2279 query = self.object.content.all()
2281 for ancestor in self.object.ancestors.all():
2282 query |= ancestor.content.all()
2284 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2287class ContainerEditView(FormView):
2288 template_name = "container_form.html"
2289 form_class = ContainerForm
2291 def get_success_url(self):
2292 if self.kwargs["pid"]:
2293 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2294 return reverse("mersenne_dashboard/published_articles")
2296 def set_success_message(self): # pylint: disable=no-self-use
2297 messages.success(self.request, "Le fascicule a été modifié")
2299 def get_form_kwargs(self):
2300 kwargs = super().get_form_kwargs()
2301 if "pid" not in self.kwargs:
2302 self.kwargs["pid"] = None
2303 if "colid" not in self.kwargs:
2304 self.kwargs["colid"] = None
2305 if "data" in kwargs and "colid" in kwargs["data"]:
2306 # colid is passed as a hidden param in the form.
2307 # It is used when you submit a new container
2308 self.kwargs["colid"] = kwargs["data"]["colid"]
2310 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2311 self.kwargs["pid"]
2312 )
2313 return kwargs
2315 def get_context_data(self, **kwargs):
2316 context = super().get_context_data(**kwargs)
2318 context["pid"] = self.kwargs["pid"]
2319 context["colid"] = self.kwargs["colid"]
2320 context["container"] = self.kwargs["container"]
2322 context["edit_container"] = context["pid"] is not None
2323 context["name"] = resolve(self.request.path_info).url_name
2325 return context
2327 def form_valid(self, form):
2328 new_pid = form.cleaned_data.get("pid")
2329 new_title = form.cleaned_data.get("title")
2330 new_trans_title = form.cleaned_data.get("trans_title")
2331 new_publisher = form.cleaned_data.get("publisher")
2332 new_year = form.cleaned_data.get("year")
2333 new_volume = form.cleaned_data.get("volume")
2334 new_number = form.cleaned_data.get("number")
2336 collection = None
2337 issue = self.kwargs["container"]
2338 if issue is not None:
2339 collection = issue.my_collection
2340 elif self.kwargs["colid"] is not None:
2341 if "CR" in self.kwargs["colid"]:
2342 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2343 else:
2344 collection = model_helpers.get_collection(self.kwargs["colid"])
2346 if collection is None:
2347 raise ValueError("Collection for " + new_pid + " does not exist")
2349 # Icon
2350 new_icon_location = ""
2351 if "icon" in self.request.FILES:
2352 filename = os.path.basename(self.request.FILES["icon"].name)
2353 file_extension = filename.split(".")[1]
2355 icon_filename = resolver.get_disk_location(
2356 settings.MERSENNE_TEST_DATA_FOLDER,
2357 collection.pid,
2358 file_extension,
2359 new_pid,
2360 None,
2361 True,
2362 )
2364 with open(icon_filename, "wb+") as destination:
2365 for chunk in self.request.FILES["icon"].chunks():
2366 destination.write(chunk)
2368 folder = resolver.get_relative_folder(collection.pid, new_pid)
2369 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2370 name = resolve(self.request.path_info).url_name
2371 if name == "special_issue_create":
2372 self.kwargs["name"] = name
2373 if self.kwargs["container"]:
2374 # Edit Issue
2375 issue = self.kwargs["container"]
2376 if issue is None:
2377 raise ValueError(self.kwargs["pid"] + " does not exist")
2379 issue.pid = new_pid
2380 issue.title_tex = issue.title_html = new_title
2381 issue.title_xml = build_title_xml(
2382 title=new_title,
2383 lang=issue.lang,
2384 title_type="issue-title",
2385 )
2387 trans_lang = ""
2388 if new_trans_title != "":
2389 trans_lang = "fr" if issue.lang == "en" else "en"
2391 if trans_lang != "" and new_trans_title != "":
2392 title_xml = build_title_xml(
2393 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2394 )
2396 issue.title_set.update_or_create(
2397 lang=trans_lang,
2398 type="main",
2399 defaults={"title_html": new_trans_title, "title_xml": title_xml},
2400 )
2402 issue.year = new_year
2403 issue.volume = new_volume
2404 issue.volume_int = make_int(new_volume)
2405 issue.number = new_number
2406 issue.number_int = make_int(new_number)
2407 issue.save()
2408 else:
2409 xissue = create_issuedata()
2411 xissue.ctype = "issue"
2412 xissue.pid = new_pid
2413 xissue.lang = "en"
2414 xissue.title_tex = new_title
2415 xissue.title_html = new_title
2416 xissue.title_xml = build_title_xml(
2417 title=new_title, lang=xissue.lang, title_type="issue-title"
2418 )
2420 if new_trans_title != "":
2421 trans_lang = "fr"
2422 title_xml = build_title_xml(
2423 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2424 )
2425 title = create_titledata(
2426 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2427 )
2428 issue.titles = [title]
2430 xissue.year = new_year
2431 xissue.volume = new_volume
2432 xissue.number = new_number
2433 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2435 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2436 cmd.add_collection(collection)
2437 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2438 issue = cmd.do()
2440 self.kwargs["pid"] = new_pid
2442 # Add objects related to the article: contribs, datastream, counts...
2443 params = {
2444 "icon_location": new_icon_location,
2445 }
2446 cmd = ptf_cmds.updateContainerPtfCmd(params)
2447 cmd.set_resource(issue)
2448 cmd.do()
2450 publisher = model_helpers.get_publisher(new_publisher)
2451 if not publisher:
2452 xpub = create_publisherdata()
2453 xpub.name = new_publisher
2454 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2455 issue.my_publisher = publisher
2456 issue.save()
2458 self.set_success_message()
2460 return super().form_valid(form)
2463# class ArticleEditView(FormView):
2464# template_name = 'article_form.html'
2465# form_class = ArticleForm
2466#
2467# def get_success_url(self):
2468# if self.kwargs['pid']:
2469# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2470# return reverse('mersenne_dashboard/published_articles')
2471#
2472# def set_success_message(self): # pylint: disable=no-self-use
2473# messages.success(self.request, "L'article a été modifié")
2474#
2475# def get_form_kwargs(self):
2476# kwargs = super(ArticleEditView, self).get_form_kwargs()
2477#
2478# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2479# # Article creation: pid is None
2480# self.kwargs['pid'] = None
2481# if 'issue_id' not in self.kwargs:
2482# # Article edit: issue_id is not passed
2483# self.kwargs['issue_id'] = None
2484# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2485# # colid is passed as a hidden param in the form.
2486# # It is used when you submit a new container
2487# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2488#
2489# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2490# return kwargs
2491#
2492# def get_context_data(self, **kwargs):
2493# context = super(ArticleEditView, self).get_context_data(**kwargs)
2494#
2495# context['pid'] = self.kwargs['pid']
2496# context['issue_id'] = self.kwargs['issue_id']
2497# context['article'] = self.kwargs['article']
2498#
2499# context['edit_article'] = context['pid'] is not None
2500#
2501# article = context['article']
2502# if article:
2503# context['author_contributions'] = article.get_author_contributions()
2504# context['kwds_fr'] = None
2505# context['kwds_en'] = None
2506# kwd_gps = article.get_non_msc_kwds()
2507# for kwd_gp in kwd_gps:
2508# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2509# if kwd_gp.value_xml:
2510# kwd_ = types.SimpleNamespace()
2511# kwd_.value = kwd_gp.value_tex
2512# context['kwd_unstructured_fr'] = kwd_
2513# context['kwds_fr'] = kwd_gp.kwd_set.all()
2514# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2515# if kwd_gp.value_xml:
2516# kwd_ = types.SimpleNamespace()
2517# kwd_.value = kwd_gp.value_tex
2518# context['kwd_unstructured_en'] = kwd_
2519# context['kwds_en'] = kwd_gp.kwd_set.all()
2520#
2521# # Article creation: init pid
2522# if context['issue_id'] and context['pid'] is None:
2523# issue = model_helpers.get_container(context['issue_id'])
2524# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2525#
2526# return context
2527#
2528# def form_valid(self, form):
2529#
2530# new_pid = form.cleaned_data.get('pid')
2531# new_title = form.cleaned_data.get('title')
2532# new_fpage = form.cleaned_data.get('fpage')
2533# new_lpage = form.cleaned_data.get('lpage')
2534# new_page_range = form.cleaned_data.get('page_range')
2535# new_page_count = form.cleaned_data.get('page_count')
2536# new_coi_statement = form.cleaned_data.get('coi_statement')
2537# new_show_body = form.cleaned_data.get('show_body')
2538# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2539#
2540# # TODO support MathML
2541# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2542# # We need to pass trans_title to get_title_xml
2543# # Meanwhile, ignore new_title_xml
2544# new_title_xml = jats_parser.get_title_xml(new_title)
2545# new_title_html = new_title
2546#
2547# authors_count = int(self.request.POST.get('authors_count', "0"))
2548# i = 1
2549# new_authors = []
2550# old_author_contributions = []
2551# if self.kwargs['article']:
2552# old_author_contributions = self.kwargs['article'].get_author_contributions()
2553#
2554# while authors_count > 0:
2555# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2556#
2557# if prefix is not None:
2558# addresses = []
2559# if len(old_author_contributions) >= i:
2560# old_author_contribution = old_author_contributions[i - 1]
2561# addresses = [contrib_address.address for contrib_address in
2562# old_author_contribution.get_addresses()]
2563#
2564# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2565# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2566# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2567# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2568# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2569# deceased_before_publication = deceased == 'on'
2570# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2571# equal_contrib = equal_contrib == 'on'
2572# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2573# corresponding = corresponding == 'on'
2574# email = self.request.POST.get('email-' + str(i), None)
2575#
2576# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2577# params['deceased_before_publication'] = deceased_before_publication
2578# params['equal_contrib'] = equal_contrib
2579# params['corresponding'] = corresponding
2580# params['addresses'] = addresses
2581# params['email'] = email
2582#
2583# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2584#
2585# new_authors.append(params)
2586#
2587# authors_count -= 1
2588# i += 1
2589#
2590# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2591# i = 1
2592# new_kwds_fr = []
2593# while kwds_fr_count > 0:
2594# value = self.request.POST.get('kwd-fr-' + str(i), None)
2595# new_kwds_fr.append(value)
2596# kwds_fr_count -= 1
2597# i += 1
2598# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2599#
2600# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2601# i = 1
2602# new_kwds_en = []
2603# while kwds_en_count > 0:
2604# value = self.request.POST.get('kwd-en-' + str(i), None)
2605# new_kwds_en.append(value)
2606# kwds_en_count -= 1
2607# i += 1
2608# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2609#
2610# if self.kwargs['article']:
2611# # Edit article
2612# container = self.kwargs['article'].my_container
2613# else:
2614# # New article
2615# container = model_helpers.get_container(self.kwargs['issue_id'])
2616#
2617# if container is None:
2618# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2619#
2620# collection = container.my_collection
2621#
2622# # Copy PDF file & extract full text
2623# body = ''
2624# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2625# collection.pid,
2626# "pdf",
2627# container.pid,
2628# new_pid,
2629# True)
2630# if 'pdf' in self.request.FILES:
2631# with open(pdf_filename, 'wb+') as destination:
2632# for chunk in self.request.FILES['pdf'].chunks():
2633# destination.write(chunk)
2634#
2635# # Extract full text from the PDF
2636# body = utils.pdf_to_text(pdf_filename)
2637#
2638# # Icon
2639# new_icon_location = ''
2640# if 'icon' in self.request.FILES:
2641# filename = os.path.basename(self.request.FILES['icon'].name)
2642# file_extension = filename.split('.')[1]
2643#
2644# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2645# collection.pid,
2646# file_extension,
2647# container.pid,
2648# new_pid,
2649# True)
2650#
2651# with open(icon_filename, 'wb+') as destination:
2652# for chunk in self.request.FILES['icon'].chunks():
2653# destination.write(chunk)
2654#
2655# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2656# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2657#
2658# if self.kwargs['article']:
2659# # Edit article
2660# article = self.kwargs['article']
2661# article.fpage = new_fpage
2662# article.lpage = new_lpage
2663# article.page_range = new_page_range
2664# article.coi_statement = new_coi_statement
2665# article.show_body = new_show_body
2666# article.do_not_publish = new_do_not_publish
2667# article.save()
2668#
2669# else:
2670# # New article
2671# params = {
2672# 'pid': new_pid,
2673# 'title_xml': new_title_xml,
2674# 'title_html': new_title_html,
2675# 'title_tex': new_title,
2676# 'fpage': new_fpage,
2677# 'lpage': new_lpage,
2678# 'page_range': new_page_range,
2679# 'seq': container.article_set.count() + 1,
2680# 'body': body,
2681# 'coi_statement': new_coi_statement,
2682# 'show_body': new_show_body,
2683# 'do_not_publish': new_do_not_publish
2684# }
2685#
2686# xarticle = create_articledata()
2687# xarticle.pid = new_pid
2688# xarticle.title_xml = new_title_xml
2689# xarticle.title_html = new_title_html
2690# xarticle.title_tex = new_title
2691# xarticle.fpage = new_fpage
2692# xarticle.lpage = new_lpage
2693# xarticle.page_range = new_page_range
2694# xarticle.seq = container.article_set.count() + 1
2695# xarticle.body = body
2696# xarticle.coi_statement = new_coi_statement
2697# params['xobj'] = xarticle
2698#
2699# cmd = ptf_cmds.addArticlePtfCmd(params)
2700# cmd.set_container(container)
2701# cmd.add_collection(container.my_collection)
2702# article = cmd.do()
2703#
2704# self.kwargs['pid'] = new_pid
2705#
2706# # Add objects related to the article: contribs, datastream, counts...
2707# params = {
2708# # 'title_xml': new_title_xml,
2709# # 'title_html': new_title_html,
2710# # 'title_tex': new_title,
2711# 'authors': new_authors,
2712# 'page_count': new_page_count,
2713# 'icon_location': new_icon_location,
2714# 'body': body,
2715# 'use_kwds': True,
2716# 'kwds_fr': new_kwds_fr,
2717# 'kwds_en': new_kwds_en,
2718# 'kwd_uns_fr': new_kwd_uns_fr,
2719# 'kwd_uns_en': new_kwd_uns_en
2720# }
2721# cmd = ptf_cmds.updateArticlePtfCmd(params)
2722# cmd.set_article(article)
2723# cmd.do()
2724#
2725# self.set_success_message()
2726#
2727# return super(ArticleEditView, self).form_valid(form)
2730@require_http_methods(["POST"])
2731def do_not_publish_article(request, *args, **kwargs):
2732 next = request.headers.get("referer")
2734 pid = kwargs.get("pid", "")
2736 article = model_helpers.get_article(pid)
2737 if article:
2738 article.do_not_publish = not article.do_not_publish
2739 article.save()
2740 else:
2741 raise Http404
2743 return HttpResponseRedirect(next)
2746@require_http_methods(["POST"])
2747def show_article_body(request, *args, **kwargs):
2748 next = request.headers.get("referer")
2750 pid = kwargs.get("pid", "")
2752 article = model_helpers.get_article(pid)
2753 if article:
2754 article.show_body = not article.show_body
2755 article.save()
2756 else:
2757 raise Http404
2759 return HttpResponseRedirect(next)
2762class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2763 """
2764 API to get/post article metadata
2765 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2766 """
2768 def __init__(self, *args, **kwargs):
2769 """
2770 we define here what fields we want in the form
2771 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2772 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2773 super().__init__(*args, **kwargs)
2774 self.fields_to_update = [
2775 "lang",
2776 "atype",
2777 "contributors",
2778 "abstracts",
2779 "kwds",
2780 "titles",
2781 "title_html",
2782 "title_xml",
2783 "title_tex",
2784 "streams",
2785 "ext_links",
2786 "date_accepted",
2787 "history_dates",
2788 "subjs",
2789 "bibitems",
2790 "references",
2791 ]
2792 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2793 self.additional_fields = [
2794 "doi",
2795 "pid",
2796 "container_pid",
2797 "pdf",
2798 "illustration",
2799 "dates",
2800 "msc_keywords",
2801 ]
2802 self.editorial_tools = [
2803 "translation",
2804 "sidebar",
2805 "lang_selection",
2806 "back_to_article_option",
2807 "msc_keywords",
2808 ]
2809 self.article_container_pid = ""
2810 self.back_url = "trammel"
2812 def save_data(self, data_article):
2813 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2814 # The icons are not preserved since we can add/edit/delete them in VueJs
2815 params = {
2816 "pid": data_article.pid,
2817 "export_folder": settings.MERSENNE_TMP_FOLDER,
2818 "export_all": True,
2819 "with_binary_files": False,
2820 }
2821 ptf_cmds.exportExtraDataPtfCmd(params).do()
2823 def restore_data(self, article):
2824 ptf_cmds.importExtraDataPtfCmd(
2825 {
2826 "pid": article.pid,
2827 "import_folder": settings.MERSENNE_TMP_FOLDER,
2828 }
2829 ).do()
2831 def get(self, request, *args, **kwargs):
2832 data = super().get(request, *args, **kwargs)
2833 return data
2835 def post(self, request, *args, **kwargs):
2836 response = super().post(request, *args, **kwargs)
2837 if response["message"] == "OK":
2838 return redirect(
2839 "api-edit-article",
2840 colid=kwargs.get("colid", ""),
2841 containerPid=kwargs.get("containerPid"),
2842 doi=kwargs.get("doi", ""),
2843 )
2844 else:
2845 raise Http404
2848class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2849 template_name = "article_form.html"
2851 def get_success_url(self):
2852 if self.kwargs["doi"]:
2853 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2854 return reverse("mersenne_dashboard/published_articles")
2856 def get_context_data(self, **kwargs):
2857 context = super().get_context_data(**kwargs)
2858 if "doi" in self.kwargs:
2859 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"])
2860 context["pid"] = context["article"].pid
2862 context["container_pid"] = kwargs.get("container_pid", "")
2863 return context
2866class ArticleDeleteView(View):
2867 def get(self, request, *args, **kwargs):
2868 pid = self.kwargs.get("pid", None)
2869 article = get_object_or_404(Article, pid=pid)
2871 try:
2872 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2873 article.undeploy(mersenneSite)
2875 cmd = ptf_cmds.addArticlePtfCmd(
2876 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2877 )
2878 cmd.set_container(article.my_container)
2879 cmd.set_object_to_be_deleted(article)
2880 cmd.undo()
2881 except Exception as exception:
2882 return HttpResponseServerError(exception)
2884 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2885 return JsonResponse(data)
2888def get_messages_in_queue():
2889 app = Celery("ptf-tools")
2890 # tasks = list(current_app.tasks)
2891 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2892 print(tasks)
2893 # i = app.control.inspect()
2895 with app.connection_or_acquire() as conn:
2896 remaining = conn.default_channel.queue_declare(
2897 queue="coordinator", passive=True
2898 ).message_count
2899 return remaining
2902class NumdamView(TemplateView, history_views.HistoryContextMixin):
2903 template_name = "numdam.html"
2905 def get_context_data(self, **kwargs):
2906 context = super().get_context_data(**kwargs)
2908 context["objs"] = ResourceInNumdam.objects.all()
2910 pre_issues = []
2911 prod_issues = []
2912 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2913 try:
2914 response = requests.get(url)
2915 if response.status_code == 200:
2916 data = response.json()
2917 if "issues" in data:
2918 pre_issues = data["issues"]
2919 except Exception:
2920 pass
2922 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2923 response = requests.get(url)
2924 if response.status_code == 200:
2925 data = response.json()
2926 if "issues" in data:
2927 prod_issues = data["issues"]
2929 new = sorted(list(set(pre_issues).difference(prod_issues)))
2930 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2931 grouped = [
2932 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2933 ]
2934 grouped_removed = [
2935 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2936 ]
2937 context["added_issues"] = grouped
2938 context["removed_issues"] = grouped_removed
2940 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2941 return context
2944class NumdamArchiveView(RedirectView):
2945 @staticmethod
2946 def reset_task_results():
2947 TaskResult.objects.all().delete()
2949 def get_redirect_url(self, *args, **kwargs):
2950 self.colid = kwargs["colid"]
2952 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2953 return Http404
2955 # we make sure archiving is not already running
2956 # if not get_messages_in_queue():
2957 # self.reset_task_results()
2959 if self.colid == "ALL":
2960 archive_numdam_collections.delay()
2961 else:
2962 archive_numdam_collection.s(self.colid).delay()
2964 return reverse("numdam")
2967class DeployAllNumdamAPIView(View):
2968 def internal_do(self, *args, **kwargs):
2969 pids = []
2971 for obj in ResourceInNumdam.objects.all():
2972 pids.append(obj.pid)
2974 return pids
2976 def get(self, request, *args, **kwargs):
2977 try:
2978 pids, status, message = history_views.execute_and_record_func(
2979 "deploy", "numdam", "ALL", self.internal_do, "numdam"
2980 )
2981 except Exception as exception:
2982 return HttpResponseServerError(exception)
2984 data = {"message": message, "ids": pids, "status": status}
2985 return JsonResponse(data)
2988class NumdamDeleteAPIView(View):
2989 def get(self, request, *args, **kwargs):
2990 pid = self.kwargs.get("pid", None)
2992 try:
2993 obj = ResourceInNumdam.objects.get(pid=pid)
2994 obj.delete()
2995 except Exception as exception:
2996 return HttpResponseServerError(exception)
2998 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
2999 return JsonResponse(data)
3002class ExtIdApiDetail(View):
3003 def get(self, request, *args, **kwargs):
3004 extid = get_object_or_404(
3005 ExtId,
3006 resource__pid=kwargs["pid"],
3007 id_type=kwargs["what"],
3008 )
3009 return JsonResponse(
3010 {
3011 "pk": extid.pk,
3012 "href": extid.get_href(),
3013 "fetch": reverse(
3014 "api-fetch-id",
3015 args=(
3016 extid.resource.pk,
3017 extid.id_value,
3018 extid.id_type,
3019 "extid",
3020 ),
3021 ),
3022 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3023 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3024 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3025 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3026 "is_valid": extid.checked,
3027 }
3028 )
3031class ExtIdFormTemplate(TemplateView):
3032 template_name = "common/externalid_form.html"
3034 def get_context_data(self, **kwargs):
3035 context = super().get_context_data(**kwargs)
3036 context["sequence"] = kwargs["sequence"]
3037 return context
3040class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3041 def get_context_data(self, **kwargs):
3042 context = super().get_context_data(**kwargs)
3043 context["helper"] = PtfFormHelper
3044 return context
3046 def get_success_url(self):
3047 self.post_process()
3048 return self.object.bibitem.resource.get_absolute_url()
3050 def post_process(self):
3051 cmd = updateBibitemCitationXmlCmd()
3052 cmd.set_bibitem(self.object.bibitem)
3053 cmd.do()
3054 model_helpers.post_resource_updated(self.object.bibitem.resource)
3057class BibItemIdCreate(BibItemIdFormView, CreateView):
3058 model = BibItemId
3059 form_class = BibItemIdForm
3061 def get_context_data(self, **kwargs):
3062 context = super().get_context_data(**kwargs)
3063 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3064 return context
3066 def get_initial(self):
3067 initial = super().get_initial()
3068 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3069 return initial
3071 def form_valid(self, form):
3072 form.instance.checked = False
3073 return super().form_valid(form)
3076class BibItemIdUpdate(BibItemIdFormView, UpdateView):
3077 model = BibItemId
3078 form_class = BibItemIdForm
3080 def get_context_data(self, **kwargs):
3081 context = super().get_context_data(**kwargs)
3082 context["bibitem"] = self.object.bibitem
3083 return context
3086class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3087 def get_context_data(self, **kwargs):
3088 context = super().get_context_data(**kwargs)
3089 context["helper"] = PtfFormHelper
3090 return context
3092 def get_success_url(self):
3093 self.post_process()
3094 return self.object.resource.get_absolute_url()
3096 def post_process(self):
3097 model_helpers.post_resource_updated(self.object.resource)
3100class ExtIdCreate(ExtIdFormView, CreateView):
3101 model = ExtId
3102 form_class = ExtIdForm
3104 def get_context_data(self, **kwargs):
3105 context = super().get_context_data(**kwargs)
3106 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3107 return context
3109 def get_initial(self):
3110 initial = super().get_initial()
3111 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3112 return initial
3114 def form_valid(self, form):
3115 form.instance.checked = False
3116 return super().form_valid(form)
3119class ExtIdUpdate(ExtIdFormView, UpdateView):
3120 model = ExtId
3121 form_class = ExtIdForm
3123 def get_context_data(self, **kwargs):
3124 context = super().get_context_data(**kwargs)
3125 context["resource"] = self.object.resource
3126 return context
3129class BibItemIdApiDetail(View):
3130 def get(self, request, *args, **kwargs):
3131 bibitemid = get_object_or_404(
3132 BibItemId,
3133 bibitem__resource__pid=kwargs["pid"],
3134 bibitem__sequence=kwargs["seq"],
3135 id_type=kwargs["what"],
3136 )
3137 return JsonResponse(
3138 {
3139 "pk": bibitemid.pk,
3140 "href": bibitemid.get_href(),
3141 "fetch": reverse(
3142 "api-fetch-id",
3143 args=(
3144 bibitemid.bibitem.pk,
3145 bibitemid.id_value,
3146 bibitemid.id_type,
3147 "bibitemid",
3148 ),
3149 ),
3150 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
3151 "uncheck": reverse(
3152 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
3153 ),
3154 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
3155 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
3156 "is_valid": bibitemid.checked,
3157 }
3158 )
3161class UpdateTexmfZipAPIView(View):
3162 def get(self, request, *args, **kwargs):
3163 def copy_zip_files(src_folder, dest_folder):
3164 os.makedirs(dest_folder, exist_ok=True)
3166 zip_files = [
3167 os.path.join(src_folder, f)
3168 for f in os.listdir(src_folder)
3169 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3170 ]
3171 for zip_file in zip_files:
3172 resolver.copy_file(zip_file, dest_folder)
3174 # Exceptions: specific zip/gz files
3175 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3176 resolver.copy_file(zip_file, dest_folder)
3178 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3179 resolver.copy_file(zip_file, dest_folder)
3181 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3182 resolver.copy_file(gz_file, dest_folder)
3184 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3186 dest_folder = os.path.join(
3187 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3188 )
3190 try:
3191 copy_zip_files(src_folder, dest_folder)
3192 except Exception as exception:
3193 return HttpResponseServerError(exception)
3195 try:
3196 dest_folder = os.path.join(
3197 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3198 )
3199 copy_zip_files(src_folder, dest_folder)
3200 except Exception as exception:
3201 return HttpResponseServerError(exception)
3203 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3204 return JsonResponse(data)
3207class TestView(TemplateView):
3208 template_name = "mersenne.html"
3210 def get_context_data(self, **kwargs):
3211 super().get_context_data(**kwargs)
3212 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True)
3213 model_data_converter.db_to_issue_data(issue)
3216class TrammelTasksProgressView(View):
3217 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3218 """
3219 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3220 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3221 """
3222 task_name = task
3224 def get_event_data():
3225 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3227 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3228 remaining_messages = get_messages_in_queue()
3230 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3231 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3232 failed_tasks = all_tasks.filter(status="FAILURE")
3234 all_tasks_count = all_tasks.count()
3235 success_count = successed_tasks.count()
3236 fail_count = failed_tasks.count()
3238 all_count = all_tasks_count + remaining_messages
3239 remaining_count = all_count - success_count - fail_count
3241 success_rate = int(success_count * 100 / all_count) if all_count else 0
3242 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3243 status = "consuming_queue" if remaining_count != 0 else "polling"
3245 last_task = successed_tasks.first()
3246 last_task = (
3247 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3248 if last_task
3249 else ""
3250 )
3252 # SSE event format
3253 event_data = {
3254 "status": status,
3255 "success_rate": success_rate,
3256 "error_rate": error_rate,
3257 "all_count": all_count,
3258 "remaining_count": remaining_count,
3259 "success_count": success_count,
3260 "fail_count": fail_count,
3261 "last_task": last_task,
3262 }
3264 return event_data
3266 def stream_response(data):
3267 # Send initial response headers
3268 yield f"data: {json.dumps(data)}\n\n"
3270 data = get_event_data()
3271 format = request.GET.get("format", "stream")
3272 if format == "json":
3273 response = JsonResponse(data)
3274 else:
3275 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3276 return response
3279user_signed_up.connect(update_user_from_invite)