Coverage for src/ptf_tools/views/base_views.py: 17%
1681 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-11-14 10:16 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-11-14 10:16 +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 pubmed.views import recordPubmed
63from requests import Timeout
64from task.tasks.archiving_tasks import archive_resource
66from comments_moderation.utils import get_comments_for_home, is_comment_moderator
67from history import models as history_models
68from history import views as history_views
69from history.utils import get_gap, get_history_last_event_by, get_last_unsolved_error
70from ptf_tools.doaj import doaj_pid_register
71from ptf_tools.doi import checkDOI, recordDOI, recordPendingPublication
72from ptf_tools.forms import (
73 BibItemIdForm,
74 CollectionForm,
75 ContainerForm,
76 DiffContainerForm,
77 ExtIdForm,
78 ExtLinkForm,
79 FormSetHelper,
80 ImportArticleForm,
81 ImportContainerForm,
82 ImportEditflowArticleForm,
83 PtfFormHelper,
84 PtfLargeModalFormHelper,
85 PtfModalFormHelper,
86 RegisterPubmedForm,
87 ResourceIdForm,
88 get_article_choices,
89)
90from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos
91from ptf_tools.models import ResourceInNumdam
92from ptf_tools.signals import update_user_from_invite
93from ptf_tools.tasks import (
94 archive_numdam_collection,
95 archive_numdam_collections,
96 archive_numdam_resource,
97)
98from ptf_tools.templatetags.tools_helpers import get_authorized_collections
99from ptf_tools.utils import is_authorized_editor
102def view_404(request: HttpRequest):
103 """
104 Dummy view raising HTTP 404 exception.
105 """
106 raise Http404
109def check_collection(collection, server_url, server_type):
110 """
111 Check if a collection exists on a serveur (test/prod)
112 and upload the collection (XML, image) if necessary
113 """
115 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
116 response = requests.get(url, verify=False)
117 # First, upload the collection XML
118 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
119 body = xml.encode("utf8")
121 url = server_url + reverse("upload-serials")
122 if response.status_code == 200:
123 # PUT http verb is used for update
124 response = requests.put(url, data=body, verify=False)
125 else:
126 # POST http verb is used for creation
127 response = requests.post(url, data=body, verify=False)
129 # Second, copy the collection images
130 # There is no need to copy files for the test server
131 # Files were already copied in /mersenne_test_data during the ptf_tools import
132 # We only need to copy files from /mersenne_test_data to
133 # /mersenne_prod_data during an upload to prod
134 if server_type == "website":
135 resolver.copy_binary_files(
136 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
137 )
138 elif server_type == "numdam":
139 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
140 if collection.pid in settings.NUMDAM_COLLECTIONS:
141 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
143 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
146def check_lock():
147 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
150def load_cedrics_article_choices(request):
151 colid = request.GET.get("colid")
152 issue = request.GET.get("issue")
153 article_choices = get_article_choices(colid, issue)
154 return render(
155 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
156 )
159class ImportCedricsArticleFormView(FormView):
160 template_name = "import_article.html"
161 form_class = ImportArticleForm
163 def dispatch(self, request, *args, **kwargs):
164 self.colid = self.kwargs["colid"]
165 return super().dispatch(request, *args, **kwargs)
167 def get_success_url(self):
168 if self.colid:
169 return reverse("collection-detail", kwargs={"pid": self.colid})
170 return "/"
172 def get_context_data(self, **kwargs):
173 context = super().get_context_data(**kwargs)
174 context["colid"] = self.colid
175 context["helper"] = PtfModalFormHelper
176 return context
178 def get_form_kwargs(self):
179 kwargs = super().get_form_kwargs()
180 kwargs["colid"] = self.colid
181 return kwargs
183 def form_valid(self, form):
184 self.issue = form.cleaned_data["issue"]
185 self.article = form.cleaned_data["article"]
186 return super().form_valid(form)
188 def import_cedrics_article(self, *args, **kwargs):
189 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
190 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
191 )
192 cmd.do()
194 def post(self, request, *args, **kwargs):
195 self.colid = self.kwargs.get("colid", None)
196 issue = request.POST["issue"]
197 self.article_pid = request.POST["article"]
198 self.issue_pid = os.path.basename(os.path.dirname(issue))
200 import_args = [self]
201 import_kwargs = {}
203 try:
204 _, status, message = history_views.execute_and_record_func(
205 "import",
206 f"{self.issue_pid} / {self.article_pid}",
207 self.colid,
208 self.import_cedrics_article,
209 "",
210 False,
211 None,
212 None,
213 *import_args,
214 **import_kwargs,
215 )
217 messages.success(
218 self.request, f"L'article {self.article_pid} a été importé avec succès"
219 )
221 except Exception as exception:
222 messages.error(
223 self.request,
224 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}",
225 )
227 return redirect(self.get_success_url())
230class ImportCedricsIssueView(FormView):
231 template_name = "import_container.html"
232 form_class = ImportContainerForm
234 def dispatch(self, request, *args, **kwargs):
235 self.colid = self.kwargs["colid"]
236 self.to_appear = self.request.GET.get("to_appear", False)
237 return super().dispatch(request, *args, **kwargs)
239 def get_success_url(self):
240 if self.filename:
241 return reverse(
242 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
243 )
244 return "/"
246 def get_context_data(self, **kwargs):
247 context = super().get_context_data(**kwargs)
248 context["colid"] = self.colid
249 context["helper"] = PtfModalFormHelper
250 return context
252 def get_form_kwargs(self):
253 kwargs = super().get_form_kwargs()
254 kwargs["colid"] = self.colid
255 kwargs["to_appear"] = self.to_appear
256 return kwargs
258 def form_valid(self, form):
259 self.filename = form.cleaned_data["filename"].split("/")[-1]
260 return super().form_valid(form)
263class DiffCedricsIssueView(FormView):
264 template_name = "diff_container_form.html"
265 form_class = DiffContainerForm
266 diffs = None
267 xissue = None
268 xissue_encoded = None
270 def get_success_url(self):
271 return reverse("collection-detail", kwargs={"pid": self.colid})
273 def dispatch(self, request, *args, **kwargs):
274 self.colid = self.kwargs["colid"]
275 # self.filename = self.kwargs['filename']
276 return super().dispatch(request, *args, **kwargs)
278 def get(self, request, *args, **kwargs):
279 self.filename = request.GET["filename"]
280 self.remove_mail = request.GET.get("remove_email", "off")
281 self.remove_date_prod = request.GET.get("remove_date_prod", "off")
282 self.remove_email = self.remove_mail == "on"
283 self.remove_date_prod = self.remove_date_prod == "on"
285 try:
286 result, status, message = history_views.execute_and_record_func(
287 "import",
288 os.path.basename(self.filename),
289 self.colid,
290 self.diff_cedrics_issue,
291 "",
292 True,
293 )
294 except Exception as exception:
295 pid = self.filename.split("/")[-1]
296 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
297 return HttpResponseRedirect(self.get_success_url())
299 no_conflict = result[0]
300 self.diffs = result[1]
301 self.xissue = result[2]
303 if no_conflict:
304 # Proceed with the import
305 self.form_valid(self.get_form())
306 return redirect(self.get_success_url())
307 else:
308 # Display the diff template
309 self.xissue_encoded = jsonpickle.encode(self.xissue)
311 return super().get(request, *args, **kwargs)
313 def post(self, request, *args, **kwargs):
314 self.filename = request.POST["filename"]
315 data = request.POST["xissue_encoded"]
316 self.xissue = jsonpickle.decode(data)
318 return super().post(request, *args, **kwargs)
320 def get_context_data(self, **kwargs):
321 context = super().get_context_data(**kwargs)
322 context["colid"] = self.colid
323 context["diff"] = self.diffs
324 context["filename"] = self.filename
325 context["xissue_encoded"] = self.xissue_encoded
326 return context
328 def get_form_kwargs(self):
329 kwargs = super().get_form_kwargs()
330 kwargs["colid"] = self.colid
331 return kwargs
333 def diff_cedrics_issue(self, *args, **kwargs):
334 params = {
335 "colid": self.colid,
336 "input_file": self.filename,
337 "remove_email": self.remove_mail,
338 "remove_date_prod": self.remove_date_prod,
339 "diff_only": True,
340 }
342 if settings.IMPORT_CEDRICS_DIRECTLY:
343 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
344 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
345 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
346 else:
347 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
349 result = cmd.do()
350 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
351 messages.warning(
352 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings
353 )
355 return result
357 def import_cedrics_issue(self, *args, **kwargs):
358 # modify xissue with data_issue if params to override
359 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
360 issue = model_helpers.get_container(self.xissue.pid)
361 if issue:
362 data_issue = model_data_converter.db_to_issue_data(issue)
363 for xarticle in self.xissue.articles:
364 filter_articles = [
365 article for article in data_issue.articles if article.doi == xarticle.doi
366 ]
367 if len(filter_articles) > 0:
368 db_article = filter_articles[0]
369 xarticle.coi_statement = db_article.coi_statement
370 xarticle.kwds = db_article.kwds
371 xarticle.contrib_groups = db_article.contrib_groups
373 params = {
374 "colid": self.colid,
375 "xissue": self.xissue,
376 "input_file": self.filename,
377 }
379 if settings.IMPORT_CEDRICS_DIRECTLY:
380 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
381 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
382 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
383 else:
384 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
386 cmd.do()
388 def form_valid(self, form):
389 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
390 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
391 else:
392 import_kwargs = {}
393 import_args = [self]
395 try:
396 _, status, message = history_views.execute_and_record_func(
397 "import",
398 self.xissue.pid,
399 self.kwargs["colid"],
400 self.import_cedrics_issue,
401 "",
402 False,
403 None,
404 None,
405 *import_args,
406 **import_kwargs,
407 )
408 except Exception as exception:
409 messages.error(
410 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
411 )
412 return super().form_invalid(form)
414 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
415 return super().form_valid(form)
418class ImportEditflowArticleFormView(FormView):
419 template_name = "import_editflow_article.html"
420 form_class = ImportEditflowArticleForm
422 def dispatch(self, request, *args, **kwargs):
423 self.colid = self.kwargs["colid"]
424 return super().dispatch(request, *args, **kwargs)
426 def get_context_data(self, **kwargs):
427 context = super().get_context_data(**kwargs)
428 context["colid"] = self.kwargs["colid"]
429 context["helper"] = PtfLargeModalFormHelper
430 return context
432 def get_success_url(self):
433 if self.colid:
434 return reverse("collection-detail", kwargs={"pid": self.colid})
435 return "/"
437 def post(self, request, *args, **kwargs):
438 self.colid = self.kwargs.get("colid", None)
439 try:
440 if not self.colid:
441 raise ValueError("Missing collection id")
443 issue_name = settings.ISSUE_PENDING_PUBLICATION_PIDS.get(self.colid)
444 if not issue_name:
445 raise ValueError(
446 "Issue not found in Pending Publications PIDs. Did you forget to add it?"
447 )
449 issue = model_helpers.get_container(issue_name)
450 if not issue:
451 raise ValueError("No issue found")
453 editflow_xml_file = request.FILES.get("editflow_xml_file")
454 if not editflow_xml_file:
455 raise ValueError("The file you specified couldn't be found")
457 body = editflow_xml_file.read().decode("utf-8")
459 cmd = xml_cmds.addArticleXmlCmd(
460 {
461 "body": body,
462 "issue": issue,
463 "assign_doi": True,
464 "standalone": True,
465 "from_folder": settings.RESOURCES_ROOT,
466 }
467 )
468 cmd.set_collection(issue.get_collection())
469 cmd.do()
471 messages.success(
472 request,
473 f'Editflow article successfully imported into issue "{issue_name}"',
474 )
476 except Exception as exception:
477 messages.error(
478 request,
479 f"Import failed: {str(exception)}",
480 )
482 return redirect(self.get_success_url())
485class BibtexAPIView(View):
486 def get(self, request, *args, **kwargs):
487 pid = self.kwargs.get("pid", None)
488 all_bibtex = ""
489 if pid:
490 article = model_helpers.get_article(pid)
491 if article:
492 for bibitem in article.bibitem_set.all():
493 bibtex_array = bibitem.get_bibtex()
494 last = len(bibtex_array)
495 i = 1
496 for bibtex in bibtex_array:
497 if i > 1 and i < last:
498 all_bibtex += " "
499 all_bibtex += bibtex + "\n"
500 i += 1
502 data = {"bibtex": all_bibtex}
503 return JsonResponse(data)
506class MatchingAPIView(View):
507 def get(self, request, *args, **kwargs):
508 pid = self.kwargs.get("pid", None)
510 url = settings.MATCHING_URL
511 headers = {"Content-Type": "application/xml"}
513 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
515 if settings.DEBUG:
516 print("Issue exported to /tmp/issue.xml")
517 f = open("/tmp/issue.xml", "w")
518 f.write(body.encode("utf8"))
519 f.close()
521 r = requests.post(url, data=body.encode("utf8"), headers=headers)
522 body = r.text.encode("utf8")
523 data = {"status": r.status_code, "message": body[:1000]}
525 if settings.DEBUG:
526 print("Matching received, new issue exported to /tmp/issue1.xml")
527 f = open("/tmp/issue1.xml", "w")
528 text = body
529 f.write(text)
530 f.close()
532 resource = model_helpers.get_resource(pid)
533 obj = resource.cast()
534 colid = obj.get_collection().pid
536 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
538 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
539 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
540 )
541 cmd.do()
543 print("Matching finished")
544 return JsonResponse(data)
547class ImportAllAPIView(View):
548 def internal_do(self, *args, **kwargs):
549 pid = self.kwargs.get("pid", None)
551 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
552 if not os.path.isdir(root_folder):
553 raise ValueError(root_folder + " does not exist")
555 resource = model_helpers.get_resource(pid)
556 if not resource:
557 file = os.path.join(root_folder, pid + ".xml")
558 body = utils.get_file_content_in_utf8(file)
559 journals = xml_cmds.addCollectionsXmlCmd(
560 {
561 "body": body,
562 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
563 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
564 }
565 ).do()
566 if not journals:
567 raise ValueError(file + " does not contain a collection")
568 resource = journals[0]
569 # resolver.copy_binary_files(
570 # resource,
571 # settings.MATHDOC_ARCHIVE_FOLDER,
572 # settings.MERSENNE_TEST_DATA_FOLDER)
574 obj = resource.cast()
576 if obj.classname != "Collection":
577 raise ValueError(pid + " does not contain a collection")
579 cmd = xml_cmds.collectEntireCollectionXmlCmd(
580 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
581 )
582 pids = cmd.do()
584 return pids
586 def get(self, request, *args, **kwargs):
587 pid = self.kwargs.get("pid", None)
589 try:
590 pids, status, message = history_views.execute_and_record_func(
591 "import", pid, pid, self.internal_do
592 )
593 except Timeout as exception:
594 return HttpResponse(exception, status=408)
595 except Exception as exception:
596 return HttpResponseServerError(exception)
598 data = {"message": message, "ids": pids, "status": status}
599 return JsonResponse(data)
602class DeployAllAPIView(View):
603 def internal_do(self, *args, **kwargs):
604 pid = self.kwargs.get("pid", None)
605 site = self.kwargs.get("site", None)
607 pids = []
609 collection = model_helpers.get_collection(pid)
610 if not collection:
611 raise RuntimeError(pid + " does not exist")
613 if site == "numdam":
614 server_url = settings.NUMDAM_PRE_URL
615 elif site != "ptf_tools":
616 server_url = getattr(collection, site)()
617 if not server_url:
618 raise RuntimeError("The collection has no " + site)
620 if site != "ptf_tools":
621 # check if the collection exists on the server
622 # if not, check_collection will upload the collection (XML,
623 # image...)
624 check_collection(collection, server_url, site)
626 for issue in collection.content.all():
627 if site != "website" or (site == "website" and issue.are_all_articles_published()):
628 pids.append(issue.pid)
630 return pids
632 def get(self, request, *args, **kwargs):
633 pid = self.kwargs.get("pid", None)
634 site = self.kwargs.get("site", None)
636 try:
637 pids, status, message = history_views.execute_and_record_func(
638 "deploy", pid, pid, self.internal_do, site
639 )
640 except Timeout as exception:
641 return HttpResponse(exception, status=408)
642 except Exception as exception:
643 return HttpResponseServerError(exception)
645 data = {"message": message, "ids": pids, "status": status}
646 return JsonResponse(data)
649class AddIssuePDFView(View):
650 def __init(self, *args, **kwargs):
651 super().__init__(*args, **kwargs)
652 self.pid = None
653 self.issue = None
654 self.collection = None
655 self.site = "test_website"
657 def post_to_site(self, url):
658 response = requests.post(url, verify=False)
659 status = response.status_code
660 if not (199 < status < 205):
661 messages.error(self.request, response.text)
662 if status == 503:
663 raise ServerUnderMaintenance(response.text)
664 else:
665 raise RuntimeError(response.text)
667 def internal_do(self, *args, **kwargs):
668 """
669 Called by history_views.execute_and_record_func to do the actual job.
670 """
672 issue_pid = self.issue.pid
673 colid = self.collection.pid
675 if self.site == "website":
676 # Copy the PDF from the test to the production folder
677 resolver.copy_binary_files(
678 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
679 )
680 else:
681 # Copy the PDF from the cedram to the test folder
682 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
683 from_path = os.path.join(from_folder, issue_pid + ".pdf")
684 if not os.path.isfile(from_path):
685 raise Http404(f"{from_path} does not exist")
687 to_path = resolver.get_disk_location(
688 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
689 )
690 resolver.copy_file(from_path, to_path)
692 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
694 if self.site == "test_website":
695 # Post to ptf-tools: it will add a Datastream to the issue
696 absolute_url = self.request.build_absolute_uri(url)
697 self.post_to_site(absolute_url)
699 server_url = getattr(self.collection, self.site)()
700 absolute_url = server_url + url
701 # Post to the test or production website
702 self.post_to_site(absolute_url)
704 def get(self, request, *args, **kwargs):
705 """
706 Send an issue PDF to the test or production website
707 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
708 :param args:
709 :param kwargs:
710 :return:
711 """
712 if check_lock():
713 m = "Trammel is under maintenance. Please try again later."
714 messages.error(self.request, m)
715 return JsonResponse({"message": m, "status": 503})
717 self.pid = self.kwargs.get("pid", None)
718 self.site = self.kwargs.get("site", "test_website")
720 self.issue = model_helpers.get_container(self.pid)
721 if not self.issue:
722 raise Http404(f"{self.pid} does not exist")
723 self.collection = self.issue.get_top_collection()
725 try:
726 pids, status, message = history_views.execute_and_record_func(
727 "deploy",
728 self.pid,
729 self.collection.pid,
730 self.internal_do,
731 f"add issue PDF to {self.site}",
732 )
734 except Timeout as exception:
735 return HttpResponse(exception, status=408)
736 except Exception as exception:
737 return HttpResponseServerError(exception)
739 data = {"message": message, "status": status}
740 return JsonResponse(data)
743class ArchiveAllAPIView(View):
744 """
745 - archive le xml de la collection ainsi que les binaires liés
746 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
747 @return array of issues pid
748 """
750 def internal_do(self, *args, **kwargs):
751 collection = kwargs["collection"]
752 pids = []
753 colid = collection.pid
755 logfile = os.path.join(settings.LOG_DIR, "archive.log")
756 if os.path.isfile(logfile):
757 os.remove(logfile)
759 ptf_cmds.exportPtfCmd(
760 {
761 "pid": colid,
762 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
763 "with_binary_files": True,
764 "for_archive": True,
765 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
766 }
767 ).do()
769 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
770 if os.path.isfile(cedramcls):
771 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
772 resolver.create_folder(dest_folder)
773 resolver.copy_file(cedramcls, dest_folder)
775 for issue in collection.content.all():
776 qs = issue.article_set.filter(
777 date_online_first__isnull=True, date_published__isnull=True
778 )
779 if qs.count() == 0:
780 pids.append(issue.pid)
782 return pids
784 def get(self, request, *args, **kwargs):
785 pid = self.kwargs.get("pid", None)
787 collection = model_helpers.get_collection(pid)
788 if not collection:
789 return HttpResponse(f"{pid} does not exist", status=400)
791 dict_ = {"collection": collection}
792 args_ = [self]
794 try:
795 pids, status, message = history_views.execute_and_record_func(
796 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_
797 )
798 except Timeout as exception:
799 return HttpResponse(exception, status=408)
800 except Exception as exception:
801 return HttpResponseServerError(exception)
803 data = {"message": message, "ids": pids, "status": status}
804 return JsonResponse(data)
807class CreateAllDjvuAPIView(View):
808 def internal_do(self, *args, **kwargs):
809 issue = kwargs["issue"]
810 pids = [issue.pid]
812 for article in issue.article_set.all():
813 pids.append(article.pid)
815 return pids
817 def get(self, request, *args, **kwargs):
818 pid = self.kwargs.get("pid", None)
819 issue = model_helpers.get_container(pid)
820 if not issue:
821 raise Http404(f"{pid} does not exist")
823 try:
824 dict_ = {"issue": issue}
825 args_ = [self]
827 pids, status, message = history_views.execute_and_record_func(
828 "numdam",
829 pid,
830 issue.get_collection().pid,
831 self.internal_do,
832 "",
833 False,
834 None,
835 None,
836 *args_,
837 **dict_,
838 )
839 except Exception as exception:
840 return HttpResponseServerError(exception)
842 data = {"message": message, "ids": pids, "status": status}
843 return JsonResponse(data)
846class ImportJatsContainerAPIView(View):
847 def internal_do(self, *args, **kwargs):
848 pid = self.kwargs.get("pid", None)
849 colid = self.kwargs.get("colid", None)
851 if pid and colid:
852 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
854 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
855 {
856 "body": body,
857 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
858 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
859 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
860 }
861 )
862 container = cmd.do()
863 if len(cmd.warnings) > 0:
864 messages.warning(
865 self.request,
866 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
867 )
869 if not container:
870 raise RuntimeError("Error: the container " + pid + " was not imported")
872 # resolver.copy_binary_files(
873 # container,
874 # settings.MATHDOC_ARCHIVE_FOLDER,
875 # settings.MERSENNE_TEST_DATA_FOLDER)
876 #
877 # for article in container.article_set.all():
878 # resolver.copy_binary_files(
879 # article,
880 # settings.MATHDOC_ARCHIVE_FOLDER,
881 # settings.MERSENNE_TEST_DATA_FOLDER)
882 else:
883 raise RuntimeError("colid or pid are not defined")
885 def get(self, request, *args, **kwargs):
886 pid = self.kwargs.get("pid", None)
887 colid = self.kwargs.get("colid", None)
889 try:
890 _, status, message = history_views.execute_and_record_func(
891 "import", pid, colid, self.internal_do
892 )
893 except Timeout as exception:
894 return HttpResponse(exception, status=408)
895 except Exception as exception:
896 return HttpResponseServerError(exception)
898 data = {"message": message, "status": status}
899 return JsonResponse(data)
902class DeployCollectionAPIView(View):
903 # Update collection.xml on a site (with its images)
905 def internal_do(self, *args, **kwargs):
906 colid = self.kwargs.get("colid", None)
907 site = self.kwargs.get("site", None)
909 collection = model_helpers.get_collection(colid)
910 if not collection:
911 raise RuntimeError(f"{colid} does not exist")
913 if site == "numdam":
914 server_url = settings.NUMDAM_PRE_URL
915 else:
916 server_url = getattr(collection, site)()
917 if not server_url:
918 raise RuntimeError(f"The collection has no {site}")
920 # check_collection creates or updates the collection (XML, image...)
921 check_collection(collection, server_url, site)
923 def get(self, request, *args, **kwargs):
924 colid = self.kwargs.get("colid", None)
925 site = self.kwargs.get("site", None)
927 try:
928 _, status, message = history_views.execute_and_record_func(
929 "deploy", colid, colid, self.internal_do, site
930 )
931 except Timeout as exception:
932 return HttpResponse(exception, status=408)
933 except Exception as exception:
934 return HttpResponseServerError(exception)
936 data = {"message": message, "status": status}
937 return JsonResponse(data)
940class DeployJatsResourceAPIView(View):
941 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
943 def internal_do(self, *args, **kwargs):
944 pid = self.kwargs.get("pid", None)
945 colid = self.kwargs.get("colid", None)
946 site = self.kwargs.get("site", None)
948 if site == "ptf_tools":
949 raise RuntimeError("Do not choose to deploy on PTF Tools")
950 if check_lock():
951 msg = "Trammel is under maintenance. Please try again later."
952 messages.error(self.request, msg)
953 return JsonResponse({"messages": msg, "status": 503})
955 resource = model_helpers.get_resource(pid)
956 if not resource:
957 raise RuntimeError(f"{pid} does not exist")
959 obj = resource.cast()
960 article = None
961 if obj.classname == "Article":
962 article = obj
963 container = article.my_container
964 articles_to_deploy = [article]
965 else:
966 container = obj
967 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
969 if container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(colid, None):
970 raise RuntimeError("Pending publications should not be deployed")
971 if site == "website" and article is not None and article.do_not_publish:
972 raise RuntimeError(f"{pid} is marked as Do not publish")
973 if site == "numdam" and article is not None:
974 raise RuntimeError("You can only deploy issues to Numdam")
976 collection = container.get_top_collection()
977 colid = collection.pid
978 djvu_exception = None
980 if site == "numdam":
981 server_url = settings.NUMDAM_PRE_URL
982 ResourceInNumdam.objects.get_or_create(pid=container.pid)
984 # 06/12/2022: DjVu are no longer added with Mersenne articles
985 # Add Djvu (before exporting the XML)
986 if False and int(container.year) < 2020:
987 for art in container.article_set.all():
988 try:
989 cmd = ptf_cmds.addDjvuPtfCmd()
990 cmd.set_resource(art)
991 cmd.do()
992 except Exception as e:
993 # Djvu are optional.
994 # Allow the deployment, but record the exception in the history
995 djvu_exception = e
996 else:
997 server_url = getattr(collection, site)()
998 if not server_url:
999 raise RuntimeError(f"The collection has no {site}")
1001 # check if the collection exists on the server
1002 # if not, check_collection will upload the collection (XML,
1003 # image...)
1004 if article is None:
1005 check_collection(collection, server_url, site)
1007 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
1008 # Create/update deployed date and published date on all container articles
1009 if site == "website":
1010 file_.write(
1011 "Create/Update deployed_date and date_published on all articles for {}\n".format(
1012 pid
1013 )
1014 )
1016 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
1017 cmd = ptf_cmds.publishResourcePtfCmd()
1018 cmd.set_resource(resource)
1019 updated_articles = cmd.do()
1021 tex.create_frontpage(colid, container, updated_articles, test=False)
1023 mersenneSite = model_helpers.get_site_mersenne(colid)
1024 # create or update deployed_date on container and articles
1025 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
1027 for art in articles_to_deploy:
1028 if art.doi and (art.date_published or art.date_online_first):
1029 if art.my_container.year is None:
1030 art.my_container.year = datetime.now().strftime("%Y")
1031 # BUG ? update the container but no save() ?
1033 file_.write(
1034 "Publication date of {} : Online First: {}, Published: {}\n".format(
1035 art.pid, art.date_online_first, art.date_published
1036 )
1037 )
1039 if article is None:
1040 resolver.copy_binary_files(
1041 container,
1042 settings.MERSENNE_TEST_DATA_FOLDER,
1043 settings.MERSENNE_PROD_DATA_FOLDER,
1044 )
1046 for art in articles_to_deploy:
1047 resolver.copy_binary_files(
1048 art,
1049 settings.MERSENNE_TEST_DATA_FOLDER,
1050 settings.MERSENNE_PROD_DATA_FOLDER,
1051 )
1053 elif site == "test_website":
1054 # create date_pre_published on articles without date_pre_published
1055 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
1056 cmd.set_resource(resource)
1057 updated_articles = cmd.do()
1059 tex.create_frontpage(colid, container, updated_articles)
1061 export_to_website = site == "website"
1063 if article is None:
1064 with_djvu = site == "numdam"
1065 xml = ptf_cmds.exportPtfCmd(
1066 {
1067 "pid": pid,
1068 "with_djvu": with_djvu,
1069 "export_to_website": export_to_website,
1070 }
1071 ).do()
1072 body = xml.encode("utf8")
1074 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1075 url = server_url + reverse("issue_upload")
1076 else:
1077 url = server_url + reverse("book_upload")
1079 # verify=False: ignore TLS certificate
1080 response = requests.post(url, data=body, verify=False)
1081 # response = requests.post(url, files=files, verify=False)
1082 else:
1083 xml = ptf_cmds.exportPtfCmd(
1084 {
1085 "pid": pid,
1086 "with_djvu": False,
1087 "article_standalone": True,
1088 "collection_pid": collection.pid,
1089 "export_to_website": export_to_website,
1090 "export_folder": settings.LOG_DIR,
1091 }
1092 ).do()
1093 # Unlike containers that send their XML as the body of the POST request,
1094 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1095 xml_file = io.StringIO(xml)
1096 files = {"xml": xml_file}
1098 url = server_url + reverse(
1099 "article_in_issue_upload", kwargs={"pid": container.pid}
1100 )
1101 # verify=False: ignore TLS certificate
1102 header = {}
1103 response = requests.post(url, headers=header, files=files, verify=False)
1105 status = response.status_code
1107 if 199 < status < 205:
1108 # There is no need to copy files for the test server
1109 # Files were already copied in /mersenne_test_data during the ptf_tools import
1110 # We only need to copy files from /mersenne_test_data to
1111 # /mersenne_prod_data during an upload to prod
1112 if site == "website":
1113 # TODO mettre ici le record doi pour un issue publié
1114 if container.doi:
1115 recordDOI(container)
1117 for art in articles_to_deploy:
1118 # record DOI automatically when deploying in prod
1120 if art.doi and art.allow_crossref():
1121 recordDOI(art)
1123 if colid == "CRBIOL":
1124 recordPubmed(
1125 art, force_update=False, updated_articles=updated_articles
1126 )
1128 if colid == "PCJ":
1129 self.update_pcj_editor(updated_articles)
1131 # Archive the container or the article
1132 if article is None:
1133 archive_resource.delay(
1134 pid,
1135 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1136 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1137 )
1139 else:
1140 archive_resource.delay(
1141 pid,
1142 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1143 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1144 article_doi=article.doi,
1145 )
1146 # cmd = ptf_cmds.archiveIssuePtfCmd({
1147 # "pid": pid,
1148 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1149 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1150 # cmd.set_article(article) # set_article allows archiving only the article
1151 # cmd.do()
1153 elif site == "numdam":
1154 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1155 if colid in settings.NUMDAM_COLLECTIONS:
1156 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1158 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1159 for article in container.article_set.all():
1160 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1162 elif status == 503:
1163 raise ServerUnderMaintenance(response.text)
1164 else:
1165 raise RuntimeError(response.text)
1167 if djvu_exception:
1168 raise djvu_exception
1170 def get(self, request, *args, **kwargs):
1171 pid = self.kwargs.get("pid", None)
1172 colid = self.kwargs.get("colid", None)
1173 site = self.kwargs.get("site", None)
1175 try:
1176 _, status, message = history_views.execute_and_record_func(
1177 "deploy", pid, colid, self.internal_do, site
1178 )
1179 except Timeout as exception:
1180 return HttpResponse(exception, status=408)
1181 except Exception as exception:
1182 return HttpResponseServerError(exception)
1184 data = {"message": message, "status": status}
1185 return JsonResponse(data)
1187 def update_pcj_editor(self, updated_articles):
1188 for article in updated_articles:
1189 data = {
1190 "date_published": article.date_published.strftime("%Y-%m-%d"),
1191 "article_number": article.article_number,
1192 }
1193 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1194 requests.post(url, json=data, verify=False)
1197class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1198 article = None
1200 def internal_do(self, *args, **kwargs):
1201 lang = self.kwargs.get("lang", None)
1203 translation = None
1204 for trans_article in self.article.translations.all():
1205 if trans_article.lang == lang:
1206 translation = trans_article
1208 if translation is None:
1209 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1211 collection = self.article.get_top_collection()
1212 colid = collection.pid
1213 container = self.article.my_container
1215 if translation.date_published is None:
1216 # Add date posted
1217 cmd = ptf_cmds.publishResourcePtfCmd()
1218 cmd.set_resource(translation)
1219 updated_articles = cmd.do()
1221 # Recompile PDF to add the date posted
1222 try:
1223 tex.create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1224 except Exception:
1225 raise PDFException(
1226 "Unable to compile the article PDF. Please contact the centre Mersenne"
1227 )
1229 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1230 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1231 resolver.copy_binary_files(
1232 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1233 )
1235 # Deploy in prod
1236 xml = ptf_cmds.exportPtfCmd(
1237 {
1238 "pid": self.article.pid,
1239 "with_djvu": False,
1240 "article_standalone": True,
1241 "collection_pid": colid,
1242 "export_to_website": True,
1243 "export_folder": settings.LOG_DIR,
1244 }
1245 ).do()
1246 xml_file = io.StringIO(xml)
1247 files = {"xml": xml_file}
1249 server_url = getattr(collection, "website")()
1250 if not server_url:
1251 raise RuntimeError("The collection has no website")
1252 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1253 header = {}
1255 try:
1256 response = requests.post(
1257 url, headers=header, files=files, verify=False
1258 ) # verify: ignore TLS certificate
1259 status = response.status_code
1260 except requests.exceptions.ConnectionError:
1261 raise ServerUnderMaintenance(
1262 "The journal is under maintenance. Please try again later."
1263 )
1265 # Register translation in Crossref
1266 if 199 < status < 205:
1267 if self.article.allow_crossref():
1268 try:
1269 recordDOI(translation)
1270 except Exception:
1271 raise DOIException(
1272 "Error while recording the DOI. Please contact the centre Mersenne"
1273 )
1275 def get(self, request, *args, **kwargs):
1276 doi = kwargs.get("doi", None)
1277 self.article = model_helpers.get_article_by_doi(doi)
1278 if self.article is None:
1279 raise Http404(f"{doi} does not exist")
1281 try:
1282 _, status, message = history_views.execute_and_record_func(
1283 "deploy",
1284 self.article.pid,
1285 self.article.get_top_collection().pid,
1286 self.internal_do,
1287 "website",
1288 )
1289 except Timeout as exception:
1290 return HttpResponse(exception, status=408)
1291 except Exception as exception:
1292 return HttpResponseServerError(exception)
1294 data = {"message": message, "status": status}
1295 return JsonResponse(data)
1298class DeleteJatsIssueAPIView(View):
1299 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1300 def get(self, request, *args, **kwargs):
1301 pid = self.kwargs.get("pid", None)
1302 colid = self.kwargs.get("colid", None)
1303 site = self.kwargs.get("site", None)
1304 message = "Le volume a bien été supprimé"
1305 status = 200
1307 issue = model_helpers.get_container(pid)
1308 if not issue:
1309 raise Http404(f"{pid} does not exist")
1310 try:
1311 mersenneSite = model_helpers.get_site_mersenne(colid)
1313 if site == "ptf_tools":
1314 if issue.is_deployed(mersenneSite):
1315 issue.undeploy(mersenneSite)
1316 for article in issue.article_set.all():
1317 article.undeploy(mersenneSite)
1319 p = model_helpers.get_provider("mathdoc-id")
1321 cmd = ptf_cmds.addContainerPtfCmd(
1322 {
1323 "pid": issue.pid,
1324 "ctype": "issue",
1325 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1326 }
1327 )
1328 cmd.set_provider(p)
1329 cmd.add_collection(issue.get_collection())
1330 cmd.set_object_to_be_deleted(issue)
1331 cmd.undo()
1333 else:
1334 if site == "numdam":
1335 server_url = settings.NUMDAM_PRE_URL
1336 else:
1337 collection = issue.get_collection()
1338 server_url = getattr(collection, site)()
1340 if not server_url:
1341 message = "The collection has no " + site
1342 status = 500
1343 else:
1344 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1345 response = requests.delete(url, verify=False)
1346 status = response.status_code
1348 if status == 404:
1349 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1350 elif status > 204:
1351 body = response.text.encode("utf8")
1352 message = body[:1000]
1353 else:
1354 status = 200
1355 # unpublish issue in collection site (site_register.json)
1356 if site == "website":
1357 if issue.is_deployed(mersenneSite):
1358 issue.undeploy(mersenneSite)
1359 for article in issue.article_set.all():
1360 article.undeploy(mersenneSite)
1361 # delete article binary files
1362 folder = article.get_relative_folder()
1363 resolver.delete_object_folder(
1364 folder,
1365 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1366 )
1367 # delete issue binary files
1368 folder = issue.get_relative_folder()
1369 resolver.delete_object_folder(
1370 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1371 )
1373 except Timeout as exception:
1374 return HttpResponse(exception, status=408)
1375 except Exception as exception:
1376 return HttpResponseServerError(exception)
1378 data = {"message": message, "status": status}
1379 return JsonResponse(data)
1382class ArchiveIssueAPIView(View):
1383 def get(self, request, *args, **kwargs):
1384 try:
1385 pid = kwargs["pid"]
1386 colid = kwargs["colid"]
1387 except IndexError:
1388 raise Http404
1390 try:
1391 cmd = ptf_cmds.archiveIssuePtfCmd(
1392 {
1393 "pid": pid,
1394 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1395 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1396 }
1397 )
1398 result_, status, message = history_views.execute_and_record_func(
1399 "archive", pid, colid, cmd.do
1400 )
1401 except Exception as exception:
1402 return HttpResponseServerError(exception)
1404 data = {"message": message, "status": 200}
1405 return JsonResponse(data)
1408class CreateDjvuAPIView(View):
1409 def internal_do(self, *args, **kwargs):
1410 pid = self.kwargs.get("pid", None)
1412 resource = model_helpers.get_resource(pid)
1413 cmd = ptf_cmds.addDjvuPtfCmd()
1414 cmd.set_resource(resource)
1415 cmd.do()
1417 def get(self, request, *args, **kwargs):
1418 pid = self.kwargs.get("pid", None)
1419 colid = pid.split("_")[0]
1421 try:
1422 _, status, message = history_views.execute_and_record_func(
1423 "numdam", pid, colid, self.internal_do
1424 )
1425 except Exception as exception:
1426 return HttpResponseServerError(exception)
1428 data = {"message": message, "status": status}
1429 return JsonResponse(data)
1432class PTFToolsHomeView(LoginRequiredMixin, View):
1433 """
1434 Home Page.
1435 - Admin & staff -> Render blank home.html
1436 - User with unique authorized collection -> Redirect to collection details page
1437 - User with multiple authorized collections -> Render home.html with data
1438 - Comment moderator -> Comments dashboard
1439 - Others -> 404 response
1440 """
1442 def get(self, request, *args, **kwargs) -> HttpResponse:
1443 # Staff or user with authorized collections
1444 if request.user.is_staff or request.user.is_superuser:
1445 return render(request, "home.html")
1447 colids = get_authorized_collections(request.user)
1448 is_mod = is_comment_moderator(request.user)
1450 # The user has no rights
1451 if not (colids or is_mod):
1452 raise Http404("No collections associated with your account.")
1453 # Comment moderator only
1454 elif not colids:
1455 return HttpResponseRedirect(reverse("comment_list"))
1457 # User with unique collection -> Redirect to collection detail page
1458 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1459 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1461 # User with multiple authorized collections - Special home
1462 context = {}
1463 context["overview"] = True
1465 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1466 all_collections = {c["pid"]: c for c in all_collections}
1468 # Comments summary
1469 try:
1470 error, comments_data = get_comments_for_home(request.user)
1471 except AttributeError:
1472 error, comments_data = True, {}
1474 context["comment_server_ok"] = False
1476 if not error:
1477 context["comment_server_ok"] = True
1478 if comments_data:
1479 for col_id, comment_nb in comments_data.items():
1480 if col_id.upper() in all_collections: 1480 ↛ 1479line 1480 didn't jump to line 1479 because the condition on line 1480 was always true
1481 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1483 # TODO: Translations summary
1484 context["translation_server_ok"] = False
1486 # Sort the collections according to the number of pending comments
1487 context["collections"] = sorted(
1488 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1489 )
1491 return render(request, "home.html", context)
1494class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1495 columns = 5
1497 def get_common_context_data(self, **kwargs):
1498 context = super().get_context_data(**kwargs)
1499 now = timezone.now()
1500 curyear = now.year
1501 years = range(curyear - self.columns + 1, curyear + 1)
1503 context["collections"] = settings.MERSENNE_COLLECTIONS
1504 context["containers_to_be_published"] = []
1505 context["last_col_events"] = []
1507 event = get_history_last_event_by("clockss", "ALL")
1508 clockss_gap = get_gap(now, event)
1510 context["years"] = years
1511 context["clockss_gap"] = clockss_gap
1513 return context
1515 def calculate_articles_and_pages(self, pid, years):
1516 data_by_year = []
1517 total_articles = [0] * len(years)
1518 total_pages = [0] * len(years)
1520 for year in years:
1521 articles = self.get_articles_for_year(pid, year)
1522 articles_count = articles.count()
1523 page_count = sum(article.get_article_page_count() for article in articles)
1525 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1526 total_articles[year - years[0]] += articles_count
1527 total_pages[year - years[0]] += page_count
1529 return data_by_year, total_articles, total_pages
1531 def get_articles_for_year(self, pid, year):
1532 return Article.objects.filter(
1533 Q(my_container__my_collection__pid=pid)
1534 & (
1535 Q(date_published__year=year, date_online_first__isnull=True)
1536 | Q(date_online_first__year=year)
1537 )
1538 ).prefetch_related("resourcecount_set")
1541class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1542 template_name = "dashboard/published_articles.html"
1544 def get_context_data(self, **kwargs):
1545 context = self.get_common_context_data(**kwargs)
1546 years = context["years"]
1548 published_articles = []
1549 total_published_articles = [
1550 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1551 ]
1553 for pid in settings.MERSENNE_COLLECTIONS:
1554 if pid != "MERSENNE":
1555 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1556 pid, years
1557 )
1558 published_articles.append({"pid": pid, "years": articles_data})
1560 for i, year in enumerate(years):
1561 total_published_articles[i]["total_articles"] += total_articles[i]
1562 total_published_articles[i]["total_pages"] += total_pages[i]
1564 context["published_articles"] = published_articles
1565 context["total_published_articles"] = total_published_articles
1567 return context
1570class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1571 template_name = "dashboard/created_volumes.html"
1573 def get_context_data(self, **kwargs):
1574 context = self.get_common_context_data(**kwargs)
1575 years = context["years"]
1577 created_volumes = []
1578 total_created_volumes = [
1579 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1580 ]
1582 for pid in settings.MERSENNE_COLLECTIONS:
1583 if pid != "MERSENNE":
1584 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1585 pid, years
1586 )
1587 created_volumes.append({"pid": pid, "years": volumes_data})
1589 for i, year in enumerate(years):
1590 total_created_volumes[i]["total_articles"] += total_articles[i]
1591 total_created_volumes[i]["total_pages"] += total_pages[i]
1593 context["created_volumes"] = created_volumes
1594 context["total_created_volumes"] = total_created_volumes
1596 return context
1598 def calculate_volumes_and_pages(self, pid, years):
1599 data_by_year = []
1600 total_articles = [0] * len(years)
1601 total_pages = [0] * len(years)
1603 for year in years:
1604 issues = Container.objects.filter(my_collection__pid=pid, year=year)
1605 articles_count = 0
1606 page_count = 0
1608 for issue in issues:
1609 articles = issue.article_set.filter(
1610 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1611 ).prefetch_related("resourcecount_set")
1613 articles_count += articles.count()
1614 page_count += sum(article.get_article_page_count() for article in articles)
1616 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1617 total_articles[year - years[0]] += articles_count
1618 total_pages[year - years[0]] += page_count
1620 return data_by_year, total_articles, total_pages
1623class ReferencingChoice(View):
1624 def post(self, request, *args, **kwargs):
1625 if request.POST.get("optSite") == "ads":
1626 return redirect(
1627 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1628 )
1629 elif request.POST.get("optSite") == "wos":
1630 comp = ReferencingCheckerWos()
1631 journal = comp.make_journal(request.POST.get("selectCol"))
1632 if journal is None:
1633 return render(
1634 request,
1635 "dashboard/referencing.html",
1636 {
1637 "error": "Collection not found",
1638 "colid": request.POST.get("selectCol"),
1639 "optSite": request.POST.get("optSite"),
1640 },
1641 )
1642 return render(
1643 request,
1644 "dashboard/referencing.html",
1645 {
1646 "journal": journal,
1647 "colid": request.POST.get("selectCol"),
1648 "optSite": request.POST.get("optSite"),
1649 },
1650 )
1653class ReferencingWosFileView(View):
1654 template_name = "dashboard/referencing.html"
1656 def post(self, request, *args, **kwargs):
1657 colid = request.POST["colid"]
1658 if request.FILES.get("risfile") is None:
1659 message = "No file uploaded"
1660 return render(
1661 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1662 )
1663 uploaded_file = request.FILES["risfile"]
1664 comp = ReferencingCheckerWos()
1665 journal = comp.check_references(colid, uploaded_file)
1666 return render(request, self.template_name, {"journal": journal})
1669class ReferencingDashboardView(BaseMersenneDashboardView):
1670 template_name = "dashboard/referencing.html"
1672 def get(self, request, *args, **kwargs):
1673 colid = self.kwargs.get("colid", None)
1674 comp = ReferencingCheckerAds()
1675 journal = comp.check_references(colid)
1676 return render(request, self.template_name, {"journal": journal})
1679class BaseCollectionView(TemplateView):
1680 def get_context_data(self, **kwargs):
1681 context = super().get_context_data(**kwargs)
1682 aid = context.get("aid")
1683 year = context.get("year")
1685 if aid and year:
1686 context["collection"] = self.get_collection(aid, year)
1688 return context
1690 def get_collection(self, aid, year):
1691 """Method to be overridden by subclasses to fetch the appropriate collection"""
1692 raise NotImplementedError("Subclasses must implement get_collection method")
1695class ArticleListView(BaseCollectionView):
1696 template_name = "collection-list.html"
1698 def get_collection(self, aid, year):
1699 return Article.objects.filter(
1700 Q(my_container__my_collection__pid=aid)
1701 & (
1702 Q(date_published__year=year, date_online_first__isnull=True)
1703 | Q(date_online_first__year=year)
1704 )
1705 ).prefetch_related("resourcecount_set")
1708class VolumeListView(BaseCollectionView):
1709 template_name = "collection-list.html"
1711 def get_collection(self, aid, year):
1712 return Article.objects.filter(
1713 Q(my_container__my_collection__pid=aid, my_container__year=year)
1714 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1715 ).prefetch_related("resourcecount_set")
1718class DOAJResourceRegisterView(View):
1719 def get(self, request, *args, **kwargs):
1720 pid = kwargs.get("pid", None)
1721 resource = model_helpers.get_resource(pid)
1722 if resource is None:
1723 raise Http404
1724 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1725 resource.colid, None
1726 ):
1727 raise RuntimeError("Pending publications should not be deployed")
1729 try:
1730 data = {}
1731 doaj_meta, response = doaj_pid_register(pid)
1732 if response is None:
1733 return HttpResponse(status=204)
1734 elif doaj_meta and 200 <= response.status_code <= 299:
1735 data.update(doaj_meta)
1736 else:
1737 return HttpResponse(status=response.status_code, reason=response.text)
1738 except Timeout as exception:
1739 return HttpResponse(exception, status=408)
1740 except Exception as exception:
1741 return HttpResponseServerError(exception)
1742 return JsonResponse(data)
1745class CROSSREFResourceRegisterView(View):
1746 def get(self, request, *args, **kwargs):
1747 pid = kwargs.get("pid", None)
1748 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1749 force = kwargs.get("force", None)
1750 if not request.user.is_superuser:
1751 force = None
1753 resource = model_helpers.get_resource(pid)
1754 if resource is None:
1755 raise Http404
1757 resource = resource.cast()
1758 meth = getattr(self, "recordDOI" + resource.classname)
1759 try:
1760 data = meth(resource, force)
1761 except Timeout as exception:
1762 return HttpResponse(exception, status=408)
1763 except Exception as exception:
1764 return HttpResponseServerError(exception)
1765 return JsonResponse(data)
1767 def recordDOIArticle(self, article, force=None):
1768 result = {"status": 404}
1769 if (
1770 article.doi
1771 and not article.do_not_publish
1772 and (article.date_published or article.date_online_first or force == "force")
1773 ):
1774 if article.my_container.year is None: # or article.my_container.year == '0':
1775 article.my_container.year = datetime.now().strftime("%Y")
1776 result = recordDOI(article)
1777 return result
1779 def recordDOICollection(self, collection, force=None):
1780 return recordDOI(collection)
1782 def recordDOIContainer(self, container, force=None):
1783 data = {"status": 200, "message": "tout va bien"}
1785 if container.ctype == "issue":
1786 if container.doi:
1787 result = recordDOI(container)
1788 if result["status"] != 200:
1789 return result
1790 if force == "force":
1791 articles = container.article_set.exclude(
1792 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1793 )
1794 else:
1795 articles = container.article_set.exclude(
1796 doi__isnull=True,
1797 do_not_publish=True,
1798 date_published__isnull=True,
1799 date_online_first__isnull=True,
1800 )
1802 for article in articles:
1803 result = self.recordDOIArticle(article, force)
1804 if result["status"] != 200:
1805 data = result
1806 else:
1807 return recordDOI(container)
1808 return data
1811class CROSSREFResourceCheckStatusView(View):
1812 def get(self, request, *args, **kwargs):
1813 pid = kwargs.get("pid", None)
1814 resource = model_helpers.get_resource(pid)
1815 if resource is None:
1816 raise Http404
1817 resource = resource.cast()
1818 meth = getattr(self, "checkDOI" + resource.classname)
1819 try:
1820 meth(resource)
1821 except Timeout as exception:
1822 return HttpResponse(exception, status=408)
1823 except Exception as exception:
1824 return HttpResponseServerError(exception)
1826 data = {"status": 200, "message": "tout va bien"}
1827 return JsonResponse(data)
1829 def checkDOIArticle(self, article):
1830 if article.my_container.year is None or article.my_container.year == "0":
1831 article.my_container.year = datetime.now().strftime("%Y")
1832 checkDOI(article)
1834 def checkDOICollection(self, collection):
1835 checkDOI(collection)
1837 def checkDOIContainer(self, container):
1838 if container.doi is not None:
1839 checkDOI(container)
1840 for article in container.article_set.all():
1841 self.checkDOIArticle(article)
1844class CROSSREFResourcePendingPublicationRegisterView(View):
1845 def get(self, request, *args, **kwargs):
1846 pid = kwargs.get("pid", None)
1847 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1848 force = kwargs.get("force", None)
1849 if not request.user.is_superuser:
1850 force = None
1852 resource = model_helpers.get_resource(pid)
1853 if resource is None:
1854 raise Http404
1856 resource = resource.cast()
1857 meth = getattr(self, "recordPendingPublication" + resource.classname)
1858 try:
1859 data = meth(resource, force)
1860 except Timeout as exception:
1861 return HttpResponse(exception, status=408)
1862 except Exception as exception:
1863 return HttpResponseServerError(exception)
1864 return JsonResponse(data)
1866 def recordPendingPublicationArticle(self, article):
1867 result = {"status": 404}
1868 if article.doi and not article.date_published and not article.date_online_first:
1869 if article.my_container.year is None: # or article.my_container.year == '0':
1870 article.my_container.year = datetime.now().strftime("%Y")
1871 result = recordPendingPublication(article)
1872 return result
1875class RegisterPubmedFormView(FormView):
1876 template_name = "record_pubmed_dialog.html"
1877 form_class = RegisterPubmedForm
1879 def get_context_data(self, **kwargs):
1880 context = super().get_context_data(**kwargs)
1881 context["pid"] = self.kwargs["pid"]
1882 context["helper"] = PtfLargeModalFormHelper
1883 return context
1886class RegisterPubmedView(View):
1887 def get(self, request, *args, **kwargs):
1888 pid = kwargs.get("pid", None)
1889 update_article = self.request.GET.get("update_article", "on") == "on"
1891 article = model_helpers.get_article(pid)
1892 if article is None:
1893 raise Http404
1894 try:
1895 recordPubmed(article, update_article)
1896 except Exception as exception:
1897 messages.error("Unable to register the article in PubMed")
1898 return HttpResponseServerError(exception)
1900 return HttpResponseRedirect(
1901 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1902 )
1905class PTFToolsContainerView(TemplateView):
1906 template_name = ""
1908 def get_context_data(self, **kwargs):
1909 context = super().get_context_data(**kwargs)
1911 container = model_helpers.get_container(self.kwargs.get("pid"))
1912 if container is None:
1913 raise Http404
1914 citing_articles = container.citations()
1915 source = self.request.GET.get("source", None)
1916 if container.ctype.startswith("book"):
1917 book_parts = (
1918 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1919 )
1920 references = False
1921 if container.ctype == "book-monograph":
1922 # on regarde si il y a au moins une bibliographie
1923 for art in container.article_set.all():
1924 if art.bibitem_set.count() > 0:
1925 references = True
1926 context.update(
1927 {
1928 "book": container,
1929 "book_parts": list(book_parts),
1930 "source": source,
1931 "citing_articles": citing_articles,
1932 "references": references,
1933 "test_website": container.get_top_collection()
1934 .extlink_set.get(rel="test_website")
1935 .location,
1936 "prod_website": container.get_top_collection()
1937 .extlink_set.get(rel="website")
1938 .location,
1939 }
1940 )
1941 self.template_name = "book-toc.html"
1942 else:
1943 articles = container.article_set.all().order_by("seq")
1944 for article in articles:
1945 try:
1946 last_match = (
1947 history_models.HistoryEvent.objects.filter(
1948 pid=article.pid,
1949 type="matching",
1950 )
1951 .only("created_on")
1952 .latest("created_on")
1953 )
1954 except history_models.HistoryEvent.DoesNotExist as _:
1955 article.last_match = None
1956 else:
1957 article.last_match = last_match.created_on
1959 # article1 = articles.first()
1960 # date = article1.deployed_date()
1961 # TODO next_issue, previous_issue
1963 # check DOI est maintenant une commande à part
1964 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
1965 # articlesWithStatus = []
1966 # for article in articles:
1967 # checkDOIExistence(article)
1968 # articlesWithStatus.append(article)
1970 test_location = prod_location = ""
1971 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
1972 if qs:
1973 test_location = qs.first().location
1974 qs = container.get_top_collection().extlink_set.filter(rel="website")
1975 if qs:
1976 prod_location = qs.first().location
1977 context.update(
1978 {
1979 "issue": container,
1980 "articles": articles,
1981 "source": source,
1982 "citing_articles": citing_articles,
1983 "test_website": test_location,
1984 "prod_website": prod_location,
1985 }
1986 )
1988 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
1989 context["is_issue_pending_publication"] = True
1990 self.template_name = "issue-items.html"
1992 context["allow_crossref"] = container.allow_crossref()
1993 context["coltype"] = container.my_collection.coltype
1994 return context
1997class ExtLinkInline(InlineFormSetFactory):
1998 model = ExtLink
1999 form_class = ExtLinkForm
2000 factory_kwargs = {"extra": 0}
2003class ResourceIdInline(InlineFormSetFactory):
2004 model = ResourceId
2005 form_class = ResourceIdForm
2006 factory_kwargs = {"extra": 0}
2009class IssueDetailAPIView(View):
2010 def get(self, request, *args, **kwargs):
2011 issue = get_object_or_404(Container, pid=kwargs["pid"])
2012 deployed_date = issue.deployed_date()
2013 result = {
2014 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2015 if deployed_date
2016 else None,
2017 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2018 "all_doi_are_registered": issue.all_doi_are_registered(),
2019 "registered_in_doaj": issue.registered_in_doaj(),
2020 "doi": issue.my_collection.doi,
2021 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2022 }
2023 try:
2024 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2025 except history_models.HistoryEvent.DoesNotExist as _:
2026 pass
2027 else:
2028 result["latest"] = latest.message
2029 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2030 "%Y-%m-%d %H:%M"
2031 )
2033 result["latest_type"] = latest.type.capitalize()
2034 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2035 try:
2036 result[event_type] = timezone.localtime(
2037 history_models.HistoryEvent.objects.filter(
2038 type=event_type,
2039 status="OK",
2040 pid__startswith=issue.pid,
2041 )
2042 .latest("created_on")
2043 .created_on
2044 ).strftime("%Y-%m-%d %H:%M")
2045 except history_models.HistoryEvent.DoesNotExist as _:
2046 result[event_type] = ""
2047 return JsonResponse(result)
2050class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2051 model = Collection
2052 form_class = CollectionForm
2053 inlines = [ResourceIdInline, ExtLinkInline]
2054 inlines_names = ["resource_ids_form", "ext_links_form"]
2056 def get_context_data(self, **kwargs):
2057 context = super().get_context_data(**kwargs)
2058 context["helper"] = PtfFormHelper
2059 context["formset_helper"] = FormSetHelper
2060 return context
2062 def add_description(self, collection, description, lang, seq):
2063 if description:
2064 la = Abstract(
2065 resource=collection,
2066 tag="description",
2067 lang=lang,
2068 seq=seq,
2069 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2070 value_html=description,
2071 value_tex=description,
2072 )
2073 la.save()
2075 def form_valid(self, form):
2076 if form.instance.abbrev:
2077 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2078 else:
2079 form.instance.title_xml = (
2080 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2081 )
2083 form.instance.title_html = form.instance.title_tex
2084 form.instance.title_sort = form.instance.title_tex
2085 result = super().form_valid(form)
2087 collection = self.object
2088 collection.abstract_set.all().delete()
2090 seq = 1
2091 description = form.cleaned_data["description_en"]
2092 if description:
2093 self.add_description(collection, description, "en", seq)
2094 seq += 1
2095 description = form.cleaned_data["description_fr"]
2096 if description:
2097 self.add_description(collection, description, "fr", seq)
2099 return result
2101 def get_success_url(self):
2102 messages.success(self.request, "La Collection a été modifiée avec succès")
2103 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2106class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2107 """
2108 Warning : Not yet finished
2109 Automatic site membership creation is still missing
2110 """
2113class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2114 slug_field = "pid"
2115 slug_url_kwarg = "pid"
2118def suggest_load_journal_dois(colid):
2119 articles = (
2120 Article.objects.filter(my_container__my_collection__pid=colid)
2121 .filter(doi__isnull=False)
2122 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2123 .values_list("doi", flat=True)
2124 )
2126 try:
2127 articles = sorted(
2128 articles,
2129 key=lambda d: (
2130 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2131 int(re.search(r".(\d+)$", d).group(1)),
2132 ),
2133 )
2134 except: # noqa: E722 (we'll look later)
2135 pass
2136 return [f'<option value="{doi}">' for doi in articles]
2139def get_context_with_volumes(journal):
2140 result = model_helpers.get_volumes_in_collection(journal)
2141 volume_count = result["volume_count"]
2142 collections = []
2143 for ancestor in journal.ancestors.all():
2144 item = model_helpers.get_volumes_in_collection(ancestor)
2145 volume_count = max(0, volume_count)
2146 item.update({"journal": ancestor})
2147 collections.append(item)
2149 # add the parent collection to its children list and sort it by date
2150 result.update({"journal": journal})
2151 collections.append(result)
2153 collections = [c for c in collections if c["sorted_issues"]]
2154 collections.sort(
2155 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2156 reverse=True,
2157 )
2159 context = {
2160 "journal": journal,
2161 "sorted_issues": result["sorted_issues"],
2162 "volume_count": volume_count,
2163 "max_width": result["max_width"],
2164 "collections": collections,
2165 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2166 }
2167 return context
2170class CollectionDetail(
2171 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2172):
2173 model = Collection
2174 slug_field = "pid"
2175 slug_url_kwarg = "pid"
2176 template_name = "ptf/collection_detail.html"
2178 def test_func(self):
2179 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2181 def get(self, request, *args, **kwargs):
2182 self.object = self.get_object(queryset=Collection.objects.all())
2183 return super().get(request, *args, **kwargs)
2185 def get_context_data(self, **kwargs):
2186 context = super().get_context_data(**kwargs)
2187 context["object_list"] = context["object_list"].filter(
2188 Q(ctype="issue") | Q(ctype="book-lecture-notes")
2189 )
2190 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2191 context.update(get_context_with_volumes(self.object))
2193 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2194 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2195 context["issue_to_appear"] = Container.objects.filter(
2196 pid=context["issue_to_appear_pid"]
2197 ).exists()
2198 try:
2199 latest_error = history_models.HistoryEvent.objects.filter(
2200 status="ERROR", col=self.object
2201 ).latest("created_on")
2202 except history_models.HistoryEvent.DoesNotExist as _:
2203 pass
2204 else:
2205 message = latest_error.message
2206 if message:
2207 i = message.find(" - ")
2208 latest_exception = message[:i]
2209 latest_error_message = message[i + 3 :]
2210 context["latest_exception"] = latest_exception
2211 context["latest_exception_date"] = latest_error.created_on
2212 context["latest_exception_type"] = latest_error.type
2213 context["latest_error_message"] = latest_error_message
2215 archive_in_error = history_models.HistoryEvent.objects.filter(
2216 status="ERROR", col=self.object, type="archive"
2217 ).exists()
2219 context["archive_in_error"] = archive_in_error
2221 return context
2223 def get_queryset(self):
2224 query = self.object.content.all()
2226 for ancestor in self.object.ancestors.all():
2227 query |= ancestor.content.all()
2229 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2232class ContainerEditView(FormView):
2233 template_name = "container_form.html"
2234 form_class = ContainerForm
2236 def get_success_url(self):
2237 if self.kwargs["pid"]:
2238 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2239 return reverse("mersenne_dashboard/published_articles")
2241 def set_success_message(self): # pylint: disable=no-self-use
2242 messages.success(self.request, "Le fascicule a été modifié")
2244 def get_form_kwargs(self):
2245 kwargs = super().get_form_kwargs()
2246 if "pid" not in self.kwargs:
2247 self.kwargs["pid"] = None
2248 if "colid" not in self.kwargs:
2249 self.kwargs["colid"] = None
2250 if "data" in kwargs and "colid" in kwargs["data"]:
2251 # colid is passed as a hidden param in the form.
2252 # It is used when you submit a new container
2253 self.kwargs["colid"] = kwargs["data"]["colid"]
2255 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2256 self.kwargs["pid"]
2257 )
2258 return kwargs
2260 def get_context_data(self, **kwargs):
2261 context = super().get_context_data(**kwargs)
2263 context["pid"] = self.kwargs["pid"]
2264 context["colid"] = self.kwargs["colid"]
2265 context["container"] = self.kwargs["container"]
2267 context["edit_container"] = context["pid"] is not None
2268 context["name"] = resolve(self.request.path_info).url_name
2270 return context
2272 def form_valid(self, form):
2273 new_pid = form.cleaned_data.get("pid")
2274 new_title = form.cleaned_data.get("title")
2275 new_trans_title = form.cleaned_data.get("trans_title")
2276 new_publisher = form.cleaned_data.get("publisher")
2277 new_year = form.cleaned_data.get("year")
2278 new_volume = form.cleaned_data.get("volume")
2279 new_number = form.cleaned_data.get("number")
2281 collection = None
2282 issue = self.kwargs["container"]
2283 if issue is not None:
2284 collection = issue.my_collection
2285 elif self.kwargs["colid"] is not None:
2286 if "CR" in self.kwargs["colid"]:
2287 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2288 else:
2289 collection = model_helpers.get_collection(self.kwargs["colid"])
2291 if collection is None:
2292 raise ValueError("Collection for " + new_pid + " does not exist")
2294 # Icon
2295 new_icon_location = ""
2296 if "icon" in self.request.FILES:
2297 filename = os.path.basename(self.request.FILES["icon"].name)
2298 file_extension = filename.split(".")[1]
2300 icon_filename = resolver.get_disk_location(
2301 settings.MERSENNE_TEST_DATA_FOLDER,
2302 collection.pid,
2303 file_extension,
2304 new_pid,
2305 None,
2306 True,
2307 )
2309 with open(icon_filename, "wb+") as destination:
2310 for chunk in self.request.FILES["icon"].chunks():
2311 destination.write(chunk)
2313 folder = resolver.get_relative_folder(collection.pid, new_pid)
2314 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2315 name = resolve(self.request.path_info).url_name
2316 if name == "special_issue_create":
2317 self.kwargs["name"] = name
2318 if self.kwargs["container"]:
2319 # Edit Issue
2320 issue = self.kwargs["container"]
2321 if issue is None:
2322 raise ValueError(self.kwargs["pid"] + " does not exist")
2324 issue.pid = new_pid
2325 issue.title_tex = issue.title_html = new_title
2326 issue.title_xml = build_title_xml(
2327 title=new_title,
2328 lang=issue.lang,
2329 title_type="issue-title",
2330 )
2332 trans_lang = ""
2333 if issue.trans_lang != "" and issue.trans_lang != "und":
2334 trans_lang = issue.trans_lang
2335 elif new_trans_title != "":
2336 trans_lang = "fr" if issue.lang == "en" else "en"
2337 issue.trans_lang = trans_lang
2339 if trans_lang != "" and new_trans_title != "":
2340 issue.trans_title_html = ""
2341 issue.trans_title_tex = ""
2342 title_xml = build_title_xml(
2343 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2344 )
2345 try:
2346 trans_title_object = Title.objects.get(resource=issue, lang=trans_lang)
2347 trans_title_object.title_html = new_trans_title
2348 trans_title_object.title_xml = title_xml
2349 trans_title_object.save()
2350 except Title.DoesNotExist:
2351 trans_title = Title(
2352 resource=issue,
2353 lang=trans_lang,
2354 type="main",
2355 title_html=new_trans_title,
2356 title_xml=title_xml,
2357 )
2358 trans_title.save()
2359 issue.year = new_year
2360 issue.volume = new_volume
2361 issue.volume_int = make_int(new_volume)
2362 issue.number = new_number
2363 issue.number_int = make_int(new_number)
2364 issue.save()
2365 else:
2366 xissue = create_issuedata()
2368 xissue.ctype = "issue"
2369 xissue.pid = new_pid
2370 xissue.lang = "en"
2371 xissue.title_tex = new_title
2372 xissue.title_html = new_title
2373 xissue.title_xml = build_title_xml(
2374 title=new_title, lang=xissue.lang, title_type="issue-title"
2375 )
2377 if new_trans_title != "":
2378 trans_lang = "fr"
2379 title_xml = build_title_xml(
2380 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2381 )
2382 title = create_titledata(
2383 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2384 )
2385 issue.titles = [title]
2387 xissue.year = new_year
2388 xissue.volume = new_volume
2389 xissue.number = new_number
2390 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2392 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2393 cmd.add_collection(collection)
2394 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2395 issue = cmd.do()
2397 self.kwargs["pid"] = new_pid
2399 # Add objects related to the article: contribs, datastream, counts...
2400 params = {
2401 "icon_location": new_icon_location,
2402 }
2403 cmd = ptf_cmds.updateContainerPtfCmd(params)
2404 cmd.set_resource(issue)
2405 cmd.do()
2407 publisher = model_helpers.get_publisher(new_publisher)
2408 if not publisher:
2409 xpub = create_publisherdata()
2410 xpub.name = new_publisher
2411 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2412 issue.my_publisher = publisher
2413 issue.save()
2415 self.set_success_message()
2417 return super().form_valid(form)
2420# class ArticleEditView(FormView):
2421# template_name = 'article_form.html'
2422# form_class = ArticleForm
2423#
2424# def get_success_url(self):
2425# if self.kwargs['pid']:
2426# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2427# return reverse('mersenne_dashboard/published_articles')
2428#
2429# def set_success_message(self): # pylint: disable=no-self-use
2430# messages.success(self.request, "L'article a été modifié")
2431#
2432# def get_form_kwargs(self):
2433# kwargs = super(ArticleEditView, self).get_form_kwargs()
2434#
2435# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2436# # Article creation: pid is None
2437# self.kwargs['pid'] = None
2438# if 'issue_id' not in self.kwargs:
2439# # Article edit: issue_id is not passed
2440# self.kwargs['issue_id'] = None
2441# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2442# # colid is passed as a hidden param in the form.
2443# # It is used when you submit a new container
2444# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2445#
2446# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2447# return kwargs
2448#
2449# def get_context_data(self, **kwargs):
2450# context = super(ArticleEditView, self).get_context_data(**kwargs)
2451#
2452# context['pid'] = self.kwargs['pid']
2453# context['issue_id'] = self.kwargs['issue_id']
2454# context['article'] = self.kwargs['article']
2455#
2456# context['edit_article'] = context['pid'] is not None
2457#
2458# article = context['article']
2459# if article:
2460# context['author_contributions'] = article.get_author_contributions()
2461# context['kwds_fr'] = None
2462# context['kwds_en'] = None
2463# kwd_gps = article.get_non_msc_kwds()
2464# for kwd_gp in kwd_gps:
2465# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2466# if kwd_gp.value_xml:
2467# kwd_ = types.SimpleNamespace()
2468# kwd_.value = kwd_gp.value_tex
2469# context['kwd_unstructured_fr'] = kwd_
2470# context['kwds_fr'] = kwd_gp.kwd_set.all()
2471# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2472# if kwd_gp.value_xml:
2473# kwd_ = types.SimpleNamespace()
2474# kwd_.value = kwd_gp.value_tex
2475# context['kwd_unstructured_en'] = kwd_
2476# context['kwds_en'] = kwd_gp.kwd_set.all()
2477#
2478# # Article creation: init pid
2479# if context['issue_id'] and context['pid'] is None:
2480# issue = model_helpers.get_container(context['issue_id'])
2481# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2482#
2483# return context
2484#
2485# def form_valid(self, form):
2486#
2487# new_pid = form.cleaned_data.get('pid')
2488# new_title = form.cleaned_data.get('title')
2489# new_fpage = form.cleaned_data.get('fpage')
2490# new_lpage = form.cleaned_data.get('lpage')
2491# new_page_range = form.cleaned_data.get('page_range')
2492# new_page_count = form.cleaned_data.get('page_count')
2493# new_coi_statement = form.cleaned_data.get('coi_statement')
2494# new_show_body = form.cleaned_data.get('show_body')
2495# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2496#
2497# # TODO support MathML
2498# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2499# # We need to pass trans_title to get_title_xml
2500# # Meanwhile, ignore new_title_xml
2501# new_title_xml = jats_parser.get_title_xml(new_title)
2502# new_title_html = new_title
2503#
2504# authors_count = int(self.request.POST.get('authors_count', "0"))
2505# i = 1
2506# new_authors = []
2507# old_author_contributions = []
2508# if self.kwargs['article']:
2509# old_author_contributions = self.kwargs['article'].get_author_contributions()
2510#
2511# while authors_count > 0:
2512# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2513#
2514# if prefix is not None:
2515# addresses = []
2516# if len(old_author_contributions) >= i:
2517# old_author_contribution = old_author_contributions[i - 1]
2518# addresses = [contrib_address.address for contrib_address in
2519# old_author_contribution.get_addresses()]
2520#
2521# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2522# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2523# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2524# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2525# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2526# deceased_before_publication = deceased == 'on'
2527# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2528# equal_contrib = equal_contrib == 'on'
2529# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2530# corresponding = corresponding == 'on'
2531# email = self.request.POST.get('email-' + str(i), None)
2532#
2533# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2534# params['deceased_before_publication'] = deceased_before_publication
2535# params['equal_contrib'] = equal_contrib
2536# params['corresponding'] = corresponding
2537# params['addresses'] = addresses
2538# params['email'] = email
2539#
2540# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2541#
2542# new_authors.append(params)
2543#
2544# authors_count -= 1
2545# i += 1
2546#
2547# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2548# i = 1
2549# new_kwds_fr = []
2550# while kwds_fr_count > 0:
2551# value = self.request.POST.get('kwd-fr-' + str(i), None)
2552# new_kwds_fr.append(value)
2553# kwds_fr_count -= 1
2554# i += 1
2555# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2556#
2557# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2558# i = 1
2559# new_kwds_en = []
2560# while kwds_en_count > 0:
2561# value = self.request.POST.get('kwd-en-' + str(i), None)
2562# new_kwds_en.append(value)
2563# kwds_en_count -= 1
2564# i += 1
2565# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2566#
2567# if self.kwargs['article']:
2568# # Edit article
2569# container = self.kwargs['article'].my_container
2570# else:
2571# # New article
2572# container = model_helpers.get_container(self.kwargs['issue_id'])
2573#
2574# if container is None:
2575# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2576#
2577# collection = container.my_collection
2578#
2579# # Copy PDF file & extract full text
2580# body = ''
2581# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2582# collection.pid,
2583# "pdf",
2584# container.pid,
2585# new_pid,
2586# True)
2587# if 'pdf' in self.request.FILES:
2588# with open(pdf_filename, 'wb+') as destination:
2589# for chunk in self.request.FILES['pdf'].chunks():
2590# destination.write(chunk)
2591#
2592# # Extract full text from the PDF
2593# body = utils.pdf_to_text(pdf_filename)
2594#
2595# # Icon
2596# new_icon_location = ''
2597# if 'icon' in self.request.FILES:
2598# filename = os.path.basename(self.request.FILES['icon'].name)
2599# file_extension = filename.split('.')[1]
2600#
2601# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2602# collection.pid,
2603# file_extension,
2604# container.pid,
2605# new_pid,
2606# True)
2607#
2608# with open(icon_filename, 'wb+') as destination:
2609# for chunk in self.request.FILES['icon'].chunks():
2610# destination.write(chunk)
2611#
2612# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2613# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2614#
2615# if self.kwargs['article']:
2616# # Edit article
2617# article = self.kwargs['article']
2618# article.fpage = new_fpage
2619# article.lpage = new_lpage
2620# article.page_range = new_page_range
2621# article.coi_statement = new_coi_statement
2622# article.show_body = new_show_body
2623# article.do_not_publish = new_do_not_publish
2624# article.save()
2625#
2626# else:
2627# # New article
2628# params = {
2629# 'pid': new_pid,
2630# 'title_xml': new_title_xml,
2631# 'title_html': new_title_html,
2632# 'title_tex': new_title,
2633# 'fpage': new_fpage,
2634# 'lpage': new_lpage,
2635# 'page_range': new_page_range,
2636# 'seq': container.article_set.count() + 1,
2637# 'body': body,
2638# 'coi_statement': new_coi_statement,
2639# 'show_body': new_show_body,
2640# 'do_not_publish': new_do_not_publish
2641# }
2642#
2643# xarticle = create_articledata()
2644# xarticle.pid = new_pid
2645# xarticle.title_xml = new_title_xml
2646# xarticle.title_html = new_title_html
2647# xarticle.title_tex = new_title
2648# xarticle.fpage = new_fpage
2649# xarticle.lpage = new_lpage
2650# xarticle.page_range = new_page_range
2651# xarticle.seq = container.article_set.count() + 1
2652# xarticle.body = body
2653# xarticle.coi_statement = new_coi_statement
2654# params['xobj'] = xarticle
2655#
2656# cmd = ptf_cmds.addArticlePtfCmd(params)
2657# cmd.set_container(container)
2658# cmd.add_collection(container.my_collection)
2659# article = cmd.do()
2660#
2661# self.kwargs['pid'] = new_pid
2662#
2663# # Add objects related to the article: contribs, datastream, counts...
2664# params = {
2665# # 'title_xml': new_title_xml,
2666# # 'title_html': new_title_html,
2667# # 'title_tex': new_title,
2668# 'authors': new_authors,
2669# 'page_count': new_page_count,
2670# 'icon_location': new_icon_location,
2671# 'body': body,
2672# 'use_kwds': True,
2673# 'kwds_fr': new_kwds_fr,
2674# 'kwds_en': new_kwds_en,
2675# 'kwd_uns_fr': new_kwd_uns_fr,
2676# 'kwd_uns_en': new_kwd_uns_en
2677# }
2678# cmd = ptf_cmds.updateArticlePtfCmd(params)
2679# cmd.set_article(article)
2680# cmd.do()
2681#
2682# self.set_success_message()
2683#
2684# return super(ArticleEditView, self).form_valid(form)
2687@require_http_methods(["POST"])
2688def do_not_publish_article(request, *args, **kwargs):
2689 next = request.headers.get("referer")
2691 pid = kwargs.get("pid", "")
2693 article = model_helpers.get_article(pid)
2694 if article:
2695 article.do_not_publish = not article.do_not_publish
2696 article.save()
2697 else:
2698 raise Http404
2700 return HttpResponseRedirect(next)
2703@require_http_methods(["POST"])
2704def show_article_body(request, *args, **kwargs):
2705 next = request.headers.get("referer")
2707 pid = kwargs.get("pid", "")
2709 article = model_helpers.get_article(pid)
2710 if article:
2711 article.show_body = not article.show_body
2712 article.save()
2713 else:
2714 raise Http404
2716 return HttpResponseRedirect(next)
2719class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2720 """
2721 API to get/post article metadata
2722 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2723 """
2725 def __init__(self, *args, **kwargs):
2726 """
2727 we define here what fields we want in the form
2728 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2729 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2730 super().__init__(*args, **kwargs)
2731 self.fields_to_update = [
2732 "lang",
2733 "atype",
2734 "contributors",
2735 "abstracts",
2736 "kwds",
2737 "titles",
2738 "trans_title_html",
2739 "title_html",
2740 "title_xml",
2741 "streams",
2742 "ext_links",
2743 ]
2744 self.additional_fields = [
2745 "pid",
2746 "doi",
2747 "container_pid",
2748 "pdf",
2749 "illustration",
2750 ]
2751 self.editorial_tools = ["translation", "sidebar", "lang_selection"]
2752 self.article_container_pid = ""
2753 self.back_url = "trammel"
2755 def save_data(self, data_article):
2756 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2757 # The icons are not preserved since we can add/edit/delete them in VueJs
2758 params = {
2759 "pid": data_article.pid,
2760 "export_folder": settings.MERSENNE_TMP_FOLDER,
2761 "export_all": True,
2762 "with_binary_files": False,
2763 }
2764 ptf_cmds.exportExtraDataPtfCmd(params).do()
2766 def restore_data(self, article):
2767 ptf_cmds.importExtraDataPtfCmd(
2768 {
2769 "pid": article.pid,
2770 "import_folder": settings.MERSENNE_TMP_FOLDER,
2771 }
2772 ).do()
2774 def get(self, request, *args, **kwargs):
2775 data = super().get(request, *args, **kwargs)
2776 return data
2778 def post(self, request, *args, **kwargs):
2779 response = super().post(request, *args, **kwargs)
2780 if response["message"] == "OK":
2781 return redirect(
2782 "api-edit-article",
2783 colid=kwargs.get("colid", ""),
2784 containerPid=kwargs.get("containerPid"),
2785 doi=kwargs.get("doi", ""),
2786 )
2787 else:
2788 raise Http404
2791class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2792 template_name = "article_form.html"
2794 def get_success_url(self):
2795 if self.kwargs["doi"]:
2796 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2797 return reverse("mersenne_dashboard/published_articles")
2799 def get_context_data(self, **kwargs):
2800 context = super().get_context_data(**kwargs)
2801 if "doi" in self.kwargs:
2802 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"])
2803 context["pid"] = context["article"].pid
2805 return context
2808class ArticleDeleteView(View):
2809 def get(self, request, *args, **kwargs):
2810 pid = self.kwargs.get("pid", None)
2811 article = get_object_or_404(Article, pid=pid)
2813 try:
2814 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2815 article.undeploy(mersenneSite)
2817 cmd = ptf_cmds.addArticlePtfCmd(
2818 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2819 )
2820 cmd.set_container(article.my_container)
2821 cmd.set_object_to_be_deleted(article)
2822 cmd.undo()
2823 except Exception as exception:
2824 return HttpResponseServerError(exception)
2826 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2827 return JsonResponse(data)
2830def get_messages_in_queue():
2831 app = Celery("ptf-tools")
2832 # tasks = list(current_app.tasks)
2833 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2834 print(tasks)
2835 # i = app.control.inspect()
2837 with app.connection_or_acquire() as conn:
2838 remaining = conn.default_channel.queue_declare(
2839 queue="coordinator", passive=True
2840 ).message_count
2841 return remaining
2844class FailedTasksListView(ListView):
2845 model = TaskResult
2846 queryset = TaskResult.objects.filter(
2847 status="FAILURE",
2848 task_name="ptf_tools.tasks.archive_numdam_issue",
2849 )
2852class FailedTasksDeleteView(DeleteView):
2853 model = TaskResult
2854 success_url = reverse_lazy("tasks-failed")
2857class FailedTasksRetryView(SingleObjectMixin, RedirectView):
2858 model = TaskResult
2860 @staticmethod
2861 def retry_task(task):
2862 colid, pid = (arg.strip("'") for arg in task.task_args.strip("()").split(", "))
2863 archive_numdam_resource.s(colid, pid).set(queue="coordinator").delay()
2864 task.delete()
2866 def get_redirect_url(self, *args, **kwargs):
2867 self.retry_task(self.get_object())
2868 return reverse("tasks-failed")
2871class NumdamView(TemplateView, history_views.HistoryContextMixin):
2872 template_name = "numdam.html"
2874 def get_context_data(self, **kwargs):
2875 context = super().get_context_data(**kwargs)
2877 context["objs"] = ResourceInNumdam.objects.all()
2879 pre_issues = []
2880 prod_issues = []
2881 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2882 try:
2883 response = requests.get(url)
2884 if response.status_code == 200:
2885 data = response.json()
2886 if "issues" in data:
2887 pre_issues = data["issues"]
2888 except Exception:
2889 pass
2891 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2892 response = requests.get(url)
2893 if response.status_code == 200:
2894 data = response.json()
2895 if "issues" in data:
2896 prod_issues = data["issues"]
2898 new = sorted(list(set(pre_issues).difference(prod_issues)))
2899 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2900 grouped = [
2901 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2902 ]
2903 grouped_removed = [
2904 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2905 ]
2906 context["added_issues"] = grouped
2907 context["removed_issues"] = grouped_removed
2909 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2910 return context
2913class TasksProgressView(View):
2914 def get(self, *args, **kwargs):
2915 task_name = self.kwargs.get("task", "archive_numdam_issue")
2916 successes = TaskResult.objects.filter(
2917 task_name=f"ptf_tools.tasks.{task_name}", status="SUCCESS"
2918 ).count()
2919 fails = TaskResult.objects.filter(
2920 task_name=f"ptf_tools.tasks.{task_name}", status="FAILURE"
2921 ).count()
2922 last_task = (
2923 TaskResult.objects.filter(
2924 task_name=f"ptf_tools.tasks.{task_name}",
2925 status="SUCCESS",
2926 )
2927 .order_by("-date_done")
2928 .first()
2929 )
2930 if last_task:
2931 last_task = " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
2932 remaining = get_messages_in_queue()
2933 all = successes + remaining
2934 progress = int(successes * 100 / all) if all else 0
2935 error_rate = int(fails * 100 / all) if all else 0
2936 status = "consuming_queue" if (successes or fails) and not progress == 100 else "polling"
2937 data = {
2938 "status": status,
2939 "progress": progress,
2940 "total": all,
2941 "remaining": remaining,
2942 "successes": successes,
2943 "fails": fails,
2944 "error_rate": error_rate,
2945 "last_task": last_task,
2946 }
2947 return JsonResponse(data)
2950class NumdamArchiveView(RedirectView):
2951 @staticmethod
2952 def reset_task_results():
2953 TaskResult.objects.all().delete()
2955 def get_redirect_url(self, *args, **kwargs):
2956 self.colid = kwargs["colid"]
2958 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2959 return Http404
2961 # we make sure archiving is not already running
2962 # if not get_messages_in_queue():
2963 # self.reset_task_results()
2965 if self.colid == "ALL":
2966 archive_numdam_collections.delay()
2967 else:
2968 archive_numdam_collection.s(self.colid).delay()
2970 return reverse("numdam")
2973class DeployAllNumdamAPIView(View):
2974 def internal_do(self, *args, **kwargs):
2975 pids = []
2977 for obj in ResourceInNumdam.objects.all():
2978 pids.append(obj.pid)
2980 return pids
2982 def get(self, request, *args, **kwargs):
2983 try:
2984 pids, status, message = history_views.execute_and_record_func(
2985 "deploy", "numdam", "numdam", self.internal_do, "numdam"
2986 )
2987 except Exception as exception:
2988 return HttpResponseServerError(exception)
2990 data = {"message": message, "ids": pids, "status": status}
2991 return JsonResponse(data)
2994class NumdamDeleteAPIView(View):
2995 def get(self, request, *args, **kwargs):
2996 pid = self.kwargs.get("pid", None)
2998 try:
2999 obj = ResourceInNumdam.objects.get(pid=pid)
3000 obj.delete()
3001 except Exception as exception:
3002 return HttpResponseServerError(exception)
3004 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
3005 return JsonResponse(data)
3008class ExtIdApiDetail(View):
3009 def get(self, request, *args, **kwargs):
3010 extid = get_object_or_404(
3011 ExtId,
3012 resource__pid=kwargs["pid"],
3013 id_type=kwargs["what"],
3014 )
3015 return JsonResponse(
3016 {
3017 "pk": extid.pk,
3018 "href": extid.get_href(),
3019 "fetch": reverse(
3020 "api-fetch-id",
3021 args=(
3022 extid.resource.pk,
3023 extid.id_value,
3024 extid.id_type,
3025 "extid",
3026 ),
3027 ),
3028 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3029 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3030 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3031 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3032 "is_valid": extid.checked,
3033 }
3034 )
3037class ExtIdFormTemplate(TemplateView):
3038 template_name = "common/externalid_form.html"
3040 def get_context_data(self, **kwargs):
3041 context = super().get_context_data(**kwargs)
3042 context["sequence"] = kwargs["sequence"]
3043 return context
3046class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3047 def get_context_data(self, **kwargs):
3048 context = super().get_context_data(**kwargs)
3049 context["helper"] = PtfFormHelper
3050 return context
3052 def get_success_url(self):
3053 self.post_process()
3054 return self.object.bibitem.resource.get_absolute_url()
3056 def post_process(self):
3057 cmd = xml_cmds.updateBibitemCitationXmlCmd()
3058 cmd.set_bibitem(self.object.bibitem)
3059 cmd.do()
3060 model_helpers.post_resource_updated(self.object.bibitem.resource)
3063class BibItemIdCreate(BibItemIdFormView, CreateView):
3064 model = BibItemId
3065 form_class = BibItemIdForm
3067 def get_context_data(self, **kwargs):
3068 context = super().get_context_data(**kwargs)
3069 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3070 return context
3072 def get_initial(self):
3073 initial = super().get_initial()
3074 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3075 return initial
3077 def form_valid(self, form):
3078 form.instance.checked = False
3079 return super().form_valid(form)
3082class BibItemIdUpdate(BibItemIdFormView, UpdateView):
3083 model = BibItemId
3084 form_class = BibItemIdForm
3086 def get_context_data(self, **kwargs):
3087 context = super().get_context_data(**kwargs)
3088 context["bibitem"] = self.object.bibitem
3089 return context
3092class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3093 def get_context_data(self, **kwargs):
3094 context = super().get_context_data(**kwargs)
3095 context["helper"] = PtfFormHelper
3096 return context
3098 def get_success_url(self):
3099 self.post_process()
3100 return self.object.resource.get_absolute_url()
3102 def post_process(self):
3103 model_helpers.post_resource_updated(self.object.resource)
3106class ExtIdCreate(ExtIdFormView, CreateView):
3107 model = ExtId
3108 form_class = ExtIdForm
3110 def get_context_data(self, **kwargs):
3111 context = super().get_context_data(**kwargs)
3112 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3113 return context
3115 def get_initial(self):
3116 initial = super().get_initial()
3117 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3118 return initial
3120 def form_valid(self, form):
3121 form.instance.checked = False
3122 return super().form_valid(form)
3125class ExtIdUpdate(ExtIdFormView, UpdateView):
3126 model = ExtId
3127 form_class = ExtIdForm
3129 def get_context_data(self, **kwargs):
3130 context = super().get_context_data(**kwargs)
3131 context["resource"] = self.object.resource
3132 return context
3135class BibItemIdApiDetail(View):
3136 def get(self, request, *args, **kwargs):
3137 bibitemid = get_object_or_404(
3138 BibItemId,
3139 bibitem__resource__pid=kwargs["pid"],
3140 bibitem__sequence=kwargs["seq"],
3141 id_type=kwargs["what"],
3142 )
3143 return JsonResponse(
3144 {
3145 "pk": bibitemid.pk,
3146 "href": bibitemid.get_href(),
3147 "fetch": reverse(
3148 "api-fetch-id",
3149 args=(
3150 bibitemid.bibitem.pk,
3151 bibitemid.id_value,
3152 bibitemid.id_type,
3153 "bibitemid",
3154 ),
3155 ),
3156 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
3157 "uncheck": reverse(
3158 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
3159 ),
3160 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
3161 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
3162 "is_valid": bibitemid.checked,
3163 }
3164 )
3167class UpdateTexmfZipAPIView(View):
3168 def get(self, request, *args, **kwargs):
3169 def copy_zip_files(src_folder, dest_folder):
3170 os.makedirs(dest_folder, exist_ok=True)
3172 zip_files = [
3173 os.path.join(src_folder, f)
3174 for f in os.listdir(src_folder)
3175 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3176 ]
3177 for zip_file in zip_files:
3178 resolver.copy_file(zip_file, dest_folder)
3180 # Exceptions: specific zip/gz files
3181 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3182 resolver.copy_file(zip_file, dest_folder)
3184 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3185 resolver.copy_file(zip_file, dest_folder)
3187 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3188 resolver.copy_file(gz_file, dest_folder)
3190 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3192 dest_folder = os.path.join(
3193 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3194 )
3196 try:
3197 copy_zip_files(src_folder, dest_folder)
3198 except Exception as exception:
3199 return HttpResponseServerError(exception)
3201 try:
3202 dest_folder = os.path.join(
3203 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3204 )
3205 copy_zip_files(src_folder, dest_folder)
3206 except Exception as exception:
3207 return HttpResponseServerError(exception)
3209 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3210 return JsonResponse(data)
3213class TestView(TemplateView):
3214 template_name = "mersenne.html"
3216 def get_context_data(self, **kwargs):
3217 super().get_context_data(**kwargs)
3218 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True)
3219 model_data_converter.db_to_issue_data(issue)
3222class TrammelTasksProgressView(View):
3223 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3224 """
3225 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3226 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3227 """
3228 task_name = task
3230 def get_event_data():
3231 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3233 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3234 remaining_messages = get_messages_in_queue()
3236 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3237 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3238 failed_tasks = all_tasks.filter(status="FAILURE")
3240 all_tasks_count = all_tasks.count()
3241 success_count = successed_tasks.count()
3242 fail_count = failed_tasks.count()
3244 all_count = all_tasks_count + remaining_messages
3245 remaining_count = all_count - success_count - fail_count
3247 success_rate = int(success_count * 100 / all_count) if all_count else 0
3248 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3249 status = "consuming_queue" if remaining_count != 0 else "polling"
3251 last_task = successed_tasks.first()
3252 last_task = (
3253 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3254 if last_task
3255 else ""
3256 )
3258 # SSE event format
3259 event_data = {
3260 "status": status,
3261 "success_rate": success_rate,
3262 "error_rate": error_rate,
3263 "all_count": all_count,
3264 "remaining_count": remaining_count,
3265 "success_count": success_count,
3266 "fail_count": fail_count,
3267 "last_task": last_task,
3268 }
3270 return event_data
3272 def stream_response(data):
3273 # Send initial response headers
3274 yield f"data: {json.dumps(data)}\n\n"
3276 data = get_event_data()
3277 format = request.GET.get("format", "stream")
3278 if format == "json":
3279 response = JsonResponse(data)
3280 else:
3281 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3282 return response
3285class TrammelFailedTasksListView(ListView):
3286 model = TaskResult
3287 queryset = TaskResult.objects.filter(
3288 status="FAILURE",
3289 task_name="ptf_tools.tasks.archive_trammel_collection",
3290 )
3293user_signed_up.connect(update_user_from_invite)