Coverage for src/ptf_tools/views/base_views.py: 17%
1682 statements
« prev ^ index » next coverage.py v7.9.0, created at 2025-12-23 16:01 +0000
« prev ^ index » next coverage.py v7.9.0, created at 2025-12-23 16:01 +0000
1import io
2import json
3import os
4import re
5from datetime import datetime
6from itertools import groupby
8import jsonpickle
9import requests
10from allauth.account.signals import user_signed_up
11from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin
12from celery import Celery, current_app
13from django.conf import settings
14from django.contrib import messages
15from django.contrib.auth.mixins import UserPassesTestMixin
16from django.db.models import Q
17from django.http import (
18 Http404,
19 HttpRequest,
20 HttpResponse,
21 HttpResponseRedirect,
22 HttpResponseServerError,
23 JsonResponse,
24)
25from django.shortcuts import get_object_or_404, redirect, render
26from django.urls import resolve, reverse, reverse_lazy
27from django.utils import timezone
28from django.views.decorators.http import require_http_methods
29from django.views.generic import ListView, TemplateView, View
30from django.views.generic.base import RedirectView
31from django.views.generic.detail import SingleObjectMixin
32from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
33from django_celery_results.models import TaskResult
34from extra_views import (
35 CreateWithInlinesView,
36 InlineFormSetFactory,
37 NamedFormsetsMixin,
38 UpdateWithInlinesView,
39)
40from ptf import model_data_converter, model_helpers, tex, utils
41from ptf.cmds import ptf_cmds, xml_cmds
42from ptf.cmds.base_cmds import make_int
43from ptf.cmds.xml.jats.builder.issue import build_title_xml
44from ptf.cmds.xml.xml_utils import replace_html_entities
45from ptf.display import resolver
46from ptf.exceptions import DOIException, PDFException, ServerUnderMaintenance
47from ptf.model_data import create_issuedata, create_publisherdata, create_titledata
48from ptf.models import (
49 Abstract,
50 Article,
51 BibItem,
52 BibItemId,
53 Collection,
54 Container,
55 ExtId,
56 ExtLink,
57 Resource,
58 ResourceId,
59 Title,
60)
61from ptf.views import ArticleEditFormWithVueAPIView
62from ptf_back.cmds.xml_cmds import updateBibitemCitationXmlCmd
63from pubmed.views import recordPubmed
64from requests import Timeout
65from task.tasks.archiving_tasks import archive_resource
67from comments_moderation.utils import get_comments_for_home, is_comment_moderator
68from history import models as history_models
69from history import views as history_views
70from history.utils import get_gap, get_history_last_event_by, get_last_unsolved_error
71from ptf_tools.doaj import doaj_pid_register
72from ptf_tools.doi import checkDOI, recordDOI, recordPendingPublication
73from ptf_tools.forms import (
74 BibItemIdForm,
75 CollectionForm,
76 ContainerForm,
77 DiffContainerForm,
78 ExtIdForm,
79 ExtLinkForm,
80 FormSetHelper,
81 ImportArticleForm,
82 ImportContainerForm,
83 ImportEditflowArticleForm,
84 PtfFormHelper,
85 PtfLargeModalFormHelper,
86 PtfModalFormHelper,
87 RegisterPubmedForm,
88 ResourceIdForm,
89 get_article_choices,
90)
91from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos
92from ptf_tools.models import ResourceInNumdam
93from ptf_tools.signals import update_user_from_invite
94from ptf_tools.tasks import (
95 archive_numdam_collection,
96 archive_numdam_collections,
97 archive_numdam_resource,
98)
99from ptf_tools.templatetags.tools_helpers import get_authorized_collections
100from ptf_tools.utils import is_authorized_editor
103def view_404(request: HttpRequest):
104 """
105 Dummy view raising HTTP 404 exception.
106 """
107 raise Http404
110def check_collection(collection, server_url, server_type):
111 """
112 Check if a collection exists on a serveur (test/prod)
113 and upload the collection (XML, image) if necessary
114 """
116 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
117 response = requests.get(url, verify=False)
118 # First, upload the collection XML
119 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
120 body = xml.encode("utf8")
122 url = server_url + reverse("upload-serials")
123 if response.status_code == 200:
124 # PUT http verb is used for update
125 response = requests.put(url, data=body, verify=False)
126 else:
127 # POST http verb is used for creation
128 response = requests.post(url, data=body, verify=False)
130 # Second, copy the collection images
131 # There is no need to copy files for the test server
132 # Files were already copied in /mersenne_test_data during the ptf_tools import
133 # We only need to copy files from /mersenne_test_data to
134 # /mersenne_prod_data during an upload to prod
135 if server_type == "website":
136 resolver.copy_binary_files(
137 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
138 )
139 elif server_type == "numdam":
140 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
141 if collection.pid in settings.NUMDAM_COLLECTIONS:
142 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
144 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
147def check_lock():
148 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
151def load_cedrics_article_choices(request):
152 colid = request.GET.get("colid")
153 issue = request.GET.get("issue")
154 article_choices = get_article_choices(colid, issue)
155 return render(
156 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
157 )
160class ImportCedricsArticleFormView(FormView):
161 template_name = "import_article.html"
162 form_class = ImportArticleForm
164 def dispatch(self, request, *args, **kwargs):
165 self.colid = self.kwargs["colid"]
166 return super().dispatch(request, *args, **kwargs)
168 def get_success_url(self):
169 if self.colid:
170 return reverse("collection-detail", kwargs={"pid": self.colid})
171 return "/"
173 def get_context_data(self, **kwargs):
174 context = super().get_context_data(**kwargs)
175 context["colid"] = self.colid
176 context["helper"] = PtfModalFormHelper
177 return context
179 def get_form_kwargs(self):
180 kwargs = super().get_form_kwargs()
181 kwargs["colid"] = self.colid
182 return kwargs
184 def form_valid(self, form):
185 self.issue = form.cleaned_data["issue"]
186 self.article = form.cleaned_data["article"]
187 return super().form_valid(form)
189 def import_cedrics_article(self, *args, **kwargs):
190 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
191 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
192 )
193 cmd.do()
195 def post(self, request, *args, **kwargs):
196 self.colid = self.kwargs.get("colid", None)
197 issue = request.POST["issue"]
198 self.article_pid = request.POST["article"]
199 self.issue_pid = os.path.basename(os.path.dirname(issue))
201 import_args = [self]
202 import_kwargs = {}
204 try:
205 _, status, message = history_views.execute_and_record_func(
206 "import",
207 f"{self.issue_pid} / {self.article_pid}",
208 self.colid,
209 self.import_cedrics_article,
210 "",
211 False,
212 None,
213 None,
214 *import_args,
215 **import_kwargs,
216 )
218 messages.success(
219 self.request, f"L'article {self.article_pid} a été importé avec succès"
220 )
222 except Exception as exception:
223 messages.error(
224 self.request,
225 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}",
226 )
228 return redirect(self.get_success_url())
231class ImportCedricsIssueView(FormView):
232 template_name = "import_container.html"
233 form_class = ImportContainerForm
235 def dispatch(self, request, *args, **kwargs):
236 self.colid = self.kwargs["colid"]
237 self.to_appear = self.request.GET.get("to_appear", False)
238 return super().dispatch(request, *args, **kwargs)
240 def get_success_url(self):
241 if self.filename:
242 return reverse(
243 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
244 )
245 return "/"
247 def get_context_data(self, **kwargs):
248 context = super().get_context_data(**kwargs)
249 context["colid"] = self.colid
250 context["helper"] = PtfModalFormHelper
251 return context
253 def get_form_kwargs(self):
254 kwargs = super().get_form_kwargs()
255 kwargs["colid"] = self.colid
256 kwargs["to_appear"] = self.to_appear
257 return kwargs
259 def form_valid(self, form):
260 self.filename = form.cleaned_data["filename"].split("/")[-1]
261 return super().form_valid(form)
264class DiffCedricsIssueView(FormView):
265 template_name = "diff_container_form.html"
266 form_class = DiffContainerForm
267 diffs = None
268 xissue = None
269 xissue_encoded = None
271 def get_success_url(self):
272 return reverse("collection-detail", kwargs={"pid": self.colid})
274 def dispatch(self, request, *args, **kwargs):
275 self.colid = self.kwargs["colid"]
276 # self.filename = self.kwargs['filename']
277 return super().dispatch(request, *args, **kwargs)
279 def get(self, request, *args, **kwargs):
280 self.filename = request.GET["filename"]
281 self.remove_mail = request.GET.get("remove_email", "off")
282 self.remove_date_prod = request.GET.get("remove_date_prod", "off")
283 self.remove_email = self.remove_mail == "on"
284 self.remove_date_prod = self.remove_date_prod == "on"
286 try:
287 result, status, message = history_views.execute_and_record_func(
288 "import",
289 os.path.basename(self.filename),
290 self.colid,
291 self.diff_cedrics_issue,
292 "",
293 True,
294 )
295 except Exception as exception:
296 pid = self.filename.split("/")[-1]
297 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
298 return HttpResponseRedirect(self.get_success_url())
300 no_conflict = result[0]
301 self.diffs = result[1]
302 self.xissue = result[2]
304 if no_conflict:
305 # Proceed with the import
306 self.form_valid(self.get_form())
307 return redirect(self.get_success_url())
308 else:
309 # Display the diff template
310 self.xissue_encoded = jsonpickle.encode(self.xissue)
312 return super().get(request, *args, **kwargs)
314 def post(self, request, *args, **kwargs):
315 self.filename = request.POST["filename"]
316 data = request.POST["xissue_encoded"]
317 self.xissue = jsonpickle.decode(data)
319 return super().post(request, *args, **kwargs)
321 def get_context_data(self, **kwargs):
322 context = super().get_context_data(**kwargs)
323 context["colid"] = self.colid
324 context["diff"] = self.diffs
325 context["filename"] = self.filename
326 context["xissue_encoded"] = self.xissue_encoded
327 return context
329 def get_form_kwargs(self):
330 kwargs = super().get_form_kwargs()
331 kwargs["colid"] = self.colid
332 return kwargs
334 def diff_cedrics_issue(self, *args, **kwargs):
335 params = {
336 "colid": self.colid,
337 "input_file": self.filename,
338 "remove_email": self.remove_mail,
339 "remove_date_prod": self.remove_date_prod,
340 "diff_only": True,
341 }
343 if settings.IMPORT_CEDRICS_DIRECTLY:
344 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
345 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
346 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
347 else:
348 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
350 result = cmd.do()
351 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
352 messages.warning(
353 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings
354 )
356 return result
358 def import_cedrics_issue(self, *args, **kwargs):
359 # modify xissue with data_issue if params to override
360 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
361 issue = model_helpers.get_container(self.xissue.pid)
362 if issue:
363 data_issue = model_data_converter.db_to_issue_data(issue)
364 for xarticle in self.xissue.articles:
365 filter_articles = [
366 article for article in data_issue.articles if article.doi == xarticle.doi
367 ]
368 if len(filter_articles) > 0:
369 db_article = filter_articles[0]
370 xarticle.coi_statement = db_article.coi_statement
371 xarticle.kwds = db_article.kwds
372 xarticle.contrib_groups = db_article.contrib_groups
374 params = {
375 "colid": self.colid,
376 "xissue": self.xissue,
377 "input_file": self.filename,
378 }
380 if settings.IMPORT_CEDRICS_DIRECTLY:
381 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
382 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
383 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
384 else:
385 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
387 cmd.do()
389 def form_valid(self, form):
390 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
391 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
392 else:
393 import_kwargs = {}
394 import_args = [self]
396 try:
397 _, status, message = history_views.execute_and_record_func(
398 "import",
399 self.xissue.pid,
400 self.kwargs["colid"],
401 self.import_cedrics_issue,
402 "",
403 False,
404 None,
405 None,
406 *import_args,
407 **import_kwargs,
408 )
409 except Exception as exception:
410 messages.error(
411 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
412 )
413 return super().form_invalid(form)
415 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
416 return super().form_valid(form)
419class ImportEditflowArticleFormView(FormView):
420 template_name = "import_editflow_article.html"
421 form_class = ImportEditflowArticleForm
423 def dispatch(self, request, *args, **kwargs):
424 self.colid = self.kwargs["colid"]
425 return super().dispatch(request, *args, **kwargs)
427 def get_context_data(self, **kwargs):
428 context = super().get_context_data(**kwargs)
429 context["colid"] = self.kwargs["colid"]
430 context["helper"] = PtfLargeModalFormHelper
431 return context
433 def get_success_url(self):
434 if self.colid:
435 return reverse("collection-detail", kwargs={"pid": self.colid})
436 return "/"
438 def post(self, request, *args, **kwargs):
439 self.colid = self.kwargs.get("colid", None)
440 try:
441 if not self.colid:
442 raise ValueError("Missing collection id")
444 issue_name = settings.ISSUE_PENDING_PUBLICATION_PIDS.get(self.colid)
445 if not issue_name:
446 raise ValueError(
447 "Issue not found in Pending Publications PIDs. Did you forget to add it?"
448 )
450 issue = model_helpers.get_container(issue_name)
451 if not issue:
452 raise ValueError("No issue found")
454 editflow_xml_file = request.FILES.get("editflow_xml_file")
455 if not editflow_xml_file:
456 raise ValueError("The file you specified couldn't be found")
458 body = editflow_xml_file.read().decode("utf-8")
460 cmd = xml_cmds.addArticleXmlCmd(
461 {
462 "body": body,
463 "issue": issue,
464 "assign_doi": True,
465 "standalone": True,
466 "from_folder": settings.RESOURCES_ROOT,
467 }
468 )
469 cmd.set_collection(issue.get_collection())
470 cmd.do()
472 messages.success(
473 request,
474 f'Editflow article successfully imported into issue "{issue_name}"',
475 )
477 except Exception as exception:
478 messages.error(
479 request,
480 f"Import failed: {str(exception)}",
481 )
483 return redirect(self.get_success_url())
486class BibtexAPIView(View):
487 def get(self, request, *args, **kwargs):
488 pid = self.kwargs.get("pid", None)
489 all_bibtex = ""
490 if pid:
491 article = model_helpers.get_article(pid)
492 if article:
493 for bibitem in article.bibitem_set.all():
494 bibtex_array = bibitem.get_bibtex()
495 last = len(bibtex_array)
496 i = 1
497 for bibtex in bibtex_array:
498 if i > 1 and i < last:
499 all_bibtex += " "
500 all_bibtex += bibtex + "\n"
501 i += 1
503 data = {"bibtex": all_bibtex}
504 return JsonResponse(data)
507class MatchingAPIView(View):
508 def get(self, request, *args, **kwargs):
509 pid = self.kwargs.get("pid", None)
511 url = settings.MATCHING_URL
512 headers = {"Content-Type": "application/xml"}
514 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
516 if settings.DEBUG:
517 print("Issue exported to /tmp/issue.xml")
518 f = open("/tmp/issue.xml", "w")
519 f.write(body.encode("utf8"))
520 f.close()
522 r = requests.post(url, data=body.encode("utf8"), headers=headers)
523 body = r.text.encode("utf8")
524 data = {"status": r.status_code, "message": body[:1000]}
526 if settings.DEBUG:
527 print("Matching received, new issue exported to /tmp/issue1.xml")
528 f = open("/tmp/issue1.xml", "w")
529 text = body
530 f.write(text)
531 f.close()
533 resource = model_helpers.get_resource(pid)
534 obj = resource.cast()
535 colid = obj.get_collection().pid
537 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
539 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
540 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
541 )
542 cmd.do()
544 print("Matching finished")
545 return JsonResponse(data)
548class ImportAllAPIView(View):
549 def internal_do(self, *args, **kwargs):
550 pid = self.kwargs.get("pid", None)
552 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
553 if not os.path.isdir(root_folder):
554 raise ValueError(root_folder + " does not exist")
556 resource = model_helpers.get_resource(pid)
557 if not resource:
558 file = os.path.join(root_folder, pid + ".xml")
559 body = utils.get_file_content_in_utf8(file)
560 journals = xml_cmds.addCollectionsXmlCmd(
561 {
562 "body": body,
563 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
564 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
565 }
566 ).do()
567 if not journals:
568 raise ValueError(file + " does not contain a collection")
569 resource = journals[0]
570 # resolver.copy_binary_files(
571 # resource,
572 # settings.MATHDOC_ARCHIVE_FOLDER,
573 # settings.MERSENNE_TEST_DATA_FOLDER)
575 obj = resource.cast()
577 if obj.classname != "Collection":
578 raise ValueError(pid + " does not contain a collection")
580 cmd = xml_cmds.collectEntireCollectionXmlCmd(
581 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
582 )
583 pids = cmd.do()
585 return pids
587 def get(self, request, *args, **kwargs):
588 pid = self.kwargs.get("pid", None)
590 try:
591 pids, status, message = history_views.execute_and_record_func(
592 "import", pid, pid, self.internal_do
593 )
594 except Timeout as exception:
595 return HttpResponse(exception, status=408)
596 except Exception as exception:
597 return HttpResponseServerError(exception)
599 data = {"message": message, "ids": pids, "status": status}
600 return JsonResponse(data)
603class DeployAllAPIView(View):
604 def internal_do(self, *args, **kwargs):
605 pid = self.kwargs.get("pid", None)
606 site = self.kwargs.get("site", None)
608 pids = []
610 collection = model_helpers.get_collection(pid)
611 if not collection:
612 raise RuntimeError(pid + " does not exist")
614 if site == "numdam":
615 server_url = settings.NUMDAM_PRE_URL
616 elif site != "ptf_tools":
617 server_url = getattr(collection, site)()
618 if not server_url:
619 raise RuntimeError("The collection has no " + site)
621 if site != "ptf_tools":
622 # check if the collection exists on the server
623 # if not, check_collection will upload the collection (XML,
624 # image...)
625 check_collection(collection, server_url, site)
627 for issue in collection.content.all():
628 if site != "website" or (site == "website" and issue.are_all_articles_published()):
629 pids.append(issue.pid)
631 return pids
633 def get(self, request, *args, **kwargs):
634 pid = self.kwargs.get("pid", None)
635 site = self.kwargs.get("site", None)
637 try:
638 pids, status, message = history_views.execute_and_record_func(
639 "deploy", pid, pid, self.internal_do, site
640 )
641 except Timeout as exception:
642 return HttpResponse(exception, status=408)
643 except Exception as exception:
644 return HttpResponseServerError(exception)
646 data = {"message": message, "ids": pids, "status": status}
647 return JsonResponse(data)
650class AddIssuePDFView(View):
651 def __init(self, *args, **kwargs):
652 super().__init__(*args, **kwargs)
653 self.pid = None
654 self.issue = None
655 self.collection = None
656 self.site = "test_website"
658 def post_to_site(self, url):
659 response = requests.post(url, verify=False)
660 status = response.status_code
661 if not (199 < status < 205):
662 messages.error(self.request, response.text)
663 if status == 503:
664 raise ServerUnderMaintenance(response.text)
665 else:
666 raise RuntimeError(response.text)
668 def internal_do(self, *args, **kwargs):
669 """
670 Called by history_views.execute_and_record_func to do the actual job.
671 """
673 issue_pid = self.issue.pid
674 colid = self.collection.pid
676 if self.site == "website":
677 # Copy the PDF from the test to the production folder
678 resolver.copy_binary_files(
679 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
680 )
681 else:
682 # Copy the PDF from the cedram to the test folder
683 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
684 from_path = os.path.join(from_folder, issue_pid + ".pdf")
685 if not os.path.isfile(from_path):
686 raise Http404(f"{from_path} does not exist")
688 to_path = resolver.get_disk_location(
689 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
690 )
691 resolver.copy_file(from_path, to_path)
693 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
695 if self.site == "test_website":
696 # Post to ptf-tools: it will add a Datastream to the issue
697 absolute_url = self.request.build_absolute_uri(url)
698 self.post_to_site(absolute_url)
700 server_url = getattr(self.collection, self.site)()
701 absolute_url = server_url + url
702 # Post to the test or production website
703 self.post_to_site(absolute_url)
705 def get(self, request, *args, **kwargs):
706 """
707 Send an issue PDF to the test or production website
708 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
709 :param args:
710 :param kwargs:
711 :return:
712 """
713 if check_lock():
714 m = "Trammel is under maintenance. Please try again later."
715 messages.error(self.request, m)
716 return JsonResponse({"message": m, "status": 503})
718 self.pid = self.kwargs.get("pid", None)
719 self.site = self.kwargs.get("site", "test_website")
721 self.issue = model_helpers.get_container(self.pid)
722 if not self.issue:
723 raise Http404(f"{self.pid} does not exist")
724 self.collection = self.issue.get_top_collection()
726 try:
727 pids, status, message = history_views.execute_and_record_func(
728 "deploy",
729 self.pid,
730 self.collection.pid,
731 self.internal_do,
732 f"add issue PDF to {self.site}",
733 )
735 except Timeout as exception:
736 return HttpResponse(exception, status=408)
737 except Exception as exception:
738 return HttpResponseServerError(exception)
740 data = {"message": message, "status": status}
741 return JsonResponse(data)
744class ArchiveAllAPIView(View):
745 """
746 - archive le xml de la collection ainsi que les binaires liés
747 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
748 @return array of issues pid
749 """
751 def internal_do(self, *args, **kwargs):
752 collection = kwargs["collection"]
753 pids = []
754 colid = collection.pid
756 logfile = os.path.join(settings.LOG_DIR, "archive.log")
757 if os.path.isfile(logfile):
758 os.remove(logfile)
760 ptf_cmds.exportPtfCmd(
761 {
762 "pid": colid,
763 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
764 "with_binary_files": True,
765 "for_archive": True,
766 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
767 }
768 ).do()
770 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
771 if os.path.isfile(cedramcls):
772 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
773 resolver.create_folder(dest_folder)
774 resolver.copy_file(cedramcls, dest_folder)
776 for issue in collection.content.all():
777 qs = issue.article_set.filter(
778 date_online_first__isnull=True, date_published__isnull=True
779 )
780 if qs.count() == 0:
781 pids.append(issue.pid)
783 return pids
785 def get(self, request, *args, **kwargs):
786 pid = self.kwargs.get("pid", None)
788 collection = model_helpers.get_collection(pid)
789 if not collection:
790 return HttpResponse(f"{pid} does not exist", status=400)
792 dict_ = {"collection": collection}
793 args_ = [self]
795 try:
796 pids, status, message = history_views.execute_and_record_func(
797 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_
798 )
799 except Timeout as exception:
800 return HttpResponse(exception, status=408)
801 except Exception as exception:
802 return HttpResponseServerError(exception)
804 data = {"message": message, "ids": pids, "status": status}
805 return JsonResponse(data)
808class CreateAllDjvuAPIView(View):
809 def internal_do(self, *args, **kwargs):
810 issue = kwargs["issue"]
811 pids = [issue.pid]
813 for article in issue.article_set.all():
814 pids.append(article.pid)
816 return pids
818 def get(self, request, *args, **kwargs):
819 pid = self.kwargs.get("pid", None)
820 issue = model_helpers.get_container(pid)
821 if not issue:
822 raise Http404(f"{pid} does not exist")
824 try:
825 dict_ = {"issue": issue}
826 args_ = [self]
828 pids, status, message = history_views.execute_and_record_func(
829 "numdam",
830 pid,
831 issue.get_collection().pid,
832 self.internal_do,
833 "",
834 False,
835 None,
836 None,
837 *args_,
838 **dict_,
839 )
840 except Exception as exception:
841 return HttpResponseServerError(exception)
843 data = {"message": message, "ids": pids, "status": status}
844 return JsonResponse(data)
847class ImportJatsContainerAPIView(View):
848 def internal_do(self, *args, **kwargs):
849 pid = self.kwargs.get("pid", None)
850 colid = self.kwargs.get("colid", None)
852 if pid and colid:
853 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
855 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
856 {
857 "body": body,
858 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
859 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
860 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
861 }
862 )
863 container = cmd.do()
864 if len(cmd.warnings) > 0:
865 messages.warning(
866 self.request,
867 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
868 )
870 if not container:
871 raise RuntimeError("Error: the container " + pid + " was not imported")
873 # resolver.copy_binary_files(
874 # container,
875 # settings.MATHDOC_ARCHIVE_FOLDER,
876 # settings.MERSENNE_TEST_DATA_FOLDER)
877 #
878 # for article in container.article_set.all():
879 # resolver.copy_binary_files(
880 # article,
881 # settings.MATHDOC_ARCHIVE_FOLDER,
882 # settings.MERSENNE_TEST_DATA_FOLDER)
883 else:
884 raise RuntimeError("colid or pid are not defined")
886 def get(self, request, *args, **kwargs):
887 pid = self.kwargs.get("pid", None)
888 colid = self.kwargs.get("colid", None)
890 try:
891 _, status, message = history_views.execute_and_record_func(
892 "import", pid, colid, self.internal_do
893 )
894 except Timeout as exception:
895 return HttpResponse(exception, status=408)
896 except Exception as exception:
897 return HttpResponseServerError(exception)
899 data = {"message": message, "status": status}
900 return JsonResponse(data)
903class DeployCollectionAPIView(View):
904 # Update collection.xml on a site (with its images)
906 def internal_do(self, *args, **kwargs):
907 colid = self.kwargs.get("colid", None)
908 site = self.kwargs.get("site", None)
910 collection = model_helpers.get_collection(colid)
911 if not collection:
912 raise RuntimeError(f"{colid} does not exist")
914 if site == "numdam":
915 server_url = settings.NUMDAM_PRE_URL
916 else:
917 server_url = getattr(collection, site)()
918 if not server_url:
919 raise RuntimeError(f"The collection has no {site}")
921 # check_collection creates or updates the collection (XML, image...)
922 check_collection(collection, server_url, site)
924 def get(self, request, *args, **kwargs):
925 colid = self.kwargs.get("colid", None)
926 site = self.kwargs.get("site", None)
928 try:
929 _, status, message = history_views.execute_and_record_func(
930 "deploy", colid, colid, self.internal_do, site
931 )
932 except Timeout as exception:
933 return HttpResponse(exception, status=408)
934 except Exception as exception:
935 return HttpResponseServerError(exception)
937 data = {"message": message, "status": status}
938 return JsonResponse(data)
941class DeployJatsResourceAPIView(View):
942 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
944 def internal_do(self, *args, **kwargs):
945 pid = self.kwargs.get("pid", None)
946 colid = self.kwargs.get("colid", None)
947 site = self.kwargs.get("site", None)
949 if site == "ptf_tools":
950 raise RuntimeError("Do not choose to deploy on PTF Tools")
951 if check_lock():
952 msg = "Trammel is under maintenance. Please try again later."
953 messages.error(self.request, msg)
954 return JsonResponse({"messages": msg, "status": 503})
956 resource = model_helpers.get_resource(pid)
957 if not resource:
958 raise RuntimeError(f"{pid} does not exist")
960 obj = resource.cast()
961 article = None
962 if obj.classname == "Article":
963 article = obj
964 container = article.my_container
965 articles_to_deploy = [article]
966 else:
967 container = obj
968 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
970 if container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(colid, None):
971 raise RuntimeError("Pending publications should not be deployed")
972 if site == "website" and article is not None and article.do_not_publish:
973 raise RuntimeError(f"{pid} is marked as Do not publish")
974 if site == "numdam" and article is not None:
975 raise RuntimeError("You can only deploy issues to Numdam")
977 collection = container.get_top_collection()
978 colid = collection.pid
979 djvu_exception = None
981 if site == "numdam":
982 server_url = settings.NUMDAM_PRE_URL
983 ResourceInNumdam.objects.get_or_create(pid=container.pid)
985 # 06/12/2022: DjVu are no longer added with Mersenne articles
986 # Add Djvu (before exporting the XML)
987 if False and int(container.year) < 2020:
988 for art in container.article_set.all():
989 try:
990 cmd = ptf_cmds.addDjvuPtfCmd()
991 cmd.set_resource(art)
992 cmd.do()
993 except Exception as e:
994 # Djvu are optional.
995 # Allow the deployment, but record the exception in the history
996 djvu_exception = e
997 else:
998 server_url = getattr(collection, site)()
999 if not server_url:
1000 raise RuntimeError(f"The collection has no {site}")
1002 # check if the collection exists on the server
1003 # if not, check_collection will upload the collection (XML,
1004 # image...)
1005 if article is None:
1006 check_collection(collection, server_url, site)
1008 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
1009 # Create/update deployed date and published date on all container articles
1010 if site == "website":
1011 file_.write(
1012 "Create/Update deployed_date and date_published on all articles for {}\n".format(
1013 pid
1014 )
1015 )
1017 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
1018 cmd = ptf_cmds.publishResourcePtfCmd()
1019 cmd.set_resource(resource)
1020 updated_articles = cmd.do()
1022 tex.create_frontpage(colid, container, updated_articles, test=False)
1024 mersenneSite = model_helpers.get_site_mersenne(colid)
1025 # create or update deployed_date on container and articles
1026 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
1028 for art in articles_to_deploy:
1029 if art.doi and (art.date_published or art.date_online_first):
1030 if art.my_container.year is None:
1031 art.my_container.year = datetime.now().strftime("%Y")
1032 # BUG ? update the container but no save() ?
1034 file_.write(
1035 "Publication date of {} : Online First: {}, Published: {}\n".format(
1036 art.pid, art.date_online_first, art.date_published
1037 )
1038 )
1040 if article is None:
1041 resolver.copy_binary_files(
1042 container,
1043 settings.MERSENNE_TEST_DATA_FOLDER,
1044 settings.MERSENNE_PROD_DATA_FOLDER,
1045 )
1047 for art in articles_to_deploy:
1048 resolver.copy_binary_files(
1049 art,
1050 settings.MERSENNE_TEST_DATA_FOLDER,
1051 settings.MERSENNE_PROD_DATA_FOLDER,
1052 )
1054 elif site == "test_website":
1055 # create date_pre_published on articles without date_pre_published
1056 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
1057 cmd.set_resource(resource)
1058 updated_articles = cmd.do()
1060 tex.create_frontpage(colid, container, updated_articles)
1062 export_to_website = site == "website"
1064 if article is None:
1065 with_djvu = site == "numdam"
1066 xml = ptf_cmds.exportPtfCmd(
1067 {
1068 "pid": pid,
1069 "with_djvu": with_djvu,
1070 "export_to_website": export_to_website,
1071 }
1072 ).do()
1073 body = xml.encode("utf8")
1075 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1076 url = server_url + reverse("issue_upload")
1077 else:
1078 url = server_url + reverse("book_upload")
1080 # verify=False: ignore TLS certificate
1081 response = requests.post(url, data=body, verify=False)
1082 # response = requests.post(url, files=files, verify=False)
1083 else:
1084 xml = ptf_cmds.exportPtfCmd(
1085 {
1086 "pid": pid,
1087 "with_djvu": False,
1088 "article_standalone": True,
1089 "collection_pid": collection.pid,
1090 "export_to_website": export_to_website,
1091 "export_folder": settings.LOG_DIR,
1092 }
1093 ).do()
1094 # Unlike containers that send their XML as the body of the POST request,
1095 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1096 xml_file = io.StringIO(xml)
1097 files = {"xml": xml_file}
1099 url = server_url + reverse(
1100 "article_in_issue_upload", kwargs={"pid": container.pid}
1101 )
1102 # verify=False: ignore TLS certificate
1103 header = {}
1104 response = requests.post(url, headers=header, files=files, verify=False)
1106 status = response.status_code
1108 if 199 < status < 205:
1109 # There is no need to copy files for the test server
1110 # Files were already copied in /mersenne_test_data during the ptf_tools import
1111 # We only need to copy files from /mersenne_test_data to
1112 # /mersenne_prod_data during an upload to prod
1113 if site == "website":
1114 # TODO mettre ici le record doi pour un issue publié
1115 if container.doi:
1116 recordDOI(container)
1118 for art in articles_to_deploy:
1119 # record DOI automatically when deploying in prod
1121 if art.doi and art.allow_crossref():
1122 recordDOI(art)
1124 if colid == "CRBIOL":
1125 recordPubmed(
1126 art, force_update=False, updated_articles=updated_articles
1127 )
1129 if colid == "PCJ":
1130 self.update_pcj_editor(updated_articles)
1132 # Archive the container or the article
1133 if article is None:
1134 archive_resource.delay(
1135 pid,
1136 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1137 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1138 )
1140 else:
1141 archive_resource.delay(
1142 pid,
1143 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1144 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1145 article_doi=article.doi,
1146 )
1147 # cmd = ptf_cmds.archiveIssuePtfCmd({
1148 # "pid": pid,
1149 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1150 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1151 # cmd.set_article(article) # set_article allows archiving only the article
1152 # cmd.do()
1154 elif site == "numdam":
1155 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1156 if colid in settings.NUMDAM_COLLECTIONS:
1157 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1159 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1160 for article in container.article_set.all():
1161 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1163 elif status == 503:
1164 raise ServerUnderMaintenance(response.text)
1165 else:
1166 raise RuntimeError(response.text)
1168 if djvu_exception:
1169 raise djvu_exception
1171 def get(self, request, *args, **kwargs):
1172 pid = self.kwargs.get("pid", None)
1173 colid = self.kwargs.get("colid", None)
1174 site = self.kwargs.get("site", None)
1176 try:
1177 _, status, message = history_views.execute_and_record_func(
1178 "deploy", pid, colid, self.internal_do, site
1179 )
1180 except Timeout as exception:
1181 return HttpResponse(exception, status=408)
1182 except Exception as exception:
1183 return HttpResponseServerError(exception)
1185 data = {"message": message, "status": status}
1186 return JsonResponse(data)
1188 def update_pcj_editor(self, updated_articles):
1189 for article in updated_articles:
1190 data = {
1191 "date_published": article.date_published.strftime("%Y-%m-%d"),
1192 "article_number": article.article_number,
1193 }
1194 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1195 requests.post(url, json=data, verify=False)
1198class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1199 article = None
1201 def internal_do(self, *args, **kwargs):
1202 lang = self.kwargs.get("lang", None)
1204 translation = None
1205 for trans_article in self.article.translations.all():
1206 if trans_article.lang == lang:
1207 translation = trans_article
1209 if translation is None:
1210 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1212 collection = self.article.get_top_collection()
1213 colid = collection.pid
1214 container = self.article.my_container
1216 if translation.date_published is None:
1217 # Add date posted
1218 cmd = ptf_cmds.publishResourcePtfCmd()
1219 cmd.set_resource(translation)
1220 updated_articles = cmd.do()
1222 # Recompile PDF to add the date posted
1223 try:
1224 tex.create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1225 except Exception:
1226 raise PDFException(
1227 "Unable to compile the article PDF. Please contact the centre Mersenne"
1228 )
1230 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1231 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1232 resolver.copy_binary_files(
1233 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1234 )
1236 # Deploy in prod
1237 xml = ptf_cmds.exportPtfCmd(
1238 {
1239 "pid": self.article.pid,
1240 "with_djvu": False,
1241 "article_standalone": True,
1242 "collection_pid": colid,
1243 "export_to_website": True,
1244 "export_folder": settings.LOG_DIR,
1245 }
1246 ).do()
1247 xml_file = io.StringIO(xml)
1248 files = {"xml": xml_file}
1250 server_url = getattr(collection, "website")()
1251 if not server_url:
1252 raise RuntimeError("The collection has no website")
1253 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1254 header = {}
1256 try:
1257 response = requests.post(
1258 url, headers=header, files=files, verify=False
1259 ) # verify: ignore TLS certificate
1260 status = response.status_code
1261 except requests.exceptions.ConnectionError:
1262 raise ServerUnderMaintenance(
1263 "The journal is under maintenance. Please try again later."
1264 )
1266 # Register translation in Crossref
1267 if 199 < status < 205:
1268 if self.article.allow_crossref():
1269 try:
1270 recordDOI(translation)
1271 except Exception:
1272 raise DOIException(
1273 "Error while recording the DOI. Please contact the centre Mersenne"
1274 )
1276 def get(self, request, *args, **kwargs):
1277 doi = kwargs.get("doi", None)
1278 self.article = model_helpers.get_article_by_doi(doi)
1279 if self.article is None:
1280 raise Http404(f"{doi} does not exist")
1282 try:
1283 _, status, message = history_views.execute_and_record_func(
1284 "deploy",
1285 self.article.pid,
1286 self.article.get_top_collection().pid,
1287 self.internal_do,
1288 "website",
1289 )
1290 except Timeout as exception:
1291 return HttpResponse(exception, status=408)
1292 except Exception as exception:
1293 return HttpResponseServerError(exception)
1295 data = {"message": message, "status": status}
1296 return JsonResponse(data)
1299class DeleteJatsIssueAPIView(View):
1300 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1301 def get(self, request, *args, **kwargs):
1302 pid = self.kwargs.get("pid", None)
1303 colid = self.kwargs.get("colid", None)
1304 site = self.kwargs.get("site", None)
1305 message = "Le volume a bien été supprimé"
1306 status = 200
1308 issue = model_helpers.get_container(pid)
1309 if not issue:
1310 raise Http404(f"{pid} does not exist")
1311 try:
1312 mersenneSite = model_helpers.get_site_mersenne(colid)
1314 if site == "ptf_tools":
1315 if issue.is_deployed(mersenneSite):
1316 issue.undeploy(mersenneSite)
1317 for article in issue.article_set.all():
1318 article.undeploy(mersenneSite)
1320 p = model_helpers.get_provider("mathdoc-id")
1322 cmd = ptf_cmds.addContainerPtfCmd(
1323 {
1324 "pid": issue.pid,
1325 "ctype": "issue",
1326 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1327 }
1328 )
1329 cmd.set_provider(p)
1330 cmd.add_collection(issue.get_collection())
1331 cmd.set_object_to_be_deleted(issue)
1332 cmd.undo()
1334 else:
1335 if site == "numdam":
1336 server_url = settings.NUMDAM_PRE_URL
1337 else:
1338 collection = issue.get_collection()
1339 server_url = getattr(collection, site)()
1341 if not server_url:
1342 message = "The collection has no " + site
1343 status = 500
1344 else:
1345 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1346 response = requests.delete(url, verify=False)
1347 status = response.status_code
1349 if status == 404:
1350 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1351 elif status > 204:
1352 body = response.text.encode("utf8")
1353 message = body[:1000]
1354 else:
1355 status = 200
1356 # unpublish issue in collection site (site_register.json)
1357 if site == "website":
1358 if issue.is_deployed(mersenneSite):
1359 issue.undeploy(mersenneSite)
1360 for article in issue.article_set.all():
1361 article.undeploy(mersenneSite)
1362 # delete article binary files
1363 folder = article.get_relative_folder()
1364 resolver.delete_object_folder(
1365 folder,
1366 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1367 )
1368 # delete issue binary files
1369 folder = issue.get_relative_folder()
1370 resolver.delete_object_folder(
1371 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1372 )
1374 except Timeout as exception:
1375 return HttpResponse(exception, status=408)
1376 except Exception as exception:
1377 return HttpResponseServerError(exception)
1379 data = {"message": message, "status": status}
1380 return JsonResponse(data)
1383class ArchiveIssueAPIView(View):
1384 def get(self, request, *args, **kwargs):
1385 try:
1386 pid = kwargs["pid"]
1387 colid = kwargs["colid"]
1388 except IndexError:
1389 raise Http404
1391 try:
1392 cmd = ptf_cmds.archiveIssuePtfCmd(
1393 {
1394 "pid": pid,
1395 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1396 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1397 }
1398 )
1399 result_, status, message = history_views.execute_and_record_func(
1400 "archive", pid, colid, cmd.do
1401 )
1402 except Exception as exception:
1403 return HttpResponseServerError(exception)
1405 data = {"message": message, "status": 200}
1406 return JsonResponse(data)
1409class CreateDjvuAPIView(View):
1410 def internal_do(self, *args, **kwargs):
1411 pid = self.kwargs.get("pid", None)
1413 resource = model_helpers.get_resource(pid)
1414 cmd = ptf_cmds.addDjvuPtfCmd()
1415 cmd.set_resource(resource)
1416 cmd.do()
1418 def get(self, request, *args, **kwargs):
1419 pid = self.kwargs.get("pid", None)
1420 colid = pid.split("_")[0]
1422 try:
1423 _, status, message = history_views.execute_and_record_func(
1424 "numdam", pid, colid, self.internal_do
1425 )
1426 except Exception as exception:
1427 return HttpResponseServerError(exception)
1429 data = {"message": message, "status": status}
1430 return JsonResponse(data)
1433class PTFToolsHomeView(LoginRequiredMixin, View):
1434 """
1435 Home Page.
1436 - Admin & staff -> Render blank home.html
1437 - User with unique authorized collection -> Redirect to collection details page
1438 - User with multiple authorized collections -> Render home.html with data
1439 - Comment moderator -> Comments dashboard
1440 - Others -> 404 response
1441 """
1443 def get(self, request, *args, **kwargs) -> HttpResponse:
1444 # Staff or user with authorized collections
1445 if request.user.is_staff or request.user.is_superuser:
1446 return render(request, "home.html")
1448 colids = get_authorized_collections(request.user)
1449 is_mod = is_comment_moderator(request.user)
1451 # The user has no rights
1452 if not (colids or is_mod):
1453 raise Http404("No collections associated with your account.")
1454 # Comment moderator only
1455 elif not colids:
1456 return HttpResponseRedirect(reverse("comment_list"))
1458 # User with unique collection -> Redirect to collection detail page
1459 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1460 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1462 # User with multiple authorized collections - Special home
1463 context = {}
1464 context["overview"] = True
1466 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1467 all_collections = {c["pid"]: c for c in all_collections}
1469 # Comments summary
1470 try:
1471 error, comments_data = get_comments_for_home(request.user)
1472 except AttributeError:
1473 error, comments_data = True, {}
1475 context["comment_server_ok"] = False
1477 if not error:
1478 context["comment_server_ok"] = True
1479 if comments_data:
1480 for col_id, comment_nb in comments_data.items():
1481 if col_id.upper() in all_collections: 1481 ↛ 1480line 1481 didn't jump to line 1480 because the condition on line 1481 was always true
1482 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1484 # TODO: Translations summary
1485 context["translation_server_ok"] = False
1487 # Sort the collections according to the number of pending comments
1488 context["collections"] = sorted(
1489 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1490 )
1492 return render(request, "home.html", context)
1495class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1496 columns = 5
1498 def get_common_context_data(self, **kwargs):
1499 context = super().get_context_data(**kwargs)
1500 now = timezone.now()
1501 curyear = now.year
1502 years = range(curyear - self.columns + 1, curyear + 1)
1504 context["collections"] = settings.MERSENNE_COLLECTIONS
1505 context["containers_to_be_published"] = []
1506 context["last_col_events"] = []
1508 event = get_history_last_event_by("clockss", "ALL")
1509 clockss_gap = get_gap(now, event)
1511 context["years"] = years
1512 context["clockss_gap"] = clockss_gap
1514 return context
1516 def calculate_articles_and_pages(self, pid, years):
1517 data_by_year = []
1518 total_articles = [0] * len(years)
1519 total_pages = [0] * len(years)
1521 for year in years:
1522 articles = self.get_articles_for_year(pid, year)
1523 articles_count = articles.count()
1524 page_count = sum(article.get_article_page_count() for article in articles)
1526 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1527 total_articles[year - years[0]] += articles_count
1528 total_pages[year - years[0]] += page_count
1530 return data_by_year, total_articles, total_pages
1532 def get_articles_for_year(self, pid, year):
1533 return Article.objects.filter(
1534 Q(my_container__my_collection__pid=pid)
1535 & (
1536 Q(date_published__year=year, date_online_first__isnull=True)
1537 | Q(date_online_first__year=year)
1538 )
1539 ).prefetch_related("resourcecount_set")
1542class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1543 template_name = "dashboard/published_articles.html"
1545 def get_context_data(self, **kwargs):
1546 context = self.get_common_context_data(**kwargs)
1547 years = context["years"]
1549 published_articles = []
1550 total_published_articles = [
1551 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1552 ]
1554 for pid in settings.MERSENNE_COLLECTIONS:
1555 if pid != "MERSENNE":
1556 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1557 pid, years
1558 )
1559 published_articles.append({"pid": pid, "years": articles_data})
1561 for i, year in enumerate(years):
1562 total_published_articles[i]["total_articles"] += total_articles[i]
1563 total_published_articles[i]["total_pages"] += total_pages[i]
1565 context["published_articles"] = published_articles
1566 context["total_published_articles"] = total_published_articles
1568 return context
1571class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1572 template_name = "dashboard/created_volumes.html"
1574 def get_context_data(self, **kwargs):
1575 context = self.get_common_context_data(**kwargs)
1576 years = context["years"]
1578 created_volumes = []
1579 total_created_volumes = [
1580 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1581 ]
1583 for pid in settings.MERSENNE_COLLECTIONS:
1584 if pid != "MERSENNE":
1585 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1586 pid, years
1587 )
1588 created_volumes.append({"pid": pid, "years": volumes_data})
1590 for i, year in enumerate(years):
1591 total_created_volumes[i]["total_articles"] += total_articles[i]
1592 total_created_volumes[i]["total_pages"] += total_pages[i]
1594 context["created_volumes"] = created_volumes
1595 context["total_created_volumes"] = total_created_volumes
1597 return context
1599 def calculate_volumes_and_pages(self, pid, years):
1600 data_by_year = []
1601 total_articles = [0] * len(years)
1602 total_pages = [0] * len(years)
1604 for year in years:
1605 issues = Container.objects.filter(my_collection__pid=pid, year=year)
1606 articles_count = 0
1607 page_count = 0
1609 for issue in issues:
1610 articles = issue.article_set.filter(
1611 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1612 ).prefetch_related("resourcecount_set")
1614 articles_count += articles.count()
1615 page_count += sum(article.get_article_page_count() for article in articles)
1617 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1618 total_articles[year - years[0]] += articles_count
1619 total_pages[year - years[0]] += page_count
1621 return data_by_year, total_articles, total_pages
1624class ReferencingChoice(View):
1625 def post(self, request, *args, **kwargs):
1626 if request.POST.get("optSite") == "ads":
1627 return redirect(
1628 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1629 )
1630 elif request.POST.get("optSite") == "wos":
1631 comp = ReferencingCheckerWos()
1632 journal = comp.make_journal(request.POST.get("selectCol"))
1633 if journal is None:
1634 return render(
1635 request,
1636 "dashboard/referencing.html",
1637 {
1638 "error": "Collection not found",
1639 "colid": request.POST.get("selectCol"),
1640 "optSite": request.POST.get("optSite"),
1641 },
1642 )
1643 return render(
1644 request,
1645 "dashboard/referencing.html",
1646 {
1647 "journal": journal,
1648 "colid": request.POST.get("selectCol"),
1649 "optSite": request.POST.get("optSite"),
1650 },
1651 )
1654class ReferencingWosFileView(View):
1655 template_name = "dashboard/referencing.html"
1657 def post(self, request, *args, **kwargs):
1658 colid = request.POST["colid"]
1659 if request.FILES.get("risfile") is None:
1660 message = "No file uploaded"
1661 return render(
1662 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1663 )
1664 uploaded_file = request.FILES["risfile"]
1665 comp = ReferencingCheckerWos()
1666 journal = comp.check_references(colid, uploaded_file)
1667 return render(request, self.template_name, {"journal": journal})
1670class ReferencingDashboardView(BaseMersenneDashboardView):
1671 template_name = "dashboard/referencing.html"
1673 def get(self, request, *args, **kwargs):
1674 colid = self.kwargs.get("colid", None)
1675 comp = ReferencingCheckerAds()
1676 journal = comp.check_references(colid)
1677 return render(request, self.template_name, {"journal": journal})
1680class BaseCollectionView(TemplateView):
1681 def get_context_data(self, **kwargs):
1682 context = super().get_context_data(**kwargs)
1683 aid = context.get("aid")
1684 year = context.get("year")
1686 if aid and year:
1687 context["collection"] = self.get_collection(aid, year)
1689 return context
1691 def get_collection(self, aid, year):
1692 """Method to be overridden by subclasses to fetch the appropriate collection"""
1693 raise NotImplementedError("Subclasses must implement get_collection method")
1696class ArticleListView(BaseCollectionView):
1697 template_name = "collection-list.html"
1699 def get_collection(self, aid, year):
1700 return Article.objects.filter(
1701 Q(my_container__my_collection__pid=aid)
1702 & (
1703 Q(date_published__year=year, date_online_first__isnull=True)
1704 | Q(date_online_first__year=year)
1705 )
1706 ).prefetch_related("resourcecount_set")
1709class VolumeListView(BaseCollectionView):
1710 template_name = "collection-list.html"
1712 def get_collection(self, aid, year):
1713 return Article.objects.filter(
1714 Q(my_container__my_collection__pid=aid, my_container__year=year)
1715 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1716 ).prefetch_related("resourcecount_set")
1719class DOAJResourceRegisterView(View):
1720 def get(self, request, *args, **kwargs):
1721 pid = kwargs.get("pid", None)
1722 resource = model_helpers.get_resource(pid)
1723 if resource is None:
1724 raise Http404
1725 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1726 resource.colid, None
1727 ):
1728 raise RuntimeError("Pending publications should not be deployed")
1730 try:
1731 data = {}
1732 doaj_meta, response = doaj_pid_register(pid)
1733 if response is None:
1734 return HttpResponse(status=204)
1735 elif doaj_meta and 200 <= response.status_code <= 299:
1736 data.update(doaj_meta)
1737 else:
1738 return HttpResponse(status=response.status_code, reason=response.text)
1739 except Timeout as exception:
1740 return HttpResponse(exception, status=408)
1741 except Exception as exception:
1742 return HttpResponseServerError(exception)
1743 return JsonResponse(data)
1746class CROSSREFResourceRegisterView(View):
1747 def get(self, request, *args, **kwargs):
1748 pid = kwargs.get("pid", None)
1749 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1750 force = kwargs.get("force", None)
1751 if not request.user.is_superuser:
1752 force = None
1754 resource = model_helpers.get_resource(pid)
1755 if resource is None:
1756 raise Http404
1758 resource = resource.cast()
1759 meth = getattr(self, "recordDOI" + resource.classname)
1760 try:
1761 data = meth(resource, force)
1762 except Timeout as exception:
1763 return HttpResponse(exception, status=408)
1764 except Exception as exception:
1765 return HttpResponseServerError(exception)
1766 return JsonResponse(data)
1768 def recordDOIArticle(self, article, force=None):
1769 result = {"status": 404}
1770 if (
1771 article.doi
1772 and not article.do_not_publish
1773 and (article.date_published or article.date_online_first or force == "force")
1774 ):
1775 if article.my_container.year is None: # or article.my_container.year == '0':
1776 article.my_container.year = datetime.now().strftime("%Y")
1777 result = recordDOI(article)
1778 return result
1780 def recordDOICollection(self, collection, force=None):
1781 return recordDOI(collection)
1783 def recordDOIContainer(self, container, force=None):
1784 data = {"status": 200, "message": "tout va bien"}
1786 if container.ctype == "issue":
1787 if container.doi:
1788 result = recordDOI(container)
1789 if result["status"] != 200:
1790 return result
1791 if force == "force":
1792 articles = container.article_set.exclude(
1793 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1794 )
1795 else:
1796 articles = container.article_set.exclude(
1797 doi__isnull=True,
1798 do_not_publish=True,
1799 date_published__isnull=True,
1800 date_online_first__isnull=True,
1801 )
1803 for article in articles:
1804 result = self.recordDOIArticle(article, force)
1805 if result["status"] != 200:
1806 data = result
1807 else:
1808 return recordDOI(container)
1809 return data
1812class CROSSREFResourceCheckStatusView(View):
1813 def get(self, request, *args, **kwargs):
1814 pid = kwargs.get("pid", None)
1815 resource = model_helpers.get_resource(pid)
1816 if resource is None:
1817 raise Http404
1818 resource = resource.cast()
1819 meth = getattr(self, "checkDOI" + resource.classname)
1820 try:
1821 meth(resource)
1822 except Timeout as exception:
1823 return HttpResponse(exception, status=408)
1824 except Exception as exception:
1825 return HttpResponseServerError(exception)
1827 data = {"status": 200, "message": "tout va bien"}
1828 return JsonResponse(data)
1830 def checkDOIArticle(self, article):
1831 if article.my_container.year is None or article.my_container.year == "0":
1832 article.my_container.year = datetime.now().strftime("%Y")
1833 checkDOI(article)
1835 def checkDOICollection(self, collection):
1836 checkDOI(collection)
1838 def checkDOIContainer(self, container):
1839 if container.doi is not None:
1840 checkDOI(container)
1841 for article in container.article_set.all():
1842 self.checkDOIArticle(article)
1845class CROSSREFResourcePendingPublicationRegisterView(View):
1846 def get(self, request, *args, **kwargs):
1847 pid = kwargs.get("pid", None)
1848 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1849 force = kwargs.get("force", None)
1850 if not request.user.is_superuser:
1851 force = None
1853 resource = model_helpers.get_resource(pid)
1854 if resource is None:
1855 raise Http404
1857 resource = resource.cast()
1858 meth = getattr(self, "recordPendingPublication" + resource.classname)
1859 try:
1860 data = meth(resource, force)
1861 except Timeout as exception:
1862 return HttpResponse(exception, status=408)
1863 except Exception as exception:
1864 return HttpResponseServerError(exception)
1865 return JsonResponse(data)
1867 def recordPendingPublicationArticle(self, article):
1868 result = {"status": 404}
1869 if article.doi and not article.date_published and not article.date_online_first:
1870 if article.my_container.year is None: # or article.my_container.year == '0':
1871 article.my_container.year = datetime.now().strftime("%Y")
1872 result = recordPendingPublication(article)
1873 return result
1876class RegisterPubmedFormView(FormView):
1877 template_name = "record_pubmed_dialog.html"
1878 form_class = RegisterPubmedForm
1880 def get_context_data(self, **kwargs):
1881 context = super().get_context_data(**kwargs)
1882 context["pid"] = self.kwargs["pid"]
1883 context["helper"] = PtfLargeModalFormHelper
1884 return context
1887class RegisterPubmedView(View):
1888 def get(self, request, *args, **kwargs):
1889 pid = kwargs.get("pid", None)
1890 update_article = self.request.GET.get("update_article", "on") == "on"
1892 article = model_helpers.get_article(pid)
1893 if article is None:
1894 raise Http404
1895 try:
1896 recordPubmed(article, update_article)
1897 except Exception as exception:
1898 messages.error("Unable to register the article in PubMed")
1899 return HttpResponseServerError(exception)
1901 return HttpResponseRedirect(
1902 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1903 )
1906class PTFToolsContainerView(TemplateView):
1907 template_name = ""
1909 def get_context_data(self, **kwargs):
1910 context = super().get_context_data(**kwargs)
1912 container = model_helpers.get_container(self.kwargs.get("pid"))
1913 if container is None:
1914 raise Http404
1915 citing_articles = container.citations()
1916 source = self.request.GET.get("source", None)
1917 if container.ctype.startswith("book"):
1918 book_parts = (
1919 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1920 )
1921 references = False
1922 if container.ctype == "book-monograph":
1923 # on regarde si il y a au moins une bibliographie
1924 for art in container.article_set.all():
1925 if art.bibitem_set.count() > 0:
1926 references = True
1927 context.update(
1928 {
1929 "book": container,
1930 "book_parts": list(book_parts),
1931 "source": source,
1932 "citing_articles": citing_articles,
1933 "references": references,
1934 "test_website": container.get_top_collection()
1935 .extlink_set.get(rel="test_website")
1936 .location,
1937 "prod_website": container.get_top_collection()
1938 .extlink_set.get(rel="website")
1939 .location,
1940 }
1941 )
1942 self.template_name = "book-toc.html"
1943 else:
1944 articles = container.article_set.all().order_by("seq")
1945 for article in articles:
1946 try:
1947 last_match = (
1948 history_models.HistoryEvent.objects.filter(
1949 pid=article.pid,
1950 type="matching",
1951 )
1952 .only("created_on")
1953 .latest("created_on")
1954 )
1955 except history_models.HistoryEvent.DoesNotExist as _:
1956 article.last_match = None
1957 else:
1958 article.last_match = last_match.created_on
1960 # article1 = articles.first()
1961 # date = article1.deployed_date()
1962 # TODO next_issue, previous_issue
1964 # check DOI est maintenant une commande à part
1965 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
1966 # articlesWithStatus = []
1967 # for article in articles:
1968 # checkDOIExistence(article)
1969 # articlesWithStatus.append(article)
1971 test_location = prod_location = ""
1972 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
1973 if qs:
1974 test_location = qs.first().location
1975 qs = container.get_top_collection().extlink_set.filter(rel="website")
1976 if qs:
1977 prod_location = qs.first().location
1978 context.update(
1979 {
1980 "issue": container,
1981 "articles": articles,
1982 "source": source,
1983 "citing_articles": citing_articles,
1984 "test_website": test_location,
1985 "prod_website": prod_location,
1986 }
1987 )
1989 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
1990 context["is_issue_pending_publication"] = True
1991 self.template_name = "issue-items.html"
1993 context["allow_crossref"] = container.allow_crossref()
1994 context["coltype"] = container.my_collection.coltype
1995 return context
1998class ExtLinkInline(InlineFormSetFactory):
1999 model = ExtLink
2000 form_class = ExtLinkForm
2001 factory_kwargs = {"extra": 0}
2004class ResourceIdInline(InlineFormSetFactory):
2005 model = ResourceId
2006 form_class = ResourceIdForm
2007 factory_kwargs = {"extra": 0}
2010class IssueDetailAPIView(View):
2011 def get(self, request, *args, **kwargs):
2012 issue = get_object_or_404(Container, pid=kwargs["pid"])
2013 deployed_date = issue.deployed_date()
2014 result = {
2015 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2016 if deployed_date
2017 else None,
2018 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2019 "all_doi_are_registered": issue.all_doi_are_registered(),
2020 "registered_in_doaj": issue.registered_in_doaj(),
2021 "doi": issue.my_collection.doi,
2022 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2023 }
2024 try:
2025 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2026 except history_models.HistoryEvent.DoesNotExist as _:
2027 pass
2028 else:
2029 result["latest"] = latest.message
2030 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2031 "%Y-%m-%d %H:%M"
2032 )
2034 result["latest_type"] = latest.type.capitalize()
2035 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2036 try:
2037 result[event_type] = timezone.localtime(
2038 history_models.HistoryEvent.objects.filter(
2039 type=event_type,
2040 status="OK",
2041 pid__startswith=issue.pid,
2042 )
2043 .latest("created_on")
2044 .created_on
2045 ).strftime("%Y-%m-%d %H:%M")
2046 except history_models.HistoryEvent.DoesNotExist as _:
2047 result[event_type] = ""
2048 return JsonResponse(result)
2051class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2052 model = Collection
2053 form_class = CollectionForm
2054 inlines = [ResourceIdInline, ExtLinkInline]
2055 inlines_names = ["resource_ids_form", "ext_links_form"]
2057 def get_context_data(self, **kwargs):
2058 context = super().get_context_data(**kwargs)
2059 context["helper"] = PtfFormHelper
2060 context["formset_helper"] = FormSetHelper
2061 return context
2063 def add_description(self, collection, description, lang, seq):
2064 if description:
2065 la = Abstract(
2066 resource=collection,
2067 tag="description",
2068 lang=lang,
2069 seq=seq,
2070 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2071 value_html=description,
2072 value_tex=description,
2073 )
2074 la.save()
2076 def form_valid(self, form):
2077 if form.instance.abbrev:
2078 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2079 else:
2080 form.instance.title_xml = (
2081 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2082 )
2084 form.instance.title_html = form.instance.title_tex
2085 form.instance.title_sort = form.instance.title_tex
2086 result = super().form_valid(form)
2088 collection = self.object
2089 collection.abstract_set.all().delete()
2091 seq = 1
2092 description = form.cleaned_data["description_en"]
2093 if description:
2094 self.add_description(collection, description, "en", seq)
2095 seq += 1
2096 description = form.cleaned_data["description_fr"]
2097 if description:
2098 self.add_description(collection, description, "fr", seq)
2100 return result
2102 def get_success_url(self):
2103 messages.success(self.request, "La Collection a été modifiée avec succès")
2104 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2107class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2108 """
2109 Warning : Not yet finished
2110 Automatic site membership creation is still missing
2111 """
2114class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2115 slug_field = "pid"
2116 slug_url_kwarg = "pid"
2119def suggest_load_journal_dois(colid):
2120 articles = (
2121 Article.objects.filter(my_container__my_collection__pid=colid)
2122 .filter(doi__isnull=False)
2123 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2124 .values_list("doi", flat=True)
2125 )
2127 try:
2128 articles = sorted(
2129 articles,
2130 key=lambda d: (
2131 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2132 int(re.search(r".(\d+)$", d).group(1)),
2133 ),
2134 )
2135 except: # noqa: E722 (we'll look later)
2136 pass
2137 return [f'<option value="{doi}">' for doi in articles]
2140def get_context_with_volumes(journal):
2141 result = model_helpers.get_volumes_in_collection(journal)
2142 volume_count = result["volume_count"]
2143 collections = []
2144 for ancestor in journal.ancestors.all():
2145 item = model_helpers.get_volumes_in_collection(ancestor)
2146 volume_count = max(0, volume_count)
2147 item.update({"journal": ancestor})
2148 collections.append(item)
2150 # add the parent collection to its children list and sort it by date
2151 result.update({"journal": journal})
2152 collections.append(result)
2154 collections = [c for c in collections if c["sorted_issues"]]
2155 collections.sort(
2156 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2157 reverse=True,
2158 )
2160 context = {
2161 "journal": journal,
2162 "sorted_issues": result["sorted_issues"],
2163 "volume_count": volume_count,
2164 "max_width": result["max_width"],
2165 "collections": collections,
2166 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2167 }
2168 return context
2171class CollectionDetail(
2172 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2173):
2174 model = Collection
2175 slug_field = "pid"
2176 slug_url_kwarg = "pid"
2177 template_name = "ptf/collection_detail.html"
2179 def test_func(self):
2180 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2182 def get(self, request, *args, **kwargs):
2183 self.object = self.get_object(queryset=Collection.objects.all())
2184 return super().get(request, *args, **kwargs)
2186 def get_context_data(self, **kwargs):
2187 context = super().get_context_data(**kwargs)
2188 context["object_list"] = context["object_list"].filter(
2189 Q(ctype="issue") | Q(ctype="book-lecture-notes")
2190 )
2191 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2192 context.update(get_context_with_volumes(self.object))
2194 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2195 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2196 context["issue_to_appear"] = Container.objects.filter(
2197 pid=context["issue_to_appear_pid"]
2198 ).exists()
2199 try:
2200 latest_error = history_models.HistoryEvent.objects.filter(
2201 status="ERROR", col=self.object
2202 ).latest("created_on")
2203 except history_models.HistoryEvent.DoesNotExist as _:
2204 pass
2205 else:
2206 message = latest_error.message
2207 if message:
2208 i = message.find(" - ")
2209 latest_exception = message[:i]
2210 latest_error_message = message[i + 3 :]
2211 context["latest_exception"] = latest_exception
2212 context["latest_exception_date"] = latest_error.created_on
2213 context["latest_exception_type"] = latest_error.type
2214 context["latest_error_message"] = latest_error_message
2216 archive_in_error = history_models.HistoryEvent.objects.filter(
2217 status="ERROR", col=self.object, type="archive"
2218 ).exists()
2220 context["archive_in_error"] = archive_in_error
2222 return context
2224 def get_queryset(self):
2225 query = self.object.content.all()
2227 for ancestor in self.object.ancestors.all():
2228 query |= ancestor.content.all()
2230 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2233class ContainerEditView(FormView):
2234 template_name = "container_form.html"
2235 form_class = ContainerForm
2237 def get_success_url(self):
2238 if self.kwargs["pid"]:
2239 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2240 return reverse("mersenne_dashboard/published_articles")
2242 def set_success_message(self): # pylint: disable=no-self-use
2243 messages.success(self.request, "Le fascicule a été modifié")
2245 def get_form_kwargs(self):
2246 kwargs = super().get_form_kwargs()
2247 if "pid" not in self.kwargs:
2248 self.kwargs["pid"] = None
2249 if "colid" not in self.kwargs:
2250 self.kwargs["colid"] = None
2251 if "data" in kwargs and "colid" in kwargs["data"]:
2252 # colid is passed as a hidden param in the form.
2253 # It is used when you submit a new container
2254 self.kwargs["colid"] = kwargs["data"]["colid"]
2256 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2257 self.kwargs["pid"]
2258 )
2259 return kwargs
2261 def get_context_data(self, **kwargs):
2262 context = super().get_context_data(**kwargs)
2264 context["pid"] = self.kwargs["pid"]
2265 context["colid"] = self.kwargs["colid"]
2266 context["container"] = self.kwargs["container"]
2268 context["edit_container"] = context["pid"] is not None
2269 context["name"] = resolve(self.request.path_info).url_name
2271 return context
2273 def form_valid(self, form):
2274 new_pid = form.cleaned_data.get("pid")
2275 new_title = form.cleaned_data.get("title")
2276 new_trans_title = form.cleaned_data.get("trans_title")
2277 new_publisher = form.cleaned_data.get("publisher")
2278 new_year = form.cleaned_data.get("year")
2279 new_volume = form.cleaned_data.get("volume")
2280 new_number = form.cleaned_data.get("number")
2282 collection = None
2283 issue = self.kwargs["container"]
2284 if issue is not None:
2285 collection = issue.my_collection
2286 elif self.kwargs["colid"] is not None:
2287 if "CR" in self.kwargs["colid"]:
2288 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2289 else:
2290 collection = model_helpers.get_collection(self.kwargs["colid"])
2292 if collection is None:
2293 raise ValueError("Collection for " + new_pid + " does not exist")
2295 # Icon
2296 new_icon_location = ""
2297 if "icon" in self.request.FILES:
2298 filename = os.path.basename(self.request.FILES["icon"].name)
2299 file_extension = filename.split(".")[1]
2301 icon_filename = resolver.get_disk_location(
2302 settings.MERSENNE_TEST_DATA_FOLDER,
2303 collection.pid,
2304 file_extension,
2305 new_pid,
2306 None,
2307 True,
2308 )
2310 with open(icon_filename, "wb+") as destination:
2311 for chunk in self.request.FILES["icon"].chunks():
2312 destination.write(chunk)
2314 folder = resolver.get_relative_folder(collection.pid, new_pid)
2315 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2316 name = resolve(self.request.path_info).url_name
2317 if name == "special_issue_create":
2318 self.kwargs["name"] = name
2319 if self.kwargs["container"]:
2320 # Edit Issue
2321 issue = self.kwargs["container"]
2322 if issue is None:
2323 raise ValueError(self.kwargs["pid"] + " does not exist")
2325 issue.pid = new_pid
2326 issue.title_tex = issue.title_html = new_title
2327 issue.title_xml = build_title_xml(
2328 title=new_title,
2329 lang=issue.lang,
2330 title_type="issue-title",
2331 )
2333 trans_lang = ""
2334 if issue.trans_lang != "" and issue.trans_lang != "und":
2335 trans_lang = issue.trans_lang
2336 elif new_trans_title != "":
2337 trans_lang = "fr" if issue.lang == "en" else "en"
2338 issue.trans_lang = trans_lang
2340 if trans_lang != "" and new_trans_title != "":
2341 issue.trans_title_html = ""
2342 issue.trans_title_tex = ""
2343 title_xml = build_title_xml(
2344 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2345 )
2346 try:
2347 trans_title_object = Title.objects.get(resource=issue, lang=trans_lang)
2348 trans_title_object.title_html = new_trans_title
2349 trans_title_object.title_xml = title_xml
2350 trans_title_object.save()
2351 except Title.DoesNotExist:
2352 trans_title = Title(
2353 resource=issue,
2354 lang=trans_lang,
2355 type="main",
2356 title_html=new_trans_title,
2357 title_xml=title_xml,
2358 )
2359 trans_title.save()
2360 issue.year = new_year
2361 issue.volume = new_volume
2362 issue.volume_int = make_int(new_volume)
2363 issue.number = new_number
2364 issue.number_int = make_int(new_number)
2365 issue.save()
2366 else:
2367 xissue = create_issuedata()
2369 xissue.ctype = "issue"
2370 xissue.pid = new_pid
2371 xissue.lang = "en"
2372 xissue.title_tex = new_title
2373 xissue.title_html = new_title
2374 xissue.title_xml = build_title_xml(
2375 title=new_title, lang=xissue.lang, title_type="issue-title"
2376 )
2378 if new_trans_title != "":
2379 trans_lang = "fr"
2380 title_xml = build_title_xml(
2381 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2382 )
2383 title = create_titledata(
2384 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2385 )
2386 issue.titles = [title]
2388 xissue.year = new_year
2389 xissue.volume = new_volume
2390 xissue.number = new_number
2391 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2393 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2394 cmd.add_collection(collection)
2395 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2396 issue = cmd.do()
2398 self.kwargs["pid"] = new_pid
2400 # Add objects related to the article: contribs, datastream, counts...
2401 params = {
2402 "icon_location": new_icon_location,
2403 }
2404 cmd = ptf_cmds.updateContainerPtfCmd(params)
2405 cmd.set_resource(issue)
2406 cmd.do()
2408 publisher = model_helpers.get_publisher(new_publisher)
2409 if not publisher:
2410 xpub = create_publisherdata()
2411 xpub.name = new_publisher
2412 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2413 issue.my_publisher = publisher
2414 issue.save()
2416 self.set_success_message()
2418 return super().form_valid(form)
2421# class ArticleEditView(FormView):
2422# template_name = 'article_form.html'
2423# form_class = ArticleForm
2424#
2425# def get_success_url(self):
2426# if self.kwargs['pid']:
2427# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2428# return reverse('mersenne_dashboard/published_articles')
2429#
2430# def set_success_message(self): # pylint: disable=no-self-use
2431# messages.success(self.request, "L'article a été modifié")
2432#
2433# def get_form_kwargs(self):
2434# kwargs = super(ArticleEditView, self).get_form_kwargs()
2435#
2436# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2437# # Article creation: pid is None
2438# self.kwargs['pid'] = None
2439# if 'issue_id' not in self.kwargs:
2440# # Article edit: issue_id is not passed
2441# self.kwargs['issue_id'] = None
2442# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2443# # colid is passed as a hidden param in the form.
2444# # It is used when you submit a new container
2445# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2446#
2447# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2448# return kwargs
2449#
2450# def get_context_data(self, **kwargs):
2451# context = super(ArticleEditView, self).get_context_data(**kwargs)
2452#
2453# context['pid'] = self.kwargs['pid']
2454# context['issue_id'] = self.kwargs['issue_id']
2455# context['article'] = self.kwargs['article']
2456#
2457# context['edit_article'] = context['pid'] is not None
2458#
2459# article = context['article']
2460# if article:
2461# context['author_contributions'] = article.get_author_contributions()
2462# context['kwds_fr'] = None
2463# context['kwds_en'] = None
2464# kwd_gps = article.get_non_msc_kwds()
2465# for kwd_gp in kwd_gps:
2466# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2467# if kwd_gp.value_xml:
2468# kwd_ = types.SimpleNamespace()
2469# kwd_.value = kwd_gp.value_tex
2470# context['kwd_unstructured_fr'] = kwd_
2471# context['kwds_fr'] = kwd_gp.kwd_set.all()
2472# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2473# if kwd_gp.value_xml:
2474# kwd_ = types.SimpleNamespace()
2475# kwd_.value = kwd_gp.value_tex
2476# context['kwd_unstructured_en'] = kwd_
2477# context['kwds_en'] = kwd_gp.kwd_set.all()
2478#
2479# # Article creation: init pid
2480# if context['issue_id'] and context['pid'] is None:
2481# issue = model_helpers.get_container(context['issue_id'])
2482# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2483#
2484# return context
2485#
2486# def form_valid(self, form):
2487#
2488# new_pid = form.cleaned_data.get('pid')
2489# new_title = form.cleaned_data.get('title')
2490# new_fpage = form.cleaned_data.get('fpage')
2491# new_lpage = form.cleaned_data.get('lpage')
2492# new_page_range = form.cleaned_data.get('page_range')
2493# new_page_count = form.cleaned_data.get('page_count')
2494# new_coi_statement = form.cleaned_data.get('coi_statement')
2495# new_show_body = form.cleaned_data.get('show_body')
2496# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2497#
2498# # TODO support MathML
2499# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2500# # We need to pass trans_title to get_title_xml
2501# # Meanwhile, ignore new_title_xml
2502# new_title_xml = jats_parser.get_title_xml(new_title)
2503# new_title_html = new_title
2504#
2505# authors_count = int(self.request.POST.get('authors_count', "0"))
2506# i = 1
2507# new_authors = []
2508# old_author_contributions = []
2509# if self.kwargs['article']:
2510# old_author_contributions = self.kwargs['article'].get_author_contributions()
2511#
2512# while authors_count > 0:
2513# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2514#
2515# if prefix is not None:
2516# addresses = []
2517# if len(old_author_contributions) >= i:
2518# old_author_contribution = old_author_contributions[i - 1]
2519# addresses = [contrib_address.address for contrib_address in
2520# old_author_contribution.get_addresses()]
2521#
2522# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2523# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2524# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2525# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2526# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2527# deceased_before_publication = deceased == 'on'
2528# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2529# equal_contrib = equal_contrib == 'on'
2530# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2531# corresponding = corresponding == 'on'
2532# email = self.request.POST.get('email-' + str(i), None)
2533#
2534# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2535# params['deceased_before_publication'] = deceased_before_publication
2536# params['equal_contrib'] = equal_contrib
2537# params['corresponding'] = corresponding
2538# params['addresses'] = addresses
2539# params['email'] = email
2540#
2541# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2542#
2543# new_authors.append(params)
2544#
2545# authors_count -= 1
2546# i += 1
2547#
2548# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2549# i = 1
2550# new_kwds_fr = []
2551# while kwds_fr_count > 0:
2552# value = self.request.POST.get('kwd-fr-' + str(i), None)
2553# new_kwds_fr.append(value)
2554# kwds_fr_count -= 1
2555# i += 1
2556# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2557#
2558# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2559# i = 1
2560# new_kwds_en = []
2561# while kwds_en_count > 0:
2562# value = self.request.POST.get('kwd-en-' + str(i), None)
2563# new_kwds_en.append(value)
2564# kwds_en_count -= 1
2565# i += 1
2566# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2567#
2568# if self.kwargs['article']:
2569# # Edit article
2570# container = self.kwargs['article'].my_container
2571# else:
2572# # New article
2573# container = model_helpers.get_container(self.kwargs['issue_id'])
2574#
2575# if container is None:
2576# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2577#
2578# collection = container.my_collection
2579#
2580# # Copy PDF file & extract full text
2581# body = ''
2582# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2583# collection.pid,
2584# "pdf",
2585# container.pid,
2586# new_pid,
2587# True)
2588# if 'pdf' in self.request.FILES:
2589# with open(pdf_filename, 'wb+') as destination:
2590# for chunk in self.request.FILES['pdf'].chunks():
2591# destination.write(chunk)
2592#
2593# # Extract full text from the PDF
2594# body = utils.pdf_to_text(pdf_filename)
2595#
2596# # Icon
2597# new_icon_location = ''
2598# if 'icon' in self.request.FILES:
2599# filename = os.path.basename(self.request.FILES['icon'].name)
2600# file_extension = filename.split('.')[1]
2601#
2602# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2603# collection.pid,
2604# file_extension,
2605# container.pid,
2606# new_pid,
2607# True)
2608#
2609# with open(icon_filename, 'wb+') as destination:
2610# for chunk in self.request.FILES['icon'].chunks():
2611# destination.write(chunk)
2612#
2613# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2614# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2615#
2616# if self.kwargs['article']:
2617# # Edit article
2618# article = self.kwargs['article']
2619# article.fpage = new_fpage
2620# article.lpage = new_lpage
2621# article.page_range = new_page_range
2622# article.coi_statement = new_coi_statement
2623# article.show_body = new_show_body
2624# article.do_not_publish = new_do_not_publish
2625# article.save()
2626#
2627# else:
2628# # New article
2629# params = {
2630# 'pid': new_pid,
2631# 'title_xml': new_title_xml,
2632# 'title_html': new_title_html,
2633# 'title_tex': new_title,
2634# 'fpage': new_fpage,
2635# 'lpage': new_lpage,
2636# 'page_range': new_page_range,
2637# 'seq': container.article_set.count() + 1,
2638# 'body': body,
2639# 'coi_statement': new_coi_statement,
2640# 'show_body': new_show_body,
2641# 'do_not_publish': new_do_not_publish
2642# }
2643#
2644# xarticle = create_articledata()
2645# xarticle.pid = new_pid
2646# xarticle.title_xml = new_title_xml
2647# xarticle.title_html = new_title_html
2648# xarticle.title_tex = new_title
2649# xarticle.fpage = new_fpage
2650# xarticle.lpage = new_lpage
2651# xarticle.page_range = new_page_range
2652# xarticle.seq = container.article_set.count() + 1
2653# xarticle.body = body
2654# xarticle.coi_statement = new_coi_statement
2655# params['xobj'] = xarticle
2656#
2657# cmd = ptf_cmds.addArticlePtfCmd(params)
2658# cmd.set_container(container)
2659# cmd.add_collection(container.my_collection)
2660# article = cmd.do()
2661#
2662# self.kwargs['pid'] = new_pid
2663#
2664# # Add objects related to the article: contribs, datastream, counts...
2665# params = {
2666# # 'title_xml': new_title_xml,
2667# # 'title_html': new_title_html,
2668# # 'title_tex': new_title,
2669# 'authors': new_authors,
2670# 'page_count': new_page_count,
2671# 'icon_location': new_icon_location,
2672# 'body': body,
2673# 'use_kwds': True,
2674# 'kwds_fr': new_kwds_fr,
2675# 'kwds_en': new_kwds_en,
2676# 'kwd_uns_fr': new_kwd_uns_fr,
2677# 'kwd_uns_en': new_kwd_uns_en
2678# }
2679# cmd = ptf_cmds.updateArticlePtfCmd(params)
2680# cmd.set_article(article)
2681# cmd.do()
2682#
2683# self.set_success_message()
2684#
2685# return super(ArticleEditView, self).form_valid(form)
2688@require_http_methods(["POST"])
2689def do_not_publish_article(request, *args, **kwargs):
2690 next = request.headers.get("referer")
2692 pid = kwargs.get("pid", "")
2694 article = model_helpers.get_article(pid)
2695 if article:
2696 article.do_not_publish = not article.do_not_publish
2697 article.save()
2698 else:
2699 raise Http404
2701 return HttpResponseRedirect(next)
2704@require_http_methods(["POST"])
2705def show_article_body(request, *args, **kwargs):
2706 next = request.headers.get("referer")
2708 pid = kwargs.get("pid", "")
2710 article = model_helpers.get_article(pid)
2711 if article:
2712 article.show_body = not article.show_body
2713 article.save()
2714 else:
2715 raise Http404
2717 return HttpResponseRedirect(next)
2720class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2721 """
2722 API to get/post article metadata
2723 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2724 """
2726 def __init__(self, *args, **kwargs):
2727 """
2728 we define here what fields we want in the form
2729 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2730 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2731 super().__init__(*args, **kwargs)
2732 self.fields_to_update = [
2733 "lang",
2734 "atype",
2735 "contributors",
2736 "abstracts",
2737 "kwds",
2738 "titles",
2739 "trans_title_html",
2740 "title_html",
2741 "title_xml",
2742 "streams",
2743 "ext_links",
2744 ]
2745 self.additional_fields = [
2746 "pid",
2747 "doi",
2748 "container_pid",
2749 "pdf",
2750 "illustration",
2751 ]
2752 self.editorial_tools = ["translation", "sidebar", "lang_selection"]
2753 self.article_container_pid = ""
2754 self.back_url = "trammel"
2756 def save_data(self, data_article):
2757 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2758 # The icons are not preserved since we can add/edit/delete them in VueJs
2759 params = {
2760 "pid": data_article.pid,
2761 "export_folder": settings.MERSENNE_TMP_FOLDER,
2762 "export_all": True,
2763 "with_binary_files": False,
2764 }
2765 ptf_cmds.exportExtraDataPtfCmd(params).do()
2767 def restore_data(self, article):
2768 ptf_cmds.importExtraDataPtfCmd(
2769 {
2770 "pid": article.pid,
2771 "import_folder": settings.MERSENNE_TMP_FOLDER,
2772 }
2773 ).do()
2775 def get(self, request, *args, **kwargs):
2776 data = super().get(request, *args, **kwargs)
2777 return data
2779 def post(self, request, *args, **kwargs):
2780 response = super().post(request, *args, **kwargs)
2781 if response["message"] == "OK":
2782 return redirect(
2783 "api-edit-article",
2784 colid=kwargs.get("colid", ""),
2785 containerPid=kwargs.get("containerPid"),
2786 doi=kwargs.get("doi", ""),
2787 )
2788 else:
2789 raise Http404
2792class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2793 template_name = "article_form.html"
2795 def get_success_url(self):
2796 if self.kwargs["doi"]:
2797 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2798 return reverse("mersenne_dashboard/published_articles")
2800 def get_context_data(self, **kwargs):
2801 context = super().get_context_data(**kwargs)
2802 if "doi" in self.kwargs:
2803 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"])
2804 context["pid"] = context["article"].pid
2806 return context
2809class ArticleDeleteView(View):
2810 def get(self, request, *args, **kwargs):
2811 pid = self.kwargs.get("pid", None)
2812 article = get_object_or_404(Article, pid=pid)
2814 try:
2815 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2816 article.undeploy(mersenneSite)
2818 cmd = ptf_cmds.addArticlePtfCmd(
2819 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2820 )
2821 cmd.set_container(article.my_container)
2822 cmd.set_object_to_be_deleted(article)
2823 cmd.undo()
2824 except Exception as exception:
2825 return HttpResponseServerError(exception)
2827 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2828 return JsonResponse(data)
2831def get_messages_in_queue():
2832 app = Celery("ptf-tools")
2833 # tasks = list(current_app.tasks)
2834 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2835 print(tasks)
2836 # i = app.control.inspect()
2838 with app.connection_or_acquire() as conn:
2839 remaining = conn.default_channel.queue_declare(
2840 queue="coordinator", passive=True
2841 ).message_count
2842 return remaining
2845class FailedTasksListView(ListView):
2846 model = TaskResult
2847 queryset = TaskResult.objects.filter(
2848 status="FAILURE",
2849 task_name="ptf_tools.tasks.archive_numdam_issue",
2850 )
2853class FailedTasksDeleteView(DeleteView):
2854 model = TaskResult
2855 success_url = reverse_lazy("tasks-failed")
2858class FailedTasksRetryView(SingleObjectMixin, RedirectView):
2859 model = TaskResult
2861 @staticmethod
2862 def retry_task(task):
2863 colid, pid = (arg.strip("'") for arg in task.task_args.strip("()").split(", "))
2864 archive_numdam_resource.s(colid, pid).set(queue="coordinator").delay()
2865 task.delete()
2867 def get_redirect_url(self, *args, **kwargs):
2868 self.retry_task(self.get_object())
2869 return reverse("tasks-failed")
2872class NumdamView(TemplateView, history_views.HistoryContextMixin):
2873 template_name = "numdam.html"
2875 def get_context_data(self, **kwargs):
2876 context = super().get_context_data(**kwargs)
2878 context["objs"] = ResourceInNumdam.objects.all()
2880 pre_issues = []
2881 prod_issues = []
2882 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2883 try:
2884 response = requests.get(url)
2885 if response.status_code == 200:
2886 data = response.json()
2887 if "issues" in data:
2888 pre_issues = data["issues"]
2889 except Exception:
2890 pass
2892 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2893 response = requests.get(url)
2894 if response.status_code == 200:
2895 data = response.json()
2896 if "issues" in data:
2897 prod_issues = data["issues"]
2899 new = sorted(list(set(pre_issues).difference(prod_issues)))
2900 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2901 grouped = [
2902 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2903 ]
2904 grouped_removed = [
2905 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2906 ]
2907 context["added_issues"] = grouped
2908 context["removed_issues"] = grouped_removed
2910 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2911 return context
2914class TasksProgressView(View):
2915 def get(self, *args, **kwargs):
2916 task_name = self.kwargs.get("task", "archive_numdam_issue")
2917 successes = TaskResult.objects.filter(
2918 task_name=f"ptf_tools.tasks.{task_name}", status="SUCCESS"
2919 ).count()
2920 fails = TaskResult.objects.filter(
2921 task_name=f"ptf_tools.tasks.{task_name}", status="FAILURE"
2922 ).count()
2923 last_task = (
2924 TaskResult.objects.filter(
2925 task_name=f"ptf_tools.tasks.{task_name}",
2926 status="SUCCESS",
2927 )
2928 .order_by("-date_done")
2929 .first()
2930 )
2931 if last_task:
2932 last_task = " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
2933 remaining = get_messages_in_queue()
2934 all = successes + remaining
2935 progress = int(successes * 100 / all) if all else 0
2936 error_rate = int(fails * 100 / all) if all else 0
2937 status = "consuming_queue" if (successes or fails) and not progress == 100 else "polling"
2938 data = {
2939 "status": status,
2940 "progress": progress,
2941 "total": all,
2942 "remaining": remaining,
2943 "successes": successes,
2944 "fails": fails,
2945 "error_rate": error_rate,
2946 "last_task": last_task,
2947 }
2948 return JsonResponse(data)
2951class NumdamArchiveView(RedirectView):
2952 @staticmethod
2953 def reset_task_results():
2954 TaskResult.objects.all().delete()
2956 def get_redirect_url(self, *args, **kwargs):
2957 self.colid = kwargs["colid"]
2959 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2960 return Http404
2962 # we make sure archiving is not already running
2963 # if not get_messages_in_queue():
2964 # self.reset_task_results()
2966 if self.colid == "ALL":
2967 archive_numdam_collections.delay()
2968 else:
2969 archive_numdam_collection.s(self.colid).delay()
2971 return reverse("numdam")
2974class DeployAllNumdamAPIView(View):
2975 def internal_do(self, *args, **kwargs):
2976 pids = []
2978 for obj in ResourceInNumdam.objects.all():
2979 pids.append(obj.pid)
2981 return pids
2983 def get(self, request, *args, **kwargs):
2984 try:
2985 pids, status, message = history_views.execute_and_record_func(
2986 "deploy", "numdam", "numdam", self.internal_do, "numdam"
2987 )
2988 except Exception as exception:
2989 return HttpResponseServerError(exception)
2991 data = {"message": message, "ids": pids, "status": status}
2992 return JsonResponse(data)
2995class NumdamDeleteAPIView(View):
2996 def get(self, request, *args, **kwargs):
2997 pid = self.kwargs.get("pid", None)
2999 try:
3000 obj = ResourceInNumdam.objects.get(pid=pid)
3001 obj.delete()
3002 except Exception as exception:
3003 return HttpResponseServerError(exception)
3005 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
3006 return JsonResponse(data)
3009class ExtIdApiDetail(View):
3010 def get(self, request, *args, **kwargs):
3011 extid = get_object_or_404(
3012 ExtId,
3013 resource__pid=kwargs["pid"],
3014 id_type=kwargs["what"],
3015 )
3016 return JsonResponse(
3017 {
3018 "pk": extid.pk,
3019 "href": extid.get_href(),
3020 "fetch": reverse(
3021 "api-fetch-id",
3022 args=(
3023 extid.resource.pk,
3024 extid.id_value,
3025 extid.id_type,
3026 "extid",
3027 ),
3028 ),
3029 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3030 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3031 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3032 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3033 "is_valid": extid.checked,
3034 }
3035 )
3038class ExtIdFormTemplate(TemplateView):
3039 template_name = "common/externalid_form.html"
3041 def get_context_data(self, **kwargs):
3042 context = super().get_context_data(**kwargs)
3043 context["sequence"] = kwargs["sequence"]
3044 return context
3047class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3048 def get_context_data(self, **kwargs):
3049 context = super().get_context_data(**kwargs)
3050 context["helper"] = PtfFormHelper
3051 return context
3053 def get_success_url(self):
3054 self.post_process()
3055 return self.object.bibitem.resource.get_absolute_url()
3057 def post_process(self):
3058 cmd = updateBibitemCitationXmlCmd()
3059 cmd.set_bibitem(self.object.bibitem)
3060 cmd.do()
3061 model_helpers.post_resource_updated(self.object.bibitem.resource)
3064class BibItemIdCreate(BibItemIdFormView, CreateView):
3065 model = BibItemId
3066 form_class = BibItemIdForm
3068 def get_context_data(self, **kwargs):
3069 context = super().get_context_data(**kwargs)
3070 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3071 return context
3073 def get_initial(self):
3074 initial = super().get_initial()
3075 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3076 return initial
3078 def form_valid(self, form):
3079 form.instance.checked = False
3080 return super().form_valid(form)
3083class BibItemIdUpdate(BibItemIdFormView, UpdateView):
3084 model = BibItemId
3085 form_class = BibItemIdForm
3087 def get_context_data(self, **kwargs):
3088 context = super().get_context_data(**kwargs)
3089 context["bibitem"] = self.object.bibitem
3090 return context
3093class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3094 def get_context_data(self, **kwargs):
3095 context = super().get_context_data(**kwargs)
3096 context["helper"] = PtfFormHelper
3097 return context
3099 def get_success_url(self):
3100 self.post_process()
3101 return self.object.resource.get_absolute_url()
3103 def post_process(self):
3104 model_helpers.post_resource_updated(self.object.resource)
3107class ExtIdCreate(ExtIdFormView, CreateView):
3108 model = ExtId
3109 form_class = ExtIdForm
3111 def get_context_data(self, **kwargs):
3112 context = super().get_context_data(**kwargs)
3113 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3114 return context
3116 def get_initial(self):
3117 initial = super().get_initial()
3118 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3119 return initial
3121 def form_valid(self, form):
3122 form.instance.checked = False
3123 return super().form_valid(form)
3126class ExtIdUpdate(ExtIdFormView, UpdateView):
3127 model = ExtId
3128 form_class = ExtIdForm
3130 def get_context_data(self, **kwargs):
3131 context = super().get_context_data(**kwargs)
3132 context["resource"] = self.object.resource
3133 return context
3136class BibItemIdApiDetail(View):
3137 def get(self, request, *args, **kwargs):
3138 bibitemid = get_object_or_404(
3139 BibItemId,
3140 bibitem__resource__pid=kwargs["pid"],
3141 bibitem__sequence=kwargs["seq"],
3142 id_type=kwargs["what"],
3143 )
3144 return JsonResponse(
3145 {
3146 "pk": bibitemid.pk,
3147 "href": bibitemid.get_href(),
3148 "fetch": reverse(
3149 "api-fetch-id",
3150 args=(
3151 bibitemid.bibitem.pk,
3152 bibitemid.id_value,
3153 bibitemid.id_type,
3154 "bibitemid",
3155 ),
3156 ),
3157 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
3158 "uncheck": reverse(
3159 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
3160 ),
3161 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
3162 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
3163 "is_valid": bibitemid.checked,
3164 }
3165 )
3168class UpdateTexmfZipAPIView(View):
3169 def get(self, request, *args, **kwargs):
3170 def copy_zip_files(src_folder, dest_folder):
3171 os.makedirs(dest_folder, exist_ok=True)
3173 zip_files = [
3174 os.path.join(src_folder, f)
3175 for f in os.listdir(src_folder)
3176 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3177 ]
3178 for zip_file in zip_files:
3179 resolver.copy_file(zip_file, dest_folder)
3181 # Exceptions: specific zip/gz files
3182 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3183 resolver.copy_file(zip_file, dest_folder)
3185 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3186 resolver.copy_file(zip_file, dest_folder)
3188 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3189 resolver.copy_file(gz_file, dest_folder)
3191 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3193 dest_folder = os.path.join(
3194 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3195 )
3197 try:
3198 copy_zip_files(src_folder, dest_folder)
3199 except Exception as exception:
3200 return HttpResponseServerError(exception)
3202 try:
3203 dest_folder = os.path.join(
3204 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3205 )
3206 copy_zip_files(src_folder, dest_folder)
3207 except Exception as exception:
3208 return HttpResponseServerError(exception)
3210 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3211 return JsonResponse(data)
3214class TestView(TemplateView):
3215 template_name = "mersenne.html"
3217 def get_context_data(self, **kwargs):
3218 super().get_context_data(**kwargs)
3219 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True)
3220 model_data_converter.db_to_issue_data(issue)
3223class TrammelTasksProgressView(View):
3224 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3225 """
3226 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3227 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3228 """
3229 task_name = task
3231 def get_event_data():
3232 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3234 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3235 remaining_messages = get_messages_in_queue()
3237 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3238 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3239 failed_tasks = all_tasks.filter(status="FAILURE")
3241 all_tasks_count = all_tasks.count()
3242 success_count = successed_tasks.count()
3243 fail_count = failed_tasks.count()
3245 all_count = all_tasks_count + remaining_messages
3246 remaining_count = all_count - success_count - fail_count
3248 success_rate = int(success_count * 100 / all_count) if all_count else 0
3249 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3250 status = "consuming_queue" if remaining_count != 0 else "polling"
3252 last_task = successed_tasks.first()
3253 last_task = (
3254 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3255 if last_task
3256 else ""
3257 )
3259 # SSE event format
3260 event_data = {
3261 "status": status,
3262 "success_rate": success_rate,
3263 "error_rate": error_rate,
3264 "all_count": all_count,
3265 "remaining_count": remaining_count,
3266 "success_count": success_count,
3267 "fail_count": fail_count,
3268 "last_task": last_task,
3269 }
3271 return event_data
3273 def stream_response(data):
3274 # Send initial response headers
3275 yield f"data: {json.dumps(data)}\n\n"
3277 data = get_event_data()
3278 format = request.GET.get("format", "stream")
3279 if format == "json":
3280 response = JsonResponse(data)
3281 else:
3282 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3283 return response
3286class TrammelFailedTasksListView(ListView):
3287 model = TaskResult
3288 queryset = TaskResult.objects.filter(
3289 status="FAILURE",
3290 task_name="ptf_tools.tasks.archive_trammel_collection",
3291 )
3294user_signed_up.connect(update_user_from_invite)