Coverage for src / ptf_tools / views / base_views.py: 17%
1672 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-04-02 14:28 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-04-02 14:28 +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, tex, 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 Title,
65)
66from ptf_back.cmds.xml_cmds import updateBibitemCitationXmlCmd
67from ptf_back.locks import (
68 is_tex_conversion_locked,
69 release_tex_conversion_lock,
70)
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 tex.create_frontpage(colid, container, updated_articles, test=False)
1037 mersenneSite = model_helpers.get_site_mersenne(colid)
1038 # create or update deployed_date on container and articles
1039 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
1041 for art in articles_to_deploy:
1042 if art.doi and (art.date_published or art.date_online_first):
1043 if art.my_container.year is None:
1044 art.my_container.year = datetime.now().strftime("%Y")
1045 # BUG ? update the container but no save() ?
1047 file_.write(
1048 "Publication date of {} : Online First: {}, Published: {}\n".format(
1049 art.pid, art.date_online_first, art.date_published
1050 )
1051 )
1053 if article is None:
1054 resolver.copy_binary_files(
1055 container,
1056 settings.MERSENNE_TEST_DATA_FOLDER,
1057 settings.MERSENNE_PROD_DATA_FOLDER,
1058 )
1060 for art in articles_to_deploy:
1061 resolver.copy_binary_files(
1062 art,
1063 settings.MERSENNE_TEST_DATA_FOLDER,
1064 settings.MERSENNE_PROD_DATA_FOLDER,
1065 )
1067 elif site == "test_website":
1068 # create date_pre_published on articles without date_pre_published
1069 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
1070 cmd.set_resource(resource)
1071 updated_articles = cmd.do()
1073 tex.create_frontpage(colid, container, updated_articles)
1075 export_to_website = site == "website"
1077 if article is None:
1078 with_djvu = site == "numdam"
1079 xml = ptf_cmds.exportPtfCmd(
1080 {
1081 "pid": pid,
1082 "with_djvu": with_djvu,
1083 "export_to_website": export_to_website,
1084 }
1085 ).do()
1086 body = xml.encode("utf8")
1088 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1089 url = server_url + reverse("issue_upload")
1090 else:
1091 url = server_url + reverse("book_upload")
1093 # verify=False: ignore TLS certificate
1094 response = requests.post(url, data=body, verify=False)
1095 # response = requests.post(url, files=files, verify=False)
1096 else:
1097 xml = ptf_cmds.exportPtfCmd(
1098 {
1099 "pid": pid,
1100 "with_djvu": False,
1101 "article_standalone": True,
1102 "collection_pid": collection.pid,
1103 "export_to_website": export_to_website,
1104 "export_folder": settings.LOG_DIR,
1105 }
1106 ).do()
1107 # Unlike containers that send their XML as the body of the POST request,
1108 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1109 xml_file = io.StringIO(xml)
1110 files = {"xml": xml_file}
1112 url = server_url + reverse(
1113 "article_in_issue_upload", kwargs={"pid": container.pid}
1114 )
1115 # verify=False: ignore TLS certificate
1116 header = {}
1117 response = requests.post(url, headers=header, files=files, verify=False)
1119 status = response.status_code
1121 if 199 < status < 205:
1122 # There is no need to copy files for the test server
1123 # Files were already copied in /mersenne_test_data during the ptf_tools import
1124 # We only need to copy files from /mersenne_test_data to
1125 # /mersenne_prod_data during an upload to prod
1126 if site == "website":
1127 # TODO mettre ici le record doi pour un issue publié
1128 if container.doi:
1129 recordDOI(container)
1131 for art in articles_to_deploy:
1132 # record DOI automatically when deploying in prod
1134 if art.doi and art.allow_crossref():
1135 recordDOI(art)
1137 if colid == "CRBIOL":
1138 recordPubmed(
1139 art, force_update=False, updated_articles=updated_articles
1140 )
1142 if colid == "PCJ":
1143 self.update_pcj_editor(updated_articles)
1145 # Archive the container or the article
1146 if article is None:
1147 archive_resource.delay(
1148 pid,
1149 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1150 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1151 )
1153 else:
1154 archive_resource.delay(
1155 pid,
1156 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1157 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1158 article_doi=article.doi,
1159 )
1160 # cmd = ptf_cmds.archiveIssuePtfCmd({
1161 # "pid": pid,
1162 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1163 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1164 # cmd.set_article(article) # set_article allows archiving only the article
1165 # cmd.do()
1167 elif site == "numdam":
1168 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1169 if colid in settings.NUMDAM_COLLECTIONS:
1170 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1172 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1173 for article in container.article_set.all():
1174 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1176 elif status == 503:
1177 raise ServerUnderMaintenance(response.text)
1178 else:
1179 raise RuntimeError(response.text)
1181 if djvu_exception:
1182 raise djvu_exception
1184 def get(self, request, *args, **kwargs):
1185 pid = self.kwargs.get("pid", None)
1186 colid = self.kwargs.get("colid", None)
1187 site = self.kwargs.get("site", None)
1189 try:
1190 _, status, message = history_views.execute_and_record_func(
1191 "deploy", pid, colid, self.internal_do, site
1192 )
1193 except Timeout as exception:
1194 return HttpResponse(exception, status=408)
1195 except Exception as exception:
1196 return HttpResponseServerError(exception)
1198 data = {"message": message, "status": status}
1199 return JsonResponse(data)
1201 def update_pcj_editor(self, updated_articles):
1202 for article in updated_articles:
1203 data = {
1204 "date_published": article.date_published.strftime("%Y-%m-%d"),
1205 "article_number": article.article_number,
1206 }
1207 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1208 requests.post(url, json=data, verify=False)
1211class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1212 article = None
1214 def internal_do(self, *args, **kwargs):
1215 lang = self.kwargs.get("lang", None)
1217 translation = None
1218 for trans_article in self.article.translations.all():
1219 if trans_article.lang == lang:
1220 translation = trans_article
1222 if translation is None:
1223 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1225 collection = self.article.get_top_collection()
1226 colid = collection.pid
1227 container = self.article.my_container
1229 if translation.date_published is None:
1230 # Add date posted
1231 cmd = ptf_cmds.publishResourcePtfCmd()
1232 cmd.set_resource(translation)
1233 updated_articles = cmd.do()
1235 # Recompile PDF to add the date posted
1236 try:
1237 tex.create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1238 except Exception:
1239 raise PDFException(
1240 "Unable to compile the article PDF. Please contact the centre Mersenne"
1241 )
1243 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1244 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1245 resolver.copy_binary_files(
1246 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1247 )
1249 # Deploy in prod
1250 xml = ptf_cmds.exportPtfCmd(
1251 {
1252 "pid": self.article.pid,
1253 "with_djvu": False,
1254 "article_standalone": True,
1255 "collection_pid": colid,
1256 "export_to_website": True,
1257 "export_folder": settings.LOG_DIR,
1258 }
1259 ).do()
1260 xml_file = io.StringIO(xml)
1261 files = {"xml": xml_file}
1263 server_url = getattr(collection, "website")()
1264 if not server_url:
1265 raise RuntimeError("The collection has no website")
1266 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1267 header = {}
1269 try:
1270 response = requests.post(
1271 url, headers=header, files=files, verify=False
1272 ) # verify: ignore TLS certificate
1273 status = response.status_code
1274 except requests.exceptions.ConnectionError:
1275 raise ServerUnderMaintenance(
1276 "The journal is under maintenance. Please try again later."
1277 )
1279 # Register translation in Crossref
1280 if 199 < status < 205:
1281 if self.article.allow_crossref():
1282 try:
1283 recordDOI(translation)
1284 except Exception:
1285 raise DOIException(
1286 "Error while recording the DOI. Please contact the centre Mersenne"
1287 )
1289 def get(self, request, *args, **kwargs):
1290 doi = kwargs.get("doi", None)
1291 self.article = model_helpers.get_article_by_doi(doi)
1292 if self.article is None:
1293 raise Http404(f"{doi} does not exist")
1295 try:
1296 _, status, message = history_views.execute_and_record_func(
1297 "deploy",
1298 self.article.pid,
1299 self.article.get_top_collection().pid,
1300 self.internal_do,
1301 "website",
1302 )
1303 except Timeout as exception:
1304 return HttpResponse(exception, status=408)
1305 except Exception as exception:
1306 return HttpResponseServerError(exception)
1308 data = {"message": message, "status": status}
1309 return JsonResponse(data)
1312class DeleteJatsIssueAPIView(View):
1313 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1314 def get(self, request, *args, **kwargs):
1315 pid = self.kwargs.get("pid", None)
1316 colid = self.kwargs.get("colid", None)
1317 site = self.kwargs.get("site", None)
1318 message = "Le volume a bien été supprimé"
1319 status = 200
1321 issue = model_helpers.get_container(pid)
1322 if not issue:
1323 raise Http404(f"{pid} does not exist")
1324 try:
1325 mersenneSite = model_helpers.get_site_mersenne(colid)
1327 if site == "ptf_tools":
1328 if issue.is_deployed(mersenneSite):
1329 issue.undeploy(mersenneSite)
1330 for article in issue.article_set.all():
1331 article.undeploy(mersenneSite)
1333 p = model_helpers.get_provider("mathdoc-id")
1335 cmd = ptf_cmds.addContainerPtfCmd(
1336 {
1337 "pid": issue.pid,
1338 "ctype": "issue",
1339 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1340 }
1341 )
1342 cmd.set_provider(p)
1343 cmd.add_collection(issue.get_collection())
1344 cmd.set_object_to_be_deleted(issue)
1345 cmd.undo()
1347 else:
1348 if site == "numdam":
1349 server_url = settings.NUMDAM_PRE_URL
1350 else:
1351 collection = issue.get_collection()
1352 server_url = getattr(collection, site)()
1354 if not server_url:
1355 message = "The collection has no " + site
1356 status = 500
1357 else:
1358 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1359 response = requests.delete(url, verify=False)
1360 status = response.status_code
1362 if status == 404:
1363 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1364 elif status > 204:
1365 body = response.text.encode("utf8")
1366 message = body[:1000]
1367 else:
1368 status = 200
1369 # unpublish issue in collection site (site_register.json)
1370 if site == "website":
1371 if issue.is_deployed(mersenneSite):
1372 issue.undeploy(mersenneSite)
1373 for article in issue.article_set.all():
1374 article.undeploy(mersenneSite)
1375 # delete article binary files
1376 folder = article.get_relative_folder()
1377 resolver.delete_object_folder(
1378 folder,
1379 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1380 )
1381 # delete issue binary files
1382 folder = issue.get_relative_folder()
1383 resolver.delete_object_folder(
1384 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1385 )
1387 except Timeout as exception:
1388 return HttpResponse(exception, status=408)
1389 except Exception as exception:
1390 return HttpResponseServerError(exception)
1392 data = {"message": message, "status": status}
1393 return JsonResponse(data)
1396class ArchiveIssueAPIView(View):
1397 def get(self, request, *args, **kwargs):
1398 try:
1399 pid = kwargs["pid"]
1400 colid = kwargs["colid"]
1401 except IndexError:
1402 raise Http404
1404 try:
1405 cmd = ptf_cmds.archiveIssuePtfCmd(
1406 {
1407 "pid": pid,
1408 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1409 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1410 "needs_publication_date": True,
1411 }
1412 )
1413 result_, status, message = history_views.execute_and_record_func(
1414 "archive", pid, colid, cmd.do
1415 )
1416 except Exception as exception:
1417 return HttpResponseServerError(exception)
1419 data = {"message": message, "status": 200}
1420 return JsonResponse(data)
1423class CreateDjvuAPIView(View):
1424 def internal_do(self, *args, **kwargs):
1425 pid = self.kwargs.get("pid", None)
1427 resource = model_helpers.get_resource(pid)
1428 cmd = ptf_cmds.addDjvuPtfCmd()
1429 cmd.set_resource(resource)
1430 cmd.do()
1432 def get(self, request, *args, **kwargs):
1433 pid = self.kwargs.get("pid", None)
1434 colid = pid.split("_")[0]
1436 try:
1437 _, status, message = history_views.execute_and_record_func(
1438 "numdam", pid, colid, self.internal_do
1439 )
1440 except Exception as exception:
1441 return HttpResponseServerError(exception)
1443 data = {"message": message, "status": status}
1444 return JsonResponse(data)
1447class PTFToolsHomeView(LoginRequiredMixin, View):
1448 """
1449 Home Page.
1450 - Admin & staff -> Render blank home.html
1451 - User with unique authorized collection -> Redirect to collection details page
1452 - User with multiple authorized collections -> Render home.html with data
1453 - Comment moderator -> Comments dashboard
1454 - Others -> 404 response
1455 """
1457 def get(self, request, *args, **kwargs) -> HttpResponse:
1458 # Staff or user with authorized collections
1459 if request.user.is_staff or request.user.is_superuser:
1460 return render(request, "home.html")
1462 colids = get_authorized_collections(request.user)
1463 is_mod = is_comment_moderator(request.user)
1465 # The user has no rights
1466 if not (colids or is_mod):
1467 raise Http404("No collections associated with your account.")
1468 # Comment moderator only
1469 elif not colids:
1470 return HttpResponseRedirect(reverse("comment_list"))
1472 # User with unique collection -> Redirect to collection detail page
1473 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1474 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1476 # User with multiple authorized collections - Special home
1477 context = {}
1478 context["overview"] = True
1480 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1481 all_collections = {c["pid"]: c for c in all_collections}
1483 # Comments summary
1484 try:
1485 error, comments_data = get_comments_for_home(request.user)
1486 except AttributeError:
1487 error, comments_data = True, {}
1489 context["comment_server_ok"] = False
1491 if not error:
1492 context["comment_server_ok"] = True
1493 if comments_data:
1494 for col_id, comment_nb in comments_data.items():
1495 if col_id.upper() in all_collections: 1495 ↛ 1494line 1495 didn't jump to line 1494 because the condition on line 1495 was always true
1496 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1498 # TODO: Translations summary
1499 context["translation_server_ok"] = False
1501 # Sort the collections according to the number of pending comments
1502 context["collections"] = sorted(
1503 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1504 )
1506 return render(request, "home.html", context)
1509class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1510 columns = 5
1512 def get_common_context_data(self, **kwargs):
1513 context = super().get_context_data(**kwargs)
1514 now = timezone.now()
1515 curyear = now.year
1516 years = range(curyear - self.columns + 1, curyear + 1)
1518 context["collections"] = settings.MERSENNE_COLLECTIONS
1519 context["containers_to_be_published"] = []
1520 context["last_col_events"] = []
1522 event = get_history_last_event_by("clockss", "ALL")
1523 clockss_gap = get_gap(now, event)
1525 context["years"] = years
1526 context["clockss_gap"] = clockss_gap
1528 return context
1530 def calculate_articles_and_pages(self, pid, years):
1531 data_by_year = []
1532 total_articles = [0] * len(years)
1533 total_pages = [0] * len(years)
1535 for year in years:
1536 articles = self.get_articles_for_year(pid, year)
1537 articles_count = articles.count()
1538 page_count = sum(article.get_article_page_count() for article in articles)
1540 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1541 total_articles[year - years[0]] += articles_count
1542 total_pages[year - years[0]] += page_count
1544 return data_by_year, total_articles, total_pages
1546 def get_articles_for_year(self, pid, year):
1547 return Article.objects.filter(
1548 Q(my_container__my_collection__pid=pid)
1549 & (
1550 Q(date_published__year=year, date_online_first__isnull=True)
1551 | Q(date_online_first__year=year)
1552 )
1553 ).prefetch_related("resourcecount_set")
1556class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1557 template_name = "dashboard/published_articles.html"
1559 def get_context_data(self, **kwargs):
1560 context = self.get_common_context_data(**kwargs)
1561 years = context["years"]
1563 published_articles = []
1564 total_published_articles = [
1565 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1566 ]
1568 for pid in settings.MERSENNE_COLLECTIONS:
1569 if pid != "MERSENNE":
1570 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1571 pid, years
1572 )
1573 published_articles.append({"pid": pid, "years": articles_data})
1575 for i, year in enumerate(years):
1576 total_published_articles[i]["total_articles"] += total_articles[i]
1577 total_published_articles[i]["total_pages"] += total_pages[i]
1579 context["published_articles"] = published_articles
1580 context["total_published_articles"] = total_published_articles
1582 return context
1585class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1586 template_name = "dashboard/created_volumes.html"
1588 def get_context_data(self, **kwargs):
1589 context = self.get_common_context_data(**kwargs)
1590 years = context["years"]
1592 created_volumes = []
1593 total_created_volumes = [
1594 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1595 ]
1597 for pid in settings.MERSENNE_COLLECTIONS:
1598 if pid != "MERSENNE":
1599 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1600 pid, years
1601 )
1602 created_volumes.append({"pid": pid, "years": volumes_data})
1604 for i, year in enumerate(years):
1605 total_created_volumes[i]["total_articles"] += total_articles[i]
1606 total_created_volumes[i]["total_pages"] += total_pages[i]
1608 context["created_volumes"] = created_volumes
1609 context["total_created_volumes"] = total_created_volumes
1611 return context
1613 def calculate_volumes_and_pages(self, pid, years):
1614 data_by_year = []
1615 total_articles = [0] * len(years)
1616 total_pages = [0] * len(years)
1618 for year in years:
1619 issues = Container.objects.filter(my_collection__pid=pid, year=year)
1620 articles_count = 0
1621 page_count = 0
1623 for issue in issues:
1624 articles = issue.article_set.filter(
1625 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1626 ).prefetch_related("resourcecount_set")
1628 articles_count += articles.count()
1629 page_count += sum(article.get_article_page_count() for article in articles)
1631 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1632 total_articles[year - years[0]] += articles_count
1633 total_pages[year - years[0]] += page_count
1635 return data_by_year, total_articles, total_pages
1638class ReferencingChoice(View):
1639 def post(self, request, *args, **kwargs):
1640 if request.POST.get("optSite") == "ads":
1641 return redirect(
1642 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1643 )
1644 elif request.POST.get("optSite") == "wos":
1645 comp = ReferencingCheckerWos()
1646 journal = comp.make_journal(request.POST.get("selectCol"))
1647 if journal is None:
1648 return render(
1649 request,
1650 "dashboard/referencing.html",
1651 {
1652 "error": "Collection not found",
1653 "colid": request.POST.get("selectCol"),
1654 "optSite": request.POST.get("optSite"),
1655 },
1656 )
1657 return render(
1658 request,
1659 "dashboard/referencing.html",
1660 {
1661 "journal": journal,
1662 "colid": request.POST.get("selectCol"),
1663 "optSite": request.POST.get("optSite"),
1664 },
1665 )
1668class ReferencingWosFileView(View):
1669 template_name = "dashboard/referencing.html"
1671 def post(self, request, *args, **kwargs):
1672 colid = request.POST["colid"]
1673 if request.FILES.get("risfile") is None:
1674 message = "No file uploaded"
1675 return render(
1676 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1677 )
1678 uploaded_file = request.FILES["risfile"]
1679 comp = ReferencingCheckerWos()
1680 journal = comp.check_references(colid, uploaded_file)
1681 return render(request, self.template_name, {"journal": journal})
1684class ReferencingDashboardView(BaseMersenneDashboardView):
1685 template_name = "dashboard/referencing.html"
1687 def get(self, request, *args, **kwargs):
1688 colid = self.kwargs.get("colid", None)
1689 comp = ReferencingCheckerAds()
1690 journal = comp.check_references(colid)
1691 return render(request, self.template_name, {"journal": journal})
1694class BaseCollectionView(TemplateView):
1695 def get_context_data(self, **kwargs):
1696 context = super().get_context_data(**kwargs)
1697 aid = context.get("aid")
1698 year = context.get("year")
1700 if aid and year:
1701 context["collection"] = self.get_collection(aid, year)
1703 return context
1705 def get_collection(self, aid, year):
1706 """Method to be overridden by subclasses to fetch the appropriate collection"""
1707 raise NotImplementedError("Subclasses must implement get_collection method")
1710class ArticleListView(BaseCollectionView):
1711 template_name = "collection-list.html"
1713 def get_collection(self, aid, year):
1714 return Article.objects.filter(
1715 Q(my_container__my_collection__pid=aid)
1716 & (
1717 Q(date_published__year=year, date_online_first__isnull=True)
1718 | Q(date_online_first__year=year)
1719 )
1720 ).prefetch_related("resourcecount_set")
1723class VolumeListView(BaseCollectionView):
1724 template_name = "collection-list.html"
1726 def get_collection(self, aid, year):
1727 return Article.objects.filter(
1728 Q(my_container__my_collection__pid=aid, my_container__year=year)
1729 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1730 ).prefetch_related("resourcecount_set")
1733class DOAJResourceRegisterView(View):
1734 def get(self, request, *args, **kwargs):
1735 pid = kwargs.get("pid", None)
1736 resource = model_helpers.get_resource(pid)
1737 if resource is None:
1738 raise Http404
1739 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1740 resource.colid, None
1741 ):
1742 raise RuntimeError("Pending publications should not be deployed")
1744 try:
1745 data = {}
1746 doaj_meta, response = doaj_pid_register(pid)
1747 if response is None:
1748 return HttpResponse(status=204)
1749 elif doaj_meta and 200 <= response.status_code <= 299:
1750 data.update(doaj_meta)
1751 else:
1752 return HttpResponse(status=response.status_code, reason=response.text)
1753 except Timeout as exception:
1754 return HttpResponse(exception, status=408)
1755 except Exception as exception:
1756 return HttpResponseServerError(exception)
1757 return JsonResponse(data)
1760class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View):
1761 """
1762 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml
1763 """
1765 def get(self, request, *args, **kwargs):
1766 pid = kwargs.get("pid")
1767 if not pid:
1768 raise Http404("Missing pid")
1770 article = Article.objects.filter(pid=pid).first()
1771 if not article:
1772 raise Http404(f"Article not found: {pid}")
1774 colid = article.get_collection().pid
1775 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS:
1776 return JsonResponse(
1777 {"status": 403, "message": f"Tex conversions are disabled in {colid}"}
1778 )
1780 if is_tex_conversion_locked(pid):
1781 logger.warning("Conversion rejected (lock exists) for %s", pid)
1782 return JsonResponse(
1783 {"status": 409, "message": f"A conversion is already running for {pid}"}
1784 )
1786 logger.info("No lock → scheduling conversion for %s", pid)
1788 try:
1789 convert_article_tex.delay(pid=pid, user_pk=request.user.pk)
1790 except Exception:
1791 logger.exception("Failed to enqueue task for %s", pid)
1792 release_tex_conversion_lock(pid)
1793 raise
1795 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"})
1798class CROSSREFResourceRegisterView(View):
1799 def get(self, request, *args, **kwargs):
1800 pid = kwargs.get("pid", None)
1801 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1802 force = kwargs.get("force", None)
1803 if not request.user.is_superuser:
1804 force = None
1806 resource = model_helpers.get_resource(pid)
1807 if resource is None:
1808 raise Http404
1810 resource = resource.cast()
1811 meth = getattr(self, "recordDOI" + resource.classname)
1812 try:
1813 data = meth(resource, force)
1814 except Timeout as exception:
1815 return HttpResponse(exception, status=408)
1816 except Exception as exception:
1817 return HttpResponseServerError(exception)
1818 return JsonResponse(data)
1820 def recordDOIArticle(self, article, force=None):
1821 result = {"status": 404}
1822 if (
1823 article.doi
1824 and not article.do_not_publish
1825 and (article.date_published or article.date_online_first or force == "force")
1826 ):
1827 if article.my_container.year is None: # or article.my_container.year == '0':
1828 article.my_container.year = datetime.now().strftime("%Y")
1829 result = recordDOI(article)
1830 return result
1832 def recordDOICollection(self, collection, force=None):
1833 return recordDOI(collection)
1835 def recordDOIContainer(self, container, force=None):
1836 data = {"status": 200, "message": "tout va bien"}
1838 if container.ctype == "issue":
1839 if container.doi:
1840 result = recordDOI(container)
1841 if result["status"] != 200:
1842 return result
1843 if force == "force":
1844 articles = container.article_set.exclude(
1845 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1846 )
1847 else:
1848 articles = container.article_set.exclude(
1849 doi__isnull=True,
1850 do_not_publish=True,
1851 date_published__isnull=True,
1852 date_online_first__isnull=True,
1853 )
1855 for article in articles:
1856 result = self.recordDOIArticle(article, force)
1857 if result["status"] != 200:
1858 data = result
1859 else:
1860 return recordDOI(container)
1861 return data
1864class CROSSREFResourceCheckStatusView(View):
1865 def get(self, request, *args, **kwargs):
1866 pid = kwargs.get("pid", None)
1867 resource = model_helpers.get_resource(pid)
1868 if resource is None:
1869 raise Http404
1870 resource = resource.cast()
1871 meth = getattr(self, "checkDOI" + resource.classname)
1872 try:
1873 meth(resource)
1874 except Timeout as exception:
1875 return HttpResponse(exception, status=408)
1876 except Exception as exception:
1877 return HttpResponseServerError(exception)
1879 data = {"status": 200, "message": "tout va bien"}
1880 return JsonResponse(data)
1882 def checkDOIArticle(self, article):
1883 if article.my_container.year is None or article.my_container.year == "0":
1884 article.my_container.year = datetime.now().strftime("%Y")
1885 checkDOI(article)
1887 def checkDOICollection(self, collection):
1888 checkDOI(collection)
1890 def checkDOIContainer(self, container):
1891 if container.doi is not None:
1892 checkDOI(container)
1893 for article in container.article_set.all():
1894 self.checkDOIArticle(article)
1897class CROSSREFResourcePendingPublicationRegisterView(View):
1898 def get(self, request, *args, **kwargs):
1899 pid = kwargs.get("pid", None)
1900 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1902 resource = model_helpers.get_resource(pid)
1903 if resource is None:
1904 raise Http404
1906 resource = resource.cast()
1907 meth = getattr(self, "recordPendingPublication" + resource.classname)
1908 try:
1909 data = meth(resource)
1910 except Timeout as exception:
1911 return HttpResponse(exception, status=408)
1912 except Exception as exception:
1913 return HttpResponseServerError(exception)
1914 return JsonResponse(data)
1916 def recordPendingPublicationArticle(self, article):
1917 result = {"status": 404}
1918 if article.doi and not article.date_published and not article.date_online_first:
1919 if article.my_container.year is None: # or article.my_container.year == '0':
1920 article.my_container.year = datetime.now().strftime("%Y")
1921 result = recordPendingPublication(article)
1922 return result
1925class RegisterPubmedFormView(FormView):
1926 template_name = "record_pubmed_dialog.html"
1927 form_class = RegisterPubmedForm
1929 def get_context_data(self, **kwargs):
1930 context = super().get_context_data(**kwargs)
1931 context["pid"] = self.kwargs["pid"]
1932 context["helper"] = PtfLargeModalFormHelper
1933 return context
1936class RegisterPubmedView(View):
1937 def get(self, request, *args, **kwargs):
1938 pid = kwargs.get("pid", None)
1939 update_article = self.request.GET.get("update_article", "on") == "on"
1941 article = model_helpers.get_article(pid)
1942 if article is None:
1943 raise Http404
1944 try:
1945 recordPubmed(article, update_article)
1946 except Exception as exception:
1947 messages.error("Unable to register the article in PubMed")
1948 return HttpResponseServerError(exception)
1950 return HttpResponseRedirect(
1951 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1952 )
1955class PTFToolsContainerView(TemplateView):
1956 template_name = ""
1958 def get_context_data(self, **kwargs):
1959 context = super().get_context_data(**kwargs)
1961 container = model_helpers.get_container(self.kwargs.get("pid"))
1962 if container is None:
1963 raise Http404
1964 citing_articles = container.citations()
1965 source = self.request.GET.get("source", None)
1966 if container.ctype.startswith("book"):
1967 book_parts = (
1968 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1969 )
1970 references = False
1971 if container.ctype == "book-monograph":
1972 # on regarde si il y a au moins une bibliographie
1973 for art in container.article_set.all():
1974 if art.bibitem_set.count() > 0:
1975 references = True
1976 context.update(
1977 {
1978 "book": container,
1979 "book_parts": list(book_parts),
1980 "source": source,
1981 "citing_articles": citing_articles,
1982 "references": references,
1983 "test_website": container.get_top_collection()
1984 .extlink_set.get(rel="test_website")
1985 .location,
1986 "prod_website": container.get_top_collection()
1987 .extlink_set.get(rel="website")
1988 .location,
1989 }
1990 )
1991 self.template_name = "book-toc.html"
1992 else:
1993 articles = container.article_set.all().order_by("seq")
1994 for article in articles:
1995 try:
1996 last_match = (
1997 history_models.HistoryEvent.objects.filter(
1998 pid=article.pid,
1999 type="matching",
2000 )
2001 .only("created_on")
2002 .latest("created_on")
2003 )
2004 except history_models.HistoryEvent.DoesNotExist as _:
2005 article.last_match = None
2006 else:
2007 article.last_match = last_match.created_on
2009 # article1 = articles.first()
2010 # date = article1.deployed_date()
2011 # TODO next_issue, previous_issue
2013 # check DOI est maintenant une commande à part
2014 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
2015 # articlesWithStatus = []
2016 # for article in articles:
2017 # checkDOIExistence(article)
2018 # articlesWithStatus.append(article)
2020 test_location = prod_location = ""
2021 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
2022 if qs:
2023 test_location = qs.first().location
2024 qs = container.get_top_collection().extlink_set.filter(rel="website")
2025 if qs:
2026 prod_location = qs.first().location
2027 context.update(
2028 {
2029 "issue": container,
2030 "articles": articles,
2031 "source": source,
2032 "citing_articles": citing_articles,
2033 "test_website": test_location,
2034 "prod_website": prod_location,
2035 }
2036 )
2038 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
2039 context["is_issue_pending_publication"] = True
2040 self.template_name = "issue-items.html"
2042 context["allow_crossref"] = container.allow_crossref()
2043 context["coltype"] = container.my_collection.coltype
2044 return context
2047class ExtLinkInline(InlineFormSetFactory):
2048 model = ExtLink
2049 form_class = ExtLinkForm
2050 factory_kwargs = {"extra": 0}
2053class ResourceIdInline(InlineFormSetFactory):
2054 model = ResourceId
2055 form_class = ResourceIdForm
2056 factory_kwargs = {"extra": 0}
2059class IssueDetailAPIView(View):
2060 def get(self, request, *args, **kwargs):
2061 issue = get_object_or_404(Container, pid=kwargs["pid"])
2062 deployed_date = issue.deployed_date()
2063 result = {
2064 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2065 if deployed_date
2066 else None,
2067 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2068 "all_doi_are_registered": issue.all_doi_are_registered(),
2069 "registered_in_doaj": issue.registered_in_doaj(),
2070 "doi": issue.my_collection.doi,
2071 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2072 }
2073 try:
2074 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2075 except history_models.HistoryEvent.DoesNotExist as _:
2076 pass
2077 else:
2078 result["latest"] = latest.message
2079 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2080 "%Y-%m-%d %H:%M"
2081 )
2083 result["latest_type"] = latest.type.capitalize()
2084 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2085 try:
2086 result[event_type] = timezone.localtime(
2087 history_models.HistoryEvent.objects.filter(
2088 type=event_type,
2089 status="OK",
2090 pid__startswith=issue.pid,
2091 )
2092 .latest("created_on")
2093 .created_on
2094 ).strftime("%Y-%m-%d %H:%M")
2095 except history_models.HistoryEvent.DoesNotExist as _:
2096 result[event_type] = ""
2097 return JsonResponse(result)
2100class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2101 model = Collection
2102 form_class = CollectionForm
2103 inlines = [ResourceIdInline, ExtLinkInline]
2104 inlines_names = ["resource_ids_form", "ext_links_form"]
2106 def get_context_data(self, **kwargs):
2107 context = super().get_context_data(**kwargs)
2108 context["helper"] = PtfFormHelper
2109 context["formset_helper"] = FormSetHelper
2110 return context
2112 def add_description(self, collection, description, lang, seq):
2113 if description:
2114 la = Abstract(
2115 resource=collection,
2116 tag="description",
2117 lang=lang,
2118 seq=seq,
2119 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2120 value_html=description,
2121 value_tex=description,
2122 )
2123 la.save()
2125 def form_valid(self, form):
2126 if form.instance.abbrev:
2127 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2128 else:
2129 form.instance.title_xml = (
2130 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2131 )
2133 form.instance.title_html = form.instance.title_tex
2134 form.instance.title_sort = form.instance.title_tex
2135 result = super().form_valid(form)
2137 collection = self.object
2138 collection.abstract_set.all().delete()
2140 seq = 1
2141 description = form.cleaned_data["description_en"]
2142 if description:
2143 self.add_description(collection, description, "en", seq)
2144 seq += 1
2145 description = form.cleaned_data["description_fr"]
2146 if description:
2147 self.add_description(collection, description, "fr", seq)
2149 return result
2151 def get_success_url(self):
2152 messages.success(self.request, "La Collection a été modifiée avec succès")
2153 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2156class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2157 """
2158 Warning : Not yet finished
2159 Automatic site membership creation is still missing
2160 """
2163class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2164 slug_field = "pid"
2165 slug_url_kwarg = "pid"
2168def suggest_load_journal_dois(colid):
2169 articles = (
2170 Article.objects.filter(my_container__my_collection__pid=colid)
2171 .filter(doi__isnull=False)
2172 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2173 .values_list("doi", flat=True)
2174 )
2176 try:
2177 articles = sorted(
2178 articles,
2179 key=lambda d: (
2180 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2181 int(re.search(r".(\d+)$", d).group(1)),
2182 ),
2183 )
2184 except: # noqa: E722 (we'll look later)
2185 pass
2186 return [f'<option value="{doi}">' for doi in articles]
2189def get_context_with_volumes(journal):
2190 result = model_helpers.get_volumes_in_collection(journal)
2191 volume_count = result["volume_count"]
2192 collections = []
2193 for ancestor in journal.ancestors.all():
2194 item = model_helpers.get_volumes_in_collection(ancestor)
2195 volume_count = max(0, volume_count)
2196 item.update({"journal": ancestor})
2197 collections.append(item)
2199 # add the parent collection to its children list and sort it by date
2200 result.update({"journal": journal})
2201 collections.append(result)
2203 collections = [c for c in collections if c["sorted_issues"]]
2204 collections.sort(
2205 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2206 reverse=True,
2207 )
2209 context = {
2210 "journal": journal,
2211 "sorted_issues": result["sorted_issues"],
2212 "volume_count": volume_count,
2213 "max_width": result["max_width"],
2214 "collections": collections,
2215 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2216 }
2217 return context
2220class CollectionDetail(
2221 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2222):
2223 model = Collection
2224 slug_field = "pid"
2225 slug_url_kwarg = "pid"
2226 template_name = "ptf/collection_detail.html"
2228 def test_func(self):
2229 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2231 def get(self, request, *args, **kwargs):
2232 self.object = self.get_object(queryset=Collection.objects.all())
2233 return super().get(request, *args, **kwargs)
2235 def get_context_data(self, **kwargs):
2236 context = super().get_context_data(**kwargs)
2237 context["object_list"] = context["object_list"].filter(
2238 Q(ctype="issue") | Q(ctype="book-lecture-notes")
2239 )
2240 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2241 context.update(get_context_with_volumes(self.object))
2243 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2244 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2245 context["issue_to_appear"] = Container.objects.filter(
2246 pid=context["issue_to_appear_pid"]
2247 ).exists()
2248 try:
2249 latest_error = history_models.HistoryEvent.objects.filter(
2250 status="ERROR", col=self.object
2251 ).latest("created_on")
2252 except history_models.HistoryEvent.DoesNotExist as _:
2253 pass
2254 else:
2255 message = latest_error.message
2256 if message:
2257 i = message.find(" - ")
2258 latest_exception = message[:i]
2259 latest_error_message = message[i + 3 :]
2260 context["latest_exception"] = latest_exception
2261 context["latest_exception_date"] = latest_error.created_on
2262 context["latest_exception_type"] = latest_error.type
2263 context["latest_error_message"] = latest_error_message
2265 archive_in_error = history_models.HistoryEvent.objects.filter(
2266 status="ERROR", col=self.object, type="archive"
2267 ).exists()
2269 context["archive_in_error"] = archive_in_error
2271 return context
2273 def get_queryset(self):
2274 query = self.object.content.all()
2276 for ancestor in self.object.ancestors.all():
2277 query |= ancestor.content.all()
2279 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2282class ContainerEditView(FormView):
2283 template_name = "container_form.html"
2284 form_class = ContainerForm
2286 def get_success_url(self):
2287 if self.kwargs["pid"]:
2288 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2289 return reverse("mersenne_dashboard/published_articles")
2291 def set_success_message(self): # pylint: disable=no-self-use
2292 messages.success(self.request, "Le fascicule a été modifié")
2294 def get_form_kwargs(self):
2295 kwargs = super().get_form_kwargs()
2296 if "pid" not in self.kwargs:
2297 self.kwargs["pid"] = None
2298 if "colid" not in self.kwargs:
2299 self.kwargs["colid"] = None
2300 if "data" in kwargs and "colid" in kwargs["data"]:
2301 # colid is passed as a hidden param in the form.
2302 # It is used when you submit a new container
2303 self.kwargs["colid"] = kwargs["data"]["colid"]
2305 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2306 self.kwargs["pid"]
2307 )
2308 return kwargs
2310 def get_context_data(self, **kwargs):
2311 context = super().get_context_data(**kwargs)
2313 context["pid"] = self.kwargs["pid"]
2314 context["colid"] = self.kwargs["colid"]
2315 context["container"] = self.kwargs["container"]
2317 context["edit_container"] = context["pid"] is not None
2318 context["name"] = resolve(self.request.path_info).url_name
2320 return context
2322 def form_valid(self, form):
2323 new_pid = form.cleaned_data.get("pid")
2324 new_title = form.cleaned_data.get("title")
2325 new_trans_title = form.cleaned_data.get("trans_title")
2326 new_publisher = form.cleaned_data.get("publisher")
2327 new_year = form.cleaned_data.get("year")
2328 new_volume = form.cleaned_data.get("volume")
2329 new_number = form.cleaned_data.get("number")
2331 collection = None
2332 issue = self.kwargs["container"]
2333 if issue is not None:
2334 collection = issue.my_collection
2335 elif self.kwargs["colid"] is not None:
2336 if "CR" in self.kwargs["colid"]:
2337 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2338 else:
2339 collection = model_helpers.get_collection(self.kwargs["colid"])
2341 if collection is None:
2342 raise ValueError("Collection for " + new_pid + " does not exist")
2344 # Icon
2345 new_icon_location = ""
2346 if "icon" in self.request.FILES:
2347 filename = os.path.basename(self.request.FILES["icon"].name)
2348 file_extension = filename.split(".")[1]
2350 icon_filename = resolver.get_disk_location(
2351 settings.MERSENNE_TEST_DATA_FOLDER,
2352 collection.pid,
2353 file_extension,
2354 new_pid,
2355 None,
2356 True,
2357 )
2359 with open(icon_filename, "wb+") as destination:
2360 for chunk in self.request.FILES["icon"].chunks():
2361 destination.write(chunk)
2363 folder = resolver.get_relative_folder(collection.pid, new_pid)
2364 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2365 name = resolve(self.request.path_info).url_name
2366 if name == "special_issue_create":
2367 self.kwargs["name"] = name
2368 if self.kwargs["container"]:
2369 # Edit Issue
2370 issue = self.kwargs["container"]
2371 if issue is None:
2372 raise ValueError(self.kwargs["pid"] + " does not exist")
2374 issue.pid = new_pid
2375 issue.title_tex = issue.title_html = new_title
2376 issue.title_xml = build_title_xml(
2377 title=new_title,
2378 lang=issue.lang,
2379 title_type="issue-title",
2380 )
2382 trans_lang = ""
2383 if issue.trans_lang != "" and issue.trans_lang != "und":
2384 trans_lang = issue.trans_lang
2385 elif new_trans_title != "":
2386 trans_lang = "fr" if issue.lang == "en" else "en"
2387 issue.trans_lang = trans_lang
2389 if trans_lang != "" and new_trans_title != "":
2390 issue.trans_title_html = ""
2391 issue.trans_title_tex = ""
2392 title_xml = build_title_xml(
2393 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2394 )
2395 try:
2396 trans_title_object = Title.objects.get(resource=issue, lang=trans_lang)
2397 trans_title_object.title_html = new_trans_title
2398 trans_title_object.title_xml = title_xml
2399 trans_title_object.save()
2400 except Title.DoesNotExist:
2401 trans_title = Title(
2402 resource=issue,
2403 lang=trans_lang,
2404 type="main",
2405 title_html=new_trans_title,
2406 title_xml=title_xml,
2407 )
2408 trans_title.save()
2409 issue.year = new_year
2410 issue.volume = new_volume
2411 issue.volume_int = make_int(new_volume)
2412 issue.number = new_number
2413 issue.number_int = make_int(new_number)
2414 issue.save()
2415 else:
2416 xissue = create_issuedata()
2418 xissue.ctype = "issue"
2419 xissue.pid = new_pid
2420 xissue.lang = "en"
2421 xissue.title_tex = new_title
2422 xissue.title_html = new_title
2423 xissue.title_xml = build_title_xml(
2424 title=new_title, lang=xissue.lang, title_type="issue-title"
2425 )
2427 if new_trans_title != "":
2428 trans_lang = "fr"
2429 title_xml = build_title_xml(
2430 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2431 )
2432 title = create_titledata(
2433 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2434 )
2435 issue.titles = [title]
2437 xissue.year = new_year
2438 xissue.volume = new_volume
2439 xissue.number = new_number
2440 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2442 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2443 cmd.add_collection(collection)
2444 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2445 issue = cmd.do()
2447 self.kwargs["pid"] = new_pid
2449 # Add objects related to the article: contribs, datastream, counts...
2450 params = {
2451 "icon_location": new_icon_location,
2452 }
2453 cmd = ptf_cmds.updateContainerPtfCmd(params)
2454 cmd.set_resource(issue)
2455 cmd.do()
2457 publisher = model_helpers.get_publisher(new_publisher)
2458 if not publisher:
2459 xpub = create_publisherdata()
2460 xpub.name = new_publisher
2461 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2462 issue.my_publisher = publisher
2463 issue.save()
2465 self.set_success_message()
2467 return super().form_valid(form)
2470# class ArticleEditView(FormView):
2471# template_name = 'article_form.html'
2472# form_class = ArticleForm
2473#
2474# def get_success_url(self):
2475# if self.kwargs['pid']:
2476# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2477# return reverse('mersenne_dashboard/published_articles')
2478#
2479# def set_success_message(self): # pylint: disable=no-self-use
2480# messages.success(self.request, "L'article a été modifié")
2481#
2482# def get_form_kwargs(self):
2483# kwargs = super(ArticleEditView, self).get_form_kwargs()
2484#
2485# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2486# # Article creation: pid is None
2487# self.kwargs['pid'] = None
2488# if 'issue_id' not in self.kwargs:
2489# # Article edit: issue_id is not passed
2490# self.kwargs['issue_id'] = None
2491# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2492# # colid is passed as a hidden param in the form.
2493# # It is used when you submit a new container
2494# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2495#
2496# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2497# return kwargs
2498#
2499# def get_context_data(self, **kwargs):
2500# context = super(ArticleEditView, self).get_context_data(**kwargs)
2501#
2502# context['pid'] = self.kwargs['pid']
2503# context['issue_id'] = self.kwargs['issue_id']
2504# context['article'] = self.kwargs['article']
2505#
2506# context['edit_article'] = context['pid'] is not None
2507#
2508# article = context['article']
2509# if article:
2510# context['author_contributions'] = article.get_author_contributions()
2511# context['kwds_fr'] = None
2512# context['kwds_en'] = None
2513# kwd_gps = article.get_non_msc_kwds()
2514# for kwd_gp in kwd_gps:
2515# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2516# if kwd_gp.value_xml:
2517# kwd_ = types.SimpleNamespace()
2518# kwd_.value = kwd_gp.value_tex
2519# context['kwd_unstructured_fr'] = kwd_
2520# context['kwds_fr'] = kwd_gp.kwd_set.all()
2521# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2522# if kwd_gp.value_xml:
2523# kwd_ = types.SimpleNamespace()
2524# kwd_.value = kwd_gp.value_tex
2525# context['kwd_unstructured_en'] = kwd_
2526# context['kwds_en'] = kwd_gp.kwd_set.all()
2527#
2528# # Article creation: init pid
2529# if context['issue_id'] and context['pid'] is None:
2530# issue = model_helpers.get_container(context['issue_id'])
2531# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2532#
2533# return context
2534#
2535# def form_valid(self, form):
2536#
2537# new_pid = form.cleaned_data.get('pid')
2538# new_title = form.cleaned_data.get('title')
2539# new_fpage = form.cleaned_data.get('fpage')
2540# new_lpage = form.cleaned_data.get('lpage')
2541# new_page_range = form.cleaned_data.get('page_range')
2542# new_page_count = form.cleaned_data.get('page_count')
2543# new_coi_statement = form.cleaned_data.get('coi_statement')
2544# new_show_body = form.cleaned_data.get('show_body')
2545# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2546#
2547# # TODO support MathML
2548# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2549# # We need to pass trans_title to get_title_xml
2550# # Meanwhile, ignore new_title_xml
2551# new_title_xml = jats_parser.get_title_xml(new_title)
2552# new_title_html = new_title
2553#
2554# authors_count = int(self.request.POST.get('authors_count', "0"))
2555# i = 1
2556# new_authors = []
2557# old_author_contributions = []
2558# if self.kwargs['article']:
2559# old_author_contributions = self.kwargs['article'].get_author_contributions()
2560#
2561# while authors_count > 0:
2562# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2563#
2564# if prefix is not None:
2565# addresses = []
2566# if len(old_author_contributions) >= i:
2567# old_author_contribution = old_author_contributions[i - 1]
2568# addresses = [contrib_address.address for contrib_address in
2569# old_author_contribution.get_addresses()]
2570#
2571# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2572# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2573# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2574# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2575# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2576# deceased_before_publication = deceased == 'on'
2577# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2578# equal_contrib = equal_contrib == 'on'
2579# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2580# corresponding = corresponding == 'on'
2581# email = self.request.POST.get('email-' + str(i), None)
2582#
2583# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2584# params['deceased_before_publication'] = deceased_before_publication
2585# params['equal_contrib'] = equal_contrib
2586# params['corresponding'] = corresponding
2587# params['addresses'] = addresses
2588# params['email'] = email
2589#
2590# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2591#
2592# new_authors.append(params)
2593#
2594# authors_count -= 1
2595# i += 1
2596#
2597# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2598# i = 1
2599# new_kwds_fr = []
2600# while kwds_fr_count > 0:
2601# value = self.request.POST.get('kwd-fr-' + str(i), None)
2602# new_kwds_fr.append(value)
2603# kwds_fr_count -= 1
2604# i += 1
2605# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2606#
2607# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2608# i = 1
2609# new_kwds_en = []
2610# while kwds_en_count > 0:
2611# value = self.request.POST.get('kwd-en-' + str(i), None)
2612# new_kwds_en.append(value)
2613# kwds_en_count -= 1
2614# i += 1
2615# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2616#
2617# if self.kwargs['article']:
2618# # Edit article
2619# container = self.kwargs['article'].my_container
2620# else:
2621# # New article
2622# container = model_helpers.get_container(self.kwargs['issue_id'])
2623#
2624# if container is None:
2625# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2626#
2627# collection = container.my_collection
2628#
2629# # Copy PDF file & extract full text
2630# body = ''
2631# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2632# collection.pid,
2633# "pdf",
2634# container.pid,
2635# new_pid,
2636# True)
2637# if 'pdf' in self.request.FILES:
2638# with open(pdf_filename, 'wb+') as destination:
2639# for chunk in self.request.FILES['pdf'].chunks():
2640# destination.write(chunk)
2641#
2642# # Extract full text from the PDF
2643# body = utils.pdf_to_text(pdf_filename)
2644#
2645# # Icon
2646# new_icon_location = ''
2647# if 'icon' in self.request.FILES:
2648# filename = os.path.basename(self.request.FILES['icon'].name)
2649# file_extension = filename.split('.')[1]
2650#
2651# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2652# collection.pid,
2653# file_extension,
2654# container.pid,
2655# new_pid,
2656# True)
2657#
2658# with open(icon_filename, 'wb+') as destination:
2659# for chunk in self.request.FILES['icon'].chunks():
2660# destination.write(chunk)
2661#
2662# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2663# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2664#
2665# if self.kwargs['article']:
2666# # Edit article
2667# article = self.kwargs['article']
2668# article.fpage = new_fpage
2669# article.lpage = new_lpage
2670# article.page_range = new_page_range
2671# article.coi_statement = new_coi_statement
2672# article.show_body = new_show_body
2673# article.do_not_publish = new_do_not_publish
2674# article.save()
2675#
2676# else:
2677# # New article
2678# params = {
2679# 'pid': new_pid,
2680# 'title_xml': new_title_xml,
2681# 'title_html': new_title_html,
2682# 'title_tex': new_title,
2683# 'fpage': new_fpage,
2684# 'lpage': new_lpage,
2685# 'page_range': new_page_range,
2686# 'seq': container.article_set.count() + 1,
2687# 'body': body,
2688# 'coi_statement': new_coi_statement,
2689# 'show_body': new_show_body,
2690# 'do_not_publish': new_do_not_publish
2691# }
2692#
2693# xarticle = create_articledata()
2694# xarticle.pid = new_pid
2695# xarticle.title_xml = new_title_xml
2696# xarticle.title_html = new_title_html
2697# xarticle.title_tex = new_title
2698# xarticle.fpage = new_fpage
2699# xarticle.lpage = new_lpage
2700# xarticle.page_range = new_page_range
2701# xarticle.seq = container.article_set.count() + 1
2702# xarticle.body = body
2703# xarticle.coi_statement = new_coi_statement
2704# params['xobj'] = xarticle
2705#
2706# cmd = ptf_cmds.addArticlePtfCmd(params)
2707# cmd.set_container(container)
2708# cmd.add_collection(container.my_collection)
2709# article = cmd.do()
2710#
2711# self.kwargs['pid'] = new_pid
2712#
2713# # Add objects related to the article: contribs, datastream, counts...
2714# params = {
2715# # 'title_xml': new_title_xml,
2716# # 'title_html': new_title_html,
2717# # 'title_tex': new_title,
2718# 'authors': new_authors,
2719# 'page_count': new_page_count,
2720# 'icon_location': new_icon_location,
2721# 'body': body,
2722# 'use_kwds': True,
2723# 'kwds_fr': new_kwds_fr,
2724# 'kwds_en': new_kwds_en,
2725# 'kwd_uns_fr': new_kwd_uns_fr,
2726# 'kwd_uns_en': new_kwd_uns_en
2727# }
2728# cmd = ptf_cmds.updateArticlePtfCmd(params)
2729# cmd.set_article(article)
2730# cmd.do()
2731#
2732# self.set_success_message()
2733#
2734# return super(ArticleEditView, self).form_valid(form)
2737@require_http_methods(["POST"])
2738def do_not_publish_article(request, *args, **kwargs):
2739 next = request.headers.get("referer")
2741 pid = kwargs.get("pid", "")
2743 article = model_helpers.get_article(pid)
2744 if article:
2745 article.do_not_publish = not article.do_not_publish
2746 article.save()
2747 else:
2748 raise Http404
2750 return HttpResponseRedirect(next)
2753@require_http_methods(["POST"])
2754def show_article_body(request, *args, **kwargs):
2755 next = request.headers.get("referer")
2757 pid = kwargs.get("pid", "")
2759 article = model_helpers.get_article(pid)
2760 if article:
2761 article.show_body = not article.show_body
2762 article.save()
2763 else:
2764 raise Http404
2766 return HttpResponseRedirect(next)
2769class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2770 """
2771 API to get/post article metadata
2772 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2773 """
2775 def __init__(self, *args, **kwargs):
2776 """
2777 we define here what fields we want in the form
2778 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2779 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2780 super().__init__(*args, **kwargs)
2781 self.fields_to_update = [
2782 "lang",
2783 "atype",
2784 "contributors",
2785 "abstracts",
2786 "kwds",
2787 "titles",
2788 "trans_title_html",
2789 "title_html",
2790 "title_xml",
2791 "title_tex",
2792 "streams",
2793 "ext_links",
2794 "date_accepted",
2795 "history_dates",
2796 "subjs",
2797 "bibitems",
2798 "references",
2799 ]
2800 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2801 self.additional_fields = ["doi", "pid", "container_pid", "pdf", "illustration", "dates"]
2802 self.editorial_tools = [
2803 "translation",
2804 "sidebar",
2805 "lang_selection",
2806 "back_to_article_option",
2807 ]
2808 self.article_container_pid = ""
2809 self.back_url = "trammel"
2811 def save_data(self, data_article):
2812 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2813 # The icons are not preserved since we can add/edit/delete them in VueJs
2814 params = {
2815 "pid": data_article.pid,
2816 "export_folder": settings.MERSENNE_TMP_FOLDER,
2817 "export_all": True,
2818 "with_binary_files": False,
2819 }
2820 ptf_cmds.exportExtraDataPtfCmd(params).do()
2822 def restore_data(self, article):
2823 ptf_cmds.importExtraDataPtfCmd(
2824 {
2825 "pid": article.pid,
2826 "import_folder": settings.MERSENNE_TMP_FOLDER,
2827 }
2828 ).do()
2830 def get(self, request, *args, **kwargs):
2831 data = super().get(request, *args, **kwargs)
2832 return data
2834 def post(self, request, *args, **kwargs):
2835 response = super().post(request, *args, **kwargs)
2836 if response["message"] == "OK":
2837 return redirect(
2838 "api-edit-article",
2839 colid=kwargs.get("colid", ""),
2840 containerPid=kwargs.get("containerPid"),
2841 doi=kwargs.get("doi", ""),
2842 )
2843 else:
2844 raise Http404
2847class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2848 template_name = "article_form.html"
2850 def get_success_url(self):
2851 if self.kwargs["doi"]:
2852 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2853 return reverse("mersenne_dashboard/published_articles")
2855 def get_context_data(self, **kwargs):
2856 context = super().get_context_data(**kwargs)
2857 if "doi" in self.kwargs:
2858 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"])
2859 context["pid"] = context["article"].pid
2861 context["container_pid"] = kwargs.get("container_pid", "")
2862 return context
2865class ArticleDeleteView(View):
2866 def get(self, request, *args, **kwargs):
2867 pid = self.kwargs.get("pid", None)
2868 article = get_object_or_404(Article, pid=pid)
2870 try:
2871 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2872 article.undeploy(mersenneSite)
2874 cmd = ptf_cmds.addArticlePtfCmd(
2875 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2876 )
2877 cmd.set_container(article.my_container)
2878 cmd.set_object_to_be_deleted(article)
2879 cmd.undo()
2880 except Exception as exception:
2881 return HttpResponseServerError(exception)
2883 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2884 return JsonResponse(data)
2887def get_messages_in_queue():
2888 app = Celery("ptf-tools")
2889 # tasks = list(current_app.tasks)
2890 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2891 print(tasks)
2892 # i = app.control.inspect()
2894 with app.connection_or_acquire() as conn:
2895 remaining = conn.default_channel.queue_declare(
2896 queue="coordinator", passive=True
2897 ).message_count
2898 return remaining
2901class NumdamView(TemplateView, history_views.HistoryContextMixin):
2902 template_name = "numdam.html"
2904 def get_context_data(self, **kwargs):
2905 context = super().get_context_data(**kwargs)
2907 context["objs"] = ResourceInNumdam.objects.all()
2909 pre_issues = []
2910 prod_issues = []
2911 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2912 try:
2913 response = requests.get(url)
2914 if response.status_code == 200:
2915 data = response.json()
2916 if "issues" in data:
2917 pre_issues = data["issues"]
2918 except Exception:
2919 pass
2921 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2922 response = requests.get(url)
2923 if response.status_code == 200:
2924 data = response.json()
2925 if "issues" in data:
2926 prod_issues = data["issues"]
2928 new = sorted(list(set(pre_issues).difference(prod_issues)))
2929 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2930 grouped = [
2931 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2932 ]
2933 grouped_removed = [
2934 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2935 ]
2936 context["added_issues"] = grouped
2937 context["removed_issues"] = grouped_removed
2939 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2940 return context
2943class NumdamArchiveView(RedirectView):
2944 @staticmethod
2945 def reset_task_results():
2946 TaskResult.objects.all().delete()
2948 def get_redirect_url(self, *args, **kwargs):
2949 self.colid = kwargs["colid"]
2951 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2952 return Http404
2954 # we make sure archiving is not already running
2955 # if not get_messages_in_queue():
2956 # self.reset_task_results()
2958 if self.colid == "ALL":
2959 archive_numdam_collections.delay()
2960 else:
2961 archive_numdam_collection.s(self.colid).delay()
2963 return reverse("numdam")
2966class DeployAllNumdamAPIView(View):
2967 def internal_do(self, *args, **kwargs):
2968 pids = []
2970 for obj in ResourceInNumdam.objects.all():
2971 pids.append(obj.pid)
2973 return pids
2975 def get(self, request, *args, **kwargs):
2976 try:
2977 pids, status, message = history_views.execute_and_record_func(
2978 "deploy", "numdam", "ALL", self.internal_do, "numdam"
2979 )
2980 except Exception as exception:
2981 return HttpResponseServerError(exception)
2983 data = {"message": message, "ids": pids, "status": status}
2984 return JsonResponse(data)
2987class NumdamDeleteAPIView(View):
2988 def get(self, request, *args, **kwargs):
2989 pid = self.kwargs.get("pid", None)
2991 try:
2992 obj = ResourceInNumdam.objects.get(pid=pid)
2993 obj.delete()
2994 except Exception as exception:
2995 return HttpResponseServerError(exception)
2997 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
2998 return JsonResponse(data)
3001class ExtIdApiDetail(View):
3002 def get(self, request, *args, **kwargs):
3003 extid = get_object_or_404(
3004 ExtId,
3005 resource__pid=kwargs["pid"],
3006 id_type=kwargs["what"],
3007 )
3008 return JsonResponse(
3009 {
3010 "pk": extid.pk,
3011 "href": extid.get_href(),
3012 "fetch": reverse(
3013 "api-fetch-id",
3014 args=(
3015 extid.resource.pk,
3016 extid.id_value,
3017 extid.id_type,
3018 "extid",
3019 ),
3020 ),
3021 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3022 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3023 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3024 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3025 "is_valid": extid.checked,
3026 }
3027 )
3030class ExtIdFormTemplate(TemplateView):
3031 template_name = "common/externalid_form.html"
3033 def get_context_data(self, **kwargs):
3034 context = super().get_context_data(**kwargs)
3035 context["sequence"] = kwargs["sequence"]
3036 return context
3039class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3040 def get_context_data(self, **kwargs):
3041 context = super().get_context_data(**kwargs)
3042 context["helper"] = PtfFormHelper
3043 return context
3045 def get_success_url(self):
3046 self.post_process()
3047 return self.object.bibitem.resource.get_absolute_url()
3049 def post_process(self):
3050 cmd = updateBibitemCitationXmlCmd()
3051 cmd.set_bibitem(self.object.bibitem)
3052 cmd.do()
3053 model_helpers.post_resource_updated(self.object.bibitem.resource)
3056class BibItemIdCreate(BibItemIdFormView, CreateView):
3057 model = BibItemId
3058 form_class = BibItemIdForm
3060 def get_context_data(self, **kwargs):
3061 context = super().get_context_data(**kwargs)
3062 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3063 return context
3065 def get_initial(self):
3066 initial = super().get_initial()
3067 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3068 return initial
3070 def form_valid(self, form):
3071 form.instance.checked = False
3072 return super().form_valid(form)
3075class BibItemIdUpdate(BibItemIdFormView, UpdateView):
3076 model = BibItemId
3077 form_class = BibItemIdForm
3079 def get_context_data(self, **kwargs):
3080 context = super().get_context_data(**kwargs)
3081 context["bibitem"] = self.object.bibitem
3082 return context
3085class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3086 def get_context_data(self, **kwargs):
3087 context = super().get_context_data(**kwargs)
3088 context["helper"] = PtfFormHelper
3089 return context
3091 def get_success_url(self):
3092 self.post_process()
3093 return self.object.resource.get_absolute_url()
3095 def post_process(self):
3096 model_helpers.post_resource_updated(self.object.resource)
3099class ExtIdCreate(ExtIdFormView, CreateView):
3100 model = ExtId
3101 form_class = ExtIdForm
3103 def get_context_data(self, **kwargs):
3104 context = super().get_context_data(**kwargs)
3105 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3106 return context
3108 def get_initial(self):
3109 initial = super().get_initial()
3110 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3111 return initial
3113 def form_valid(self, form):
3114 form.instance.checked = False
3115 return super().form_valid(form)
3118class ExtIdUpdate(ExtIdFormView, UpdateView):
3119 model = ExtId
3120 form_class = ExtIdForm
3122 def get_context_data(self, **kwargs):
3123 context = super().get_context_data(**kwargs)
3124 context["resource"] = self.object.resource
3125 return context
3128class BibItemIdApiDetail(View):
3129 def get(self, request, *args, **kwargs):
3130 bibitemid = get_object_or_404(
3131 BibItemId,
3132 bibitem__resource__pid=kwargs["pid"],
3133 bibitem__sequence=kwargs["seq"],
3134 id_type=kwargs["what"],
3135 )
3136 return JsonResponse(
3137 {
3138 "pk": bibitemid.pk,
3139 "href": bibitemid.get_href(),
3140 "fetch": reverse(
3141 "api-fetch-id",
3142 args=(
3143 bibitemid.bibitem.pk,
3144 bibitemid.id_value,
3145 bibitemid.id_type,
3146 "bibitemid",
3147 ),
3148 ),
3149 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
3150 "uncheck": reverse(
3151 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
3152 ),
3153 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
3154 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
3155 "is_valid": bibitemid.checked,
3156 }
3157 )
3160class UpdateTexmfZipAPIView(View):
3161 def get(self, request, *args, **kwargs):
3162 def copy_zip_files(src_folder, dest_folder):
3163 os.makedirs(dest_folder, exist_ok=True)
3165 zip_files = [
3166 os.path.join(src_folder, f)
3167 for f in os.listdir(src_folder)
3168 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3169 ]
3170 for zip_file in zip_files:
3171 resolver.copy_file(zip_file, dest_folder)
3173 # Exceptions: specific zip/gz files
3174 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3175 resolver.copy_file(zip_file, dest_folder)
3177 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3178 resolver.copy_file(zip_file, dest_folder)
3180 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3181 resolver.copy_file(gz_file, dest_folder)
3183 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3185 dest_folder = os.path.join(
3186 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3187 )
3189 try:
3190 copy_zip_files(src_folder, dest_folder)
3191 except Exception as exception:
3192 return HttpResponseServerError(exception)
3194 try:
3195 dest_folder = os.path.join(
3196 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3197 )
3198 copy_zip_files(src_folder, dest_folder)
3199 except Exception as exception:
3200 return HttpResponseServerError(exception)
3202 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3203 return JsonResponse(data)
3206class TestView(TemplateView):
3207 template_name = "mersenne.html"
3209 def get_context_data(self, **kwargs):
3210 super().get_context_data(**kwargs)
3211 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True)
3212 model_data_converter.db_to_issue_data(issue)
3215class TrammelTasksProgressView(View):
3216 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3217 """
3218 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3219 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3220 """
3221 task_name = task
3223 def get_event_data():
3224 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3226 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3227 remaining_messages = get_messages_in_queue()
3229 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3230 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3231 failed_tasks = all_tasks.filter(status="FAILURE")
3233 all_tasks_count = all_tasks.count()
3234 success_count = successed_tasks.count()
3235 fail_count = failed_tasks.count()
3237 all_count = all_tasks_count + remaining_messages
3238 remaining_count = all_count - success_count - fail_count
3240 success_rate = int(success_count * 100 / all_count) if all_count else 0
3241 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3242 status = "consuming_queue" if remaining_count != 0 else "polling"
3244 last_task = successed_tasks.first()
3245 last_task = (
3246 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3247 if last_task
3248 else ""
3249 )
3251 # SSE event format
3252 event_data = {
3253 "status": status,
3254 "success_rate": success_rate,
3255 "error_rate": error_rate,
3256 "all_count": all_count,
3257 "remaining_count": remaining_count,
3258 "success_count": success_count,
3259 "fail_count": fail_count,
3260 "last_task": last_task,
3261 }
3263 return event_data
3265 def stream_response(data):
3266 # Send initial response headers
3267 yield f"data: {json.dumps(data)}\n\n"
3269 data = get_event_data()
3270 format = request.GET.get("format", "stream")
3271 if format == "json":
3272 response = JsonResponse(data)
3273 else:
3274 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3275 return response
3278user_signed_up.connect(update_user_from_invite)