Coverage for src/ptf_tools/views/base_views.py: 18%
1589 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-03 12:11 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-03 12:11 +0000
1import io
2import json
3import os
4import re
5from datetime import datetime
6from itertools import groupby
8import jsonpickle
9import requests
10from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin
11from celery import Celery, current_app
12from django.conf import settings
13from django.contrib import messages
14from django.contrib.auth.mixins import UserPassesTestMixin
15from django.db.models import Q
16from django.http import (
17 Http404,
18 HttpRequest,
19 HttpResponse,
20 HttpResponseRedirect,
21 HttpResponseServerError,
22 JsonResponse,
23)
24from django.shortcuts import get_object_or_404, redirect, render
25from django.urls import resolve, reverse, reverse_lazy
26from django.utils import timezone
27from django.views.decorators.http import require_http_methods
28from django.views.generic import ListView, TemplateView, View
29from django.views.generic.base import RedirectView
30from django.views.generic.detail import SingleObjectMixin
31from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
32from django_celery_results.models import TaskResult
33from extra_views import (
34 CreateWithInlinesView,
35 InlineFormSetFactory,
36 NamedFormsetsMixin,
37 UpdateWithInlinesView,
38)
39from ptf import model_data_converter, model_helpers, tex, utils
40from ptf.cmds import ptf_cmds, xml_cmds
41from ptf.cmds.base_cmds import make_int
42from ptf.cmds.xml.jats.builder.issue import get_issue_title_xml
43from ptf.cmds.xml.xml_utils import replace_html_entities
44from ptf.display import resolver
45from ptf.exceptions import DOIException, PDFException, ServerUnderMaintenance
46from ptf.model_data import create_issuedata, create_publisherdata
47from ptf.models import (
48 Abstract,
49 Article,
50 BibItem,
51 BibItemId,
52 Collection,
53 Container,
54 ExtId,
55 ExtLink,
56 Resource,
57 ResourceId,
58)
59from ptf.views import ArticleEditAPIView
60from pubmed.views import recordPubmed
61from requests import Timeout
63from comments_moderation.utils import get_comments_for_home, is_comment_moderator
64from history import models as history_models
65from history import views as history_views
66from ptf_tools.doaj import doaj_pid_register
67from ptf_tools.doi import get_or_create_doibatch, recordDOI
68from ptf_tools.forms import (
69 BibItemIdForm,
70 CollectionForm,
71 ContainerForm,
72 DiffContainerForm,
73 ExtIdForm,
74 ExtLinkForm,
75 FormSetHelper,
76 ImportArticleForm,
77 ImportContainerForm,
78 PtfFormHelper,
79 PtfLargeModalFormHelper,
80 PtfModalFormHelper,
81 RegisterPubmedForm,
82 ResourceIdForm,
83 get_article_choices,
84)
85from ptf_tools.indexingChecker import ReferencingChecker
86from ptf_tools.models import ResourceInNumdam
87from ptf_tools.tasks import (
88 archive_numdam_collection,
89 archive_numdam_issue,
90 archive_trammel_collection,
91 archive_trammel_resource,
92)
93from ptf_tools.templatetags.tools_helpers import get_authorized_collections
94from ptf_tools.utils import is_authorized_editor
97def view_404(request: HttpRequest):
98 """
99 Dummy view raising HTTP 404 exception.
100 """
101 raise Http404
104def check_collection(collection, server_url, server_type):
105 """
106 Check if a collection exists on a serveur (test/prod)
107 and upload the collection (XML, image) if necessary
108 """
110 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
111 response = requests.get(url, verify=False)
112 # First, upload the collection XML
113 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
114 body = xml.encode("utf8")
116 url = server_url + reverse("upload-serials")
117 if response.status_code == 200:
118 # PUT http verb is used for update
119 response = requests.put(url, data=body, verify=False)
120 else:
121 # POST http verb is used for creation
122 response = requests.post(url, data=body, verify=False)
124 # Second, copy the collection images
125 # There is no need to copy files for the test server
126 # Files were already copied in /mersenne_test_data during the ptf_tools import
127 # We only need to copy files from /mersenne_test_data to
128 # /mersenne_prod_data during an upload to prod
129 if server_type == "website":
130 resolver.copy_binary_files(
131 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
132 )
133 elif server_type == "numdam":
134 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
135 if collection.pid in settings.NUMDAM_COLLECTIONS:
136 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
138 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
141def check_lock():
142 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
145def load_cedrics_article_choices(request):
146 colid = request.GET.get("colid")
147 issue = request.GET.get("issue")
148 article_choices = get_article_choices(colid, issue)
149 return render(
150 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
151 )
154class ImportCedricsArticleFormView(FormView):
155 template_name = "import_article.html"
156 form_class = ImportArticleForm
158 def dispatch(self, request, *args, **kwargs):
159 self.colid = self.kwargs["colid"]
160 return super().dispatch(request, *args, **kwargs)
162 def get_success_url(self):
163 if self.colid:
164 return reverse("collection-detail", kwargs={"pid": self.colid})
165 return "/"
167 def get_context_data(self, **kwargs):
168 context = super().get_context_data(**kwargs)
169 context["colid"] = self.colid
170 context["helper"] = PtfModalFormHelper
171 return context
173 def get_form_kwargs(self):
174 kwargs = super().get_form_kwargs()
175 kwargs["colid"] = self.colid
176 return kwargs
178 def form_valid(self, form):
179 self.issue = form.cleaned_data["issue"]
180 self.article = form.cleaned_data["article"]
181 return super().form_valid(form)
183 def import_cedrics_article(self, *args, **kwargs):
184 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
185 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
186 )
187 cmd.do()
189 def post(self, request, *args, **kwargs):
190 self.colid = self.kwargs.get("colid", None)
191 issue = request.POST["issue"]
192 self.article_pid = request.POST["article"]
193 self.issue_pid = os.path.basename(os.path.dirname(issue))
195 import_args = [self]
196 import_kwargs = {}
198 try:
199 _, status, message = history_views.execute_and_record_func(
200 "import",
201 f"{self.issue_pid} / {self.article_pid}",
202 self.colid,
203 self.import_cedrics_article,
204 "",
205 False,
206 *import_args,
207 **import_kwargs,
208 )
210 messages.success(
211 self.request, f"L'article {self.article_pid} a été importé avec succès"
212 )
214 except Exception as exception:
215 messages.error(
216 self.request,
217 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}",
218 )
220 return redirect(self.get_success_url())
223class ImportCedricsIssueView(FormView):
224 template_name = "import_container.html"
225 form_class = ImportContainerForm
227 def dispatch(self, request, *args, **kwargs):
228 self.colid = self.kwargs["colid"]
229 self.to_appear = self.request.GET.get("to_appear", False)
230 return super().dispatch(request, *args, **kwargs)
232 def get_success_url(self):
233 if self.filename:
234 return reverse(
235 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
236 )
237 return "/"
239 def get_context_data(self, **kwargs):
240 context = super().get_context_data(**kwargs)
241 context["colid"] = self.colid
242 context["helper"] = PtfModalFormHelper
243 return context
245 def get_form_kwargs(self):
246 kwargs = super().get_form_kwargs()
247 kwargs["colid"] = self.colid
248 kwargs["to_appear"] = self.to_appear
249 return kwargs
251 def form_valid(self, form):
252 self.filename = form.cleaned_data["filename"].split("/")[-1]
253 return super().form_valid(form)
256class DiffCedricsIssueView(FormView):
257 template_name = "diff_container_form.html"
258 form_class = DiffContainerForm
259 diffs = None
260 xissue = None
261 xissue_encoded = None
263 def get_success_url(self):
264 return reverse("collection-detail", kwargs={"pid": self.colid})
266 def dispatch(self, request, *args, **kwargs):
267 self.colid = self.kwargs["colid"]
268 # self.filename = self.kwargs['filename']
269 return super().dispatch(request, *args, **kwargs)
271 def get(self, request, *args, **kwargs):
272 self.filename = request.GET["filename"]
273 self.remove_mail = request.GET["remove_email"]
274 self.remove_date_prod = request.GET["remove_date_prod"]
276 try:
277 result, status, message = history_views.execute_and_record_func(
278 "import",
279 os.path.basename(self.filename),
280 self.colid,
281 self.diff_cedrics_issue,
282 "",
283 True,
284 )
285 except Exception as exception:
286 pid = self.filename.split("/")[-1]
287 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
288 return HttpResponseRedirect(self.get_success_url())
290 no_conflict = result[0]
291 self.diffs = result[1]
292 self.xissue = result[2]
294 if no_conflict:
295 # Proceed with the import
296 self.form_valid(self.get_form())
297 return redirect(self.get_success_url())
298 else:
299 # Display the diff template
300 self.xissue_encoded = jsonpickle.encode(self.xissue)
302 return super().get(request, *args, **kwargs)
304 def post(self, request, *args, **kwargs):
305 self.filename = request.POST["filename"]
306 data = request.POST["xissue_encoded"]
307 self.xissue = jsonpickle.decode(data)
309 return super().post(request, *args, **kwargs)
311 def get_context_data(self, **kwargs):
312 context = super().get_context_data(**kwargs)
313 context["colid"] = self.colid
314 context["diff"] = self.diffs
315 context["filename"] = self.filename
316 context["xissue_encoded"] = self.xissue_encoded
317 return context
319 def get_form_kwargs(self):
320 kwargs = super().get_form_kwargs()
321 kwargs["colid"] = self.colid
322 return kwargs
324 def diff_cedrics_issue(self, *args, **kwargs):
325 params = {
326 "colid": self.colid,
327 "input_file": self.filename,
328 "remove_email": self.remove_mail,
329 "remove_date_prod": self.remove_date_prod,
330 "diff_only": True,
331 }
333 if settings.IMPORT_CEDRICS_DIRECTLY:
334 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
335 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
336 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
337 else:
338 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
340 result = cmd.do()
341 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
342 messages.warning(
343 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings
344 )
346 return result
348 def import_cedrics_issue(self, *args, **kwargs):
349 # modify xissue with data_issue if params to override
350 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
351 issue = model_helpers.get_container(self.xissue.pid)
352 if issue:
353 data_issue = model_data_converter.db_to_issue_data(issue)
354 for xarticle in self.xissue.articles:
355 filter_articles = [
356 article for article in data_issue.articles if article.doi == xarticle.doi
357 ]
358 if len(filter_articles) > 0:
359 db_article = filter_articles[0]
360 xarticle.coi_statement = db_article.coi_statement
361 xarticle.kwds = db_article.kwds
362 xarticle.contrib_groups = db_article.contrib_groups
364 params = {
365 "colid": self.colid,
366 "xissue": self.xissue,
367 "input_file": self.filename,
368 }
370 if settings.IMPORT_CEDRICS_DIRECTLY:
371 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
372 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
373 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
374 else:
375 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
377 cmd.do()
379 def form_valid(self, form):
380 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
381 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
382 else:
383 import_kwargs = {}
384 import_args = [self]
386 try:
387 _, status, message = history_views.execute_and_record_func(
388 "import",
389 self.xissue.pid,
390 self.kwargs["colid"],
391 self.import_cedrics_issue,
392 "",
393 False,
394 *import_args,
395 **import_kwargs,
396 )
397 except Exception as exception:
398 messages.error(
399 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
400 )
401 return super().form_invalid(form)
403 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
404 return super().form_valid(form)
407class BibtexAPIView(View):
408 def get(self, request, *args, **kwargs):
409 pid = self.kwargs.get("pid", None)
410 all_bibtex = ""
411 if pid:
412 article = model_helpers.get_article(pid)
413 if article:
414 for bibitem in article.bibitem_set.all():
415 bibtex_array = bibitem.get_bibtex()
416 last = len(bibtex_array)
417 i = 1
418 for bibtex in bibtex_array:
419 if i > 1 and i < last:
420 all_bibtex += " "
421 all_bibtex += bibtex + "\n"
422 i += 1
424 data = {"bibtex": all_bibtex}
425 return JsonResponse(data)
428class MatchingAPIView(View):
429 def get(self, request, *args, **kwargs):
430 pid = self.kwargs.get("pid", None)
432 url = settings.MATCHING_URL
433 headers = {"Content-Type": "application/xml"}
435 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
437 if settings.DEBUG:
438 print("Issue exported to /tmp/issue.xml")
439 f = open("/tmp/issue.xml", "w")
440 f.write(body.encode("utf8"))
441 f.close()
443 r = requests.post(url, data=body.encode("utf8"), headers=headers)
444 body = r.text.encode("utf8")
445 data = {"status": r.status_code, "message": body[:1000]}
447 if settings.DEBUG:
448 print("Matching received, new issue exported to /tmp/issue1.xml")
449 f = open("/tmp/issue1.xml", "w")
450 text = body
451 f.write(text)
452 f.close()
454 resource = model_helpers.get_resource(pid)
455 obj = resource.cast()
456 colid = obj.get_collection().pid
458 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
460 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
461 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
462 )
463 cmd.do()
465 print("Matching finished")
466 return JsonResponse(data)
469class ImportAllAPIView(View):
470 def internal_do(self, *args, **kwargs):
471 pid = self.kwargs.get("pid", None)
473 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
474 if not os.path.isdir(root_folder):
475 raise ValueError(root_folder + " does not exist")
477 resource = model_helpers.get_resource(pid)
478 if not resource:
479 file = os.path.join(root_folder, pid + ".xml")
480 body = utils.get_file_content_in_utf8(file)
481 journals = xml_cmds.addCollectionsXmlCmd(
482 {
483 "body": body,
484 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
485 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
486 }
487 ).do()
488 if not journals:
489 raise ValueError(file + " does not contain a collection")
490 resource = journals[0]
491 # resolver.copy_binary_files(
492 # resource,
493 # settings.MATHDOC_ARCHIVE_FOLDER,
494 # settings.MERSENNE_TEST_DATA_FOLDER)
496 obj = resource.cast()
498 if obj.classname != "Collection":
499 raise ValueError(pid + " does not contain a collection")
501 cmd = xml_cmds.collectEntireCollectionXmlCmd(
502 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
503 )
504 pids = cmd.do()
506 return pids
508 def get(self, request, *args, **kwargs):
509 pid = self.kwargs.get("pid", None)
511 try:
512 pids, status, message = history_views.execute_and_record_func(
513 "import", pid, pid, self.internal_do
514 )
515 except Timeout as exception:
516 return HttpResponse(exception, status=408)
517 except Exception as exception:
518 return HttpResponseServerError(exception)
520 data = {"message": message, "ids": pids, "status": status}
521 return JsonResponse(data)
524class DeployAllAPIView(View):
525 def internal_do(self, *args, **kwargs):
526 pid = self.kwargs.get("pid", None)
527 site = self.kwargs.get("site", None)
529 pids = []
531 collection = model_helpers.get_collection(pid)
532 if not collection:
533 raise RuntimeError(pid + " does not exist")
535 if site == "numdam":
536 server_url = settings.NUMDAM_PRE_URL
537 elif site != "ptf_tools":
538 server_url = getattr(collection, site)()
539 if not server_url:
540 raise RuntimeError("The collection has no " + site)
542 if site != "ptf_tools":
543 # check if the collection exists on the server
544 # if not, check_collection will upload the collection (XML,
545 # image...)
546 check_collection(collection, server_url, site)
548 for issue in collection.content.all():
549 if site != "website" or (site == "website" and issue.are_all_articles_published()):
550 pids.append(issue.pid)
552 return pids
554 def get(self, request, *args, **kwargs):
555 pid = self.kwargs.get("pid", None)
556 site = self.kwargs.get("site", None)
558 try:
559 pids, status, message = history_views.execute_and_record_func(
560 "deploy", pid, pid, self.internal_do, site
561 )
562 except Timeout as exception:
563 return HttpResponse(exception, status=408)
564 except Exception as exception:
565 return HttpResponseServerError(exception)
567 data = {"message": message, "ids": pids, "status": status}
568 return JsonResponse(data)
571class AddIssuePDFView(View):
572 def __init(self, *args, **kwargs):
573 super().__init__(*args, **kwargs)
574 self.pid = None
575 self.issue = None
576 self.collection = None
577 self.site = "test_website"
579 def post_to_site(self, url):
580 response = requests.post(url, verify=False)
581 status = response.status_code
582 if not (199 < status < 205):
583 messages.error(self.request, response.text)
584 if status == 503:
585 raise ServerUnderMaintenance(response.text)
586 else:
587 raise RuntimeError(response.text)
589 def internal_do(self, *args, **kwargs):
590 """
591 Called by history_views.execute_and_record_func to do the actual job.
592 """
594 issue_pid = self.issue.pid
595 colid = self.collection.pid
597 if self.site == "website":
598 # Copy the PDF from the test to the production folder
599 resolver.copy_binary_files(
600 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
601 )
602 else:
603 # Copy the PDF from the cedram to the test folder
604 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
605 from_path = os.path.join(from_folder, issue_pid + ".pdf")
606 if not os.path.isfile(from_path):
607 raise Http404(f"{from_path} does not exist")
609 to_path = resolver.get_disk_location(
610 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
611 )
612 resolver.copy_file(from_path, to_path)
614 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
616 if self.site == "test_website":
617 # Post to ptf-tools: it will add a Datastream to the issue
618 absolute_url = self.request.build_absolute_uri(url)
619 self.post_to_site(absolute_url)
621 server_url = getattr(self.collection, self.site)()
622 absolute_url = server_url + url
623 # Post to the test or production website
624 self.post_to_site(absolute_url)
626 def get(self, request, *args, **kwargs):
627 """
628 Send an issue PDF to the test or production website
629 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
630 :param args:
631 :param kwargs:
632 :return:
633 """
634 if check_lock():
635 m = "Trammel is under maintenance. Please try again later."
636 messages.error(self.request, m)
637 return JsonResponse({"message": m, "status": 503})
639 self.pid = self.kwargs.get("pid", None)
640 self.site = self.kwargs.get("site", "test_website")
642 self.issue = model_helpers.get_container(self.pid)
643 if not self.issue:
644 raise Http404(f"{self.pid} does not exist")
645 self.collection = self.issue.get_top_collection()
647 try:
648 pids, status, message = history_views.execute_and_record_func(
649 "deploy",
650 self.pid,
651 self.collection.pid,
652 self.internal_do,
653 f"add issue PDF to {self.site}",
654 )
656 except Timeout as exception:
657 return HttpResponse(exception, status=408)
658 except Exception as exception:
659 return HttpResponseServerError(exception)
661 data = {"message": message, "status": status}
662 return JsonResponse(data)
665class ArchiveAllAPIView(View):
666 """
667 - archive le xml de la collection ainsi que les binaires liés
668 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
669 @return array of issues pid
670 """
672 def internal_do(self, *args, **kwargs):
673 collection = kwargs["collection"]
674 pids = []
675 colid = collection.pid
677 logfile = os.path.join(settings.LOG_DIR, "archive.log")
678 if os.path.isfile(logfile):
679 os.remove(logfile)
681 ptf_cmds.exportPtfCmd(
682 {
683 "pid": colid,
684 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
685 "with_binary_files": True,
686 "for_archive": True,
687 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
688 }
689 ).do()
691 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
692 if os.path.isfile(cedramcls):
693 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
694 resolver.create_folder(dest_folder)
695 resolver.copy_file(cedramcls, dest_folder)
697 for issue in collection.content.all():
698 qs = issue.article_set.filter(
699 date_online_first__isnull=True, date_published__isnull=True
700 )
701 if qs.count() == 0:
702 pids.append(issue.pid)
704 return pids
706 def get(self, request, *args, **kwargs):
707 pid = self.kwargs.get("pid", None)
709 collection = model_helpers.get_collection(pid)
710 if not collection:
711 return HttpResponse(f"{pid} does not exist", status=400)
713 dict_ = {"collection": collection}
714 args_ = [self]
716 try:
717 pids, status, message = history_views.execute_and_record_func(
718 "archive", pid, pid, self.internal_do, "", False, *args_, **dict_
719 )
720 except Timeout as exception:
721 return HttpResponse(exception, status=408)
722 except Exception as exception:
723 return HttpResponseServerError(exception)
725 data = {"message": message, "ids": pids, "status": status}
726 return JsonResponse(data)
729class CreateAllDjvuAPIView(View):
730 def internal_do(self, *args, **kwargs):
731 issue = kwargs["issue"]
732 pids = [issue.pid]
734 for article in issue.article_set.all():
735 pids.append(article.pid)
737 return pids
739 def get(self, request, *args, **kwargs):
740 pid = self.kwargs.get("pid", None)
741 issue = model_helpers.get_container(pid)
742 if not issue:
743 raise Http404(f"{pid} does not exist")
745 try:
746 dict_ = {"issue": issue}
747 args_ = [self]
749 pids, status, message = history_views.execute_and_record_func(
750 "numdam",
751 pid,
752 issue.get_collection().pid,
753 self.internal_do,
754 "",
755 False,
756 *args_,
757 **dict_,
758 )
759 except Exception as exception:
760 return HttpResponseServerError(exception)
762 data = {"message": message, "ids": pids, "status": status}
763 return JsonResponse(data)
766class ImportJatsContainerAPIView(View):
767 def internal_do(self, *args, **kwargs):
768 pid = self.kwargs.get("pid", None)
769 colid = self.kwargs.get("colid", None)
771 if pid and colid:
772 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
774 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
775 {
776 "body": body,
777 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
778 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
779 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
780 }
781 )
782 container = cmd.do()
783 if len(cmd.warnings) > 0:
784 messages.warning(
785 self.request,
786 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
787 )
789 if not container:
790 raise RuntimeError("Error: the container " + pid + " was not imported")
792 # resolver.copy_binary_files(
793 # container,
794 # settings.MATHDOC_ARCHIVE_FOLDER,
795 # settings.MERSENNE_TEST_DATA_FOLDER)
796 #
797 # for article in container.article_set.all():
798 # resolver.copy_binary_files(
799 # article,
800 # settings.MATHDOC_ARCHIVE_FOLDER,
801 # settings.MERSENNE_TEST_DATA_FOLDER)
802 else:
803 raise RuntimeError("colid or pid are not defined")
805 def get(self, request, *args, **kwargs):
806 pid = self.kwargs.get("pid", None)
807 colid = self.kwargs.get("colid", None)
809 try:
810 _, status, message = history_views.execute_and_record_func(
811 "import", pid, colid, self.internal_do
812 )
813 except Timeout as exception:
814 return HttpResponse(exception, status=408)
815 except Exception as exception:
816 return HttpResponseServerError(exception)
818 data = {"message": message, "status": status}
819 return JsonResponse(data)
822class DeployCollectionAPIView(View):
823 # Update collection.xml on a site (with its images)
825 def internal_do(self, *args, **kwargs):
826 colid = self.kwargs.get("colid", None)
827 site = self.kwargs.get("site", None)
829 collection = model_helpers.get_collection(colid)
830 if not collection:
831 raise RuntimeError(f"{colid} does not exist")
833 if site == "numdam":
834 server_url = settings.NUMDAM_PRE_URL
835 else:
836 server_url = getattr(collection, site)()
837 if not server_url:
838 raise RuntimeError(f"The collection has no {site}")
840 # check_collection creates or updates the collection (XML, image...)
841 check_collection(collection, server_url, site)
843 def get(self, request, *args, **kwargs):
844 colid = self.kwargs.get("colid", None)
845 site = self.kwargs.get("site", None)
847 try:
848 _, status, message = history_views.execute_and_record_func(
849 "deploy", colid, colid, self.internal_do, site
850 )
851 except Timeout as exception:
852 return HttpResponse(exception, status=408)
853 except Exception as exception:
854 return HttpResponseServerError(exception)
856 data = {"message": message, "status": status}
857 return JsonResponse(data)
860class DeployJatsResourceAPIView(View):
861 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
863 def internal_do(self, *args, **kwargs):
864 pid = self.kwargs.get("pid", None)
865 colid = self.kwargs.get("colid", None)
866 site = self.kwargs.get("site", None)
868 if site == "ptf_tools":
869 raise RuntimeError("Do not choose to deploy on PTF Tools")
871 resource = model_helpers.get_resource(pid)
872 if not resource:
873 raise RuntimeError(f"{pid} does not exist")
875 obj = resource.cast()
876 article = None
877 if obj.classname == "Article":
878 article = obj
879 container = article.my_container
880 articles_to_deploy = [article]
881 else:
882 container = obj
883 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
885 if site == "website" and article is not None and article.do_not_publish:
886 raise RuntimeError(f"{pid} is marked as Do not publish")
887 if site == "numdam" and article is not None:
888 raise RuntimeError("You can only deploy issues to Numdam")
890 collection = container.get_top_collection()
891 colid = collection.pid
892 djvu_exception = None
894 if site == "numdam":
895 server_url = settings.NUMDAM_PRE_URL
896 ResourceInNumdam.objects.get_or_create(pid=container.pid)
898 # 06/12/2022: DjVu are no longer added with Mersenne articles
899 # Add Djvu (before exporting the XML)
900 if False and int(container.year) < 2020:
901 for art in container.article_set.all():
902 try:
903 cmd = ptf_cmds.addDjvuPtfCmd()
904 cmd.set_resource(art)
905 cmd.do()
906 except Exception as e:
907 # Djvu are optional.
908 # Allow the deployment, but record the exception in the history
909 djvu_exception = e
910 else:
911 server_url = getattr(collection, site)()
912 if not server_url:
913 raise RuntimeError(f"The collection has no {site}")
915 # check if the collection exists on the server
916 # if not, check_collection will upload the collection (XML,
917 # image...)
918 if article is None:
919 check_collection(collection, server_url, site)
921 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
922 # Create/update deployed date and published date on all container articles
923 if site == "website":
924 file_.write(
925 "Create/Update deployed_date and date_published on all articles for {}\n".format(
926 pid
927 )
928 )
930 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
931 cmd = ptf_cmds.publishResourcePtfCmd()
932 cmd.set_resource(resource)
933 updated_articles = cmd.do()
935 tex.create_frontpage(colid, container, updated_articles, test=False)
937 mersenneSite = model_helpers.get_site_mersenne(colid)
938 # create or update deployed_date on container and articles
939 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
941 for art in articles_to_deploy:
942 if art.doi and (art.date_published or art.date_online_first):
943 if art.my_container.year is None:
944 art.my_container.year = datetime.now().strftime("%Y")
945 # BUG ? update the container but no save() ?
947 file_.write(
948 "Publication date of {} : Online First: {}, Published: {}\n".format(
949 art.pid, art.date_online_first, art.date_published
950 )
951 )
953 if article is None:
954 resolver.copy_binary_files(
955 container,
956 settings.MERSENNE_TEST_DATA_FOLDER,
957 settings.MERSENNE_PROD_DATA_FOLDER,
958 )
960 for art in articles_to_deploy:
961 resolver.copy_binary_files(
962 art,
963 settings.MERSENNE_TEST_DATA_FOLDER,
964 settings.MERSENNE_PROD_DATA_FOLDER,
965 )
967 elif site == "test_website":
968 # create date_pre_published on articles without date_pre_published
969 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
970 cmd.set_resource(resource)
971 updated_articles = cmd.do()
973 tex.create_frontpage(colid, container, updated_articles)
975 export_to_website = site == "website"
977 if article is None:
978 with_djvu = site == "numdam"
979 xml = ptf_cmds.exportPtfCmd(
980 {
981 "pid": pid,
982 "with_djvu": with_djvu,
983 "export_to_website": export_to_website,
984 }
985 ).do()
986 body = xml.encode("utf8")
988 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
989 url = server_url + reverse("issue_upload")
990 else:
991 url = server_url + reverse("book_upload")
993 # verify=False: ignore TLS certificate
994 response = requests.post(url, data=body, verify=False)
995 # response = requests.post(url, files=files, verify=False)
996 else:
997 xml = ptf_cmds.exportPtfCmd(
998 {
999 "pid": pid,
1000 "with_djvu": False,
1001 "article_standalone": True,
1002 "collection_pid": collection.pid,
1003 "export_to_website": export_to_website,
1004 "export_folder": settings.LOG_DIR,
1005 }
1006 ).do()
1007 # Unlike containers that send their XML as the body of the POST request,
1008 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1009 xml_file = io.StringIO(xml)
1010 files = {"xml": xml_file}
1012 url = server_url + reverse(
1013 "article_in_issue_upload", kwargs={"pid": container.pid}
1014 )
1015 # verify=False: ignore TLS certificate
1016 header = {}
1017 response = requests.post(url, headers=header, files=files, verify=False)
1019 status = response.status_code
1021 if 199 < status < 205:
1022 # There is no need to copy files for the test server
1023 # Files were already copied in /mersenne_test_data during the ptf_tools import
1024 # We only need to copy files from /mersenne_test_data to
1025 # /mersenne_prod_data during an upload to prod
1026 if site == "website":
1027 # TODO mettre ici le record doi pour un issue publié
1028 if container.doi:
1029 recordDOI(container)
1031 for art in articles_to_deploy:
1032 # record DOI automatically when deploying in prod
1034 if art.doi and art.allow_crossref():
1035 recordDOI(art)
1037 if colid == "CRBIOL":
1038 recordPubmed(
1039 art, force_update=False, updated_articles=updated_articles
1040 )
1042 if colid == "PCJ":
1043 self.update_pcj_editor(updated_articles)
1045 # Archive the container or the article
1046 if article is None:
1047 archive_trammel_resource.delay(
1048 colid=colid,
1049 pid=pid,
1050 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1051 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1052 )
1053 else:
1054 archive_trammel_resource.delay(
1055 colid=colid,
1056 pid=pid,
1057 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1058 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1059 article_doi=article.doi,
1060 )
1061 # cmd = ptf_cmds.archiveIssuePtfCmd({
1062 # "pid": pid,
1063 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1064 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1065 # cmd.set_article(article) # set_article allows archiving only the article
1066 # cmd.do()
1068 elif site == "numdam":
1069 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1070 if colid in settings.NUMDAM_COLLECTIONS:
1071 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1073 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1074 for article in container.article_set.all():
1075 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1077 elif status == 503:
1078 raise ServerUnderMaintenance(response.text)
1079 else:
1080 raise RuntimeError(response.text)
1082 if djvu_exception:
1083 raise djvu_exception
1085 def get(self, request, *args, **kwargs):
1086 pid = self.kwargs.get("pid", None)
1087 colid = self.kwargs.get("colid", None)
1088 site = self.kwargs.get("site", None)
1090 try:
1091 _, status, message = history_views.execute_and_record_func(
1092 "deploy", pid, colid, self.internal_do, site
1093 )
1094 except Timeout as exception:
1095 return HttpResponse(exception, status=408)
1096 except Exception as exception:
1097 return HttpResponseServerError(exception)
1099 data = {"message": message, "status": status}
1100 return JsonResponse(data)
1102 def update_pcj_editor(self, updated_articles):
1103 for article in updated_articles:
1104 data = {
1105 "date_published": article.date_published.strftime("%Y-%m-%d"),
1106 "article_number": article.article_number,
1107 }
1108 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1109 requests.post(url, json=data, verify=False)
1112class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1113 article = None
1115 def internal_do(self, *args, **kwargs):
1116 lang = self.kwargs.get("lang", None)
1118 translation = None
1119 for trans_article in self.article.translations.all():
1120 if trans_article.lang == lang:
1121 translation = trans_article
1123 if translation is None:
1124 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1126 collection = self.article.get_top_collection()
1127 colid = collection.pid
1128 container = self.article.my_container
1130 if translation.date_published is None:
1131 # Add date posted
1132 cmd = ptf_cmds.publishResourcePtfCmd()
1133 cmd.set_resource(translation)
1134 updated_articles = cmd.do()
1136 # Recompile PDF to add the date posted
1137 try:
1138 tex.create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1139 except Exception:
1140 raise PDFException(
1141 "Unable to compile the article PDF. Please contact the centre Mersenne"
1142 )
1144 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1145 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1146 resolver.copy_binary_files(
1147 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1148 )
1150 # Deploy in prod
1151 xml = ptf_cmds.exportPtfCmd(
1152 {
1153 "pid": self.article.pid,
1154 "with_djvu": False,
1155 "article_standalone": True,
1156 "collection_pid": colid,
1157 "export_to_website": True,
1158 "export_folder": settings.LOG_DIR,
1159 }
1160 ).do()
1161 xml_file = io.StringIO(xml)
1162 files = {"xml": xml_file}
1164 server_url = getattr(collection, "website")()
1165 if not server_url:
1166 raise RuntimeError("The collection has no website")
1167 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1168 header = {}
1170 try:
1171 response = requests.post(
1172 url, headers=header, files=files, verify=False
1173 ) # verify: ignore TLS certificate
1174 status = response.status_code
1175 except requests.exceptions.ConnectionError:
1176 raise ServerUnderMaintenance(
1177 "The journal is under maintenance. Please try again later."
1178 )
1180 # Register translation in Crossref
1181 if 199 < status < 205:
1182 if self.article.allow_crossref():
1183 try:
1184 recordDOI(translation)
1185 except Exception:
1186 raise DOIException(
1187 "Error while recording the DOI. Please contact the centre Mersenne"
1188 )
1190 def get(self, request, *args, **kwargs):
1191 doi = kwargs.get("doi", None)
1192 self.article = model_helpers.get_article_by_doi(doi)
1193 if self.article is None:
1194 raise Http404(f"{doi} does not exist")
1196 try:
1197 _, status, message = history_views.execute_and_record_func(
1198 "deploy",
1199 self.article.pid,
1200 self.article.get_top_collection().pid,
1201 self.internal_do,
1202 "website",
1203 )
1204 except Timeout as exception:
1205 return HttpResponse(exception, status=408)
1206 except Exception as exception:
1207 return HttpResponseServerError(exception)
1209 data = {"message": message, "status": status}
1210 return JsonResponse(data)
1213class DeleteJatsIssueAPIView(View):
1214 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1215 def get(self, request, *args, **kwargs):
1216 pid = self.kwargs.get("pid", None)
1217 colid = self.kwargs.get("colid", None)
1218 site = self.kwargs.get("site", None)
1219 message = "Le volume a bien été supprimé"
1220 status = 200
1222 issue = model_helpers.get_container(pid)
1223 if not issue:
1224 raise Http404(f"{pid} does not exist")
1225 try:
1226 mersenneSite = model_helpers.get_site_mersenne(colid)
1228 if site == "ptf_tools":
1229 if issue.is_deployed(mersenneSite):
1230 issue.undeploy(mersenneSite)
1231 for article in issue.article_set.all():
1232 article.undeploy(mersenneSite)
1234 p = model_helpers.get_provider("mathdoc-id")
1236 cmd = ptf_cmds.addContainerPtfCmd(
1237 {
1238 "pid": issue.pid,
1239 "ctype": "issue",
1240 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1241 }
1242 )
1243 cmd.set_provider(p)
1244 cmd.add_collection(issue.get_collection())
1245 cmd.set_object_to_be_deleted(issue)
1246 cmd.undo()
1248 else:
1249 if site == "numdam":
1250 server_url = settings.NUMDAM_PRE_URL
1251 else:
1252 collection = issue.get_collection()
1253 server_url = getattr(collection, site)()
1255 if not server_url:
1256 message = "The collection has no " + site
1257 status = 500
1258 else:
1259 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1260 response = requests.delete(url, verify=False)
1261 status = response.status_code
1263 if status == 404:
1264 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1265 elif status > 204:
1266 body = response.text.encode("utf8")
1267 message = body[:1000]
1268 else:
1269 status = 200
1270 # unpublish issue in collection site (site_register.json)
1271 if site == "website":
1272 if issue.is_deployed(mersenneSite):
1273 issue.undeploy(mersenneSite)
1274 for article in issue.article_set.all():
1275 article.undeploy(mersenneSite)
1276 # delete article binary files
1277 folder = article.get_relative_folder()
1278 resolver.delete_object_folder(
1279 folder,
1280 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1281 )
1282 # delete issue binary files
1283 folder = issue.get_relative_folder()
1284 resolver.delete_object_folder(
1285 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1286 )
1288 except Timeout as exception:
1289 return HttpResponse(exception, status=408)
1290 except Exception as exception:
1291 return HttpResponseServerError(exception)
1293 data = {"message": message, "status": status}
1294 return JsonResponse(data)
1297class ArchiveIssueAPIView(View):
1298 def get(self, request, *args, **kwargs):
1299 try:
1300 pid = kwargs["pid"]
1301 colid = kwargs["colid"]
1302 except IndexError:
1303 raise Http404
1305 try:
1306 cmd = ptf_cmds.archiveIssuePtfCmd(
1307 {
1308 "pid": pid,
1309 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1310 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1311 }
1312 )
1313 result_, status, message = history_views.execute_and_record_func(
1314 "archive", pid, colid, cmd.do
1315 )
1316 except Exception as exception:
1317 return HttpResponseServerError(exception)
1319 data = {"message": message, "status": 200}
1320 return JsonResponse(data)
1323class CreateDjvuAPIView(View):
1324 def internal_do(self, *args, **kwargs):
1325 pid = self.kwargs.get("pid", None)
1327 resource = model_helpers.get_resource(pid)
1328 cmd = ptf_cmds.addDjvuPtfCmd()
1329 cmd.set_resource(resource)
1330 cmd.do()
1332 def get(self, request, *args, **kwargs):
1333 pid = self.kwargs.get("pid", None)
1334 colid = pid.split("_")[0]
1336 try:
1337 _, status, message = history_views.execute_and_record_func(
1338 "numdam", pid, colid, self.internal_do
1339 )
1340 except Exception as exception:
1341 return HttpResponseServerError(exception)
1343 data = {"message": message, "status": status}
1344 return JsonResponse(data)
1347class PTFToolsHomeView(LoginRequiredMixin, View):
1348 """
1349 Home Page.
1350 - Admin & staff -> Render blank home.html
1351 - User with unique authorized collection -> Redirect to collection details page
1352 - User with multiple authorized collections -> Render home.html with data
1353 - Comment moderator -> Comments dashboard
1354 - Others -> 404 response
1355 """
1357 def get(self, request, *args, **kwargs) -> HttpResponse:
1358 # Staff or user with authorized collections
1359 if request.user.is_staff or request.user.is_superuser:
1360 return render(request, "home.html")
1362 colids = get_authorized_collections(request.user)
1363 is_mod = is_comment_moderator(request.user)
1365 # The user has no rights
1366 if not (colids or is_mod):
1367 raise Http404("No collections associated with your account.")
1368 # Comment moderator only
1369 elif not colids:
1370 return HttpResponseRedirect(reverse("comment_list"))
1372 # User with unique collection -> Redirect to collection detail page
1373 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1374 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1376 # User with multiple authorized collections - Special home
1377 context = {}
1378 context["overview"] = True
1380 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1381 all_collections = {c["pid"]: c for c in all_collections}
1383 # Comments summary
1384 try:
1385 error, comments_data = get_comments_for_home(request.user)
1386 except AttributeError:
1387 error, comments_data = True, {}
1389 context["comment_server_ok"] = False
1391 if not error:
1392 context["comment_server_ok"] = True
1393 if comments_data:
1394 for col_id, comment_nb in comments_data.items():
1395 if col_id.upper() in all_collections: 1395 ↛ 1394line 1395 didn't jump to line 1394 because the condition on line 1395 was always true
1396 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1398 # TODO: Translations summary
1399 context["translation_server_ok"] = False
1401 # Sort the collections according to the number of pending comments
1402 context["collections"] = sorted(
1403 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1404 )
1406 return render(request, "home.html", context)
1409class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1410 columns = 5
1412 def get_common_context_data(self, **kwargs):
1413 context = super().get_context_data(**kwargs)
1414 now = timezone.now()
1415 curyear = now.year
1416 years = range(curyear - self.columns + 1, curyear + 1)
1418 context["collections"] = settings.MERSENNE_COLLECTIONS
1419 context["containers_to_be_published"] = []
1420 context["last_col_events"] = []
1422 event = history_models.get_history_last_event_by("clockss", "ALL")
1423 clockss_gap = history_models.get_gap(now, event)
1425 context["years"] = years
1426 context["clockss_gap"] = clockss_gap
1428 return context
1430 def calculate_articles_and_pages(self, pid, years):
1431 data_by_year = []
1432 total_articles = [0] * len(years)
1433 total_pages = [0] * len(years)
1435 for year in years:
1436 articles = self.get_articles_for_year(pid, year)
1437 articles_count = articles.count()
1438 page_count = sum(article.get_article_page_count() for article in articles)
1440 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1441 total_articles[year - years[0]] += articles_count
1442 total_pages[year - years[0]] += page_count
1444 return data_by_year, total_articles, total_pages
1446 def get_articles_for_year(self, pid, year):
1447 return Article.objects.filter(
1448 Q(my_container__my_collection__pid=pid)
1449 & (
1450 Q(date_published__year=year, date_online_first__isnull=True)
1451 | Q(date_online_first__year=year)
1452 )
1453 ).prefetch_related("resourcecount_set")
1456class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1457 template_name = "dashboard/published_articles.html"
1459 def get_context_data(self, **kwargs):
1460 context = self.get_common_context_data(**kwargs)
1461 years = context["years"]
1463 published_articles = []
1464 total_published_articles = [
1465 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1466 ]
1468 for pid in settings.MERSENNE_COLLECTIONS:
1469 if pid != "MERSENNE":
1470 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1471 pid, years
1472 )
1473 published_articles.append({"pid": pid, "years": articles_data})
1475 for i, year in enumerate(years):
1476 total_published_articles[i]["total_articles"] += total_articles[i]
1477 total_published_articles[i]["total_pages"] += total_pages[i]
1479 context["published_articles"] = published_articles
1480 context["total_published_articles"] = total_published_articles
1482 return context
1485class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1486 template_name = "dashboard/created_volumes.html"
1488 def get_context_data(self, **kwargs):
1489 context = self.get_common_context_data(**kwargs)
1490 years = context["years"]
1492 created_volumes = []
1493 total_created_volumes = [
1494 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1495 ]
1497 for pid in settings.MERSENNE_COLLECTIONS:
1498 if pid != "MERSENNE":
1499 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1500 pid, years
1501 )
1502 created_volumes.append({"pid": pid, "years": volumes_data})
1504 for i, year in enumerate(years):
1505 total_created_volumes[i]["total_articles"] += total_articles[i]
1506 total_created_volumes[i]["total_pages"] += total_pages[i]
1508 context["created_volumes"] = created_volumes
1509 context["total_created_volumes"] = total_created_volumes
1511 return context
1513 def calculate_volumes_and_pages(self, pid, years):
1514 data_by_year = []
1515 total_articles = [0] * len(years)
1516 total_pages = [0] * len(years)
1518 for year in years:
1519 issues = Container.objects.filter(my_collection__pid=pid, year=year)
1520 articles_count = 0
1521 page_count = 0
1523 for issue in issues:
1524 articles = issue.article_set.filter(
1525 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1526 ).prefetch_related("resourcecount_set")
1528 articles_count += articles.count()
1529 page_count += sum(article.get_article_page_count() for article in articles)
1531 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1532 total_articles[year - years[0]] += articles_count
1533 total_pages[year - years[0]] += page_count
1535 return data_by_year, total_articles, total_pages
1538class ReferencingDashboardView(BaseMersenneDashboardView):
1539 template_name = "dashboard/referencing.html"
1541 def get(self, request, *args, **kwargs):
1542 colid = self.kwargs.get("colid", None)
1543 comp = ReferencingChecker()
1544 journal = comp.check_references(colid)
1545 return render(request, self.template_name, {"journal": journal})
1548class BaseCollectionView(TemplateView):
1549 def get_context_data(self, **kwargs):
1550 context = super().get_context_data(**kwargs)
1551 aid = context.get("aid")
1552 year = context.get("year")
1554 if aid and year:
1555 context["collection"] = self.get_collection(aid, year)
1557 return context
1559 def get_collection(self, aid, year):
1560 """Method to be overridden by subclasses to fetch the appropriate collection"""
1561 raise NotImplementedError("Subclasses must implement get_collection method")
1564class ArticleListView(BaseCollectionView):
1565 template_name = "collection-list.html"
1567 def get_collection(self, aid, year):
1568 return Article.objects.filter(
1569 Q(my_container__my_collection__pid=aid)
1570 & (
1571 Q(date_published__year=year, date_online_first__isnull=True)
1572 | Q(date_online_first__year=year)
1573 )
1574 ).prefetch_related("resourcecount_set")
1577class VolumeListView(BaseCollectionView):
1578 template_name = "collection-list.html"
1580 def get_collection(self, aid, year):
1581 return Article.objects.filter(
1582 Q(my_container__my_collection__pid=aid, my_container__year=year)
1583 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1584 ).prefetch_related("resourcecount_set")
1587class DOAJResourceRegisterView(View):
1588 def get(self, request, *args, **kwargs):
1589 pid = kwargs.get("pid", None)
1590 resource = model_helpers.get_resource(pid)
1591 if resource is None:
1592 raise Http404
1594 try:
1595 data = {}
1596 doaj_meta, response = doaj_pid_register(pid)
1597 if response is None:
1598 return HttpResponse(status=204)
1599 elif doaj_meta and 200 <= response.status_code <= 299:
1600 data.update(doaj_meta)
1601 else:
1602 return HttpResponse(status=response.status_code, reason=response.text)
1603 except Timeout as exception:
1604 return HttpResponse(exception, status=408)
1605 except Exception as exception:
1606 return HttpResponseServerError(exception)
1607 return JsonResponse(data)
1610class CROSSREFResourceRegisterView(View):
1611 def get(self, request, *args, **kwargs):
1612 pid = kwargs.get("pid", None)
1613 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1614 force = kwargs.get("force", None)
1615 if not request.user.is_superuser:
1616 force = None
1618 resource = model_helpers.get_resource(pid)
1619 if resource is None:
1620 raise Http404
1622 resource = resource.cast()
1623 meth = getattr(self, "recordDOI" + resource.classname)
1624 try:
1625 data = meth(resource, force)
1626 except Timeout as exception:
1627 return HttpResponse(exception, status=408)
1628 except Exception as exception:
1629 return HttpResponseServerError(exception)
1630 return JsonResponse(data)
1632 def recordDOIArticle(self, article, force=None):
1633 result = {"status": 404}
1634 if (
1635 article.doi
1636 and not article.do_not_publish
1637 and (article.date_published or article.date_online_first or force == "force")
1638 ):
1639 if article.my_container.year is None: # or article.my_container.year == '0':
1640 article.my_container.year = datetime.now().strftime("%Y")
1641 result = recordDOI(article)
1642 return result
1644 def recordDOICollection(self, collection, force=None):
1645 return recordDOI(collection)
1647 def recordDOIContainer(self, container, force=None):
1648 data = {"status": 200, "message": "tout va bien"}
1650 if container.ctype == "issue":
1651 if container.doi:
1652 result = recordDOI(container)
1653 if result["status"] != 200:
1654 return result
1655 if force == "force":
1656 articles = container.article_set.exclude(
1657 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1658 )
1659 else:
1660 articles = container.article_set.exclude(
1661 doi__isnull=True,
1662 do_not_publish=True,
1663 date_published__isnull=True,
1664 date_online_first__isnull=True,
1665 )
1667 for article in articles:
1668 result = self.recordDOIArticle(article, force)
1669 if result["status"] != 200:
1670 data = result
1671 else:
1672 return recordDOI(container)
1673 return data
1676class CROSSREFResourceCheckStatusView(View):
1677 def get(self, request, *args, **kwargs):
1678 pid = kwargs.get("pid", None)
1679 resource = model_helpers.get_resource(pid)
1680 if resource is None:
1681 raise Http404
1682 resource = resource.cast()
1683 meth = getattr(self, "checkDOI" + resource.classname)
1684 try:
1685 meth(resource)
1686 except Timeout as exception:
1687 return HttpResponse(exception, status=408)
1688 except Exception as exception:
1689 return HttpResponseServerError(exception)
1691 data = {"status": 200, "message": "tout va bien"}
1692 return JsonResponse(data)
1694 def checkDOIArticle(self, article):
1695 if article.my_container.year is None or article.my_container.year == "0":
1696 article.my_container.year = datetime.now().strftime("%Y")
1697 get_or_create_doibatch(article)
1699 def checkDOICollection(self, collection):
1700 get_or_create_doibatch(collection)
1702 def checkDOIContainer(self, container):
1703 if container.doi is not None:
1704 get_or_create_doibatch(container)
1705 for article in container.article_set.all():
1706 self.checkDOIArticle(article)
1709class RegisterPubmedFormView(FormView):
1710 template_name = "record_pubmed_dialog.html"
1711 form_class = RegisterPubmedForm
1713 def get_context_data(self, **kwargs):
1714 context = super().get_context_data(**kwargs)
1715 context["pid"] = self.kwargs["pid"]
1716 context["helper"] = PtfLargeModalFormHelper
1717 return context
1720class RegisterPubmedView(View):
1721 def get(self, request, *args, **kwargs):
1722 pid = kwargs.get("pid", None)
1723 update_article = self.request.GET.get("update_article", "on") == "on"
1725 article = model_helpers.get_article(pid)
1726 if article is None:
1727 raise Http404
1728 try:
1729 recordPubmed(article, update_article)
1730 except Exception as exception:
1731 messages.error("Unable to register the article in PubMed")
1732 return HttpResponseServerError(exception)
1734 return HttpResponseRedirect(
1735 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1736 )
1739class PTFToolsContainerView(TemplateView):
1740 template_name = ""
1742 def get_context_data(self, **kwargs):
1743 context = super().get_context_data(**kwargs)
1745 container = model_helpers.get_container(self.kwargs.get("pid"))
1746 if container is None:
1747 raise Http404
1748 citing_articles = container.citations()
1749 source = self.request.GET.get("source", None)
1750 if container.ctype.startswith("book"):
1751 book_parts = (
1752 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1753 )
1754 references = False
1755 if container.ctype == "book-monograph":
1756 # on regarde si il y a au moins une bibliographie
1757 for art in container.article_set.all():
1758 if art.bibitem_set.count() > 0:
1759 references = True
1760 context.update(
1761 {
1762 "book": container,
1763 "book_parts": list(book_parts),
1764 "source": source,
1765 "citing_articles": citing_articles,
1766 "references": references,
1767 "test_website": container.get_top_collection()
1768 .extlink_set.get(rel="test_website")
1769 .location,
1770 "prod_website": container.get_top_collection()
1771 .extlink_set.get(rel="website")
1772 .location,
1773 }
1774 )
1775 self.template_name = "book-toc.html"
1776 else:
1777 articles = container.article_set.all().order_by("seq")
1778 for article in articles:
1779 try:
1780 last_match = (
1781 history_models.HistoryEvent.objects.filter(
1782 pid=article.pid,
1783 type="matching",
1784 )
1785 .only("created_on")
1786 .latest("created_on")
1787 )
1788 except history_models.HistoryEvent.DoesNotExist as _:
1789 article.last_match = None
1790 else:
1791 article.last_match = last_match.created_on
1793 # article1 = articles.first()
1794 # date = article1.deployed_date()
1795 # TODO next_issue, previous_issue
1797 # check DOI est maintenant une commande à part
1798 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
1799 # articlesWithStatus = []
1800 # for article in articles:
1801 # get_or_create_doibatch(article)
1802 # articlesWithStatus.append(article)
1804 test_location = prod_location = ""
1805 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
1806 if qs:
1807 test_location = qs.first().location
1808 qs = container.get_top_collection().extlink_set.filter(rel="website")
1809 if qs:
1810 prod_location = qs.first().location
1811 context.update(
1812 {
1813 "issue": container,
1814 "articles": articles,
1815 "source": source,
1816 "citing_articles": citing_articles,
1817 "test_website": test_location,
1818 "prod_website": prod_location,
1819 }
1820 )
1821 self.template_name = "issue-items.html"
1823 context["allow_crossref"] = container.allow_crossref()
1824 context["coltype"] = container.my_collection.coltype
1825 return context
1828class ExtLinkInline(InlineFormSetFactory):
1829 model = ExtLink
1830 form_class = ExtLinkForm
1831 factory_kwargs = {"extra": 0}
1834class ResourceIdInline(InlineFormSetFactory):
1835 model = ResourceId
1836 form_class = ResourceIdForm
1837 factory_kwargs = {"extra": 0}
1840class IssueDetailAPIView(View):
1841 def get(self, request, *args, **kwargs):
1842 issue = get_object_or_404(Container, pid=kwargs["pid"])
1843 deployed_date = issue.deployed_date()
1844 result = {
1845 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
1846 if deployed_date
1847 else None,
1848 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
1849 "all_doi_are_registered": issue.all_doi_are_registered(),
1850 "registered_in_doaj": issue.registered_in_doaj(),
1851 "doi": issue.my_collection.doi,
1852 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
1853 }
1854 try:
1855 latest = history_models.HistoryEvent.objects.get_last_unsolved_error(
1856 pid=issue.pid, strict=False
1857 )
1858 except history_models.HistoryEvent.DoesNotExist as _:
1859 pass
1860 else:
1861 result["latest"] = latest.data["message"]
1862 result["latest_target"] = latest.data.get("target", "")
1863 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
1864 "%Y-%m-%d %H:%M"
1865 )
1867 result["latest_type"] = latest.type.capitalize()
1868 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
1869 try:
1870 result[event_type] = timezone.localtime(
1871 history_models.HistoryEvent.objects.filter(
1872 type=event_type,
1873 status="OK",
1874 pid__startswith=issue.pid,
1875 )
1876 .latest("created_on")
1877 .created_on
1878 ).strftime("%Y-%m-%d %H:%M")
1879 except history_models.HistoryEvent.DoesNotExist as _:
1880 result[event_type] = ""
1881 return JsonResponse(result)
1884class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
1885 model = Collection
1886 form_class = CollectionForm
1887 inlines = [ResourceIdInline, ExtLinkInline]
1888 inlines_names = ["resource_ids_form", "ext_links_form"]
1890 def get_context_data(self, **kwargs):
1891 context = super().get_context_data(**kwargs)
1892 context["helper"] = PtfFormHelper
1893 context["formset_helper"] = FormSetHelper
1894 return context
1896 def add_description(self, collection, description, lang, seq):
1897 if description:
1898 la = Abstract(
1899 resource=collection,
1900 tag="description",
1901 lang=lang,
1902 seq=seq,
1903 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
1904 value_html=description,
1905 value_tex=description,
1906 )
1907 la.save()
1909 def form_valid(self, form):
1910 if form.instance.abbrev:
1911 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
1912 else:
1913 form.instance.title_xml = (
1914 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
1915 )
1917 form.instance.title_html = form.instance.title_tex
1918 form.instance.title_sort = form.instance.title_tex
1919 result = super().form_valid(form)
1921 collection = self.object
1922 collection.abstract_set.all().delete()
1924 seq = 1
1925 description = form.cleaned_data["description_en"]
1926 if description:
1927 self.add_description(collection, description, "en", seq)
1928 seq += 1
1929 description = form.cleaned_data["description_fr"]
1930 if description:
1931 self.add_description(collection, description, "fr", seq)
1933 return result
1935 def get_success_url(self):
1936 messages.success(self.request, "La Collection a été modifiée avec succès")
1937 return reverse("collection-detail", kwargs={"pid": self.object.pid})
1940class CollectionCreate(CollectionFormView, CreateWithInlinesView):
1941 """
1942 Warning : Not yet finished
1943 Automatic site membership creation is still missing
1944 """
1947class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
1948 slug_field = "pid"
1949 slug_url_kwarg = "pid"
1952def suggest_load_journal_dois(colid):
1953 articles = (
1954 Article.objects.filter(my_container__my_collection__pid=colid)
1955 .filter(doi__isnull=False)
1956 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1957 .values_list("doi", flat=True)
1958 )
1960 try:
1961 articles = sorted(
1962 articles,
1963 key=lambda d: (
1964 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
1965 int(re.search(r".(\d+)$", d).group(1)),
1966 ),
1967 )
1968 except: # noqa: E722 (we'll look later)
1969 pass
1970 return [f'<option value="{doi}">' for doi in articles]
1973def get_context_with_volumes(journal):
1974 result = model_helpers.get_volumes_in_collection(journal)
1975 volume_count = result["volume_count"]
1976 collections = []
1977 for ancestor in journal.ancestors.all():
1978 item = model_helpers.get_volumes_in_collection(ancestor)
1979 volume_count = max(0, volume_count)
1980 item.update({"journal": ancestor})
1981 collections.append(item)
1983 # add the parent collection to its children list and sort it by date
1984 result.update({"journal": journal})
1985 collections.append(result)
1987 collections = [c for c in collections if c["sorted_issues"]]
1988 collections.sort(
1989 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
1990 reverse=True,
1991 )
1993 context = {
1994 "journal": journal,
1995 "sorted_issues": result["sorted_issues"],
1996 "volume_count": volume_count,
1997 "max_width": result["max_width"],
1998 "collections": collections,
1999 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2000 }
2001 return context
2004class CollectionDetail(
2005 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2006):
2007 model = Collection
2008 slug_field = "pid"
2009 slug_url_kwarg = "pid"
2010 template_name = "ptf/collection_detail.html"
2012 def test_func(self):
2013 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2015 def get(self, request, *args, **kwargs):
2016 self.object = self.get_object(queryset=Collection.objects.all())
2017 return super().get(request, *args, **kwargs)
2019 def get_context_data(self, **kwargs):
2020 context = super().get_context_data(**kwargs)
2021 context["object_list"] = context["object_list"].filter(ctype="issue")
2022 context.update(get_context_with_volumes(self.object))
2024 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2025 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2026 context["issue_to_appear"] = Container.objects.filter(
2027 pid=context["issue_to_appear_pid"]
2028 ).exists()
2029 try:
2030 latest_error = history_models.HistoryEvent.objects.get_last_unsolved_error(
2031 self.object.pid,
2032 strict=True,
2033 )
2034 except history_models.HistoryEvent.DoesNotExist as _:
2035 pass
2036 else:
2037 message = latest_error.data["message"]
2038 i = message.find(" - ")
2039 latest_exception = message[:i]
2040 latest_error_message = message[i + 3 :]
2041 context["latest_exception"] = latest_exception
2042 context["latest_exception_date"] = latest_error.created_on
2043 context["latest_exception_type"] = latest_error.type
2044 context["latest_error_message"] = latest_error_message
2045 return context
2047 def get_queryset(self):
2048 query = self.object.content.all()
2050 for ancestor in self.object.ancestors.all():
2051 query |= ancestor.content.all()
2053 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2056class ContainerEditView(FormView):
2057 template_name = "container_form.html"
2058 form_class = ContainerForm
2060 def get_success_url(self):
2061 if self.kwargs["pid"]:
2062 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2063 return reverse("mersenne_dashboard/published_articles")
2065 def set_success_message(self): # pylint: disable=no-self-use
2066 messages.success(self.request, "Le fascicule a été modifié")
2068 def get_form_kwargs(self):
2069 kwargs = super().get_form_kwargs()
2070 if "pid" not in self.kwargs:
2071 self.kwargs["pid"] = None
2072 if "colid" not in self.kwargs:
2073 self.kwargs["colid"] = None
2074 if "data" in kwargs and "colid" in kwargs["data"]:
2075 # colid is passed as a hidden param in the form.
2076 # It is used when you submit a new container
2077 self.kwargs["colid"] = kwargs["data"]["colid"]
2079 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2080 self.kwargs["pid"]
2081 )
2082 return kwargs
2084 def get_context_data(self, **kwargs):
2085 context = super().get_context_data(**kwargs)
2087 context["pid"] = self.kwargs["pid"]
2088 context["colid"] = self.kwargs["colid"]
2089 context["container"] = self.kwargs["container"]
2091 context["edit_container"] = context["pid"] is not None
2092 context["name"] = resolve(self.request.path_info).url_name
2094 return context
2096 def form_valid(self, form):
2097 new_pid = form.cleaned_data.get("pid")
2098 new_title = form.cleaned_data.get("title")
2099 new_trans_title = form.cleaned_data.get("trans_title")
2100 new_publisher = form.cleaned_data.get("publisher")
2101 new_year = form.cleaned_data.get("year")
2102 new_volume = form.cleaned_data.get("volume")
2103 new_number = form.cleaned_data.get("number")
2105 collection = None
2106 issue = self.kwargs["container"]
2107 if issue is not None:
2108 collection = issue.my_collection
2109 elif self.kwargs["colid"] is not None:
2110 if "CR" in self.kwargs["colid"]:
2111 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2112 else:
2113 collection = model_helpers.get_collection(self.kwargs["colid"])
2115 if collection is None:
2116 raise ValueError("Collection for " + new_pid + " does not exist")
2118 # Icon
2119 new_icon_location = ""
2120 if "icon" in self.request.FILES:
2121 filename = os.path.basename(self.request.FILES["icon"].name)
2122 file_extension = filename.split(".")[1]
2124 icon_filename = resolver.get_disk_location(
2125 settings.MERSENNE_TEST_DATA_FOLDER,
2126 collection.pid,
2127 file_extension,
2128 new_pid,
2129 None,
2130 True,
2131 )
2133 with open(icon_filename, "wb+") as destination:
2134 for chunk in self.request.FILES["icon"].chunks():
2135 destination.write(chunk)
2137 folder = resolver.get_relative_folder(collection.pid, new_pid)
2138 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2139 name = resolve(self.request.path_info).url_name
2140 if name == "special_issue_create":
2141 self.kwargs["name"] = name
2142 if self.kwargs["container"]:
2143 # Edit Issue
2144 issue = self.kwargs["container"]
2145 if issue is None:
2146 raise ValueError(self.kwargs["pid"] + " does not exist")
2148 if new_trans_title and (not issue.trans_lang or issue.trans_lang == "und"):
2149 issue.trans_lang = "fr" if issue.lang == "en" else "en"
2151 issue.pid = new_pid
2152 issue.title_tex = issue.title_html = new_title
2153 issue.trans_title_tex = issue.trans_title_html = new_trans_title
2154 issue.title_xml = get_issue_title_xml(
2155 new_title, issue.lang, new_trans_title, issue.trans_lang
2156 )
2157 issue.year = new_year
2158 issue.volume = new_volume
2159 issue.volume_int = make_int(new_volume)
2160 issue.number = new_number
2161 issue.number_int = make_int(new_number)
2162 issue.save()
2163 else:
2164 xissue = create_issuedata()
2166 xissue.ctype = "issue"
2167 xissue.pid = new_pid
2168 # TODO: add lang + trans_lang
2169 xissue.title_tex = new_title
2170 xissue.title_html = new_title
2171 xissue.trans_title_tex = new_trans_title
2172 xissue.title_xml = get_issue_title_xml(new_title)
2174 # new_title, new_trans_title, issue.trans_lang
2175 # )
2176 xissue.year = new_year
2177 xissue.volume = new_volume
2178 xissue.number = new_number
2179 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2181 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2182 cmd.add_collection(collection)
2183 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2184 issue = cmd.do()
2186 self.kwargs["pid"] = new_pid
2188 # Add objects related to the article: contribs, datastream, counts...
2189 params = {
2190 "icon_location": new_icon_location,
2191 }
2192 cmd = ptf_cmds.updateContainerPtfCmd(params)
2193 cmd.set_resource(issue)
2194 cmd.do()
2196 publisher = model_helpers.get_publisher(new_publisher)
2197 if not publisher:
2198 xpub = create_publisherdata()
2199 xpub.name = new_publisher
2200 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2201 issue.my_publisher = publisher
2202 issue.save()
2204 self.set_success_message()
2206 return super().form_valid(form)
2209# class ArticleEditView(FormView):
2210# template_name = 'article_form.html'
2211# form_class = ArticleForm
2212#
2213# def get_success_url(self):
2214# if self.kwargs['pid']:
2215# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2216# return reverse('mersenne_dashboard/published_articles')
2217#
2218# def set_success_message(self): # pylint: disable=no-self-use
2219# messages.success(self.request, "L'article a été modifié")
2220#
2221# def get_form_kwargs(self):
2222# kwargs = super(ArticleEditView, self).get_form_kwargs()
2223#
2224# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2225# # Article creation: pid is None
2226# self.kwargs['pid'] = None
2227# if 'issue_id' not in self.kwargs:
2228# # Article edit: issue_id is not passed
2229# self.kwargs['issue_id'] = None
2230# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2231# # colid is passed as a hidden param in the form.
2232# # It is used when you submit a new container
2233# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2234#
2235# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2236# return kwargs
2237#
2238# def get_context_data(self, **kwargs):
2239# context = super(ArticleEditView, self).get_context_data(**kwargs)
2240#
2241# context['pid'] = self.kwargs['pid']
2242# context['issue_id'] = self.kwargs['issue_id']
2243# context['article'] = self.kwargs['article']
2244#
2245# context['edit_article'] = context['pid'] is not None
2246#
2247# article = context['article']
2248# if article:
2249# context['author_contributions'] = article.get_author_contributions()
2250# context['kwds_fr'] = None
2251# context['kwds_en'] = None
2252# kwd_gps = article.get_non_msc_kwds()
2253# for kwd_gp in kwd_gps:
2254# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2255# if kwd_gp.value_xml:
2256# kwd_ = types.SimpleNamespace()
2257# kwd_.value = kwd_gp.value_tex
2258# context['kwd_unstructured_fr'] = kwd_
2259# context['kwds_fr'] = kwd_gp.kwd_set.all()
2260# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2261# if kwd_gp.value_xml:
2262# kwd_ = types.SimpleNamespace()
2263# kwd_.value = kwd_gp.value_tex
2264# context['kwd_unstructured_en'] = kwd_
2265# context['kwds_en'] = kwd_gp.kwd_set.all()
2266#
2267# # Article creation: init pid
2268# if context['issue_id'] and context['pid'] is None:
2269# issue = model_helpers.get_container(context['issue_id'])
2270# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2271#
2272# return context
2273#
2274# def form_valid(self, form):
2275#
2276# new_pid = form.cleaned_data.get('pid')
2277# new_title = form.cleaned_data.get('title')
2278# new_fpage = form.cleaned_data.get('fpage')
2279# new_lpage = form.cleaned_data.get('lpage')
2280# new_page_range = form.cleaned_data.get('page_range')
2281# new_page_count = form.cleaned_data.get('page_count')
2282# new_coi_statement = form.cleaned_data.get('coi_statement')
2283# new_show_body = form.cleaned_data.get('show_body')
2284# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2285#
2286# # TODO support MathML
2287# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2288# # We need to pass trans_title to get_title_xml
2289# # Meanwhile, ignore new_title_xml
2290# new_title_xml = jats_parser.get_title_xml(new_title)
2291# new_title_html = new_title
2292#
2293# authors_count = int(self.request.POST.get('authors_count', "0"))
2294# i = 1
2295# new_authors = []
2296# old_author_contributions = []
2297# if self.kwargs['article']:
2298# old_author_contributions = self.kwargs['article'].get_author_contributions()
2299#
2300# while authors_count > 0:
2301# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2302#
2303# if prefix is not None:
2304# addresses = []
2305# if len(old_author_contributions) >= i:
2306# old_author_contribution = old_author_contributions[i - 1]
2307# addresses = [contrib_address.address for contrib_address in
2308# old_author_contribution.get_addresses()]
2309#
2310# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2311# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2312# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2313# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2314# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2315# deceased_before_publication = deceased == 'on'
2316# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2317# equal_contrib = equal_contrib == 'on'
2318# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2319# corresponding = corresponding == 'on'
2320# email = self.request.POST.get('email-' + str(i), None)
2321#
2322# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2323# params['deceased_before_publication'] = deceased_before_publication
2324# params['equal_contrib'] = equal_contrib
2325# params['corresponding'] = corresponding
2326# params['addresses'] = addresses
2327# params['email'] = email
2328#
2329# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2330#
2331# new_authors.append(params)
2332#
2333# authors_count -= 1
2334# i += 1
2335#
2336# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2337# i = 1
2338# new_kwds_fr = []
2339# while kwds_fr_count > 0:
2340# value = self.request.POST.get('kwd-fr-' + str(i), None)
2341# new_kwds_fr.append(value)
2342# kwds_fr_count -= 1
2343# i += 1
2344# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2345#
2346# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2347# i = 1
2348# new_kwds_en = []
2349# while kwds_en_count > 0:
2350# value = self.request.POST.get('kwd-en-' + str(i), None)
2351# new_kwds_en.append(value)
2352# kwds_en_count -= 1
2353# i += 1
2354# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2355#
2356# if self.kwargs['article']:
2357# # Edit article
2358# container = self.kwargs['article'].my_container
2359# else:
2360# # New article
2361# container = model_helpers.get_container(self.kwargs['issue_id'])
2362#
2363# if container is None:
2364# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2365#
2366# collection = container.my_collection
2367#
2368# # Copy PDF file & extract full text
2369# body = ''
2370# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2371# collection.pid,
2372# "pdf",
2373# container.pid,
2374# new_pid,
2375# True)
2376# if 'pdf' in self.request.FILES:
2377# with open(pdf_filename, 'wb+') as destination:
2378# for chunk in self.request.FILES['pdf'].chunks():
2379# destination.write(chunk)
2380#
2381# # Extract full text from the PDF
2382# body = utils.pdf_to_text(pdf_filename)
2383#
2384# # Icon
2385# new_icon_location = ''
2386# if 'icon' in self.request.FILES:
2387# filename = os.path.basename(self.request.FILES['icon'].name)
2388# file_extension = filename.split('.')[1]
2389#
2390# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2391# collection.pid,
2392# file_extension,
2393# container.pid,
2394# new_pid,
2395# True)
2396#
2397# with open(icon_filename, 'wb+') as destination:
2398# for chunk in self.request.FILES['icon'].chunks():
2399# destination.write(chunk)
2400#
2401# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2402# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2403#
2404# if self.kwargs['article']:
2405# # Edit article
2406# article = self.kwargs['article']
2407# article.fpage = new_fpage
2408# article.lpage = new_lpage
2409# article.page_range = new_page_range
2410# article.coi_statement = new_coi_statement
2411# article.show_body = new_show_body
2412# article.do_not_publish = new_do_not_publish
2413# article.save()
2414#
2415# else:
2416# # New article
2417# params = {
2418# 'pid': new_pid,
2419# 'title_xml': new_title_xml,
2420# 'title_html': new_title_html,
2421# 'title_tex': new_title,
2422# 'fpage': new_fpage,
2423# 'lpage': new_lpage,
2424# 'page_range': new_page_range,
2425# 'seq': container.article_set.count() + 1,
2426# 'body': body,
2427# 'coi_statement': new_coi_statement,
2428# 'show_body': new_show_body,
2429# 'do_not_publish': new_do_not_publish
2430# }
2431#
2432# xarticle = create_articledata()
2433# xarticle.pid = new_pid
2434# xarticle.title_xml = new_title_xml
2435# xarticle.title_html = new_title_html
2436# xarticle.title_tex = new_title
2437# xarticle.fpage = new_fpage
2438# xarticle.lpage = new_lpage
2439# xarticle.page_range = new_page_range
2440# xarticle.seq = container.article_set.count() + 1
2441# xarticle.body = body
2442# xarticle.coi_statement = new_coi_statement
2443# params['xobj'] = xarticle
2444#
2445# cmd = ptf_cmds.addArticlePtfCmd(params)
2446# cmd.set_container(container)
2447# cmd.add_collection(container.my_collection)
2448# article = cmd.do()
2449#
2450# self.kwargs['pid'] = new_pid
2451#
2452# # Add objects related to the article: contribs, datastream, counts...
2453# params = {
2454# # 'title_xml': new_title_xml,
2455# # 'title_html': new_title_html,
2456# # 'title_tex': new_title,
2457# 'authors': new_authors,
2458# 'page_count': new_page_count,
2459# 'icon_location': new_icon_location,
2460# 'body': body,
2461# 'use_kwds': True,
2462# 'kwds_fr': new_kwds_fr,
2463# 'kwds_en': new_kwds_en,
2464# 'kwd_uns_fr': new_kwd_uns_fr,
2465# 'kwd_uns_en': new_kwd_uns_en
2466# }
2467# cmd = ptf_cmds.updateArticlePtfCmd(params)
2468# cmd.set_article(article)
2469# cmd.do()
2470#
2471# self.set_success_message()
2472#
2473# return super(ArticleEditView, self).form_valid(form)
2476@require_http_methods(["POST"])
2477def do_not_publish_article(request, *args, **kwargs):
2478 next = request.headers.get("referer")
2480 pid = kwargs.get("pid", "")
2482 article = model_helpers.get_article(pid)
2483 if article:
2484 article.do_not_publish = not article.do_not_publish
2485 article.save()
2486 else:
2487 raise Http404
2489 return HttpResponseRedirect(next)
2492@require_http_methods(["POST"])
2493def show_article_body(request, *args, **kwargs):
2494 next = request.headers.get("referer")
2496 pid = kwargs.get("pid", "")
2498 article = model_helpers.get_article(pid)
2499 if article:
2500 article.show_body = not article.show_body
2501 article.save()
2502 else:
2503 raise Http404
2505 return HttpResponseRedirect(next)
2508class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditAPIView):
2509 """
2510 API to get/post article metadata
2511 The class is derived from ArticleEditAPIView (see ptf.views)
2512 """
2514 def __init__(self, *args, **kwargs):
2515 super().__init__(*args, **kwargs)
2516 self.fields_to_update = [
2517 "lang",
2518 "title_xml",
2519 "title_tex",
2520 "title_html",
2521 "trans_lang",
2522 "trans_title_html",
2523 "trans_title_tex",
2524 "trans_title_xml",
2525 "atype",
2526 "contributors",
2527 "abstracts",
2528 "subjs",
2529 "kwds",
2530 "ext_links",
2531 ]
2533 def convert_data_for_editor(self, data_article):
2534 super().convert_data_for_editor(data_article)
2535 data_article.is_staff = self.request.user.is_staff
2537 def save_data(self, data_article):
2538 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2539 # The icons are not preserved since we can add/edit/delete them in VueJs
2540 params = {
2541 "pid": data_article.pid,
2542 "export_folder": settings.MERSENNE_TMP_FOLDER,
2543 "export_all": True,
2544 "with_binary_files": False,
2545 }
2546 ptf_cmds.exportExtraDataPtfCmd(params).do()
2548 def restore_data(self, article):
2549 ptf_cmds.importExtraDataPtfCmd(
2550 {
2551 "pid": article.pid,
2552 "import_folder": settings.MERSENNE_TMP_FOLDER,
2553 }
2554 ).do()
2557class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2558 template_name = "article_form.html"
2560 def get_success_url(self):
2561 if self.kwargs["doi"]:
2562 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2563 return reverse("mersenne_dashboard/published_articles")
2565 def get_context_data(self, **kwargs):
2566 context = super().get_context_data(**kwargs)
2567 if "doi" in self.kwargs:
2568 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"])
2569 context["pid"] = context["article"].pid
2571 return context
2574class ArticleDeleteView(View):
2575 def get(self, request, *args, **kwargs):
2576 pid = self.kwargs.get("pid", None)
2577 article = get_object_or_404(Article, pid=pid)
2579 try:
2580 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2581 article.undeploy(mersenneSite)
2583 cmd = ptf_cmds.addArticlePtfCmd(
2584 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2585 )
2586 cmd.set_container(article.my_container)
2587 cmd.set_object_to_be_deleted(article)
2588 cmd.undo()
2589 except Exception as exception:
2590 return HttpResponseServerError(exception)
2592 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2593 return JsonResponse(data)
2596def get_messages_in_queue():
2597 app = Celery("ptf-tools")
2598 # tasks = list(current_app.tasks)
2599 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2600 print(tasks)
2601 # i = app.control.inspect()
2603 with app.connection_or_acquire() as conn:
2604 remaining = conn.default_channel.queue_declare(queue="celery", passive=True).message_count
2605 return remaining
2608class FailedTasksListView(ListView):
2609 model = TaskResult
2610 queryset = TaskResult.objects.filter(
2611 status="FAILURE",
2612 task_name="ptf_tools.tasks.archive_numdam_issue",
2613 )
2616class FailedTasksDeleteView(DeleteView):
2617 model = TaskResult
2618 success_url = reverse_lazy("tasks-failed")
2621class FailedTasksRetryView(SingleObjectMixin, RedirectView):
2622 model = TaskResult
2624 @staticmethod
2625 def retry_task(task):
2626 colid, pid = (arg.strip("'") for arg in task.task_args.strip("()").split(", "))
2627 archive_numdam_issue.delay(colid, pid)
2628 task.delete()
2630 def get_redirect_url(self, *args, **kwargs):
2631 self.retry_task(self.get_object())
2632 return reverse("tasks-failed")
2635class NumdamView(TemplateView, history_views.HistoryContextMixin):
2636 template_name = "numdam.html"
2638 def get_context_data(self, **kwargs):
2639 context = super().get_context_data(**kwargs)
2641 context["objs"] = ResourceInNumdam.objects.all()
2643 pre_issues = []
2644 prod_issues = []
2645 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2646 try:
2647 response = requests.get(url)
2648 if response.status_code == 200:
2649 data = response.json()
2650 if "issues" in data:
2651 pre_issues = data["issues"]
2652 except Exception:
2653 pass
2655 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2656 response = requests.get(url)
2657 if response.status_code == 200:
2658 data = response.json()
2659 if "issues" in data:
2660 prod_issues = data["issues"]
2662 new = sorted(list(set(pre_issues).difference(prod_issues)))
2663 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2664 grouped = [
2665 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2666 ]
2667 grouped_removed = [
2668 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2669 ]
2670 context["added_issues"] = grouped
2671 context["removed_issues"] = grouped_removed
2673 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2674 return context
2677class TasksProgressView(View):
2678 def get(self, *args, **kwargs):
2679 task_name = self.kwargs.get("task", "archive_numdam_issue")
2680 successes = TaskResult.objects.filter(
2681 task_name=f"ptf_tools.tasks.{task_name}", status="SUCCESS"
2682 ).count()
2683 fails = TaskResult.objects.filter(
2684 task_name=f"ptf_tools.tasks.{task_name}", status="FAILURE"
2685 ).count()
2686 last_task = (
2687 TaskResult.objects.filter(
2688 task_name=f"ptf_tools.tasks.{task_name}",
2689 status="SUCCESS",
2690 )
2691 .order_by("-date_done")
2692 .first()
2693 )
2694 if last_task:
2695 last_task = " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
2696 remaining = get_messages_in_queue()
2697 all = successes + remaining
2698 progress = int(successes * 100 / all) if all else 0
2699 error_rate = int(fails * 100 / all) if all else 0
2700 status = "consuming_queue" if (successes or fails) and not progress == 100 else "polling"
2701 data = {
2702 "status": status,
2703 "progress": progress,
2704 "total": all,
2705 "remaining": remaining,
2706 "successes": successes,
2707 "fails": fails,
2708 "error_rate": error_rate,
2709 "last_task": last_task,
2710 }
2711 return JsonResponse(data)
2714class TasksView(TemplateView):
2715 template_name = "tasks.html"
2717 def get_context_data(self, **kwargs):
2718 context = super().get_context_data(**kwargs)
2719 context["tasks"] = TaskResult.objects.all()
2720 return context
2723class NumdamArchiveView(RedirectView):
2724 @staticmethod
2725 def reset_task_results():
2726 TaskResult.objects.all().delete()
2728 def get_redirect_url(self, *args, **kwargs):
2729 self.colid = kwargs["colid"]
2731 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2732 return Http404
2734 # we make sure archiving is not already running
2735 if not get_messages_in_queue():
2736 self.reset_task_results()
2737 response = requests.get(f"{settings.NUMDAM_URL}/api-all-collections/")
2738 if response.status_code == 200:
2739 data = sorted(response.json()["collections"])
2741 if self.colid != "ALL" and self.colid not in data:
2742 return Http404
2744 colids = [self.colid] if self.colid != "ALL" else data
2746 with open(
2747 os.path.join(settings.LOG_DIR, "archive.log"), "w", encoding="utf-8"
2748 ) as file_:
2749 file_.write("Archive " + " ".join([colid for colid in colids]) + "\n")
2751 for colid in colids:
2752 if colid not in settings.MERSENNE_COLLECTIONS:
2753 archive_numdam_collection.delay(colid)
2754 return reverse("numdam")
2757class DeployAllNumdamAPIView(View):
2758 def internal_do(self, *args, **kwargs):
2759 pids = []
2761 for obj in ResourceInNumdam.objects.all():
2762 pids.append(obj.pid)
2764 return pids
2766 def get(self, request, *args, **kwargs):
2767 try:
2768 pids, status, message = history_views.execute_and_record_func(
2769 "deploy", "numdam", "numdam", self.internal_do, "numdam"
2770 )
2771 except Exception as exception:
2772 return HttpResponseServerError(exception)
2774 data = {"message": message, "ids": pids, "status": status}
2775 return JsonResponse(data)
2778class NumdamDeleteAPIView(View):
2779 def get(self, request, *args, **kwargs):
2780 pid = self.kwargs.get("pid", None)
2782 try:
2783 obj = ResourceInNumdam.objects.get(pid=pid)
2784 obj.delete()
2785 except Exception as exception:
2786 return HttpResponseServerError(exception)
2788 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
2789 return JsonResponse(data)
2792class ExtIdApiDetail(View):
2793 def get(self, request, *args, **kwargs):
2794 extid = get_object_or_404(
2795 ExtId,
2796 resource__pid=kwargs["pid"],
2797 id_type=kwargs["what"],
2798 )
2799 return JsonResponse(
2800 {
2801 "pk": extid.pk,
2802 "href": extid.get_href(),
2803 "fetch": reverse(
2804 "api-fetch-id",
2805 args=(
2806 extid.resource.pk,
2807 extid.id_value,
2808 extid.id_type,
2809 "extid",
2810 ),
2811 ),
2812 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
2813 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
2814 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
2815 "delete": reverse("update-extid", args=(extid.pk, "delete")),
2816 "is_valid": extid.checked,
2817 }
2818 )
2821class ExtIdFormTemplate(TemplateView):
2822 template_name = "common/externalid_form.html"
2824 def get_context_data(self, **kwargs):
2825 context = super().get_context_data(**kwargs)
2826 context["sequence"] = kwargs["sequence"]
2827 return context
2830class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
2831 def get_context_data(self, **kwargs):
2832 context = super().get_context_data(**kwargs)
2833 context["helper"] = PtfFormHelper
2834 return context
2836 def get_success_url(self):
2837 self.post_process()
2838 return self.object.bibitem.resource.get_absolute_url()
2840 def post_process(self):
2841 cmd = xml_cmds.updateBibitemCitationXmlCmd()
2842 cmd.set_bibitem(self.object.bibitem)
2843 cmd.do()
2844 model_helpers.post_resource_updated(self.object.bibitem.resource)
2847class BibItemIdCreate(BibItemIdFormView, CreateView):
2848 model = BibItemId
2849 form_class = BibItemIdForm
2851 def get_context_data(self, **kwargs):
2852 context = super().get_context_data(**kwargs)
2853 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
2854 return context
2856 def get_initial(self):
2857 initial = super().get_initial()
2858 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
2859 return initial
2861 def form_valid(self, form):
2862 form.instance.checked = False
2863 return super().form_valid(form)
2866class BibItemIdUpdate(BibItemIdFormView, UpdateView):
2867 model = BibItemId
2868 form_class = BibItemIdForm
2870 def get_context_data(self, **kwargs):
2871 context = super().get_context_data(**kwargs)
2872 context["bibitem"] = self.object.bibitem
2873 return context
2876class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
2877 def get_context_data(self, **kwargs):
2878 context = super().get_context_data(**kwargs)
2879 context["helper"] = PtfFormHelper
2880 return context
2882 def get_success_url(self):
2883 self.post_process()
2884 return self.object.resource.get_absolute_url()
2886 def post_process(self):
2887 model_helpers.post_resource_updated(self.object.resource)
2890class ExtIdCreate(ExtIdFormView, CreateView):
2891 model = ExtId
2892 form_class = ExtIdForm
2894 def get_context_data(self, **kwargs):
2895 context = super().get_context_data(**kwargs)
2896 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
2897 return context
2899 def get_initial(self):
2900 initial = super().get_initial()
2901 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
2902 return initial
2904 def form_valid(self, form):
2905 form.instance.checked = False
2906 return super().form_valid(form)
2909class ExtIdUpdate(ExtIdFormView, UpdateView):
2910 model = ExtId
2911 form_class = ExtIdForm
2913 def get_context_data(self, **kwargs):
2914 context = super().get_context_data(**kwargs)
2915 context["resource"] = self.object.resource
2916 return context
2919class BibItemIdApiDetail(View):
2920 def get(self, request, *args, **kwargs):
2921 bibitemid = get_object_or_404(
2922 BibItemId,
2923 bibitem__resource__pid=kwargs["pid"],
2924 bibitem__sequence=kwargs["seq"],
2925 id_type=kwargs["what"],
2926 )
2927 return JsonResponse(
2928 {
2929 "pk": bibitemid.pk,
2930 "href": bibitemid.get_href(),
2931 "fetch": reverse(
2932 "api-fetch-id",
2933 args=(
2934 bibitemid.bibitem.pk,
2935 bibitemid.id_value,
2936 bibitemid.id_type,
2937 "bibitemid",
2938 ),
2939 ),
2940 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
2941 "uncheck": reverse(
2942 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
2943 ),
2944 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
2945 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
2946 "is_valid": bibitemid.checked,
2947 }
2948 )
2951class UpdateTexmfZipAPIView(View):
2952 def get(self, request, *args, **kwargs):
2953 def copy_zip_files(src_folder, dest_folder):
2954 os.makedirs(dest_folder, exist_ok=True)
2956 zip_files = [
2957 os.path.join(src_folder, f)
2958 for f in os.listdir(src_folder)
2959 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
2960 ]
2961 for zip_file in zip_files:
2962 resolver.copy_file(zip_file, dest_folder)
2964 # Exceptions: specific zip/gz files
2965 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
2966 resolver.copy_file(zip_file, dest_folder)
2968 zip_file = os.path.join(src_folder, "texmf-cg.zip")
2969 resolver.copy_file(zip_file, dest_folder)
2971 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
2972 resolver.copy_file(gz_file, dest_folder)
2974 src_folder = settings.CEDRAM_DISTRIB_FOLDER
2976 dest_folder = os.path.join(
2977 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
2978 )
2980 try:
2981 copy_zip_files(src_folder, dest_folder)
2982 except Exception as exception:
2983 return HttpResponseServerError(exception)
2985 try:
2986 dest_folder = os.path.join(
2987 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
2988 )
2989 copy_zip_files(src_folder, dest_folder)
2990 except Exception as exception:
2991 return HttpResponseServerError(exception)
2993 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
2994 return JsonResponse(data)
2997class TestView(TemplateView):
2998 template_name = "mersenne.html"
3000 def get_context_data(self, **kwargs):
3001 super().get_context_data(**kwargs)
3002 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True)
3003 model_data_converter.db_to_issue_data(issue)
3006class TrammelArchiveView(RedirectView):
3007 @staticmethod
3008 def reset_task_results():
3009 TaskResult.objects.all().delete()
3011 def get_redirect_url(self, *args, **kwargs):
3012 self.colid = kwargs["colid"]
3013 self.mathdoc_archive = settings.MATHDOC_ARCHIVE_FOLDER
3014 self.binary_files_folder = settings.MERSENNE_PROD_DATA_FOLDER
3015 # Make sure archiving is not already running
3016 if not get_messages_in_queue():
3017 self.reset_task_results()
3018 if "progress/" in self.colid:
3019 self.colid = self.colid.replace("progress/", "")
3020 if "/progress" in self.colid:
3021 self.colid = self.colid.replace("/progress", "")
3023 if self.colid != "ALL" and self.colid not in settings.MERSENNE_COLLECTIONS:
3024 return Http404
3026 colids = [self.colid] if self.colid != "ALL" else settings.MERSENNE_COLLECTIONS
3028 with open(
3029 os.path.join(settings.LOG_DIR, "archive.log"), "w", encoding="utf-8"
3030 ) as file_:
3031 file_.write("Archive " + " ".join([colid for colid in colids]) + "\n")
3033 for colid in colids:
3034 archive_trammel_collection.delay(
3035 colid, self.mathdoc_archive, self.binary_files_folder
3036 )
3038 if self.colid == "ALL":
3039 return reverse("home")
3040 else:
3041 return reverse("collection-detail", kwargs={"pid": self.colid})
3044class TrammelTasksProgressView(View):
3045 def get(self, request, *args, **kwargs):
3046 """
3047 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3048 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3049 """
3050 task_name = self.kwargs.get("task", "archive_numdam_issue")
3052 def get_event_data():
3053 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3055 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3056 remaining_messages = get_messages_in_queue()
3058 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3059 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3060 failed_tasks = all_tasks.filter(status="FAILURE")
3062 all_tasks_count = all_tasks.count()
3063 success_count = successed_tasks.count()
3064 fail_count = failed_tasks.count()
3066 all_count = all_tasks_count + remaining_messages
3067 remaining_count = all_count - success_count - fail_count
3069 success_rate = int(success_count * 100 / all_count) if all_count else 0
3070 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3071 status = "consuming_queue" if remaining_count != 0 else "polling"
3073 last_task = successed_tasks.first()
3074 last_task = (
3075 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3076 if last_task
3077 else ""
3078 )
3080 # SSE event format
3081 event_data = {
3082 "status": status,
3083 "success_rate": success_rate,
3084 "error_rate": error_rate,
3085 "all_count": all_count,
3086 "remaining_count": remaining_count,
3087 "success_count": success_count,
3088 "fail_count": fail_count,
3089 "last_task": last_task,
3090 }
3092 return event_data
3094 def stream_response(data):
3095 # Send initial response headers
3096 yield f"data: {json.dumps(data)}\n\n"
3098 data = get_event_data()
3099 format = request.GET.get("format", "stream")
3100 if format == "json":
3101 response = JsonResponse(data)
3102 else:
3103 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3104 return response
3107class TrammelFailedTasksListView(ListView):
3108 model = TaskResult
3109 queryset = TaskResult.objects.filter(
3110 status="FAILURE",
3111 task_name="ptf_tools.tasks.archive_trammel_resource",
3112 )