Coverage for src / ptf_tools / views / base_views.py: 17%
1671 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-03-04 14:25 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-03-04 14:25 +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 ConvertArticleTexToXmlAndUpdateBodyView(View, LoginRequiredMixin, StaffuserRequiredMixin):
1747 """
1748 Convert an Article TeX source to XML on mathdoc-tex host,
1749 then parse the resulting XML to update only body_html and body_xml fields
1750 of the Article.
1751 """
1753 def get(self, request, *args, **kwargs):
1754 pid = kwargs.get("pid", None)
1755 if not pid:
1756 return JsonResponse(
1757 {"status": 400, "message": "Missing resource pid"},
1758 )
1760 resource = model_helpers.get_resource(pid)
1761 if resource is None:
1762 return JsonResponse(
1763 {"status": 404, "message": f"Resource not found: {pid}"},
1764 )
1766 resource = resource.cast()
1767 if resource.classname != "Article":
1768 return JsonResponse(
1769 {"status": 400, "message": "Resource is not an Article"},
1770 )
1772 try:
1773 xml_body = tex.convert_tex_to_xml_remote(resource)
1774 except Exception as exc:
1775 return JsonResponse(
1776 {
1777 "status": 500,
1778 "message": f"XML parsing or article update failed: {exc}",
1779 },
1780 )
1782 if not xml_body or not xml_body.strip():
1783 return JsonResponse(
1784 {
1785 "status": 500,
1786 "message": "Remote conversion returned empty XML body",
1787 }
1788 )
1790 try:
1791 cmd = xml_cmds.addBodyInHtmlXmlCmd({"body": xml_body})
1792 cmd.set_article(resource)
1793 cmd.do()
1794 except Exception:
1795 return JsonResponse(
1796 {
1797 "status": 500,
1798 "message": "XML parsing or article update failed",
1799 }
1800 )
1802 data = {"status": 200}
1803 return JsonResponse(data)
1806class CROSSREFResourceRegisterView(View):
1807 def get(self, request, *args, **kwargs):
1808 pid = kwargs.get("pid", None)
1809 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1810 force = kwargs.get("force", None)
1811 if not request.user.is_superuser:
1812 force = None
1814 resource = model_helpers.get_resource(pid)
1815 if resource is None:
1816 raise Http404
1818 resource = resource.cast()
1819 meth = getattr(self, "recordDOI" + resource.classname)
1820 try:
1821 data = meth(resource, force)
1822 except Timeout as exception:
1823 return HttpResponse(exception, status=408)
1824 except Exception as exception:
1825 return HttpResponseServerError(exception)
1826 return JsonResponse(data)
1828 def recordDOIArticle(self, article, force=None):
1829 result = {"status": 404}
1830 if (
1831 article.doi
1832 and not article.do_not_publish
1833 and (article.date_published or article.date_online_first or force == "force")
1834 ):
1835 if article.my_container.year is None: # or article.my_container.year == '0':
1836 article.my_container.year = datetime.now().strftime("%Y")
1837 result = recordDOI(article)
1838 return result
1840 def recordDOICollection(self, collection, force=None):
1841 return recordDOI(collection)
1843 def recordDOIContainer(self, container, force=None):
1844 data = {"status": 200, "message": "tout va bien"}
1846 if container.ctype == "issue":
1847 if container.doi:
1848 result = recordDOI(container)
1849 if result["status"] != 200:
1850 return result
1851 if force == "force":
1852 articles = container.article_set.exclude(
1853 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1854 )
1855 else:
1856 articles = container.article_set.exclude(
1857 doi__isnull=True,
1858 do_not_publish=True,
1859 date_published__isnull=True,
1860 date_online_first__isnull=True,
1861 )
1863 for article in articles:
1864 result = self.recordDOIArticle(article, force)
1865 if result["status"] != 200:
1866 data = result
1867 else:
1868 return recordDOI(container)
1869 return data
1872class CROSSREFResourceCheckStatusView(View):
1873 def get(self, request, *args, **kwargs):
1874 pid = kwargs.get("pid", None)
1875 resource = model_helpers.get_resource(pid)
1876 if resource is None:
1877 raise Http404
1878 resource = resource.cast()
1879 meth = getattr(self, "checkDOI" + resource.classname)
1880 try:
1881 meth(resource)
1882 except Timeout as exception:
1883 return HttpResponse(exception, status=408)
1884 except Exception as exception:
1885 return HttpResponseServerError(exception)
1887 data = {"status": 200, "message": "tout va bien"}
1888 return JsonResponse(data)
1890 def checkDOIArticle(self, article):
1891 if article.my_container.year is None or article.my_container.year == "0":
1892 article.my_container.year = datetime.now().strftime("%Y")
1893 checkDOI(article)
1895 def checkDOICollection(self, collection):
1896 checkDOI(collection)
1898 def checkDOIContainer(self, container):
1899 if container.doi is not None:
1900 checkDOI(container)
1901 for article in container.article_set.all():
1902 self.checkDOIArticle(article)
1905class CROSSREFResourcePendingPublicationRegisterView(View):
1906 def get(self, request, *args, **kwargs):
1907 pid = kwargs.get("pid", None)
1908 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1910 resource = model_helpers.get_resource(pid)
1911 if resource is None:
1912 raise Http404
1914 resource = resource.cast()
1915 meth = getattr(self, "recordPendingPublication" + resource.classname)
1916 try:
1917 data = meth(resource)
1918 except Timeout as exception:
1919 return HttpResponse(exception, status=408)
1920 except Exception as exception:
1921 return HttpResponseServerError(exception)
1922 return JsonResponse(data)
1924 def recordPendingPublicationArticle(self, article):
1925 result = {"status": 404}
1926 if article.doi and not article.date_published and not article.date_online_first:
1927 if article.my_container.year is None: # or article.my_container.year == '0':
1928 article.my_container.year = datetime.now().strftime("%Y")
1929 result = recordPendingPublication(article)
1930 return result
1933class RegisterPubmedFormView(FormView):
1934 template_name = "record_pubmed_dialog.html"
1935 form_class = RegisterPubmedForm
1937 def get_context_data(self, **kwargs):
1938 context = super().get_context_data(**kwargs)
1939 context["pid"] = self.kwargs["pid"]
1940 context["helper"] = PtfLargeModalFormHelper
1941 return context
1944class RegisterPubmedView(View):
1945 def get(self, request, *args, **kwargs):
1946 pid = kwargs.get("pid", None)
1947 update_article = self.request.GET.get("update_article", "on") == "on"
1949 article = model_helpers.get_article(pid)
1950 if article is None:
1951 raise Http404
1952 try:
1953 recordPubmed(article, update_article)
1954 except Exception as exception:
1955 messages.error("Unable to register the article in PubMed")
1956 return HttpResponseServerError(exception)
1958 return HttpResponseRedirect(
1959 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1960 )
1963class PTFToolsContainerView(TemplateView):
1964 template_name = ""
1966 def get_context_data(self, **kwargs):
1967 context = super().get_context_data(**kwargs)
1969 container = model_helpers.get_container(self.kwargs.get("pid"))
1970 if container is None:
1971 raise Http404
1972 citing_articles = container.citations()
1973 source = self.request.GET.get("source", None)
1974 if container.ctype.startswith("book"):
1975 book_parts = (
1976 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1977 )
1978 references = False
1979 if container.ctype == "book-monograph":
1980 # on regarde si il y a au moins une bibliographie
1981 for art in container.article_set.all():
1982 if art.bibitem_set.count() > 0:
1983 references = True
1984 context.update(
1985 {
1986 "book": container,
1987 "book_parts": list(book_parts),
1988 "source": source,
1989 "citing_articles": citing_articles,
1990 "references": references,
1991 "test_website": container.get_top_collection()
1992 .extlink_set.get(rel="test_website")
1993 .location,
1994 "prod_website": container.get_top_collection()
1995 .extlink_set.get(rel="website")
1996 .location,
1997 }
1998 )
1999 self.template_name = "book-toc.html"
2000 else:
2001 articles = container.article_set.all().order_by("seq")
2002 for article in articles:
2003 try:
2004 last_match = (
2005 history_models.HistoryEvent.objects.filter(
2006 pid=article.pid,
2007 type="matching",
2008 )
2009 .only("created_on")
2010 .latest("created_on")
2011 )
2012 except history_models.HistoryEvent.DoesNotExist as _:
2013 article.last_match = None
2014 else:
2015 article.last_match = last_match.created_on
2017 # article1 = articles.first()
2018 # date = article1.deployed_date()
2019 # TODO next_issue, previous_issue
2021 # check DOI est maintenant une commande à part
2022 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
2023 # articlesWithStatus = []
2024 # for article in articles:
2025 # checkDOIExistence(article)
2026 # articlesWithStatus.append(article)
2028 test_location = prod_location = ""
2029 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
2030 if qs:
2031 test_location = qs.first().location
2032 qs = container.get_top_collection().extlink_set.filter(rel="website")
2033 if qs:
2034 prod_location = qs.first().location
2035 context.update(
2036 {
2037 "issue": container,
2038 "articles": articles,
2039 "source": source,
2040 "citing_articles": citing_articles,
2041 "test_website": test_location,
2042 "prod_website": prod_location,
2043 }
2044 )
2046 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values():
2047 context["is_issue_pending_publication"] = True
2048 self.template_name = "issue-items.html"
2050 context["allow_crossref"] = container.allow_crossref()
2051 context["coltype"] = container.my_collection.coltype
2052 return context
2055class ExtLinkInline(InlineFormSetFactory):
2056 model = ExtLink
2057 form_class = ExtLinkForm
2058 factory_kwargs = {"extra": 0}
2061class ResourceIdInline(InlineFormSetFactory):
2062 model = ResourceId
2063 form_class = ResourceIdForm
2064 factory_kwargs = {"extra": 0}
2067class IssueDetailAPIView(View):
2068 def get(self, request, *args, **kwargs):
2069 issue = get_object_or_404(Container, pid=kwargs["pid"])
2070 deployed_date = issue.deployed_date()
2071 result = {
2072 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
2073 if deployed_date
2074 else None,
2075 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
2076 "all_doi_are_registered": issue.all_doi_are_registered(),
2077 "registered_in_doaj": issue.registered_in_doaj(),
2078 "doi": issue.my_collection.doi,
2079 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
2080 }
2081 try:
2082 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
2083 except history_models.HistoryEvent.DoesNotExist as _:
2084 pass
2085 else:
2086 result["latest"] = latest.message
2087 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
2088 "%Y-%m-%d %H:%M"
2089 )
2091 result["latest_type"] = latest.type.capitalize()
2092 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
2093 try:
2094 result[event_type] = timezone.localtime(
2095 history_models.HistoryEvent.objects.filter(
2096 type=event_type,
2097 status="OK",
2098 pid__startswith=issue.pid,
2099 )
2100 .latest("created_on")
2101 .created_on
2102 ).strftime("%Y-%m-%d %H:%M")
2103 except history_models.HistoryEvent.DoesNotExist as _:
2104 result[event_type] = ""
2105 return JsonResponse(result)
2108class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
2109 model = Collection
2110 form_class = CollectionForm
2111 inlines = [ResourceIdInline, ExtLinkInline]
2112 inlines_names = ["resource_ids_form", "ext_links_form"]
2114 def get_context_data(self, **kwargs):
2115 context = super().get_context_data(**kwargs)
2116 context["helper"] = PtfFormHelper
2117 context["formset_helper"] = FormSetHelper
2118 return context
2120 def add_description(self, collection, description, lang, seq):
2121 if description:
2122 la = Abstract(
2123 resource=collection,
2124 tag="description",
2125 lang=lang,
2126 seq=seq,
2127 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
2128 value_html=description,
2129 value_tex=description,
2130 )
2131 la.save()
2133 def form_valid(self, form):
2134 if form.instance.abbrev:
2135 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
2136 else:
2137 form.instance.title_xml = (
2138 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
2139 )
2141 form.instance.title_html = form.instance.title_tex
2142 form.instance.title_sort = form.instance.title_tex
2143 result = super().form_valid(form)
2145 collection = self.object
2146 collection.abstract_set.all().delete()
2148 seq = 1
2149 description = form.cleaned_data["description_en"]
2150 if description:
2151 self.add_description(collection, description, "en", seq)
2152 seq += 1
2153 description = form.cleaned_data["description_fr"]
2154 if description:
2155 self.add_description(collection, description, "fr", seq)
2157 return result
2159 def get_success_url(self):
2160 messages.success(self.request, "La Collection a été modifiée avec succès")
2161 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2164class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2165 """
2166 Warning : Not yet finished
2167 Automatic site membership creation is still missing
2168 """
2171class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2172 slug_field = "pid"
2173 slug_url_kwarg = "pid"
2176def suggest_load_journal_dois(colid):
2177 articles = (
2178 Article.objects.filter(my_container__my_collection__pid=colid)
2179 .filter(doi__isnull=False)
2180 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2181 .values_list("doi", flat=True)
2182 )
2184 try:
2185 articles = sorted(
2186 articles,
2187 key=lambda d: (
2188 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2189 int(re.search(r".(\d+)$", d).group(1)),
2190 ),
2191 )
2192 except: # noqa: E722 (we'll look later)
2193 pass
2194 return [f'<option value="{doi}">' for doi in articles]
2197def get_context_with_volumes(journal):
2198 result = model_helpers.get_volumes_in_collection(journal)
2199 volume_count = result["volume_count"]
2200 collections = []
2201 for ancestor in journal.ancestors.all():
2202 item = model_helpers.get_volumes_in_collection(ancestor)
2203 volume_count = max(0, volume_count)
2204 item.update({"journal": ancestor})
2205 collections.append(item)
2207 # add the parent collection to its children list and sort it by date
2208 result.update({"journal": journal})
2209 collections.append(result)
2211 collections = [c for c in collections if c["sorted_issues"]]
2212 collections.sort(
2213 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2214 reverse=True,
2215 )
2217 context = {
2218 "journal": journal,
2219 "sorted_issues": result["sorted_issues"],
2220 "volume_count": volume_count,
2221 "max_width": result["max_width"],
2222 "collections": collections,
2223 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2224 }
2225 return context
2228class CollectionDetail(
2229 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2230):
2231 model = Collection
2232 slug_field = "pid"
2233 slug_url_kwarg = "pid"
2234 template_name = "ptf/collection_detail.html"
2236 def test_func(self):
2237 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2239 def get(self, request, *args, **kwargs):
2240 self.object = self.get_object(queryset=Collection.objects.all())
2241 return super().get(request, *args, **kwargs)
2243 def get_context_data(self, **kwargs):
2244 context = super().get_context_data(**kwargs)
2245 context["object_list"] = context["object_list"].filter(
2246 Q(ctype="issue") | Q(ctype="book-lecture-notes")
2247 )
2248 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2249 context.update(get_context_with_volumes(self.object))
2251 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2252 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2253 context["issue_to_appear"] = Container.objects.filter(
2254 pid=context["issue_to_appear_pid"]
2255 ).exists()
2256 try:
2257 latest_error = history_models.HistoryEvent.objects.filter(
2258 status="ERROR", col=self.object
2259 ).latest("created_on")
2260 except history_models.HistoryEvent.DoesNotExist as _:
2261 pass
2262 else:
2263 message = latest_error.message
2264 if message:
2265 i = message.find(" - ")
2266 latest_exception = message[:i]
2267 latest_error_message = message[i + 3 :]
2268 context["latest_exception"] = latest_exception
2269 context["latest_exception_date"] = latest_error.created_on
2270 context["latest_exception_type"] = latest_error.type
2271 context["latest_error_message"] = latest_error_message
2273 archive_in_error = history_models.HistoryEvent.objects.filter(
2274 status="ERROR", col=self.object, type="archive"
2275 ).exists()
2277 context["archive_in_error"] = archive_in_error
2279 return context
2281 def get_queryset(self):
2282 query = self.object.content.all()
2284 for ancestor in self.object.ancestors.all():
2285 query |= ancestor.content.all()
2287 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2290class ContainerEditView(FormView):
2291 template_name = "container_form.html"
2292 form_class = ContainerForm
2294 def get_success_url(self):
2295 if self.kwargs["pid"]:
2296 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2297 return reverse("mersenne_dashboard/published_articles")
2299 def set_success_message(self): # pylint: disable=no-self-use
2300 messages.success(self.request, "Le fascicule a été modifié")
2302 def get_form_kwargs(self):
2303 kwargs = super().get_form_kwargs()
2304 if "pid" not in self.kwargs:
2305 self.kwargs["pid"] = None
2306 if "colid" not in self.kwargs:
2307 self.kwargs["colid"] = None
2308 if "data" in kwargs and "colid" in kwargs["data"]:
2309 # colid is passed as a hidden param in the form.
2310 # It is used when you submit a new container
2311 self.kwargs["colid"] = kwargs["data"]["colid"]
2313 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2314 self.kwargs["pid"]
2315 )
2316 return kwargs
2318 def get_context_data(self, **kwargs):
2319 context = super().get_context_data(**kwargs)
2321 context["pid"] = self.kwargs["pid"]
2322 context["colid"] = self.kwargs["colid"]
2323 context["container"] = self.kwargs["container"]
2325 context["edit_container"] = context["pid"] is not None
2326 context["name"] = resolve(self.request.path_info).url_name
2328 return context
2330 def form_valid(self, form):
2331 new_pid = form.cleaned_data.get("pid")
2332 new_title = form.cleaned_data.get("title")
2333 new_trans_title = form.cleaned_data.get("trans_title")
2334 new_publisher = form.cleaned_data.get("publisher")
2335 new_year = form.cleaned_data.get("year")
2336 new_volume = form.cleaned_data.get("volume")
2337 new_number = form.cleaned_data.get("number")
2339 collection = None
2340 issue = self.kwargs["container"]
2341 if issue is not None:
2342 collection = issue.my_collection
2343 elif self.kwargs["colid"] is not None:
2344 if "CR" in self.kwargs["colid"]:
2345 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2346 else:
2347 collection = model_helpers.get_collection(self.kwargs["colid"])
2349 if collection is None:
2350 raise ValueError("Collection for " + new_pid + " does not exist")
2352 # Icon
2353 new_icon_location = ""
2354 if "icon" in self.request.FILES:
2355 filename = os.path.basename(self.request.FILES["icon"].name)
2356 file_extension = filename.split(".")[1]
2358 icon_filename = resolver.get_disk_location(
2359 settings.MERSENNE_TEST_DATA_FOLDER,
2360 collection.pid,
2361 file_extension,
2362 new_pid,
2363 None,
2364 True,
2365 )
2367 with open(icon_filename, "wb+") as destination:
2368 for chunk in self.request.FILES["icon"].chunks():
2369 destination.write(chunk)
2371 folder = resolver.get_relative_folder(collection.pid, new_pid)
2372 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2373 name = resolve(self.request.path_info).url_name
2374 if name == "special_issue_create":
2375 self.kwargs["name"] = name
2376 if self.kwargs["container"]:
2377 # Edit Issue
2378 issue = self.kwargs["container"]
2379 if issue is None:
2380 raise ValueError(self.kwargs["pid"] + " does not exist")
2382 issue.pid = new_pid
2383 issue.title_tex = issue.title_html = new_title
2384 issue.title_xml = build_title_xml(
2385 title=new_title,
2386 lang=issue.lang,
2387 title_type="issue-title",
2388 )
2390 trans_lang = ""
2391 if issue.trans_lang != "" and issue.trans_lang != "und":
2392 trans_lang = issue.trans_lang
2393 elif new_trans_title != "":
2394 trans_lang = "fr" if issue.lang == "en" else "en"
2395 issue.trans_lang = trans_lang
2397 if trans_lang != "" and new_trans_title != "":
2398 issue.trans_title_html = ""
2399 issue.trans_title_tex = ""
2400 title_xml = build_title_xml(
2401 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2402 )
2403 try:
2404 trans_title_object = Title.objects.get(resource=issue, lang=trans_lang)
2405 trans_title_object.title_html = new_trans_title
2406 trans_title_object.title_xml = title_xml
2407 trans_title_object.save()
2408 except Title.DoesNotExist:
2409 trans_title = Title(
2410 resource=issue,
2411 lang=trans_lang,
2412 type="main",
2413 title_html=new_trans_title,
2414 title_xml=title_xml,
2415 )
2416 trans_title.save()
2417 issue.year = new_year
2418 issue.volume = new_volume
2419 issue.volume_int = make_int(new_volume)
2420 issue.number = new_number
2421 issue.number_int = make_int(new_number)
2422 issue.save()
2423 else:
2424 xissue = create_issuedata()
2426 xissue.ctype = "issue"
2427 xissue.pid = new_pid
2428 xissue.lang = "en"
2429 xissue.title_tex = new_title
2430 xissue.title_html = new_title
2431 xissue.title_xml = build_title_xml(
2432 title=new_title, lang=xissue.lang, title_type="issue-title"
2433 )
2435 if new_trans_title != "":
2436 trans_lang = "fr"
2437 title_xml = build_title_xml(
2438 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2439 )
2440 title = create_titledata(
2441 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2442 )
2443 issue.titles = [title]
2445 xissue.year = new_year
2446 xissue.volume = new_volume
2447 xissue.number = new_number
2448 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2450 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2451 cmd.add_collection(collection)
2452 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2453 issue = cmd.do()
2455 self.kwargs["pid"] = new_pid
2457 # Add objects related to the article: contribs, datastream, counts...
2458 params = {
2459 "icon_location": new_icon_location,
2460 }
2461 cmd = ptf_cmds.updateContainerPtfCmd(params)
2462 cmd.set_resource(issue)
2463 cmd.do()
2465 publisher = model_helpers.get_publisher(new_publisher)
2466 if not publisher:
2467 xpub = create_publisherdata()
2468 xpub.name = new_publisher
2469 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2470 issue.my_publisher = publisher
2471 issue.save()
2473 self.set_success_message()
2475 return super().form_valid(form)
2478# class ArticleEditView(FormView):
2479# template_name = 'article_form.html'
2480# form_class = ArticleForm
2481#
2482# def get_success_url(self):
2483# if self.kwargs['pid']:
2484# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2485# return reverse('mersenne_dashboard/published_articles')
2486#
2487# def set_success_message(self): # pylint: disable=no-self-use
2488# messages.success(self.request, "L'article a été modifié")
2489#
2490# def get_form_kwargs(self):
2491# kwargs = super(ArticleEditView, self).get_form_kwargs()
2492#
2493# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2494# # Article creation: pid is None
2495# self.kwargs['pid'] = None
2496# if 'issue_id' not in self.kwargs:
2497# # Article edit: issue_id is not passed
2498# self.kwargs['issue_id'] = None
2499# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2500# # colid is passed as a hidden param in the form.
2501# # It is used when you submit a new container
2502# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2503#
2504# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2505# return kwargs
2506#
2507# def get_context_data(self, **kwargs):
2508# context = super(ArticleEditView, self).get_context_data(**kwargs)
2509#
2510# context['pid'] = self.kwargs['pid']
2511# context['issue_id'] = self.kwargs['issue_id']
2512# context['article'] = self.kwargs['article']
2513#
2514# context['edit_article'] = context['pid'] is not None
2515#
2516# article = context['article']
2517# if article:
2518# context['author_contributions'] = article.get_author_contributions()
2519# context['kwds_fr'] = None
2520# context['kwds_en'] = None
2521# kwd_gps = article.get_non_msc_kwds()
2522# for kwd_gp in kwd_gps:
2523# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2524# if kwd_gp.value_xml:
2525# kwd_ = types.SimpleNamespace()
2526# kwd_.value = kwd_gp.value_tex
2527# context['kwd_unstructured_fr'] = kwd_
2528# context['kwds_fr'] = kwd_gp.kwd_set.all()
2529# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2530# if kwd_gp.value_xml:
2531# kwd_ = types.SimpleNamespace()
2532# kwd_.value = kwd_gp.value_tex
2533# context['kwd_unstructured_en'] = kwd_
2534# context['kwds_en'] = kwd_gp.kwd_set.all()
2535#
2536# # Article creation: init pid
2537# if context['issue_id'] and context['pid'] is None:
2538# issue = model_helpers.get_container(context['issue_id'])
2539# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2540#
2541# return context
2542#
2543# def form_valid(self, form):
2544#
2545# new_pid = form.cleaned_data.get('pid')
2546# new_title = form.cleaned_data.get('title')
2547# new_fpage = form.cleaned_data.get('fpage')
2548# new_lpage = form.cleaned_data.get('lpage')
2549# new_page_range = form.cleaned_data.get('page_range')
2550# new_page_count = form.cleaned_data.get('page_count')
2551# new_coi_statement = form.cleaned_data.get('coi_statement')
2552# new_show_body = form.cleaned_data.get('show_body')
2553# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2554#
2555# # TODO support MathML
2556# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2557# # We need to pass trans_title to get_title_xml
2558# # Meanwhile, ignore new_title_xml
2559# new_title_xml = jats_parser.get_title_xml(new_title)
2560# new_title_html = new_title
2561#
2562# authors_count = int(self.request.POST.get('authors_count', "0"))
2563# i = 1
2564# new_authors = []
2565# old_author_contributions = []
2566# if self.kwargs['article']:
2567# old_author_contributions = self.kwargs['article'].get_author_contributions()
2568#
2569# while authors_count > 0:
2570# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2571#
2572# if prefix is not None:
2573# addresses = []
2574# if len(old_author_contributions) >= i:
2575# old_author_contribution = old_author_contributions[i - 1]
2576# addresses = [contrib_address.address for contrib_address in
2577# old_author_contribution.get_addresses()]
2578#
2579# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2580# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2581# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2582# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2583# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2584# deceased_before_publication = deceased == 'on'
2585# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2586# equal_contrib = equal_contrib == 'on'
2587# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2588# corresponding = corresponding == 'on'
2589# email = self.request.POST.get('email-' + str(i), None)
2590#
2591# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2592# params['deceased_before_publication'] = deceased_before_publication
2593# params['equal_contrib'] = equal_contrib
2594# params['corresponding'] = corresponding
2595# params['addresses'] = addresses
2596# params['email'] = email
2597#
2598# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2599#
2600# new_authors.append(params)
2601#
2602# authors_count -= 1
2603# i += 1
2604#
2605# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2606# i = 1
2607# new_kwds_fr = []
2608# while kwds_fr_count > 0:
2609# value = self.request.POST.get('kwd-fr-' + str(i), None)
2610# new_kwds_fr.append(value)
2611# kwds_fr_count -= 1
2612# i += 1
2613# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2614#
2615# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2616# i = 1
2617# new_kwds_en = []
2618# while kwds_en_count > 0:
2619# value = self.request.POST.get('kwd-en-' + str(i), None)
2620# new_kwds_en.append(value)
2621# kwds_en_count -= 1
2622# i += 1
2623# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2624#
2625# if self.kwargs['article']:
2626# # Edit article
2627# container = self.kwargs['article'].my_container
2628# else:
2629# # New article
2630# container = model_helpers.get_container(self.kwargs['issue_id'])
2631#
2632# if container is None:
2633# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2634#
2635# collection = container.my_collection
2636#
2637# # Copy PDF file & extract full text
2638# body = ''
2639# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2640# collection.pid,
2641# "pdf",
2642# container.pid,
2643# new_pid,
2644# True)
2645# if 'pdf' in self.request.FILES:
2646# with open(pdf_filename, 'wb+') as destination:
2647# for chunk in self.request.FILES['pdf'].chunks():
2648# destination.write(chunk)
2649#
2650# # Extract full text from the PDF
2651# body = utils.pdf_to_text(pdf_filename)
2652#
2653# # Icon
2654# new_icon_location = ''
2655# if 'icon' in self.request.FILES:
2656# filename = os.path.basename(self.request.FILES['icon'].name)
2657# file_extension = filename.split('.')[1]
2658#
2659# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2660# collection.pid,
2661# file_extension,
2662# container.pid,
2663# new_pid,
2664# True)
2665#
2666# with open(icon_filename, 'wb+') as destination:
2667# for chunk in self.request.FILES['icon'].chunks():
2668# destination.write(chunk)
2669#
2670# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2671# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2672#
2673# if self.kwargs['article']:
2674# # Edit article
2675# article = self.kwargs['article']
2676# article.fpage = new_fpage
2677# article.lpage = new_lpage
2678# article.page_range = new_page_range
2679# article.coi_statement = new_coi_statement
2680# article.show_body = new_show_body
2681# article.do_not_publish = new_do_not_publish
2682# article.save()
2683#
2684# else:
2685# # New article
2686# params = {
2687# 'pid': new_pid,
2688# 'title_xml': new_title_xml,
2689# 'title_html': new_title_html,
2690# 'title_tex': new_title,
2691# 'fpage': new_fpage,
2692# 'lpage': new_lpage,
2693# 'page_range': new_page_range,
2694# 'seq': container.article_set.count() + 1,
2695# 'body': body,
2696# 'coi_statement': new_coi_statement,
2697# 'show_body': new_show_body,
2698# 'do_not_publish': new_do_not_publish
2699# }
2700#
2701# xarticle = create_articledata()
2702# xarticle.pid = new_pid
2703# xarticle.title_xml = new_title_xml
2704# xarticle.title_html = new_title_html
2705# xarticle.title_tex = new_title
2706# xarticle.fpage = new_fpage
2707# xarticle.lpage = new_lpage
2708# xarticle.page_range = new_page_range
2709# xarticle.seq = container.article_set.count() + 1
2710# xarticle.body = body
2711# xarticle.coi_statement = new_coi_statement
2712# params['xobj'] = xarticle
2713#
2714# cmd = ptf_cmds.addArticlePtfCmd(params)
2715# cmd.set_container(container)
2716# cmd.add_collection(container.my_collection)
2717# article = cmd.do()
2718#
2719# self.kwargs['pid'] = new_pid
2720#
2721# # Add objects related to the article: contribs, datastream, counts...
2722# params = {
2723# # 'title_xml': new_title_xml,
2724# # 'title_html': new_title_html,
2725# # 'title_tex': new_title,
2726# 'authors': new_authors,
2727# 'page_count': new_page_count,
2728# 'icon_location': new_icon_location,
2729# 'body': body,
2730# 'use_kwds': True,
2731# 'kwds_fr': new_kwds_fr,
2732# 'kwds_en': new_kwds_en,
2733# 'kwd_uns_fr': new_kwd_uns_fr,
2734# 'kwd_uns_en': new_kwd_uns_en
2735# }
2736# cmd = ptf_cmds.updateArticlePtfCmd(params)
2737# cmd.set_article(article)
2738# cmd.do()
2739#
2740# self.set_success_message()
2741#
2742# return super(ArticleEditView, self).form_valid(form)
2745@require_http_methods(["POST"])
2746def do_not_publish_article(request, *args, **kwargs):
2747 next = request.headers.get("referer")
2749 pid = kwargs.get("pid", "")
2751 article = model_helpers.get_article(pid)
2752 if article:
2753 article.do_not_publish = not article.do_not_publish
2754 article.save()
2755 else:
2756 raise Http404
2758 return HttpResponseRedirect(next)
2761@require_http_methods(["POST"])
2762def show_article_body(request, *args, **kwargs):
2763 next = request.headers.get("referer")
2765 pid = kwargs.get("pid", "")
2767 article = model_helpers.get_article(pid)
2768 if article:
2769 article.show_body = not article.show_body
2770 article.save()
2771 else:
2772 raise Http404
2774 return HttpResponseRedirect(next)
2777class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2778 """
2779 API to get/post article metadata
2780 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2781 """
2783 def __init__(self, *args, **kwargs):
2784 """
2785 we define here what fields we want in the form
2786 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2787 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2788 super().__init__(*args, **kwargs)
2789 self.fields_to_update = [
2790 "lang",
2791 "atype",
2792 "contributors",
2793 "abstracts",
2794 "kwds",
2795 "titles",
2796 "trans_title_html",
2797 "title_html",
2798 "title_xml",
2799 "streams",
2800 "ext_links",
2801 "date_accepted",
2802 "history_dates",
2803 ]
2804 # order between doi and pid is important as for pending article we need doi to create a temporary pid
2805 self.additional_fields = ["doi", "pid", "container_pid", "pdf", "illustration", "dates"]
2806 self.editorial_tools = [
2807 "translation",
2808 "sidebar",
2809 "lang_selection",
2810 "back_to_article_option",
2811 ]
2812 self.article_container_pid = ""
2813 self.back_url = "trammel"
2815 def save_data(self, data_article):
2816 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2817 # The icons are not preserved since we can add/edit/delete them in VueJs
2818 params = {
2819 "pid": data_article.pid,
2820 "export_folder": settings.MERSENNE_TMP_FOLDER,
2821 "export_all": True,
2822 "with_binary_files": False,
2823 }
2824 ptf_cmds.exportExtraDataPtfCmd(params).do()
2826 def restore_data(self, article):
2827 ptf_cmds.importExtraDataPtfCmd(
2828 {
2829 "pid": article.pid,
2830 "import_folder": settings.MERSENNE_TMP_FOLDER,
2831 }
2832 ).do()
2834 def get(self, request, *args, **kwargs):
2835 data = super().get(request, *args, **kwargs)
2836 return data
2838 def post(self, request, *args, **kwargs):
2839 response = super().post(request, *args, **kwargs)
2840 if response["message"] == "OK":
2841 return redirect(
2842 "api-edit-article",
2843 colid=kwargs.get("colid", ""),
2844 containerPid=kwargs.get("containerPid"),
2845 doi=response["data"].doi,
2846 )
2847 else:
2848 raise Http404
2851class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2852 template_name = "article_form.html"
2854 def get_success_url(self):
2855 if self.kwargs["doi"]:
2856 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2857 return reverse("mersenne_dashboard/published_articles")
2859 def get_context_data(self, **kwargs):
2860 context = super().get_context_data(**kwargs)
2861 if "doi" in self.kwargs:
2862 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"])
2863 context["pid"] = context["article"].pid
2865 context["container_pid"] = kwargs.get("container_pid", "")
2866 return context
2869class ArticleDeleteView(View):
2870 def get(self, request, *args, **kwargs):
2871 pid = self.kwargs.get("pid", None)
2872 article = get_object_or_404(Article, pid=pid)
2874 try:
2875 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2876 article.undeploy(mersenneSite)
2878 cmd = ptf_cmds.addArticlePtfCmd(
2879 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2880 )
2881 cmd.set_container(article.my_container)
2882 cmd.set_object_to_be_deleted(article)
2883 cmd.undo()
2884 except Exception as exception:
2885 return HttpResponseServerError(exception)
2887 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2888 return JsonResponse(data)
2891def get_messages_in_queue():
2892 app = Celery("ptf-tools")
2893 # tasks = list(current_app.tasks)
2894 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2895 print(tasks)
2896 # i = app.control.inspect()
2898 with app.connection_or_acquire() as conn:
2899 remaining = conn.default_channel.queue_declare(
2900 queue="coordinator", passive=True
2901 ).message_count
2902 return remaining
2905class NumdamView(TemplateView, history_views.HistoryContextMixin):
2906 template_name = "numdam.html"
2908 def get_context_data(self, **kwargs):
2909 context = super().get_context_data(**kwargs)
2911 context["objs"] = ResourceInNumdam.objects.all()
2913 pre_issues = []
2914 prod_issues = []
2915 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2916 try:
2917 response = requests.get(url)
2918 if response.status_code == 200:
2919 data = response.json()
2920 if "issues" in data:
2921 pre_issues = data["issues"]
2922 except Exception:
2923 pass
2925 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2926 response = requests.get(url)
2927 if response.status_code == 200:
2928 data = response.json()
2929 if "issues" in data:
2930 prod_issues = data["issues"]
2932 new = sorted(list(set(pre_issues).difference(prod_issues)))
2933 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2934 grouped = [
2935 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2936 ]
2937 grouped_removed = [
2938 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2939 ]
2940 context["added_issues"] = grouped
2941 context["removed_issues"] = grouped_removed
2943 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2944 return context
2947class NumdamArchiveView(RedirectView):
2948 @staticmethod
2949 def reset_task_results():
2950 TaskResult.objects.all().delete()
2952 def get_redirect_url(self, *args, **kwargs):
2953 self.colid = kwargs["colid"]
2955 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2956 return Http404
2958 # we make sure archiving is not already running
2959 # if not get_messages_in_queue():
2960 # self.reset_task_results()
2962 if self.colid == "ALL":
2963 archive_numdam_collections.delay()
2964 else:
2965 archive_numdam_collection.s(self.colid).delay()
2967 return reverse("numdam")
2970class DeployAllNumdamAPIView(View):
2971 def internal_do(self, *args, **kwargs):
2972 pids = []
2974 for obj in ResourceInNumdam.objects.all():
2975 pids.append(obj.pid)
2977 return pids
2979 def get(self, request, *args, **kwargs):
2980 try:
2981 pids, status, message = history_views.execute_and_record_func(
2982 "deploy", "numdam", "ALL", self.internal_do, "numdam"
2983 )
2984 except Exception as exception:
2985 return HttpResponseServerError(exception)
2987 data = {"message": message, "ids": pids, "status": status}
2988 return JsonResponse(data)
2991class NumdamDeleteAPIView(View):
2992 def get(self, request, *args, **kwargs):
2993 pid = self.kwargs.get("pid", None)
2995 try:
2996 obj = ResourceInNumdam.objects.get(pid=pid)
2997 obj.delete()
2998 except Exception as exception:
2999 return HttpResponseServerError(exception)
3001 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
3002 return JsonResponse(data)
3005class ExtIdApiDetail(View):
3006 def get(self, request, *args, **kwargs):
3007 extid = get_object_or_404(
3008 ExtId,
3009 resource__pid=kwargs["pid"],
3010 id_type=kwargs["what"],
3011 )
3012 return JsonResponse(
3013 {
3014 "pk": extid.pk,
3015 "href": extid.get_href(),
3016 "fetch": reverse(
3017 "api-fetch-id",
3018 args=(
3019 extid.resource.pk,
3020 extid.id_value,
3021 extid.id_type,
3022 "extid",
3023 ),
3024 ),
3025 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
3026 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
3027 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
3028 "delete": reverse("update-extid", args=(extid.pk, "delete")),
3029 "is_valid": extid.checked,
3030 }
3031 )
3034class ExtIdFormTemplate(TemplateView):
3035 template_name = "common/externalid_form.html"
3037 def get_context_data(self, **kwargs):
3038 context = super().get_context_data(**kwargs)
3039 context["sequence"] = kwargs["sequence"]
3040 return context
3043class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3044 def get_context_data(self, **kwargs):
3045 context = super().get_context_data(**kwargs)
3046 context["helper"] = PtfFormHelper
3047 return context
3049 def get_success_url(self):
3050 self.post_process()
3051 return self.object.bibitem.resource.get_absolute_url()
3053 def post_process(self):
3054 cmd = updateBibitemCitationXmlCmd()
3055 cmd.set_bibitem(self.object.bibitem)
3056 cmd.do()
3057 model_helpers.post_resource_updated(self.object.bibitem.resource)
3060class BibItemIdCreate(BibItemIdFormView, CreateView):
3061 model = BibItemId
3062 form_class = BibItemIdForm
3064 def get_context_data(self, **kwargs):
3065 context = super().get_context_data(**kwargs)
3066 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3067 return context
3069 def get_initial(self):
3070 initial = super().get_initial()
3071 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
3072 return initial
3074 def form_valid(self, form):
3075 form.instance.checked = False
3076 return super().form_valid(form)
3079class BibItemIdUpdate(BibItemIdFormView, UpdateView):
3080 model = BibItemId
3081 form_class = BibItemIdForm
3083 def get_context_data(self, **kwargs):
3084 context = super().get_context_data(**kwargs)
3085 context["bibitem"] = self.object.bibitem
3086 return context
3089class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3090 def get_context_data(self, **kwargs):
3091 context = super().get_context_data(**kwargs)
3092 context["helper"] = PtfFormHelper
3093 return context
3095 def get_success_url(self):
3096 self.post_process()
3097 return self.object.resource.get_absolute_url()
3099 def post_process(self):
3100 model_helpers.post_resource_updated(self.object.resource)
3103class ExtIdCreate(ExtIdFormView, CreateView):
3104 model = ExtId
3105 form_class = ExtIdForm
3107 def get_context_data(self, **kwargs):
3108 context = super().get_context_data(**kwargs)
3109 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3110 return context
3112 def get_initial(self):
3113 initial = super().get_initial()
3114 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3115 return initial
3117 def form_valid(self, form):
3118 form.instance.checked = False
3119 return super().form_valid(form)
3122class ExtIdUpdate(ExtIdFormView, UpdateView):
3123 model = ExtId
3124 form_class = ExtIdForm
3126 def get_context_data(self, **kwargs):
3127 context = super().get_context_data(**kwargs)
3128 context["resource"] = self.object.resource
3129 return context
3132class BibItemIdApiDetail(View):
3133 def get(self, request, *args, **kwargs):
3134 bibitemid = get_object_or_404(
3135 BibItemId,
3136 bibitem__resource__pid=kwargs["pid"],
3137 bibitem__sequence=kwargs["seq"],
3138 id_type=kwargs["what"],
3139 )
3140 return JsonResponse(
3141 {
3142 "pk": bibitemid.pk,
3143 "href": bibitemid.get_href(),
3144 "fetch": reverse(
3145 "api-fetch-id",
3146 args=(
3147 bibitemid.bibitem.pk,
3148 bibitemid.id_value,
3149 bibitemid.id_type,
3150 "bibitemid",
3151 ),
3152 ),
3153 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
3154 "uncheck": reverse(
3155 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
3156 ),
3157 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
3158 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
3159 "is_valid": bibitemid.checked,
3160 }
3161 )
3164class UpdateTexmfZipAPIView(View):
3165 def get(self, request, *args, **kwargs):
3166 def copy_zip_files(src_folder, dest_folder):
3167 os.makedirs(dest_folder, exist_ok=True)
3169 zip_files = [
3170 os.path.join(src_folder, f)
3171 for f in os.listdir(src_folder)
3172 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3173 ]
3174 for zip_file in zip_files:
3175 resolver.copy_file(zip_file, dest_folder)
3177 # Exceptions: specific zip/gz files
3178 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3179 resolver.copy_file(zip_file, dest_folder)
3181 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3182 resolver.copy_file(zip_file, dest_folder)
3184 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3185 resolver.copy_file(gz_file, dest_folder)
3187 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3189 dest_folder = os.path.join(
3190 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3191 )
3193 try:
3194 copy_zip_files(src_folder, dest_folder)
3195 except Exception as exception:
3196 return HttpResponseServerError(exception)
3198 try:
3199 dest_folder = os.path.join(
3200 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3201 )
3202 copy_zip_files(src_folder, dest_folder)
3203 except Exception as exception:
3204 return HttpResponseServerError(exception)
3206 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3207 return JsonResponse(data)
3210class TestView(TemplateView):
3211 template_name = "mersenne.html"
3213 def get_context_data(self, **kwargs):
3214 super().get_context_data(**kwargs)
3215 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True)
3216 model_data_converter.db_to_issue_data(issue)
3219class TrammelTasksProgressView(View):
3220 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3221 """
3222 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3223 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3224 """
3225 task_name = task
3227 def get_event_data():
3228 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3230 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3231 remaining_messages = get_messages_in_queue()
3233 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3234 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3235 failed_tasks = all_tasks.filter(status="FAILURE")
3237 all_tasks_count = all_tasks.count()
3238 success_count = successed_tasks.count()
3239 fail_count = failed_tasks.count()
3241 all_count = all_tasks_count + remaining_messages
3242 remaining_count = all_count - success_count - fail_count
3244 success_rate = int(success_count * 100 / all_count) if all_count else 0
3245 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3246 status = "consuming_queue" if remaining_count != 0 else "polling"
3248 last_task = successed_tasks.first()
3249 last_task = (
3250 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3251 if last_task
3252 else ""
3253 )
3255 # SSE event format
3256 event_data = {
3257 "status": status,
3258 "success_rate": success_rate,
3259 "error_rate": error_rate,
3260 "all_count": all_count,
3261 "remaining_count": remaining_count,
3262 "success_count": success_count,
3263 "fail_count": fail_count,
3264 "last_task": last_task,
3265 }
3267 return event_data
3269 def stream_response(data):
3270 # Send initial response headers
3271 yield f"data: {json.dumps(data)}\n\n"
3273 data = get_event_data()
3274 format = request.GET.get("format", "stream")
3275 if format == "json":
3276 response = JsonResponse(data)
3277 else:
3278 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3279 return response
3282user_signed_up.connect(update_user_from_invite)