Coverage for src/ptf_tools/views/base_views.py: 17%
1646 statements
« prev ^ index » next coverage.py v7.9.0, created at 2026-01-21 15:26 +0000
« prev ^ index » next coverage.py v7.9.0, created at 2026-01-21 15:26 +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
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, FormView, UpdateView
33from django_celery_results.models import TaskResult
34from external.back.crossref.doi import checkDOI, recordDOI, recordPendingPublication
35from extra_views import (
36 CreateWithInlinesView,
37 InlineFormSetFactory,
38 NamedFormsetsMixin,
39 UpdateWithInlinesView,
40)
41from ptf import model_data_converter, model_helpers, tex, utils
42from ptf.cmds import ptf_cmds, xml_cmds
43from ptf.cmds.base_cmds import make_int
44from ptf.cmds.xml.jats.builder.issue import build_title_xml
45from ptf.cmds.xml.xml_utils import replace_html_entities
46from ptf.display import resolver
47from ptf.exceptions import DOIException, PDFException, ServerUnderMaintenance
48from ptf.model_data import create_issuedata, create_publisherdata, create_titledata
49from ptf.models import (
50 Abstract,
51 Article,
52 BibItem,
53 BibItemId,
54 Collection,
55 Container,
56 ExtId,
57 ExtLink,
58 Resource,
59 ResourceId,
60 Title,
61)
62from ptf.views import ArticleEditFormWithVueAPIView
63from ptf_back.cmds.xml_cmds import updateBibitemCitationXmlCmd
64from pubmed.views import recordPubmed
65from requests import Timeout
66from task.tasks.archiving_tasks import archive_resource
68from comments_moderation.utils import get_comments_for_home, is_comment_moderator
69from history import models as history_models
70from history import views as history_views
71from history.utils import get_gap, get_history_last_event_by, get_last_unsolved_error
72from ptf_tools.doaj import doaj_pid_register
73from ptf_tools.forms import (
74 BibItemIdForm,
75 CollectionForm,
76 ContainerForm,
77 DiffContainerForm,
78 ExtIdForm,
79 ExtLinkForm,
80 FormSetHelper,
81 ImportArticleForm,
82 ImportContainerForm,
83 ImportEditflowArticleForm,
84 PtfFormHelper,
85 PtfLargeModalFormHelper,
86 PtfModalFormHelper,
87 RegisterPubmedForm,
88 ResourceIdForm,
89 get_article_choices,
90)
91from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos
92from ptf_tools.models import ResourceInNumdam
93from ptf_tools.signals import update_user_from_invite
94from ptf_tools.tasks import (
95 archive_numdam_collection,
96 archive_numdam_collections,
97)
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 "needs_publication_date": True,
1397 }
1398 )
1399 result_, status, message = history_views.execute_and_record_func(
1400 "archive", pid, colid, cmd.do
1401 )
1402 except Exception as exception:
1403 return HttpResponseServerError(exception)
1405 data = {"message": message, "status": 200}
1406 return JsonResponse(data)
1409class CreateDjvuAPIView(View):
1410 def internal_do(self, *args, **kwargs):
1411 pid = self.kwargs.get("pid", None)
1413 resource = model_helpers.get_resource(pid)
1414 cmd = ptf_cmds.addDjvuPtfCmd()
1415 cmd.set_resource(resource)
1416 cmd.do()
1418 def get(self, request, *args, **kwargs):
1419 pid = self.kwargs.get("pid", None)
1420 colid = pid.split("_")[0]
1422 try:
1423 _, status, message = history_views.execute_and_record_func(
1424 "numdam", pid, colid, self.internal_do
1425 )
1426 except Exception as exception:
1427 return HttpResponseServerError(exception)
1429 data = {"message": message, "status": status}
1430 return JsonResponse(data)
1433class PTFToolsHomeView(LoginRequiredMixin, View):
1434 """
1435 Home Page.
1436 - Admin & staff -> Render blank home.html
1437 - User with unique authorized collection -> Redirect to collection details page
1438 - User with multiple authorized collections -> Render home.html with data
1439 - Comment moderator -> Comments dashboard
1440 - Others -> 404 response
1441 """
1443 def get(self, request, *args, **kwargs) -> HttpResponse:
1444 # Staff or user with authorized collections
1445 if request.user.is_staff or request.user.is_superuser:
1446 return render(request, "home.html")
1448 colids = get_authorized_collections(request.user)
1449 is_mod = is_comment_moderator(request.user)
1451 # The user has no rights
1452 if not (colids or is_mod):
1453 raise Http404("No collections associated with your account.")
1454 # Comment moderator only
1455 elif not colids:
1456 return HttpResponseRedirect(reverse("comment_list"))
1458 # User with unique collection -> Redirect to collection detail page
1459 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1460 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1462 # User with multiple authorized collections - Special home
1463 context = {}
1464 context["overview"] = True
1466 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1467 all_collections = {c["pid"]: c for c in all_collections}
1469 # Comments summary
1470 try:
1471 error, comments_data = get_comments_for_home(request.user)
1472 except AttributeError:
1473 error, comments_data = True, {}
1475 context["comment_server_ok"] = False
1477 if not error:
1478 context["comment_server_ok"] = True
1479 if comments_data:
1480 for col_id, comment_nb in comments_data.items():
1481 if col_id.upper() in all_collections: 1481 ↛ 1480line 1481 didn't jump to line 1480 because the condition on line 1481 was always true
1482 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1484 # TODO: Translations summary
1485 context["translation_server_ok"] = False
1487 # Sort the collections according to the number of pending comments
1488 context["collections"] = sorted(
1489 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1490 )
1492 return render(request, "home.html", context)
1495class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1496 columns = 5
1498 def get_common_context_data(self, **kwargs):
1499 context = super().get_context_data(**kwargs)
1500 now = timezone.now()
1501 curyear = now.year
1502 years = range(curyear - self.columns + 1, curyear + 1)
1504 context["collections"] = settings.MERSENNE_COLLECTIONS
1505 context["containers_to_be_published"] = []
1506 context["last_col_events"] = []
1508 event = get_history_last_event_by("clockss", "ALL")
1509 clockss_gap = get_gap(now, event)
1511 context["years"] = years
1512 context["clockss_gap"] = clockss_gap
1514 return context
1516 def calculate_articles_and_pages(self, pid, years):
1517 data_by_year = []
1518 total_articles = [0] * len(years)
1519 total_pages = [0] * len(years)
1521 for year in years:
1522 articles = self.get_articles_for_year(pid, year)
1523 articles_count = articles.count()
1524 page_count = sum(article.get_article_page_count() for article in articles)
1526 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1527 total_articles[year - years[0]] += articles_count
1528 total_pages[year - years[0]] += page_count
1530 return data_by_year, total_articles, total_pages
1532 def get_articles_for_year(self, pid, year):
1533 return Article.objects.filter(
1534 Q(my_container__my_collection__pid=pid)
1535 & (
1536 Q(date_published__year=year, date_online_first__isnull=True)
1537 | Q(date_online_first__year=year)
1538 )
1539 ).prefetch_related("resourcecount_set")
1542class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1543 template_name = "dashboard/published_articles.html"
1545 def get_context_data(self, **kwargs):
1546 context = self.get_common_context_data(**kwargs)
1547 years = context["years"]
1549 published_articles = []
1550 total_published_articles = [
1551 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1552 ]
1554 for pid in settings.MERSENNE_COLLECTIONS:
1555 if pid != "MERSENNE":
1556 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1557 pid, years
1558 )
1559 published_articles.append({"pid": pid, "years": articles_data})
1561 for i, year in enumerate(years):
1562 total_published_articles[i]["total_articles"] += total_articles[i]
1563 total_published_articles[i]["total_pages"] += total_pages[i]
1565 context["published_articles"] = published_articles
1566 context["total_published_articles"] = total_published_articles
1568 return context
1571class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1572 template_name = "dashboard/created_volumes.html"
1574 def get_context_data(self, **kwargs):
1575 context = self.get_common_context_data(**kwargs)
1576 years = context["years"]
1578 created_volumes = []
1579 total_created_volumes = [
1580 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1581 ]
1583 for pid in settings.MERSENNE_COLLECTIONS:
1584 if pid != "MERSENNE":
1585 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1586 pid, years
1587 )
1588 created_volumes.append({"pid": pid, "years": volumes_data})
1590 for i, year in enumerate(years):
1591 total_created_volumes[i]["total_articles"] += total_articles[i]
1592 total_created_volumes[i]["total_pages"] += total_pages[i]
1594 context["created_volumes"] = created_volumes
1595 context["total_created_volumes"] = total_created_volumes
1597 return context
1599 def calculate_volumes_and_pages(self, pid, years):
1600 data_by_year = []
1601 total_articles = [0] * len(years)
1602 total_pages = [0] * len(years)
1604 for year in years:
1605 issues = Container.objects.filter(my_collection__pid=pid, year=year)
1606 articles_count = 0
1607 page_count = 0
1609 for issue in issues:
1610 articles = issue.article_set.filter(
1611 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1612 ).prefetch_related("resourcecount_set")
1614 articles_count += articles.count()
1615 page_count += sum(article.get_article_page_count() for article in articles)
1617 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1618 total_articles[year - years[0]] += articles_count
1619 total_pages[year - years[0]] += page_count
1621 return data_by_year, total_articles, total_pages
1624class ReferencingChoice(View):
1625 def post(self, request, *args, **kwargs):
1626 if request.POST.get("optSite") == "ads":
1627 return redirect(
1628 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1629 )
1630 elif request.POST.get("optSite") == "wos":
1631 comp = ReferencingCheckerWos()
1632 journal = comp.make_journal(request.POST.get("selectCol"))
1633 if journal is None:
1634 return render(
1635 request,
1636 "dashboard/referencing.html",
1637 {
1638 "error": "Collection not found",
1639 "colid": request.POST.get("selectCol"),
1640 "optSite": request.POST.get("optSite"),
1641 },
1642 )
1643 return render(
1644 request,
1645 "dashboard/referencing.html",
1646 {
1647 "journal": journal,
1648 "colid": request.POST.get("selectCol"),
1649 "optSite": request.POST.get("optSite"),
1650 },
1651 )
1654class ReferencingWosFileView(View):
1655 template_name = "dashboard/referencing.html"
1657 def post(self, request, *args, **kwargs):
1658 colid = request.POST["colid"]
1659 if request.FILES.get("risfile") is None:
1660 message = "No file uploaded"
1661 return render(
1662 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1663 )
1664 uploaded_file = request.FILES["risfile"]
1665 comp = ReferencingCheckerWos()
1666 journal = comp.check_references(colid, uploaded_file)
1667 return render(request, self.template_name, {"journal": journal})
1670class ReferencingDashboardView(BaseMersenneDashboardView):
1671 template_name = "dashboard/referencing.html"
1673 def get(self, request, *args, **kwargs):
1674 colid = self.kwargs.get("colid", None)
1675 comp = ReferencingCheckerAds()
1676 journal = comp.check_references(colid)
1677 return render(request, self.template_name, {"journal": journal})
1680class BaseCollectionView(TemplateView):
1681 def get_context_data(self, **kwargs):
1682 context = super().get_context_data(**kwargs)
1683 aid = context.get("aid")
1684 year = context.get("year")
1686 if aid and year:
1687 context["collection"] = self.get_collection(aid, year)
1689 return context
1691 def get_collection(self, aid, year):
1692 """Method to be overridden by subclasses to fetch the appropriate collection"""
1693 raise NotImplementedError("Subclasses must implement get_collection method")
1696class ArticleListView(BaseCollectionView):
1697 template_name = "collection-list.html"
1699 def get_collection(self, aid, year):
1700 return Article.objects.filter(
1701 Q(my_container__my_collection__pid=aid)
1702 & (
1703 Q(date_published__year=year, date_online_first__isnull=True)
1704 | Q(date_online_first__year=year)
1705 )
1706 ).prefetch_related("resourcecount_set")
1709class VolumeListView(BaseCollectionView):
1710 template_name = "collection-list.html"
1712 def get_collection(self, aid, year):
1713 return Article.objects.filter(
1714 Q(my_container__my_collection__pid=aid, my_container__year=year)
1715 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1716 ).prefetch_related("resourcecount_set")
1719class DOAJResourceRegisterView(View):
1720 def get(self, request, *args, **kwargs):
1721 pid = kwargs.get("pid", None)
1722 resource = model_helpers.get_resource(pid)
1723 if resource is None:
1724 raise Http404
1725 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(
1726 resource.colid, None
1727 ):
1728 raise RuntimeError("Pending publications should not be deployed")
1730 try:
1731 data = {}
1732 doaj_meta, response = doaj_pid_register(pid)
1733 if response is None:
1734 return HttpResponse(status=204)
1735 elif doaj_meta and 200 <= response.status_code <= 299:
1736 data.update(doaj_meta)
1737 else:
1738 return HttpResponse(status=response.status_code, reason=response.text)
1739 except Timeout as exception:
1740 return HttpResponse(exception, status=408)
1741 except Exception as exception:
1742 return HttpResponseServerError(exception)
1743 return JsonResponse(data)
1746class CROSSREFResourceRegisterView(View):
1747 def get(self, request, *args, **kwargs):
1748 pid = kwargs.get("pid", None)
1749 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1750 force = kwargs.get("force", None)
1751 if not request.user.is_superuser:
1752 force = None
1754 resource = model_helpers.get_resource(pid)
1755 if resource is None:
1756 raise Http404
1758 resource = resource.cast()
1759 meth = getattr(self, "recordDOI" + resource.classname)
1760 try:
1761 data = meth(resource, force)
1762 except Timeout as exception:
1763 return HttpResponse(exception, status=408)
1764 except Exception as exception:
1765 return HttpResponseServerError(exception)
1766 return JsonResponse(data)
1768 def recordDOIArticle(self, article, force=None):
1769 result = {"status": 404}
1770 if (
1771 article.doi
1772 and not article.do_not_publish
1773 and (article.date_published or article.date_online_first or force == "force")
1774 ):
1775 if article.my_container.year is None: # or article.my_container.year == '0':
1776 article.my_container.year = datetime.now().strftime("%Y")
1777 result = recordDOI(article)
1778 return result
1780 def recordDOICollection(self, collection, force=None):
1781 return recordDOI(collection)
1783 def recordDOIContainer(self, container, force=None):
1784 data = {"status": 200, "message": "tout va bien"}
1786 if container.ctype == "issue":
1787 if container.doi:
1788 result = recordDOI(container)
1789 if result["status"] != 200:
1790 return result
1791 if force == "force":
1792 articles = container.article_set.exclude(
1793 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1794 )
1795 else:
1796 articles = container.article_set.exclude(
1797 doi__isnull=True,
1798 do_not_publish=True,
1799 date_published__isnull=True,
1800 date_online_first__isnull=True,
1801 )
1803 for article in articles:
1804 result = self.recordDOIArticle(article, force)
1805 if result["status"] != 200:
1806 data = result
1807 else:
1808 return recordDOI(container)
1809 return data
1812class CROSSREFResourceCheckStatusView(View):
1813 def get(self, request, *args, **kwargs):
1814 pid = kwargs.get("pid", None)
1815 resource = model_helpers.get_resource(pid)
1816 if resource is None:
1817 raise Http404
1818 resource = resource.cast()
1819 meth = getattr(self, "checkDOI" + resource.classname)
1820 try:
1821 meth(resource)
1822 except Timeout as exception:
1823 return HttpResponse(exception, status=408)
1824 except Exception as exception:
1825 return HttpResponseServerError(exception)
1827 data = {"status": 200, "message": "tout va bien"}
1828 return JsonResponse(data)
1830 def checkDOIArticle(self, article):
1831 if article.my_container.year is None or article.my_container.year == "0":
1832 article.my_container.year = datetime.now().strftime("%Y")
1833 checkDOI(article)
1835 def checkDOICollection(self, collection):
1836 checkDOI(collection)
1838 def checkDOIContainer(self, container):
1839 if container.doi is not None:
1840 checkDOI(container)
1841 for article in container.article_set.all():
1842 self.checkDOIArticle(article)
1845class CROSSREFResourcePendingPublicationRegisterView(View):
1846 def get(self, request, *args, **kwargs):
1847 pid = kwargs.get("pid", None)
1848 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1850 resource = model_helpers.get_resource(pid)
1851 if resource is None:
1852 raise Http404
1854 resource = resource.cast()
1855 meth = getattr(self, "recordPendingPublication" + resource.classname)
1856 try:
1857 data = meth(resource)
1858 except Timeout as exception:
1859 return HttpResponse(exception, status=408)
1860 except Exception as exception:
1861 return HttpResponseServerError(exception)
1862 return JsonResponse(data)
1864 def recordPendingPublicationArticle(self, article):
1865 result = {"status": 404}
1866 if article.doi and not article.date_published and not article.date_online_first:
1867 if article.my_container.year is None: # or article.my_container.year == '0':
1868 article.my_container.year = datetime.now().strftime("%Y")
1869 result = recordPendingPublication(article)
1870 return result
1873class RegisterPubmedFormView(FormView):
1874 template_name = "record_pubmed_dialog.html"
1875 form_class = RegisterPubmedForm
1877 def get_context_data(self, **kwargs):
1878 context = super().get_context_data(**kwargs)
1879 context["pid"] = self.kwargs["pid"]
1880 context["helper"] = PtfLargeModalFormHelper
1881 return context
1884class RegisterPubmedView(View):
1885 def get(self, request, *args, **kwargs):
1886 pid = kwargs.get("pid", None)
1887 update_article = self.request.GET.get("update_article", "on") == "on"
1889 article = model_helpers.get_article(pid)
1890 if article is None:
1891 raise Http404
1892 try:
1893 recordPubmed(article, update_article)
1894 except Exception as exception:
1895 messages.error("Unable to register the article in PubMed")
1896 return HttpResponseServerError(exception)
1898 return HttpResponseRedirect(
1899 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1900 )
1903class PTFToolsContainerView(TemplateView):
1904 template_name = ""
1906 def get_context_data(self, **kwargs):
1907 context = super().get_context_data(**kwargs)
1909 container = model_helpers.get_container(self.kwargs.get("pid"))
1910 if container is None:
1911 raise Http404
1912 citing_articles = container.citations()
1913 source = self.request.GET.get("source", None)
1914 if container.ctype.startswith("book"):
1915 book_parts = (
1916 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1917 )
1918 references = False
1919 if container.ctype == "book-monograph":
1920 # on regarde si il y a au moins une bibliographie
1921 for art in container.article_set.all():
1922 if art.bibitem_set.count() > 0:
1923 references = True
1924 context.update(
1925 {
1926 "book": container,
1927 "book_parts": list(book_parts),
1928 "source": source,
1929 "citing_articles": citing_articles,
1930 "references": references,
1931 "test_website": container.get_top_collection()
1932 .extlink_set.get(rel="test_website")
1933 .location,
1934 "prod_website": container.get_top_collection()
1935 .extlink_set.get(rel="website")
1936 .location,
1937 }
1938 )
1939 self.template_name = "book-toc.html"
1940 else:
1941 articles = container.article_set.all().order_by("seq")
1942 for article in articles:
1943 try:
1944 last_match = (
1945 history_models.HistoryEvent.objects.filter(
1946 pid=article.pid,
1947 type="matching",
1948 )
1949 .only("created_on")
1950 .latest("created_on")
1951 )
1952 except history_models.HistoryEvent.DoesNotExist as _:
1953 article.last_match = None
1954 else:
1955 article.last_match = last_match.created_on
1957 # article1 = articles.first()
1958 # date = article1.deployed_date()
1959 # TODO next_issue, previous_issue
1961 # check DOI est maintenant une commande à part
1962 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
1963 # articlesWithStatus = []
1964 # for article in articles:
1965 # checkDOIExistence(article)
1966 # articlesWithStatus.append(article)
1968 test_location = prod_location = ""
1969 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
1970 if qs:
1971 test_location = qs.first().location
1972 qs = container.get_top_collection().extlink_set.filter(rel="website")
1973 if qs:
1974 prod_location = qs.first().location
1975 context.update(
1976 {
1977 "issue": container,
1978 "articles": articles,
1979 "source": source,
1980 "citing_articles": citing_articles,
1981 "test_website": test_location,
1982 "prod_website": prod_location,
1983 }
1984 )
1986 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
1987 context["is_issue_pending_publication"] = True
1988 self.template_name = "issue-items.html"
1990 context["allow_crossref"] = container.allow_crossref()
1991 context["coltype"] = container.my_collection.coltype
1992 return context
1995class ExtLinkInline(InlineFormSetFactory):
1996 model = ExtLink
1997 form_class = ExtLinkForm
1998 factory_kwargs = {"extra": 0}
2001class ResourceIdInline(InlineFormSetFactory):
2002 model = ResourceId
2003 form_class = ResourceIdForm
2004 factory_kwargs = {"extra": 0}
2007class IssueDetailAPIView(View):
2008 def get(self, request, *args, **kwargs):
2009 issue = get_object_or_404(Container, pid=kwargs["pid"])
2010 deployed_date = issue.deployed_date()
2011 result = {
2012 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2013 if deployed_date
2014 else None,
2015 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2016 "all_doi_are_registered": issue.all_doi_are_registered(),
2017 "registered_in_doaj": issue.registered_in_doaj(),
2018 "doi": issue.my_collection.doi,
2019 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2020 }
2021 try:
2022 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2023 except history_models.HistoryEvent.DoesNotExist as _:
2024 pass
2025 else:
2026 result["latest"] = latest.message
2027 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2028 "%Y-%m-%d %H:%M"
2029 )
2031 result["latest_type"] = latest.type.capitalize()
2032 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2033 try:
2034 result[event_type] = timezone.localtime(
2035 history_models.HistoryEvent.objects.filter(
2036 type=event_type,
2037 status="OK",
2038 pid__startswith=issue.pid,
2039 )
2040 .latest("created_on")
2041 .created_on
2042 ).strftime("%Y-%m-%d %H:%M")
2043 except history_models.HistoryEvent.DoesNotExist as _:
2044 result[event_type] = ""
2045 return JsonResponse(result)
2048class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2049 model = Collection
2050 form_class = CollectionForm
2051 inlines = [ResourceIdInline, ExtLinkInline]
2052 inlines_names = ["resource_ids_form", "ext_links_form"]
2054 def get_context_data(self, **kwargs):
2055 context = super().get_context_data(**kwargs)
2056 context["helper"] = PtfFormHelper
2057 context["formset_helper"] = FormSetHelper
2058 return context
2060 def add_description(self, collection, description, lang, seq):
2061 if description:
2062 la = Abstract(
2063 resource=collection,
2064 tag="description",
2065 lang=lang,
2066 seq=seq,
2067 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2068 value_html=description,
2069 value_tex=description,
2070 )
2071 la.save()
2073 def form_valid(self, form):
2074 if form.instance.abbrev:
2075 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2076 else:
2077 form.instance.title_xml = (
2078 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2079 )
2081 form.instance.title_html = form.instance.title_tex
2082 form.instance.title_sort = form.instance.title_tex
2083 result = super().form_valid(form)
2085 collection = self.object
2086 collection.abstract_set.all().delete()
2088 seq = 1
2089 description = form.cleaned_data["description_en"]
2090 if description:
2091 self.add_description(collection, description, "en", seq)
2092 seq += 1
2093 description = form.cleaned_data["description_fr"]
2094 if description:
2095 self.add_description(collection, description, "fr", seq)
2097 return result
2099 def get_success_url(self):
2100 messages.success(self.request, "La Collection a été modifiée avec succès")
2101 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2104class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2105 """
2106 Warning : Not yet finished
2107 Automatic site membership creation is still missing
2108 """
2111class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2112 slug_field = "pid"
2113 slug_url_kwarg = "pid"
2116def suggest_load_journal_dois(colid):
2117 articles = (
2118 Article.objects.filter(my_container__my_collection__pid=colid)
2119 .filter(doi__isnull=False)
2120 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2121 .values_list("doi", flat=True)
2122 )
2124 try:
2125 articles = sorted(
2126 articles,
2127 key=lambda d: (
2128 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2129 int(re.search(r".(\d+)$", d).group(1)),
2130 ),
2131 )
2132 except: # noqa: E722 (we'll look later)
2133 pass
2134 return [f'<option value="{doi}">' for doi in articles]
2137def get_context_with_volumes(journal):
2138 result = model_helpers.get_volumes_in_collection(journal)
2139 volume_count = result["volume_count"]
2140 collections = []
2141 for ancestor in journal.ancestors.all():
2142 item = model_helpers.get_volumes_in_collection(ancestor)
2143 volume_count = max(0, volume_count)
2144 item.update({"journal": ancestor})
2145 collections.append(item)
2147 # add the parent collection to its children list and sort it by date
2148 result.update({"journal": journal})
2149 collections.append(result)
2151 collections = [c for c in collections if c["sorted_issues"]]
2152 collections.sort(
2153 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2154 reverse=True,
2155 )
2157 context = {
2158 "journal": journal,
2159 "sorted_issues": result["sorted_issues"],
2160 "volume_count": volume_count,
2161 "max_width": result["max_width"],
2162 "collections": collections,
2163 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2164 }
2165 return context
2168class CollectionDetail(
2169 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2170):
2171 model = Collection
2172 slug_field = "pid"
2173 slug_url_kwarg = "pid"
2174 template_name = "ptf/collection_detail.html"
2176 def test_func(self):
2177 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2179 def get(self, request, *args, **kwargs):
2180 self.object = self.get_object(queryset=Collection.objects.all())
2181 return super().get(request, *args, **kwargs)
2183 def get_context_data(self, **kwargs):
2184 context = super().get_context_data(**kwargs)
2185 context["object_list"] = context["object_list"].filter(
2186 Q(ctype="issue") | Q(ctype="book-lecture-notes")
2187 )
2188 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2189 context.update(get_context_with_volumes(self.object))
2191 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2192 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2193 context["issue_to_appear"] = Container.objects.filter(
2194 pid=context["issue_to_appear_pid"]
2195 ).exists()
2196 try:
2197 latest_error = history_models.HistoryEvent.objects.filter(
2198 status="ERROR", col=self.object
2199 ).latest("created_on")
2200 except history_models.HistoryEvent.DoesNotExist as _:
2201 pass
2202 else:
2203 message = latest_error.message
2204 if message:
2205 i = message.find(" - ")
2206 latest_exception = message[:i]
2207 latest_error_message = message[i + 3 :]
2208 context["latest_exception"] = latest_exception
2209 context["latest_exception_date"] = latest_error.created_on
2210 context["latest_exception_type"] = latest_error.type
2211 context["latest_error_message"] = latest_error_message
2213 archive_in_error = history_models.HistoryEvent.objects.filter(
2214 status="ERROR", col=self.object, type="archive"
2215 ).exists()
2217 context["archive_in_error"] = archive_in_error
2219 return context
2221 def get_queryset(self):
2222 query = self.object.content.all()
2224 for ancestor in self.object.ancestors.all():
2225 query |= ancestor.content.all()
2227 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2230class ContainerEditView(FormView):
2231 template_name = "container_form.html"
2232 form_class = ContainerForm
2234 def get_success_url(self):
2235 if self.kwargs["pid"]:
2236 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2237 return reverse("mersenne_dashboard/published_articles")
2239 def set_success_message(self): # pylint: disable=no-self-use
2240 messages.success(self.request, "Le fascicule a été modifié")
2242 def get_form_kwargs(self):
2243 kwargs = super().get_form_kwargs()
2244 if "pid" not in self.kwargs:
2245 self.kwargs["pid"] = None
2246 if "colid" not in self.kwargs:
2247 self.kwargs["colid"] = None
2248 if "data" in kwargs and "colid" in kwargs["data"]:
2249 # colid is passed as a hidden param in the form.
2250 # It is used when you submit a new container
2251 self.kwargs["colid"] = kwargs["data"]["colid"]
2253 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2254 self.kwargs["pid"]
2255 )
2256 return kwargs
2258 def get_context_data(self, **kwargs):
2259 context = super().get_context_data(**kwargs)
2261 context["pid"] = self.kwargs["pid"]
2262 context["colid"] = self.kwargs["colid"]
2263 context["container"] = self.kwargs["container"]
2265 context["edit_container"] = context["pid"] is not None
2266 context["name"] = resolve(self.request.path_info).url_name
2268 return context
2270 def form_valid(self, form):
2271 new_pid = form.cleaned_data.get("pid")
2272 new_title = form.cleaned_data.get("title")
2273 new_trans_title = form.cleaned_data.get("trans_title")
2274 new_publisher = form.cleaned_data.get("publisher")
2275 new_year = form.cleaned_data.get("year")
2276 new_volume = form.cleaned_data.get("volume")
2277 new_number = form.cleaned_data.get("number")
2279 collection = None
2280 issue = self.kwargs["container"]
2281 if issue is not None:
2282 collection = issue.my_collection
2283 elif self.kwargs["colid"] is not None:
2284 if "CR" in self.kwargs["colid"]:
2285 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2286 else:
2287 collection = model_helpers.get_collection(self.kwargs["colid"])
2289 if collection is None:
2290 raise ValueError("Collection for " + new_pid + " does not exist")
2292 # Icon
2293 new_icon_location = ""
2294 if "icon" in self.request.FILES:
2295 filename = os.path.basename(self.request.FILES["icon"].name)
2296 file_extension = filename.split(".")[1]
2298 icon_filename = resolver.get_disk_location(
2299 settings.MERSENNE_TEST_DATA_FOLDER,
2300 collection.pid,
2301 file_extension,
2302 new_pid,
2303 None,
2304 True,
2305 )
2307 with open(icon_filename, "wb+") as destination:
2308 for chunk in self.request.FILES["icon"].chunks():
2309 destination.write(chunk)
2311 folder = resolver.get_relative_folder(collection.pid, new_pid)
2312 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2313 name = resolve(self.request.path_info).url_name
2314 if name == "special_issue_create":
2315 self.kwargs["name"] = name
2316 if self.kwargs["container"]:
2317 # Edit Issue
2318 issue = self.kwargs["container"]
2319 if issue is None:
2320 raise ValueError(self.kwargs["pid"] + " does not exist")
2322 issue.pid = new_pid
2323 issue.title_tex = issue.title_html = new_title
2324 issue.title_xml = build_title_xml(
2325 title=new_title,
2326 lang=issue.lang,
2327 title_type="issue-title",
2328 )
2330 trans_lang = ""
2331 if issue.trans_lang != "" and issue.trans_lang != "und":
2332 trans_lang = issue.trans_lang
2333 elif new_trans_title != "":
2334 trans_lang = "fr" if issue.lang == "en" else "en"
2335 issue.trans_lang = trans_lang
2337 if trans_lang != "" and new_trans_title != "":
2338 issue.trans_title_html = ""
2339 issue.trans_title_tex = ""
2340 title_xml = build_title_xml(
2341 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2342 )
2343 try:
2344 trans_title_object = Title.objects.get(resource=issue, lang=trans_lang)
2345 trans_title_object.title_html = new_trans_title
2346 trans_title_object.title_xml = title_xml
2347 trans_title_object.save()
2348 except Title.DoesNotExist:
2349 trans_title = Title(
2350 resource=issue,
2351 lang=trans_lang,
2352 type="main",
2353 title_html=new_trans_title,
2354 title_xml=title_xml,
2355 )
2356 trans_title.save()
2357 issue.year = new_year
2358 issue.volume = new_volume
2359 issue.volume_int = make_int(new_volume)
2360 issue.number = new_number
2361 issue.number_int = make_int(new_number)
2362 issue.save()
2363 else:
2364 xissue = create_issuedata()
2366 xissue.ctype = "issue"
2367 xissue.pid = new_pid
2368 xissue.lang = "en"
2369 xissue.title_tex = new_title
2370 xissue.title_html = new_title
2371 xissue.title_xml = build_title_xml(
2372 title=new_title, lang=xissue.lang, title_type="issue-title"
2373 )
2375 if new_trans_title != "":
2376 trans_lang = "fr"
2377 title_xml = build_title_xml(
2378 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2379 )
2380 title = create_titledata(
2381 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2382 )
2383 issue.titles = [title]
2385 xissue.year = new_year
2386 xissue.volume = new_volume
2387 xissue.number = new_number
2388 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2390 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2391 cmd.add_collection(collection)
2392 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2393 issue = cmd.do()
2395 self.kwargs["pid"] = new_pid
2397 # Add objects related to the article: contribs, datastream, counts...
2398 params = {
2399 "icon_location": new_icon_location,
2400 }
2401 cmd = ptf_cmds.updateContainerPtfCmd(params)
2402 cmd.set_resource(issue)
2403 cmd.do()
2405 publisher = model_helpers.get_publisher(new_publisher)
2406 if not publisher:
2407 xpub = create_publisherdata()
2408 xpub.name = new_publisher
2409 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2410 issue.my_publisher = publisher
2411 issue.save()
2413 self.set_success_message()
2415 return super().form_valid(form)
2418# class ArticleEditView(FormView):
2419# template_name = 'article_form.html'
2420# form_class = ArticleForm
2421#
2422# def get_success_url(self):
2423# if self.kwargs['pid']:
2424# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2425# return reverse('mersenne_dashboard/published_articles')
2426#
2427# def set_success_message(self): # pylint: disable=no-self-use
2428# messages.success(self.request, "L'article a été modifié")
2429#
2430# def get_form_kwargs(self):
2431# kwargs = super(ArticleEditView, self).get_form_kwargs()
2432#
2433# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2434# # Article creation: pid is None
2435# self.kwargs['pid'] = None
2436# if 'issue_id' not in self.kwargs:
2437# # Article edit: issue_id is not passed
2438# self.kwargs['issue_id'] = None
2439# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2440# # colid is passed as a hidden param in the form.
2441# # It is used when you submit a new container
2442# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2443#
2444# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2445# return kwargs
2446#
2447# def get_context_data(self, **kwargs):
2448# context = super(ArticleEditView, self).get_context_data(**kwargs)
2449#
2450# context['pid'] = self.kwargs['pid']
2451# context['issue_id'] = self.kwargs['issue_id']
2452# context['article'] = self.kwargs['article']
2453#
2454# context['edit_article'] = context['pid'] is not None
2455#
2456# article = context['article']
2457# if article:
2458# context['author_contributions'] = article.get_author_contributions()
2459# context['kwds_fr'] = None
2460# context['kwds_en'] = None
2461# kwd_gps = article.get_non_msc_kwds()
2462# for kwd_gp in kwd_gps:
2463# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2464# if kwd_gp.value_xml:
2465# kwd_ = types.SimpleNamespace()
2466# kwd_.value = kwd_gp.value_tex
2467# context['kwd_unstructured_fr'] = kwd_
2468# context['kwds_fr'] = kwd_gp.kwd_set.all()
2469# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2470# if kwd_gp.value_xml:
2471# kwd_ = types.SimpleNamespace()
2472# kwd_.value = kwd_gp.value_tex
2473# context['kwd_unstructured_en'] = kwd_
2474# context['kwds_en'] = kwd_gp.kwd_set.all()
2475#
2476# # Article creation: init pid
2477# if context['issue_id'] and context['pid'] is None:
2478# issue = model_helpers.get_container(context['issue_id'])
2479# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2480#
2481# return context
2482#
2483# def form_valid(self, form):
2484#
2485# new_pid = form.cleaned_data.get('pid')
2486# new_title = form.cleaned_data.get('title')
2487# new_fpage = form.cleaned_data.get('fpage')
2488# new_lpage = form.cleaned_data.get('lpage')
2489# new_page_range = form.cleaned_data.get('page_range')
2490# new_page_count = form.cleaned_data.get('page_count')
2491# new_coi_statement = form.cleaned_data.get('coi_statement')
2492# new_show_body = form.cleaned_data.get('show_body')
2493# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2494#
2495# # TODO support MathML
2496# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2497# # We need to pass trans_title to get_title_xml
2498# # Meanwhile, ignore new_title_xml
2499# new_title_xml = jats_parser.get_title_xml(new_title)
2500# new_title_html = new_title
2501#
2502# authors_count = int(self.request.POST.get('authors_count', "0"))
2503# i = 1
2504# new_authors = []
2505# old_author_contributions = []
2506# if self.kwargs['article']:
2507# old_author_contributions = self.kwargs['article'].get_author_contributions()
2508#
2509# while authors_count > 0:
2510# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2511#
2512# if prefix is not None:
2513# addresses = []
2514# if len(old_author_contributions) >= i:
2515# old_author_contribution = old_author_contributions[i - 1]
2516# addresses = [contrib_address.address for contrib_address in
2517# old_author_contribution.get_addresses()]
2518#
2519# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2520# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2521# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2522# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2523# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2524# deceased_before_publication = deceased == 'on'
2525# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2526# equal_contrib = equal_contrib == 'on'
2527# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2528# corresponding = corresponding == 'on'
2529# email = self.request.POST.get('email-' + str(i), None)
2530#
2531# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2532# params['deceased_before_publication'] = deceased_before_publication
2533# params['equal_contrib'] = equal_contrib
2534# params['corresponding'] = corresponding
2535# params['addresses'] = addresses
2536# params['email'] = email
2537#
2538# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2539#
2540# new_authors.append(params)
2541#
2542# authors_count -= 1
2543# i += 1
2544#
2545# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2546# i = 1
2547# new_kwds_fr = []
2548# while kwds_fr_count > 0:
2549# value = self.request.POST.get('kwd-fr-' + str(i), None)
2550# new_kwds_fr.append(value)
2551# kwds_fr_count -= 1
2552# i += 1
2553# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2554#
2555# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2556# i = 1
2557# new_kwds_en = []
2558# while kwds_en_count > 0:
2559# value = self.request.POST.get('kwd-en-' + str(i), None)
2560# new_kwds_en.append(value)
2561# kwds_en_count -= 1
2562# i += 1
2563# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2564#
2565# if self.kwargs['article']:
2566# # Edit article
2567# container = self.kwargs['article'].my_container
2568# else:
2569# # New article
2570# container = model_helpers.get_container(self.kwargs['issue_id'])
2571#
2572# if container is None:
2573# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2574#
2575# collection = container.my_collection
2576#
2577# # Copy PDF file & extract full text
2578# body = ''
2579# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2580# collection.pid,
2581# "pdf",
2582# container.pid,
2583# new_pid,
2584# True)
2585# if 'pdf' in self.request.FILES:
2586# with open(pdf_filename, 'wb+') as destination:
2587# for chunk in self.request.FILES['pdf'].chunks():
2588# destination.write(chunk)
2589#
2590# # Extract full text from the PDF
2591# body = utils.pdf_to_text(pdf_filename)
2592#
2593# # Icon
2594# new_icon_location = ''
2595# if 'icon' in self.request.FILES:
2596# filename = os.path.basename(self.request.FILES['icon'].name)
2597# file_extension = filename.split('.')[1]
2598#
2599# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2600# collection.pid,
2601# file_extension,
2602# container.pid,
2603# new_pid,
2604# True)
2605#
2606# with open(icon_filename, 'wb+') as destination:
2607# for chunk in self.request.FILES['icon'].chunks():
2608# destination.write(chunk)
2609#
2610# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2611# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2612#
2613# if self.kwargs['article']:
2614# # Edit article
2615# article = self.kwargs['article']
2616# article.fpage = new_fpage
2617# article.lpage = new_lpage
2618# article.page_range = new_page_range
2619# article.coi_statement = new_coi_statement
2620# article.show_body = new_show_body
2621# article.do_not_publish = new_do_not_publish
2622# article.save()
2623#
2624# else:
2625# # New article
2626# params = {
2627# 'pid': new_pid,
2628# 'title_xml': new_title_xml,
2629# 'title_html': new_title_html,
2630# 'title_tex': new_title,
2631# 'fpage': new_fpage,
2632# 'lpage': new_lpage,
2633# 'page_range': new_page_range,
2634# 'seq': container.article_set.count() + 1,
2635# 'body': body,
2636# 'coi_statement': new_coi_statement,
2637# 'show_body': new_show_body,
2638# 'do_not_publish': new_do_not_publish
2639# }
2640#
2641# xarticle = create_articledata()
2642# xarticle.pid = new_pid
2643# xarticle.title_xml = new_title_xml
2644# xarticle.title_html = new_title_html
2645# xarticle.title_tex = new_title
2646# xarticle.fpage = new_fpage
2647# xarticle.lpage = new_lpage
2648# xarticle.page_range = new_page_range
2649# xarticle.seq = container.article_set.count() + 1
2650# xarticle.body = body
2651# xarticle.coi_statement = new_coi_statement
2652# params['xobj'] = xarticle
2653#
2654# cmd = ptf_cmds.addArticlePtfCmd(params)
2655# cmd.set_container(container)
2656# cmd.add_collection(container.my_collection)
2657# article = cmd.do()
2658#
2659# self.kwargs['pid'] = new_pid
2660#
2661# # Add objects related to the article: contribs, datastream, counts...
2662# params = {
2663# # 'title_xml': new_title_xml,
2664# # 'title_html': new_title_html,
2665# # 'title_tex': new_title,
2666# 'authors': new_authors,
2667# 'page_count': new_page_count,
2668# 'icon_location': new_icon_location,
2669# 'body': body,
2670# 'use_kwds': True,
2671# 'kwds_fr': new_kwds_fr,
2672# 'kwds_en': new_kwds_en,
2673# 'kwd_uns_fr': new_kwd_uns_fr,
2674# 'kwd_uns_en': new_kwd_uns_en
2675# }
2676# cmd = ptf_cmds.updateArticlePtfCmd(params)
2677# cmd.set_article(article)
2678# cmd.do()
2679#
2680# self.set_success_message()
2681#
2682# return super(ArticleEditView, self).form_valid(form)
2685@require_http_methods(["POST"])
2686def do_not_publish_article(request, *args, **kwargs):
2687 next = request.headers.get("referer")
2689 pid = kwargs.get("pid", "")
2691 article = model_helpers.get_article(pid)
2692 if article:
2693 article.do_not_publish = not article.do_not_publish
2694 article.save()
2695 else:
2696 raise Http404
2698 return HttpResponseRedirect(next)
2701@require_http_methods(["POST"])
2702def show_article_body(request, *args, **kwargs):
2703 next = request.headers.get("referer")
2705 pid = kwargs.get("pid", "")
2707 article = model_helpers.get_article(pid)
2708 if article:
2709 article.show_body = not article.show_body
2710 article.save()
2711 else:
2712 raise Http404
2714 return HttpResponseRedirect(next)
2717class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2718 """
2719 API to get/post article metadata
2720 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2721 """
2723 def __init__(self, *args, **kwargs):
2724 """
2725 we define here what fields we want in the form
2726 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2727 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2728 super().__init__(*args, **kwargs)
2729 self.fields_to_update = [
2730 "lang",
2731 "atype",
2732 "contributors",
2733 "abstracts",
2734 "kwds",
2735 "titles",
2736 "trans_title_html",
2737 "title_html",
2738 "title_xml",
2739 "streams",
2740 "ext_links",
2741 "date_accepted",
2742 "history_dates",
2743 ]
2744 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2745 self.additional_fields = ["doi", "pid", "container_pid", "pdf", "illustration", "dates"]
2746 self.editorial_tools = [
2747 "translation",
2748 "sidebar",
2749 "lang_selection",
2750 "back_to_article_option",
2751 ]
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=response["data"].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 context["container_pid"] = kwargs.get("container_pid", "")
2806 return context
2809class ArticleDeleteView(View):
2810 def get(self, request, *args, **kwargs):
2811 pid = self.kwargs.get("pid", None)
2812 article = get_object_or_404(Article, pid=pid)
2814 try:
2815 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2816 article.undeploy(mersenneSite)
2818 cmd = ptf_cmds.addArticlePtfCmd(
2819 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2820 )
2821 cmd.set_container(article.my_container)
2822 cmd.set_object_to_be_deleted(article)
2823 cmd.undo()
2824 except Exception as exception:
2825 return HttpResponseServerError(exception)
2827 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2828 return JsonResponse(data)
2831def get_messages_in_queue():
2832 app = Celery("ptf-tools")
2833 # tasks = list(current_app.tasks)
2834 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2835 print(tasks)
2836 # i = app.control.inspect()
2838 with app.connection_or_acquire() as conn:
2839 remaining = conn.default_channel.queue_declare(
2840 queue="coordinator", passive=True
2841 ).message_count
2842 return remaining
2845class NumdamView(TemplateView, history_views.HistoryContextMixin):
2846 template_name = "numdam.html"
2848 def get_context_data(self, **kwargs):
2849 context = super().get_context_data(**kwargs)
2851 context["objs"] = ResourceInNumdam.objects.all()
2853 pre_issues = []
2854 prod_issues = []
2855 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2856 try:
2857 response = requests.get(url)
2858 if response.status_code == 200:
2859 data = response.json()
2860 if "issues" in data:
2861 pre_issues = data["issues"]
2862 except Exception:
2863 pass
2865 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2866 response = requests.get(url)
2867 if response.status_code == 200:
2868 data = response.json()
2869 if "issues" in data:
2870 prod_issues = data["issues"]
2872 new = sorted(list(set(pre_issues).difference(prod_issues)))
2873 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2874 grouped = [
2875 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2876 ]
2877 grouped_removed = [
2878 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2879 ]
2880 context["added_issues"] = grouped
2881 context["removed_issues"] = grouped_removed
2883 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2884 return context
2887class NumdamArchiveView(RedirectView):
2888 @staticmethod
2889 def reset_task_results():
2890 TaskResult.objects.all().delete()
2892 def get_redirect_url(self, *args, **kwargs):
2893 self.colid = kwargs["colid"]
2895 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2896 return Http404
2898 # we make sure archiving is not already running
2899 # if not get_messages_in_queue():
2900 # self.reset_task_results()
2902 if self.colid == "ALL":
2903 archive_numdam_collections.delay()
2904 else:
2905 archive_numdam_collection.s(self.colid).delay()
2907 return reverse("numdam")
2910class DeployAllNumdamAPIView(View):
2911 def internal_do(self, *args, **kwargs):
2912 pids = []
2914 for obj in ResourceInNumdam.objects.all():
2915 pids.append(obj.pid)
2917 return pids
2919 def get(self, request, *args, **kwargs):
2920 try:
2921 pids, status, message = history_views.execute_and_record_func(
2922 "deploy", "numdam", "ALL", self.internal_do, "numdam"
2923 )
2924 except Exception as exception:
2925 return HttpResponseServerError(exception)
2927 data = {"message": message, "ids": pids, "status": status}
2928 return JsonResponse(data)
2931class NumdamDeleteAPIView(View):
2932 def get(self, request, *args, **kwargs):
2933 pid = self.kwargs.get("pid", None)
2935 try:
2936 obj = ResourceInNumdam.objects.get(pid=pid)
2937 obj.delete()
2938 except Exception as exception:
2939 return HttpResponseServerError(exception)
2941 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
2942 return JsonResponse(data)
2945class ExtIdApiDetail(View):
2946 def get(self, request, *args, **kwargs):
2947 extid = get_object_or_404(
2948 ExtId,
2949 resource__pid=kwargs["pid"],
2950 id_type=kwargs["what"],
2951 )
2952 return JsonResponse(
2953 {
2954 "pk": extid.pk,
2955 "href": extid.get_href(),
2956 "fetch": reverse(
2957 "api-fetch-id",
2958 args=(
2959 extid.resource.pk,
2960 extid.id_value,
2961 extid.id_type,
2962 "extid",
2963 ),
2964 ),
2965 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
2966 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
2967 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
2968 "delete": reverse("update-extid", args=(extid.pk, "delete")),
2969 "is_valid": extid.checked,
2970 }
2971 )
2974class ExtIdFormTemplate(TemplateView):
2975 template_name = "common/externalid_form.html"
2977 def get_context_data(self, **kwargs):
2978 context = super().get_context_data(**kwargs)
2979 context["sequence"] = kwargs["sequence"]
2980 return context
2983class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
2984 def get_context_data(self, **kwargs):
2985 context = super().get_context_data(**kwargs)
2986 context["helper"] = PtfFormHelper
2987 return context
2989 def get_success_url(self):
2990 self.post_process()
2991 return self.object.bibitem.resource.get_absolute_url()
2993 def post_process(self):
2994 cmd = updateBibitemCitationXmlCmd()
2995 cmd.set_bibitem(self.object.bibitem)
2996 cmd.do()
2997 model_helpers.post_resource_updated(self.object.bibitem.resource)
3000class BibItemIdCreate(BibItemIdFormView, CreateView):
3001 model = BibItemId
3002 form_class = BibItemIdForm
3004 def get_context_data(self, **kwargs):
3005 context = super().get_context_data(**kwargs)
3006 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3007 return context
3009 def get_initial(self):
3010 initial = super().get_initial()
3011 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3012 return initial
3014 def form_valid(self, form):
3015 form.instance.checked = False
3016 return super().form_valid(form)
3019class BibItemIdUpdate(BibItemIdFormView, UpdateView):
3020 model = BibItemId
3021 form_class = BibItemIdForm
3023 def get_context_data(self, **kwargs):
3024 context = super().get_context_data(**kwargs)
3025 context["bibitem"] = self.object.bibitem
3026 return context
3029class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3030 def get_context_data(self, **kwargs):
3031 context = super().get_context_data(**kwargs)
3032 context["helper"] = PtfFormHelper
3033 return context
3035 def get_success_url(self):
3036 self.post_process()
3037 return self.object.resource.get_absolute_url()
3039 def post_process(self):
3040 model_helpers.post_resource_updated(self.object.resource)
3043class ExtIdCreate(ExtIdFormView, CreateView):
3044 model = ExtId
3045 form_class = ExtIdForm
3047 def get_context_data(self, **kwargs):
3048 context = super().get_context_data(**kwargs)
3049 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3050 return context
3052 def get_initial(self):
3053 initial = super().get_initial()
3054 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3055 return initial
3057 def form_valid(self, form):
3058 form.instance.checked = False
3059 return super().form_valid(form)
3062class ExtIdUpdate(ExtIdFormView, UpdateView):
3063 model = ExtId
3064 form_class = ExtIdForm
3066 def get_context_data(self, **kwargs):
3067 context = super().get_context_data(**kwargs)
3068 context["resource"] = self.object.resource
3069 return context
3072class BibItemIdApiDetail(View):
3073 def get(self, request, *args, **kwargs):
3074 bibitemid = get_object_or_404(
3075 BibItemId,
3076 bibitem__resource__pid=kwargs["pid"],
3077 bibitem__sequence=kwargs["seq"],
3078 id_type=kwargs["what"],
3079 )
3080 return JsonResponse(
3081 {
3082 "pk": bibitemid.pk,
3083 "href": bibitemid.get_href(),
3084 "fetch": reverse(
3085 "api-fetch-id",
3086 args=(
3087 bibitemid.bibitem.pk,
3088 bibitemid.id_value,
3089 bibitemid.id_type,
3090 "bibitemid",
3091 ),
3092 ),
3093 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
3094 "uncheck": reverse(
3095 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
3096 ),
3097 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
3098 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
3099 "is_valid": bibitemid.checked,
3100 }
3101 )
3104class UpdateTexmfZipAPIView(View):
3105 def get(self, request, *args, **kwargs):
3106 def copy_zip_files(src_folder, dest_folder):
3107 os.makedirs(dest_folder, exist_ok=True)
3109 zip_files = [
3110 os.path.join(src_folder, f)
3111 for f in os.listdir(src_folder)
3112 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3113 ]
3114 for zip_file in zip_files:
3115 resolver.copy_file(zip_file, dest_folder)
3117 # Exceptions: specific zip/gz files
3118 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3119 resolver.copy_file(zip_file, dest_folder)
3121 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3122 resolver.copy_file(zip_file, dest_folder)
3124 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3125 resolver.copy_file(gz_file, dest_folder)
3127 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3129 dest_folder = os.path.join(
3130 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3131 )
3133 try:
3134 copy_zip_files(src_folder, dest_folder)
3135 except Exception as exception:
3136 return HttpResponseServerError(exception)
3138 try:
3139 dest_folder = os.path.join(
3140 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3141 )
3142 copy_zip_files(src_folder, dest_folder)
3143 except Exception as exception:
3144 return HttpResponseServerError(exception)
3146 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3147 return JsonResponse(data)
3150class TestView(TemplateView):
3151 template_name = "mersenne.html"
3153 def get_context_data(self, **kwargs):
3154 super().get_context_data(**kwargs)
3155 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True)
3156 model_data_converter.db_to_issue_data(issue)
3159class TrammelTasksProgressView(View):
3160 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3161 """
3162 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3163 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3164 """
3165 task_name = task
3167 def get_event_data():
3168 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3170 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3171 remaining_messages = get_messages_in_queue()
3173 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3174 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3175 failed_tasks = all_tasks.filter(status="FAILURE")
3177 all_tasks_count = all_tasks.count()
3178 success_count = successed_tasks.count()
3179 fail_count = failed_tasks.count()
3181 all_count = all_tasks_count + remaining_messages
3182 remaining_count = all_count - success_count - fail_count
3184 success_rate = int(success_count * 100 / all_count) if all_count else 0
3185 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3186 status = "consuming_queue" if remaining_count != 0 else "polling"
3188 last_task = successed_tasks.first()
3189 last_task = (
3190 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3191 if last_task
3192 else ""
3193 )
3195 # SSE event format
3196 event_data = {
3197 "status": status,
3198 "success_rate": success_rate,
3199 "error_rate": error_rate,
3200 "all_count": all_count,
3201 "remaining_count": remaining_count,
3202 "success_count": success_count,
3203 "fail_count": fail_count,
3204 "last_task": last_task,
3205 }
3207 return event_data
3209 def stream_response(data):
3210 # Send initial response headers
3211 yield f"data: {json.dumps(data)}\n\n"
3213 data = get_event_data()
3214 format = request.GET.get("format", "stream")
3215 if format == "json":
3216 response = JsonResponse(data)
3217 else:
3218 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3219 return response
3222user_signed_up.connect(update_user_from_invite)