Coverage for src/ptf_tools/views/base_views.py: 17%
1616 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-07-01 07:12 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-07-01 07:12 +0000
1import io
2import json
3import os
4import re
5from datetime import datetime
6from itertools import groupby
8import jsonpickle
9import requests
10from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin
11from celery import Celery, current_app
12from django.conf import settings
13from django.contrib import messages
14from django.contrib.auth.mixins import UserPassesTestMixin
15from django.db.models import Q
16from django.http import (
17 Http404,
18 HttpRequest,
19 HttpResponse,
20 HttpResponseRedirect,
21 HttpResponseServerError,
22 JsonResponse,
23)
24from django.shortcuts import get_object_or_404, redirect, render
25from django.urls import resolve, reverse, reverse_lazy
26from django.utils import timezone
27from django.views.decorators.http import require_http_methods
28from django.views.generic import ListView, TemplateView, View
29from django.views.generic.base import RedirectView
30from django.views.generic.detail import SingleObjectMixin
31from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
32from django_celery_results.models import TaskResult
33from extra_views import (
34 CreateWithInlinesView,
35 InlineFormSetFactory,
36 NamedFormsetsMixin,
37 UpdateWithInlinesView,
38)
39from ptf import model_data_converter, model_helpers, tex, utils
40from ptf.cmds import ptf_cmds, xml_cmds
41from ptf.cmds.base_cmds import make_int
42from ptf.cmds.xml.jats.builder.issue import build_title_xml
43from ptf.cmds.xml.xml_utils import replace_html_entities
44from ptf.display import resolver
45from ptf.exceptions import DOIException, PDFException, ServerUnderMaintenance
46from ptf.model_data import create_issuedata, create_publisherdata, create_titledata
47from ptf.models import (
48 Abstract,
49 Article,
50 BibItem,
51 BibItemId,
52 Collection,
53 Container,
54 ExtId,
55 ExtLink,
56 Resource,
57 ResourceId,
58 Title,
59)
60from ptf.views import ArticleEditFormWithVueAPIView
61from pubmed.views import recordPubmed
62from requests import Timeout
64from comments_moderation.utils import get_comments_for_home, is_comment_moderator
65from history import models as history_models
66from history import views as history_views
67from ptf_tools.doaj import doaj_pid_register
68from ptf_tools.doi import get_or_create_doibatch, recordDOI
69from ptf_tools.forms import (
70 BibItemIdForm,
71 CollectionForm,
72 ContainerForm,
73 DiffContainerForm,
74 ExtIdForm,
75 ExtLinkForm,
76 FormSetHelper,
77 ImportArticleForm,
78 ImportContainerForm,
79 PtfFormHelper,
80 PtfLargeModalFormHelper,
81 PtfModalFormHelper,
82 RegisterPubmedForm,
83 ResourceIdForm,
84 get_article_choices,
85)
86from ptf_tools.indexingChecker import ReferencingChecker
87from ptf_tools.models import ResourceInNumdam
88from ptf_tools.tasks import (
89 archive_numdam_collection,
90 archive_numdam_issue,
91 archive_trammel_collection,
92 archive_trammel_resource,
93)
94from ptf_tools.templatetags.tools_helpers import get_authorized_collections
95from ptf_tools.utils import is_authorized_editor
98def view_404(request: HttpRequest):
99 """
100 Dummy view raising HTTP 404 exception.
101 """
102 raise Http404
105def check_collection(collection, server_url, server_type):
106 """
107 Check if a collection exists on a serveur (test/prod)
108 and upload the collection (XML, image) if necessary
109 """
111 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
112 response = requests.get(url, verify=False)
113 # First, upload the collection XML
114 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
115 body = xml.encode("utf8")
117 url = server_url + reverse("upload-serials")
118 if response.status_code == 200:
119 # PUT http verb is used for update
120 response = requests.put(url, data=body, verify=False)
121 else:
122 # POST http verb is used for creation
123 response = requests.post(url, data=body, verify=False)
125 # Second, copy the collection images
126 # There is no need to copy files for the test server
127 # Files were already copied in /mersenne_test_data during the ptf_tools import
128 # We only need to copy files from /mersenne_test_data to
129 # /mersenne_prod_data during an upload to prod
130 if server_type == "website":
131 resolver.copy_binary_files(
132 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
133 )
134 elif server_type == "numdam":
135 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
136 if collection.pid in settings.NUMDAM_COLLECTIONS:
137 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
139 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
142def check_lock():
143 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
146def load_cedrics_article_choices(request):
147 colid = request.GET.get("colid")
148 issue = request.GET.get("issue")
149 article_choices = get_article_choices(colid, issue)
150 return render(
151 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
152 )
155class ImportCedricsArticleFormView(FormView):
156 template_name = "import_article.html"
157 form_class = ImportArticleForm
159 def dispatch(self, request, *args, **kwargs):
160 self.colid = self.kwargs["colid"]
161 return super().dispatch(request, *args, **kwargs)
163 def get_success_url(self):
164 if self.colid:
165 return reverse("collection-detail", kwargs={"pid": self.colid})
166 return "/"
168 def get_context_data(self, **kwargs):
169 context = super().get_context_data(**kwargs)
170 context["colid"] = self.colid
171 context["helper"] = PtfModalFormHelper
172 return context
174 def get_form_kwargs(self):
175 kwargs = super().get_form_kwargs()
176 kwargs["colid"] = self.colid
177 return kwargs
179 def form_valid(self, form):
180 self.issue = form.cleaned_data["issue"]
181 self.article = form.cleaned_data["article"]
182 return super().form_valid(form)
184 def import_cedrics_article(self, *args, **kwargs):
185 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
186 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
187 )
188 cmd.do()
190 def post(self, request, *args, **kwargs):
191 self.colid = self.kwargs.get("colid", None)
192 issue = request.POST["issue"]
193 self.article_pid = request.POST["article"]
194 self.issue_pid = os.path.basename(os.path.dirname(issue))
196 import_args = [self]
197 import_kwargs = {}
199 try:
200 _, status, message = history_views.execute_and_record_func(
201 "import",
202 f"{self.issue_pid} / {self.article_pid}",
203 self.colid,
204 self.import_cedrics_article,
205 "",
206 False,
207 *import_args,
208 **import_kwargs,
209 )
211 messages.success(
212 self.request, f"L'article {self.article_pid} a été importé avec succès"
213 )
215 except Exception as exception:
216 messages.error(
217 self.request,
218 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}",
219 )
221 return redirect(self.get_success_url())
224class ImportCedricsIssueView(FormView):
225 template_name = "import_container.html"
226 form_class = ImportContainerForm
228 def dispatch(self, request, *args, **kwargs):
229 self.colid = self.kwargs["colid"]
230 self.to_appear = self.request.GET.get("to_appear", False)
231 return super().dispatch(request, *args, **kwargs)
233 def get_success_url(self):
234 if self.filename:
235 return reverse(
236 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
237 )
238 return "/"
240 def get_context_data(self, **kwargs):
241 context = super().get_context_data(**kwargs)
242 context["colid"] = self.colid
243 context["helper"] = PtfModalFormHelper
244 return context
246 def get_form_kwargs(self):
247 kwargs = super().get_form_kwargs()
248 kwargs["colid"] = self.colid
249 kwargs["to_appear"] = self.to_appear
250 return kwargs
252 def form_valid(self, form):
253 self.filename = form.cleaned_data["filename"].split("/")[-1]
254 return super().form_valid(form)
257class DiffCedricsIssueView(FormView):
258 template_name = "diff_container_form.html"
259 form_class = DiffContainerForm
260 diffs = None
261 xissue = None
262 xissue_encoded = None
264 def get_success_url(self):
265 return reverse("collection-detail", kwargs={"pid": self.colid})
267 def dispatch(self, request, *args, **kwargs):
268 self.colid = self.kwargs["colid"]
269 # self.filename = self.kwargs['filename']
270 return super().dispatch(request, *args, **kwargs)
272 def get(self, request, *args, **kwargs):
273 self.filename = request.GET["filename"]
274 self.remove_mail = request.GET["remove_email"]
275 self.remove_date_prod = request.GET["remove_date_prod"]
277 try:
278 result, status, message = history_views.execute_and_record_func(
279 "import",
280 os.path.basename(self.filename),
281 self.colid,
282 self.diff_cedrics_issue,
283 "",
284 True,
285 )
286 except Exception as exception:
287 pid = self.filename.split("/")[-1]
288 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
289 return HttpResponseRedirect(self.get_success_url())
291 no_conflict = result[0]
292 self.diffs = result[1]
293 self.xissue = result[2]
295 if no_conflict:
296 # Proceed with the import
297 self.form_valid(self.get_form())
298 return redirect(self.get_success_url())
299 else:
300 # Display the diff template
301 self.xissue_encoded = jsonpickle.encode(self.xissue)
303 return super().get(request, *args, **kwargs)
305 def post(self, request, *args, **kwargs):
306 self.filename = request.POST["filename"]
307 data = request.POST["xissue_encoded"]
308 self.xissue = jsonpickle.decode(data)
310 return super().post(request, *args, **kwargs)
312 def get_context_data(self, **kwargs):
313 context = super().get_context_data(**kwargs)
314 context["colid"] = self.colid
315 context["diff"] = self.diffs
316 context["filename"] = self.filename
317 context["xissue_encoded"] = self.xissue_encoded
318 return context
320 def get_form_kwargs(self):
321 kwargs = super().get_form_kwargs()
322 kwargs["colid"] = self.colid
323 return kwargs
325 def diff_cedrics_issue(self, *args, **kwargs):
326 params = {
327 "colid": self.colid,
328 "input_file": self.filename,
329 "remove_email": self.remove_mail,
330 "remove_date_prod": self.remove_date_prod,
331 "diff_only": True,
332 }
334 if settings.IMPORT_CEDRICS_DIRECTLY:
335 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
336 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
337 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
338 else:
339 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
341 result = cmd.do()
342 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
343 messages.warning(
344 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings
345 )
347 return result
349 def import_cedrics_issue(self, *args, **kwargs):
350 # modify xissue with data_issue if params to override
351 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
352 issue = model_helpers.get_container(self.xissue.pid)
353 if issue:
354 data_issue = model_data_converter.db_to_issue_data(issue)
355 for xarticle in self.xissue.articles:
356 filter_articles = [
357 article for article in data_issue.articles if article.doi == xarticle.doi
358 ]
359 if len(filter_articles) > 0:
360 db_article = filter_articles[0]
361 xarticle.coi_statement = db_article.coi_statement
362 xarticle.kwds = db_article.kwds
363 xarticle.contrib_groups = db_article.contrib_groups
365 params = {
366 "colid": self.colid,
367 "xissue": self.xissue,
368 "input_file": self.filename,
369 }
371 if settings.IMPORT_CEDRICS_DIRECTLY:
372 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
373 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
374 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
375 else:
376 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
378 cmd.do()
380 def form_valid(self, form):
381 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
382 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
383 else:
384 import_kwargs = {}
385 import_args = [self]
387 try:
388 _, status, message = history_views.execute_and_record_func(
389 "import",
390 self.xissue.pid,
391 self.kwargs["colid"],
392 self.import_cedrics_issue,
393 "",
394 False,
395 *import_args,
396 **import_kwargs,
397 )
398 except Exception as exception:
399 messages.error(
400 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
401 )
402 return super().form_invalid(form)
404 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
405 return super().form_valid(form)
408class BibtexAPIView(View):
409 def get(self, request, *args, **kwargs):
410 pid = self.kwargs.get("pid", None)
411 all_bibtex = ""
412 if pid:
413 article = model_helpers.get_article(pid)
414 if article:
415 for bibitem in article.bibitem_set.all():
416 bibtex_array = bibitem.get_bibtex()
417 last = len(bibtex_array)
418 i = 1
419 for bibtex in bibtex_array:
420 if i > 1 and i < last:
421 all_bibtex += " "
422 all_bibtex += bibtex + "\n"
423 i += 1
425 data = {"bibtex": all_bibtex}
426 return JsonResponse(data)
429class MatchingAPIView(View):
430 def get(self, request, *args, **kwargs):
431 pid = self.kwargs.get("pid", None)
433 url = settings.MATCHING_URL
434 headers = {"Content-Type": "application/xml"}
436 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
438 if settings.DEBUG:
439 print("Issue exported to /tmp/issue.xml")
440 f = open("/tmp/issue.xml", "w")
441 f.write(body.encode("utf8"))
442 f.close()
444 r = requests.post(url, data=body.encode("utf8"), headers=headers)
445 body = r.text.encode("utf8")
446 data = {"status": r.status_code, "message": body[:1000]}
448 if settings.DEBUG:
449 print("Matching received, new issue exported to /tmp/issue1.xml")
450 f = open("/tmp/issue1.xml", "w")
451 text = body
452 f.write(text)
453 f.close()
455 resource = model_helpers.get_resource(pid)
456 obj = resource.cast()
457 colid = obj.get_collection().pid
459 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
461 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
462 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
463 )
464 cmd.do()
466 print("Matching finished")
467 return JsonResponse(data)
470class ImportAllAPIView(View):
471 def internal_do(self, *args, **kwargs):
472 pid = self.kwargs.get("pid", None)
474 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
475 if not os.path.isdir(root_folder):
476 raise ValueError(root_folder + " does not exist")
478 resource = model_helpers.get_resource(pid)
479 if not resource:
480 file = os.path.join(root_folder, pid + ".xml")
481 body = utils.get_file_content_in_utf8(file)
482 journals = xml_cmds.addCollectionsXmlCmd(
483 {
484 "body": body,
485 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
486 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
487 }
488 ).do()
489 if not journals:
490 raise ValueError(file + " does not contain a collection")
491 resource = journals[0]
492 # resolver.copy_binary_files(
493 # resource,
494 # settings.MATHDOC_ARCHIVE_FOLDER,
495 # settings.MERSENNE_TEST_DATA_FOLDER)
497 obj = resource.cast()
499 if obj.classname != "Collection":
500 raise ValueError(pid + " does not contain a collection")
502 cmd = xml_cmds.collectEntireCollectionXmlCmd(
503 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
504 )
505 pids = cmd.do()
507 return pids
509 def get(self, request, *args, **kwargs):
510 pid = self.kwargs.get("pid", None)
512 try:
513 pids, status, message = history_views.execute_and_record_func(
514 "import", pid, pid, self.internal_do
515 )
516 except Timeout as exception:
517 return HttpResponse(exception, status=408)
518 except Exception as exception:
519 return HttpResponseServerError(exception)
521 data = {"message": message, "ids": pids, "status": status}
522 return JsonResponse(data)
525class DeployAllAPIView(View):
526 def internal_do(self, *args, **kwargs):
527 pid = self.kwargs.get("pid", None)
528 site = self.kwargs.get("site", None)
530 pids = []
532 collection = model_helpers.get_collection(pid)
533 if not collection:
534 raise RuntimeError(pid + " does not exist")
536 if site == "numdam":
537 server_url = settings.NUMDAM_PRE_URL
538 elif site != "ptf_tools":
539 server_url = getattr(collection, site)()
540 if not server_url:
541 raise RuntimeError("The collection has no " + site)
543 if site != "ptf_tools":
544 # check if the collection exists on the server
545 # if not, check_collection will upload the collection (XML,
546 # image...)
547 check_collection(collection, server_url, site)
549 for issue in collection.content.all():
550 if site != "website" or (site == "website" and issue.are_all_articles_published()):
551 pids.append(issue.pid)
553 return pids
555 def get(self, request, *args, **kwargs):
556 pid = self.kwargs.get("pid", None)
557 site = self.kwargs.get("site", None)
559 try:
560 pids, status, message = history_views.execute_and_record_func(
561 "deploy", pid, pid, self.internal_do, site
562 )
563 except Timeout as exception:
564 return HttpResponse(exception, status=408)
565 except Exception as exception:
566 return HttpResponseServerError(exception)
568 data = {"message": message, "ids": pids, "status": status}
569 return JsonResponse(data)
572class AddIssuePDFView(View):
573 def __init(self, *args, **kwargs):
574 super().__init__(*args, **kwargs)
575 self.pid = None
576 self.issue = None
577 self.collection = None
578 self.site = "test_website"
580 def post_to_site(self, url):
581 response = requests.post(url, verify=False)
582 status = response.status_code
583 if not (199 < status < 205):
584 messages.error(self.request, response.text)
585 if status == 503:
586 raise ServerUnderMaintenance(response.text)
587 else:
588 raise RuntimeError(response.text)
590 def internal_do(self, *args, **kwargs):
591 """
592 Called by history_views.execute_and_record_func to do the actual job.
593 """
595 issue_pid = self.issue.pid
596 colid = self.collection.pid
598 if self.site == "website":
599 # Copy the PDF from the test to the production folder
600 resolver.copy_binary_files(
601 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
602 )
603 else:
604 # Copy the PDF from the cedram to the test folder
605 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
606 from_path = os.path.join(from_folder, issue_pid + ".pdf")
607 if not os.path.isfile(from_path):
608 raise Http404(f"{from_path} does not exist")
610 to_path = resolver.get_disk_location(
611 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
612 )
613 resolver.copy_file(from_path, to_path)
615 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
617 if self.site == "test_website":
618 # Post to ptf-tools: it will add a Datastream to the issue
619 absolute_url = self.request.build_absolute_uri(url)
620 self.post_to_site(absolute_url)
622 server_url = getattr(self.collection, self.site)()
623 absolute_url = server_url + url
624 # Post to the test or production website
625 self.post_to_site(absolute_url)
627 def get(self, request, *args, **kwargs):
628 """
629 Send an issue PDF to the test or production website
630 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
631 :param args:
632 :param kwargs:
633 :return:
634 """
635 if check_lock():
636 m = "Trammel is under maintenance. Please try again later."
637 messages.error(self.request, m)
638 return JsonResponse({"message": m, "status": 503})
640 self.pid = self.kwargs.get("pid", None)
641 self.site = self.kwargs.get("site", "test_website")
643 self.issue = model_helpers.get_container(self.pid)
644 if not self.issue:
645 raise Http404(f"{self.pid} does not exist")
646 self.collection = self.issue.get_top_collection()
648 try:
649 pids, status, message = history_views.execute_and_record_func(
650 "deploy",
651 self.pid,
652 self.collection.pid,
653 self.internal_do,
654 f"add issue PDF to {self.site}",
655 )
657 except Timeout as exception:
658 return HttpResponse(exception, status=408)
659 except Exception as exception:
660 return HttpResponseServerError(exception)
662 data = {"message": message, "status": status}
663 return JsonResponse(data)
666class ArchiveAllAPIView(View):
667 """
668 - archive le xml de la collection ainsi que les binaires liés
669 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
670 @return array of issues pid
671 """
673 def internal_do(self, *args, **kwargs):
674 collection = kwargs["collection"]
675 pids = []
676 colid = collection.pid
678 logfile = os.path.join(settings.LOG_DIR, "archive.log")
679 if os.path.isfile(logfile):
680 os.remove(logfile)
682 ptf_cmds.exportPtfCmd(
683 {
684 "pid": colid,
685 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
686 "with_binary_files": True,
687 "for_archive": True,
688 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
689 }
690 ).do()
692 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
693 if os.path.isfile(cedramcls):
694 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
695 resolver.create_folder(dest_folder)
696 resolver.copy_file(cedramcls, dest_folder)
698 for issue in collection.content.all():
699 qs = issue.article_set.filter(
700 date_online_first__isnull=True, date_published__isnull=True
701 )
702 if qs.count() == 0:
703 pids.append(issue.pid)
705 return pids
707 def get(self, request, *args, **kwargs):
708 pid = self.kwargs.get("pid", None)
710 collection = model_helpers.get_collection(pid)
711 if not collection:
712 return HttpResponse(f"{pid} does not exist", status=400)
714 dict_ = {"collection": collection}
715 args_ = [self]
717 try:
718 pids, status, message = history_views.execute_and_record_func(
719 "archive", pid, pid, self.internal_do, "", False, *args_, **dict_
720 )
721 except Timeout as exception:
722 return HttpResponse(exception, status=408)
723 except Exception as exception:
724 return HttpResponseServerError(exception)
726 data = {"message": message, "ids": pids, "status": status}
727 return JsonResponse(data)
730class CreateAllDjvuAPIView(View):
731 def internal_do(self, *args, **kwargs):
732 issue = kwargs["issue"]
733 pids = [issue.pid]
735 for article in issue.article_set.all():
736 pids.append(article.pid)
738 return pids
740 def get(self, request, *args, **kwargs):
741 pid = self.kwargs.get("pid", None)
742 issue = model_helpers.get_container(pid)
743 if not issue:
744 raise Http404(f"{pid} does not exist")
746 try:
747 dict_ = {"issue": issue}
748 args_ = [self]
750 pids, status, message = history_views.execute_and_record_func(
751 "numdam",
752 pid,
753 issue.get_collection().pid,
754 self.internal_do,
755 "",
756 False,
757 *args_,
758 **dict_,
759 )
760 except Exception as exception:
761 return HttpResponseServerError(exception)
763 data = {"message": message, "ids": pids, "status": status}
764 return JsonResponse(data)
767class ImportJatsContainerAPIView(View):
768 def internal_do(self, *args, **kwargs):
769 pid = self.kwargs.get("pid", None)
770 colid = self.kwargs.get("colid", None)
772 if pid and colid:
773 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
775 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
776 {
777 "body": body,
778 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
779 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
780 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
781 }
782 )
783 container = cmd.do()
784 if len(cmd.warnings) > 0:
785 messages.warning(
786 self.request,
787 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
788 )
790 if not container:
791 raise RuntimeError("Error: the container " + pid + " was not imported")
793 # resolver.copy_binary_files(
794 # container,
795 # settings.MATHDOC_ARCHIVE_FOLDER,
796 # settings.MERSENNE_TEST_DATA_FOLDER)
797 #
798 # for article in container.article_set.all():
799 # resolver.copy_binary_files(
800 # article,
801 # settings.MATHDOC_ARCHIVE_FOLDER,
802 # settings.MERSENNE_TEST_DATA_FOLDER)
803 else:
804 raise RuntimeError("colid or pid are not defined")
806 def get(self, request, *args, **kwargs):
807 pid = self.kwargs.get("pid", None)
808 colid = self.kwargs.get("colid", None)
810 try:
811 _, status, message = history_views.execute_and_record_func(
812 "import", pid, colid, self.internal_do
813 )
814 except Timeout as exception:
815 return HttpResponse(exception, status=408)
816 except Exception as exception:
817 return HttpResponseServerError(exception)
819 data = {"message": message, "status": status}
820 return JsonResponse(data)
823class DeployCollectionAPIView(View):
824 # Update collection.xml on a site (with its images)
826 def internal_do(self, *args, **kwargs):
827 colid = self.kwargs.get("colid", None)
828 site = self.kwargs.get("site", None)
830 collection = model_helpers.get_collection(colid)
831 if not collection:
832 raise RuntimeError(f"{colid} does not exist")
834 if site == "numdam":
835 server_url = settings.NUMDAM_PRE_URL
836 else:
837 server_url = getattr(collection, site)()
838 if not server_url:
839 raise RuntimeError(f"The collection has no {site}")
841 # check_collection creates or updates the collection (XML, image...)
842 check_collection(collection, server_url, site)
844 def get(self, request, *args, **kwargs):
845 colid = self.kwargs.get("colid", None)
846 site = self.kwargs.get("site", None)
848 try:
849 _, status, message = history_views.execute_and_record_func(
850 "deploy", colid, colid, self.internal_do, site
851 )
852 except Timeout as exception:
853 return HttpResponse(exception, status=408)
854 except Exception as exception:
855 return HttpResponseServerError(exception)
857 data = {"message": message, "status": status}
858 return JsonResponse(data)
861class DeployJatsResourceAPIView(View):
862 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
864 def internal_do(self, *args, **kwargs):
865 pid = self.kwargs.get("pid", None)
866 colid = self.kwargs.get("colid", None)
867 site = self.kwargs.get("site", None)
869 if site == "ptf_tools":
870 raise RuntimeError("Do not choose to deploy on PTF Tools")
871 if check_lock():
872 msg = "Trammel is under maintenance. Please try again later."
873 messages.error(self.request, msg)
874 return JsonResponse({"messages": msg, "status": 503})
876 resource = model_helpers.get_resource(pid)
877 if not resource:
878 raise RuntimeError(f"{pid} does not exist")
880 obj = resource.cast()
881 article = None
882 if obj.classname == "Article":
883 article = obj
884 container = article.my_container
885 articles_to_deploy = [article]
886 else:
887 container = obj
888 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
890 if site == "website" and article is not None and article.do_not_publish:
891 raise RuntimeError(f"{pid} is marked as Do not publish")
892 if site == "numdam" and article is not None:
893 raise RuntimeError("You can only deploy issues to Numdam")
895 collection = container.get_top_collection()
896 colid = collection.pid
897 djvu_exception = None
899 if site == "numdam":
900 server_url = settings.NUMDAM_PRE_URL
901 ResourceInNumdam.objects.get_or_create(pid=container.pid)
903 # 06/12/2022: DjVu are no longer added with Mersenne articles
904 # Add Djvu (before exporting the XML)
905 if False and int(container.year) < 2020:
906 for art in container.article_set.all():
907 try:
908 cmd = ptf_cmds.addDjvuPtfCmd()
909 cmd.set_resource(art)
910 cmd.do()
911 except Exception as e:
912 # Djvu are optional.
913 # Allow the deployment, but record the exception in the history
914 djvu_exception = e
915 else:
916 server_url = getattr(collection, site)()
917 if not server_url:
918 raise RuntimeError(f"The collection has no {site}")
920 # check if the collection exists on the server
921 # if not, check_collection will upload the collection (XML,
922 # image...)
923 if article is None:
924 check_collection(collection, server_url, site)
926 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
927 # Create/update deployed date and published date on all container articles
928 if site == "website":
929 file_.write(
930 "Create/Update deployed_date and date_published on all articles for {}\n".format(
931 pid
932 )
933 )
935 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
936 cmd = ptf_cmds.publishResourcePtfCmd()
937 cmd.set_resource(resource)
938 updated_articles = cmd.do()
940 tex.create_frontpage(colid, container, updated_articles, test=False)
942 mersenneSite = model_helpers.get_site_mersenne(colid)
943 # create or update deployed_date on container and articles
944 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
946 for art in articles_to_deploy:
947 if art.doi and (art.date_published or art.date_online_first):
948 if art.my_container.year is None:
949 art.my_container.year = datetime.now().strftime("%Y")
950 # BUG ? update the container but no save() ?
952 file_.write(
953 "Publication date of {} : Online First: {}, Published: {}\n".format(
954 art.pid, art.date_online_first, art.date_published
955 )
956 )
958 if article is None:
959 resolver.copy_binary_files(
960 container,
961 settings.MERSENNE_TEST_DATA_FOLDER,
962 settings.MERSENNE_PROD_DATA_FOLDER,
963 )
965 for art in articles_to_deploy:
966 resolver.copy_binary_files(
967 art,
968 settings.MERSENNE_TEST_DATA_FOLDER,
969 settings.MERSENNE_PROD_DATA_FOLDER,
970 )
972 elif site == "test_website":
973 # create date_pre_published on articles without date_pre_published
974 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
975 cmd.set_resource(resource)
976 updated_articles = cmd.do()
978 tex.create_frontpage(colid, container, updated_articles)
980 export_to_website = site == "website"
982 if article is None:
983 with_djvu = site == "numdam"
984 xml = ptf_cmds.exportPtfCmd(
985 {
986 "pid": pid,
987 "with_djvu": with_djvu,
988 "export_to_website": export_to_website,
989 }
990 ).do()
991 body = xml.encode("utf8")
993 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
994 url = server_url + reverse("issue_upload")
995 else:
996 url = server_url + reverse("book_upload")
998 # verify=False: ignore TLS certificate
999 response = requests.post(url, data=body, verify=False)
1000 # response = requests.post(url, files=files, verify=False)
1001 else:
1002 xml = ptf_cmds.exportPtfCmd(
1003 {
1004 "pid": pid,
1005 "with_djvu": False,
1006 "article_standalone": True,
1007 "collection_pid": collection.pid,
1008 "export_to_website": export_to_website,
1009 "export_folder": settings.LOG_DIR,
1010 }
1011 ).do()
1012 # Unlike containers that send their XML as the body of the POST request,
1013 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1014 xml_file = io.StringIO(xml)
1015 files = {"xml": xml_file}
1017 url = server_url + reverse(
1018 "article_in_issue_upload", kwargs={"pid": container.pid}
1019 )
1020 # verify=False: ignore TLS certificate
1021 header = {}
1022 response = requests.post(url, headers=header, files=files, verify=False)
1024 status = response.status_code
1026 if 199 < status < 205:
1027 # There is no need to copy files for the test server
1028 # Files were already copied in /mersenne_test_data during the ptf_tools import
1029 # We only need to copy files from /mersenne_test_data to
1030 # /mersenne_prod_data during an upload to prod
1031 if site == "website":
1032 # TODO mettre ici le record doi pour un issue publié
1033 if container.doi:
1034 recordDOI(container)
1036 for art in articles_to_deploy:
1037 # record DOI automatically when deploying in prod
1039 if art.doi and art.allow_crossref():
1040 recordDOI(art)
1042 if colid == "CRBIOL":
1043 recordPubmed(
1044 art, force_update=False, updated_articles=updated_articles
1045 )
1047 if colid == "PCJ":
1048 self.update_pcj_editor(updated_articles)
1050 # Archive the container or the article
1051 if article is None:
1052 archive_trammel_resource.delay(
1053 colid=colid,
1054 pid=pid,
1055 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1056 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1057 )
1058 else:
1059 archive_trammel_resource.delay(
1060 colid=colid,
1061 pid=pid,
1062 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1063 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1064 article_doi=article.doi,
1065 )
1066 # cmd = ptf_cmds.archiveIssuePtfCmd({
1067 # "pid": pid,
1068 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1069 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1070 # cmd.set_article(article) # set_article allows archiving only the article
1071 # cmd.do()
1073 elif site == "numdam":
1074 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1075 if colid in settings.NUMDAM_COLLECTIONS:
1076 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1078 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1079 for article in container.article_set.all():
1080 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1082 elif status == 503:
1083 raise ServerUnderMaintenance(response.text)
1084 else:
1085 raise RuntimeError(response.text)
1087 if djvu_exception:
1088 raise djvu_exception
1090 def get(self, request, *args, **kwargs):
1091 pid = self.kwargs.get("pid", None)
1092 colid = self.kwargs.get("colid", None)
1093 site = self.kwargs.get("site", None)
1095 try:
1096 _, status, message = history_views.execute_and_record_func(
1097 "deploy", pid, colid, self.internal_do, site
1098 )
1099 except Timeout as exception:
1100 return HttpResponse(exception, status=408)
1101 except Exception as exception:
1102 return HttpResponseServerError(exception)
1104 data = {"message": message, "status": status}
1105 return JsonResponse(data)
1107 def update_pcj_editor(self, updated_articles):
1108 for article in updated_articles:
1109 data = {
1110 "date_published": article.date_published.strftime("%Y-%m-%d"),
1111 "article_number": article.article_number,
1112 }
1113 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1114 requests.post(url, json=data, verify=False)
1117class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1118 article = None
1120 def internal_do(self, *args, **kwargs):
1121 lang = self.kwargs.get("lang", None)
1123 translation = None
1124 for trans_article in self.article.translations.all():
1125 if trans_article.lang == lang:
1126 translation = trans_article
1128 if translation is None:
1129 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1131 collection = self.article.get_top_collection()
1132 colid = collection.pid
1133 container = self.article.my_container
1135 if translation.date_published is None:
1136 # Add date posted
1137 cmd = ptf_cmds.publishResourcePtfCmd()
1138 cmd.set_resource(translation)
1139 updated_articles = cmd.do()
1141 # Recompile PDF to add the date posted
1142 try:
1143 tex.create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1144 except Exception:
1145 raise PDFException(
1146 "Unable to compile the article PDF. Please contact the centre Mersenne"
1147 )
1149 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1150 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1151 resolver.copy_binary_files(
1152 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1153 )
1155 # Deploy in prod
1156 xml = ptf_cmds.exportPtfCmd(
1157 {
1158 "pid": self.article.pid,
1159 "with_djvu": False,
1160 "article_standalone": True,
1161 "collection_pid": colid,
1162 "export_to_website": True,
1163 "export_folder": settings.LOG_DIR,
1164 }
1165 ).do()
1166 xml_file = io.StringIO(xml)
1167 files = {"xml": xml_file}
1169 server_url = getattr(collection, "website")()
1170 if not server_url:
1171 raise RuntimeError("The collection has no website")
1172 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1173 header = {}
1175 try:
1176 response = requests.post(
1177 url, headers=header, files=files, verify=False
1178 ) # verify: ignore TLS certificate
1179 status = response.status_code
1180 except requests.exceptions.ConnectionError:
1181 raise ServerUnderMaintenance(
1182 "The journal is under maintenance. Please try again later."
1183 )
1185 # Register translation in Crossref
1186 if 199 < status < 205:
1187 if self.article.allow_crossref():
1188 try:
1189 recordDOI(translation)
1190 except Exception:
1191 raise DOIException(
1192 "Error while recording the DOI. Please contact the centre Mersenne"
1193 )
1195 def get(self, request, *args, **kwargs):
1196 doi = kwargs.get("doi", None)
1197 self.article = model_helpers.get_article_by_doi(doi)
1198 if self.article is None:
1199 raise Http404(f"{doi} does not exist")
1201 try:
1202 _, status, message = history_views.execute_and_record_func(
1203 "deploy",
1204 self.article.pid,
1205 self.article.get_top_collection().pid,
1206 self.internal_do,
1207 "website",
1208 )
1209 except Timeout as exception:
1210 return HttpResponse(exception, status=408)
1211 except Exception as exception:
1212 return HttpResponseServerError(exception)
1214 data = {"message": message, "status": status}
1215 return JsonResponse(data)
1218class DeleteJatsIssueAPIView(View):
1219 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1220 def get(self, request, *args, **kwargs):
1221 pid = self.kwargs.get("pid", None)
1222 colid = self.kwargs.get("colid", None)
1223 site = self.kwargs.get("site", None)
1224 message = "Le volume a bien été supprimé"
1225 status = 200
1227 issue = model_helpers.get_container(pid)
1228 if not issue:
1229 raise Http404(f"{pid} does not exist")
1230 try:
1231 mersenneSite = model_helpers.get_site_mersenne(colid)
1233 if site == "ptf_tools":
1234 if issue.is_deployed(mersenneSite):
1235 issue.undeploy(mersenneSite)
1236 for article in issue.article_set.all():
1237 article.undeploy(mersenneSite)
1239 p = model_helpers.get_provider("mathdoc-id")
1241 cmd = ptf_cmds.addContainerPtfCmd(
1242 {
1243 "pid": issue.pid,
1244 "ctype": "issue",
1245 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1246 }
1247 )
1248 cmd.set_provider(p)
1249 cmd.add_collection(issue.get_collection())
1250 cmd.set_object_to_be_deleted(issue)
1251 cmd.undo()
1253 else:
1254 if site == "numdam":
1255 server_url = settings.NUMDAM_PRE_URL
1256 else:
1257 collection = issue.get_collection()
1258 server_url = getattr(collection, site)()
1260 if not server_url:
1261 message = "The collection has no " + site
1262 status = 500
1263 else:
1264 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1265 response = requests.delete(url, verify=False)
1266 status = response.status_code
1268 if status == 404:
1269 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1270 elif status > 204:
1271 body = response.text.encode("utf8")
1272 message = body[:1000]
1273 else:
1274 status = 200
1275 # unpublish issue in collection site (site_register.json)
1276 if site == "website":
1277 if issue.is_deployed(mersenneSite):
1278 issue.undeploy(mersenneSite)
1279 for article in issue.article_set.all():
1280 article.undeploy(mersenneSite)
1281 # delete article binary files
1282 folder = article.get_relative_folder()
1283 resolver.delete_object_folder(
1284 folder,
1285 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1286 )
1287 # delete issue binary files
1288 folder = issue.get_relative_folder()
1289 resolver.delete_object_folder(
1290 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1291 )
1293 except Timeout as exception:
1294 return HttpResponse(exception, status=408)
1295 except Exception as exception:
1296 return HttpResponseServerError(exception)
1298 data = {"message": message, "status": status}
1299 return JsonResponse(data)
1302class ArchiveIssueAPIView(View):
1303 def get(self, request, *args, **kwargs):
1304 try:
1305 pid = kwargs["pid"]
1306 colid = kwargs["colid"]
1307 except IndexError:
1308 raise Http404
1310 try:
1311 cmd = ptf_cmds.archiveIssuePtfCmd(
1312 {
1313 "pid": pid,
1314 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1315 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1316 }
1317 )
1318 result_, status, message = history_views.execute_and_record_func(
1319 "archive", pid, colid, cmd.do
1320 )
1321 except Exception as exception:
1322 return HttpResponseServerError(exception)
1324 data = {"message": message, "status": 200}
1325 return JsonResponse(data)
1328class CreateDjvuAPIView(View):
1329 def internal_do(self, *args, **kwargs):
1330 pid = self.kwargs.get("pid", None)
1332 resource = model_helpers.get_resource(pid)
1333 cmd = ptf_cmds.addDjvuPtfCmd()
1334 cmd.set_resource(resource)
1335 cmd.do()
1337 def get(self, request, *args, **kwargs):
1338 pid = self.kwargs.get("pid", None)
1339 colid = pid.split("_")[0]
1341 try:
1342 _, status, message = history_views.execute_and_record_func(
1343 "numdam", pid, colid, self.internal_do
1344 )
1345 except Exception as exception:
1346 return HttpResponseServerError(exception)
1348 data = {"message": message, "status": status}
1349 return JsonResponse(data)
1352class PTFToolsHomeView(LoginRequiredMixin, View):
1353 """
1354 Home Page.
1355 - Admin & staff -> Render blank home.html
1356 - User with unique authorized collection -> Redirect to collection details page
1357 - User with multiple authorized collections -> Render home.html with data
1358 - Comment moderator -> Comments dashboard
1359 - Others -> 404 response
1360 """
1362 def get(self, request, *args, **kwargs) -> HttpResponse:
1363 # Staff or user with authorized collections
1364 if request.user.is_staff or request.user.is_superuser:
1365 return render(request, "home.html")
1367 colids = get_authorized_collections(request.user)
1368 is_mod = is_comment_moderator(request.user)
1370 # The user has no rights
1371 if not (colids or is_mod):
1372 raise Http404("No collections associated with your account.")
1373 # Comment moderator only
1374 elif not colids:
1375 return HttpResponseRedirect(reverse("comment_list"))
1377 # User with unique collection -> Redirect to collection detail page
1378 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1379 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1381 # User with multiple authorized collections - Special home
1382 context = {}
1383 context["overview"] = True
1385 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1386 all_collections = {c["pid"]: c for c in all_collections}
1388 # Comments summary
1389 try:
1390 error, comments_data = get_comments_for_home(request.user)
1391 except AttributeError:
1392 error, comments_data = True, {}
1394 context["comment_server_ok"] = False
1396 if not error:
1397 context["comment_server_ok"] = True
1398 if comments_data:
1399 for col_id, comment_nb in comments_data.items():
1400 if col_id.upper() in all_collections: 1400 ↛ 1399line 1400 didn't jump to line 1399 because the condition on line 1400 was always true
1401 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1403 # TODO: Translations summary
1404 context["translation_server_ok"] = False
1406 # Sort the collections according to the number of pending comments
1407 context["collections"] = sorted(
1408 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1409 )
1411 return render(request, "home.html", context)
1414class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1415 columns = 5
1417 def get_common_context_data(self, **kwargs):
1418 context = super().get_context_data(**kwargs)
1419 now = timezone.now()
1420 curyear = now.year
1421 years = range(curyear - self.columns + 1, curyear + 1)
1423 context["collections"] = settings.MERSENNE_COLLECTIONS
1424 context["containers_to_be_published"] = []
1425 context["last_col_events"] = []
1427 event = history_models.get_history_last_event_by("clockss", "ALL")
1428 clockss_gap = history_models.get_gap(now, event)
1430 context["years"] = years
1431 context["clockss_gap"] = clockss_gap
1433 return context
1435 def calculate_articles_and_pages(self, pid, years):
1436 data_by_year = []
1437 total_articles = [0] * len(years)
1438 total_pages = [0] * len(years)
1440 for year in years:
1441 articles = self.get_articles_for_year(pid, year)
1442 articles_count = articles.count()
1443 page_count = sum(article.get_article_page_count() for article in articles)
1445 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1446 total_articles[year - years[0]] += articles_count
1447 total_pages[year - years[0]] += page_count
1449 return data_by_year, total_articles, total_pages
1451 def get_articles_for_year(self, pid, year):
1452 return Article.objects.filter(
1453 Q(my_container__my_collection__pid=pid)
1454 & (
1455 Q(date_published__year=year, date_online_first__isnull=True)
1456 | Q(date_online_first__year=year)
1457 )
1458 ).prefetch_related("resourcecount_set")
1461class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1462 template_name = "dashboard/published_articles.html"
1464 def get_context_data(self, **kwargs):
1465 context = self.get_common_context_data(**kwargs)
1466 years = context["years"]
1468 published_articles = []
1469 total_published_articles = [
1470 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1471 ]
1473 for pid in settings.MERSENNE_COLLECTIONS:
1474 if pid != "MERSENNE":
1475 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1476 pid, years
1477 )
1478 published_articles.append({"pid": pid, "years": articles_data})
1480 for i, year in enumerate(years):
1481 total_published_articles[i]["total_articles"] += total_articles[i]
1482 total_published_articles[i]["total_pages"] += total_pages[i]
1484 context["published_articles"] = published_articles
1485 context["total_published_articles"] = total_published_articles
1487 return context
1490class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1491 template_name = "dashboard/created_volumes.html"
1493 def get_context_data(self, **kwargs):
1494 context = self.get_common_context_data(**kwargs)
1495 years = context["years"]
1497 created_volumes = []
1498 total_created_volumes = [
1499 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1500 ]
1502 for pid in settings.MERSENNE_COLLECTIONS:
1503 if pid != "MERSENNE":
1504 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1505 pid, years
1506 )
1507 created_volumes.append({"pid": pid, "years": volumes_data})
1509 for i, year in enumerate(years):
1510 total_created_volumes[i]["total_articles"] += total_articles[i]
1511 total_created_volumes[i]["total_pages"] += total_pages[i]
1513 context["created_volumes"] = created_volumes
1514 context["total_created_volumes"] = total_created_volumes
1516 return context
1518 def calculate_volumes_and_pages(self, pid, years):
1519 data_by_year = []
1520 total_articles = [0] * len(years)
1521 total_pages = [0] * len(years)
1523 for year in years:
1524 issues = Container.objects.filter(my_collection__pid=pid, year=year)
1525 articles_count = 0
1526 page_count = 0
1528 for issue in issues:
1529 articles = issue.article_set.filter(
1530 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1531 ).prefetch_related("resourcecount_set")
1533 articles_count += articles.count()
1534 page_count += sum(article.get_article_page_count() for article in articles)
1536 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1537 total_articles[year - years[0]] += articles_count
1538 total_pages[year - years[0]] += page_count
1540 return data_by_year, total_articles, total_pages
1543class ReferencingDashboardView(BaseMersenneDashboardView):
1544 template_name = "dashboard/referencing.html"
1546 def get(self, request, *args, **kwargs):
1547 colid = self.kwargs.get("colid", None)
1548 comp = ReferencingChecker()
1549 journal = comp.check_references(colid)
1550 return render(request, self.template_name, {"journal": journal})
1553class BaseCollectionView(TemplateView):
1554 def get_context_data(self, **kwargs):
1555 context = super().get_context_data(**kwargs)
1556 aid = context.get("aid")
1557 year = context.get("year")
1559 if aid and year:
1560 context["collection"] = self.get_collection(aid, year)
1562 return context
1564 def get_collection(self, aid, year):
1565 """Method to be overridden by subclasses to fetch the appropriate collection"""
1566 raise NotImplementedError("Subclasses must implement get_collection method")
1569class ArticleListView(BaseCollectionView):
1570 template_name = "collection-list.html"
1572 def get_collection(self, aid, year):
1573 return Article.objects.filter(
1574 Q(my_container__my_collection__pid=aid)
1575 & (
1576 Q(date_published__year=year, date_online_first__isnull=True)
1577 | Q(date_online_first__year=year)
1578 )
1579 ).prefetch_related("resourcecount_set")
1582class VolumeListView(BaseCollectionView):
1583 template_name = "collection-list.html"
1585 def get_collection(self, aid, year):
1586 return Article.objects.filter(
1587 Q(my_container__my_collection__pid=aid, my_container__year=year)
1588 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1589 ).prefetch_related("resourcecount_set")
1592class DOAJResourceRegisterView(View):
1593 def get(self, request, *args, **kwargs):
1594 pid = kwargs.get("pid", None)
1595 resource = model_helpers.get_resource(pid)
1596 if resource is None:
1597 raise Http404
1599 try:
1600 data = {}
1601 doaj_meta, response = doaj_pid_register(pid)
1602 if response is None:
1603 return HttpResponse(status=204)
1604 elif doaj_meta and 200 <= response.status_code <= 299:
1605 data.update(doaj_meta)
1606 else:
1607 return HttpResponse(status=response.status_code, reason=response.text)
1608 except Timeout as exception:
1609 return HttpResponse(exception, status=408)
1610 except Exception as exception:
1611 return HttpResponseServerError(exception)
1612 return JsonResponse(data)
1615class CROSSREFResourceRegisterView(View):
1616 def get(self, request, *args, **kwargs):
1617 pid = kwargs.get("pid", None)
1618 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1619 force = kwargs.get("force", None)
1620 if not request.user.is_superuser:
1621 force = None
1623 resource = model_helpers.get_resource(pid)
1624 if resource is None:
1625 raise Http404
1627 resource = resource.cast()
1628 meth = getattr(self, "recordDOI" + resource.classname)
1629 try:
1630 data = meth(resource, force)
1631 except Timeout as exception:
1632 return HttpResponse(exception, status=408)
1633 except Exception as exception:
1634 return HttpResponseServerError(exception)
1635 return JsonResponse(data)
1637 def recordDOIArticle(self, article, force=None):
1638 result = {"status": 404}
1639 if (
1640 article.doi
1641 and not article.do_not_publish
1642 and (article.date_published or article.date_online_first or force == "force")
1643 ):
1644 if article.my_container.year is None: # or article.my_container.year == '0':
1645 article.my_container.year = datetime.now().strftime("%Y")
1646 result = recordDOI(article)
1647 return result
1649 def recordDOICollection(self, collection, force=None):
1650 return recordDOI(collection)
1652 def recordDOIContainer(self, container, force=None):
1653 data = {"status": 200, "message": "tout va bien"}
1655 if container.ctype == "issue":
1656 if container.doi:
1657 result = recordDOI(container)
1658 if result["status"] != 200:
1659 return result
1660 if force == "force":
1661 articles = container.article_set.exclude(
1662 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1663 )
1664 else:
1665 articles = container.article_set.exclude(
1666 doi__isnull=True,
1667 do_not_publish=True,
1668 date_published__isnull=True,
1669 date_online_first__isnull=True,
1670 )
1672 for article in articles:
1673 result = self.recordDOIArticle(article, force)
1674 if result["status"] != 200:
1675 data = result
1676 else:
1677 return recordDOI(container)
1678 return data
1681class CROSSREFResourceCheckStatusView(View):
1682 def get(self, request, *args, **kwargs):
1683 pid = kwargs.get("pid", None)
1684 resource = model_helpers.get_resource(pid)
1685 if resource is None:
1686 raise Http404
1687 resource = resource.cast()
1688 meth = getattr(self, "checkDOI" + resource.classname)
1689 try:
1690 meth(resource)
1691 except Timeout as exception:
1692 return HttpResponse(exception, status=408)
1693 except Exception as exception:
1694 return HttpResponseServerError(exception)
1696 data = {"status": 200, "message": "tout va bien"}
1697 return JsonResponse(data)
1699 def checkDOIArticle(self, article):
1700 if article.my_container.year is None or article.my_container.year == "0":
1701 article.my_container.year = datetime.now().strftime("%Y")
1702 get_or_create_doibatch(article)
1704 def checkDOICollection(self, collection):
1705 get_or_create_doibatch(collection)
1707 def checkDOIContainer(self, container):
1708 if container.doi is not None:
1709 get_or_create_doibatch(container)
1710 for article in container.article_set.all():
1711 self.checkDOIArticle(article)
1714class RegisterPubmedFormView(FormView):
1715 template_name = "record_pubmed_dialog.html"
1716 form_class = RegisterPubmedForm
1718 def get_context_data(self, **kwargs):
1719 context = super().get_context_data(**kwargs)
1720 context["pid"] = self.kwargs["pid"]
1721 context["helper"] = PtfLargeModalFormHelper
1722 return context
1725class RegisterPubmedView(View):
1726 def get(self, request, *args, **kwargs):
1727 pid = kwargs.get("pid", None)
1728 update_article = self.request.GET.get("update_article", "on") == "on"
1730 article = model_helpers.get_article(pid)
1731 if article is None:
1732 raise Http404
1733 try:
1734 recordPubmed(article, update_article)
1735 except Exception as exception:
1736 messages.error("Unable to register the article in PubMed")
1737 return HttpResponseServerError(exception)
1739 return HttpResponseRedirect(
1740 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1741 )
1744class PTFToolsContainerView(TemplateView):
1745 template_name = ""
1747 def get_context_data(self, **kwargs):
1748 context = super().get_context_data(**kwargs)
1750 container = model_helpers.get_container(self.kwargs.get("pid"))
1751 if container is None:
1752 raise Http404
1753 citing_articles = container.citations()
1754 source = self.request.GET.get("source", None)
1755 if container.ctype.startswith("book"):
1756 book_parts = (
1757 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1758 )
1759 references = False
1760 if container.ctype == "book-monograph":
1761 # on regarde si il y a au moins une bibliographie
1762 for art in container.article_set.all():
1763 if art.bibitem_set.count() > 0:
1764 references = True
1765 context.update(
1766 {
1767 "book": container,
1768 "book_parts": list(book_parts),
1769 "source": source,
1770 "citing_articles": citing_articles,
1771 "references": references,
1772 "test_website": container.get_top_collection()
1773 .extlink_set.get(rel="test_website")
1774 .location,
1775 "prod_website": container.get_top_collection()
1776 .extlink_set.get(rel="website")
1777 .location,
1778 }
1779 )
1780 self.template_name = "book-toc.html"
1781 else:
1782 articles = container.article_set.all().order_by("seq")
1783 for article in articles:
1784 try:
1785 last_match = (
1786 history_models.HistoryEvent.objects.filter(
1787 pid=article.pid,
1788 type="matching",
1789 )
1790 .only("created_on")
1791 .latest("created_on")
1792 )
1793 except history_models.HistoryEvent.DoesNotExist as _:
1794 article.last_match = None
1795 else:
1796 article.last_match = last_match.created_on
1798 # article1 = articles.first()
1799 # date = article1.deployed_date()
1800 # TODO next_issue, previous_issue
1802 # check DOI est maintenant une commande à part
1803 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
1804 # articlesWithStatus = []
1805 # for article in articles:
1806 # get_or_create_doibatch(article)
1807 # articlesWithStatus.append(article)
1809 test_location = prod_location = ""
1810 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
1811 if qs:
1812 test_location = qs.first().location
1813 qs = container.get_top_collection().extlink_set.filter(rel="website")
1814 if qs:
1815 prod_location = qs.first().location
1816 context.update(
1817 {
1818 "issue": container,
1819 "articles": articles,
1820 "source": source,
1821 "citing_articles": citing_articles,
1822 "test_website": test_location,
1823 "prod_website": prod_location,
1824 }
1825 )
1826 self.template_name = "issue-items.html"
1828 context["allow_crossref"] = container.allow_crossref()
1829 context["coltype"] = container.my_collection.coltype
1830 return context
1833class ExtLinkInline(InlineFormSetFactory):
1834 model = ExtLink
1835 form_class = ExtLinkForm
1836 factory_kwargs = {"extra": 0}
1839class ResourceIdInline(InlineFormSetFactory):
1840 model = ResourceId
1841 form_class = ResourceIdForm
1842 factory_kwargs = {"extra": 0}
1845class IssueDetailAPIView(View):
1846 def get(self, request, *args, **kwargs):
1847 issue = get_object_or_404(Container, pid=kwargs["pid"])
1848 deployed_date = issue.deployed_date()
1849 result = {
1850 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
1851 if deployed_date
1852 else None,
1853 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
1854 "all_doi_are_registered": issue.all_doi_are_registered(),
1855 "registered_in_doaj": issue.registered_in_doaj(),
1856 "doi": issue.my_collection.doi,
1857 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
1858 }
1859 try:
1860 latest = history_models.HistoryEvent.objects.get_last_unsolved_error(
1861 pid=issue.pid, strict=False
1862 )
1863 except history_models.HistoryEvent.DoesNotExist as _:
1864 pass
1865 else:
1866 result["latest"] = latest.data["message"]
1867 result["latest_target"] = latest.data.get("target", "")
1868 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
1869 "%Y-%m-%d %H:%M"
1870 )
1872 result["latest_type"] = latest.type.capitalize()
1873 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
1874 try:
1875 result[event_type] = timezone.localtime(
1876 history_models.HistoryEvent.objects.filter(
1877 type=event_type,
1878 status="OK",
1879 pid__startswith=issue.pid,
1880 )
1881 .latest("created_on")
1882 .created_on
1883 ).strftime("%Y-%m-%d %H:%M")
1884 except history_models.HistoryEvent.DoesNotExist as _:
1885 result[event_type] = ""
1886 return JsonResponse(result)
1889class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
1890 model = Collection
1891 form_class = CollectionForm
1892 inlines = [ResourceIdInline, ExtLinkInline]
1893 inlines_names = ["resource_ids_form", "ext_links_form"]
1895 def get_context_data(self, **kwargs):
1896 context = super().get_context_data(**kwargs)
1897 context["helper"] = PtfFormHelper
1898 context["formset_helper"] = FormSetHelper
1899 return context
1901 def add_description(self, collection, description, lang, seq):
1902 if description:
1903 la = Abstract(
1904 resource=collection,
1905 tag="description",
1906 lang=lang,
1907 seq=seq,
1908 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
1909 value_html=description,
1910 value_tex=description,
1911 )
1912 la.save()
1914 def form_valid(self, form):
1915 if form.instance.abbrev:
1916 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
1917 else:
1918 form.instance.title_xml = (
1919 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
1920 )
1922 form.instance.title_html = form.instance.title_tex
1923 form.instance.title_sort = form.instance.title_tex
1924 result = super().form_valid(form)
1926 collection = self.object
1927 collection.abstract_set.all().delete()
1929 seq = 1
1930 description = form.cleaned_data["description_en"]
1931 if description:
1932 self.add_description(collection, description, "en", seq)
1933 seq += 1
1934 description = form.cleaned_data["description_fr"]
1935 if description:
1936 self.add_description(collection, description, "fr", seq)
1938 return result
1940 def get_success_url(self):
1941 messages.success(self.request, "La Collection a été modifiée avec succès")
1942 return reverse("collection-detail", kwargs={"pid": self.object.pid})
1945class CollectionCreate(CollectionFormView, CreateWithInlinesView):
1946 """
1947 Warning : Not yet finished
1948 Automatic site membership creation is still missing
1949 """
1952class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
1953 slug_field = "pid"
1954 slug_url_kwarg = "pid"
1957def suggest_load_journal_dois(colid):
1958 articles = (
1959 Article.objects.filter(my_container__my_collection__pid=colid)
1960 .filter(doi__isnull=False)
1961 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1962 .values_list("doi", flat=True)
1963 )
1965 try:
1966 articles = sorted(
1967 articles,
1968 key=lambda d: (
1969 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
1970 int(re.search(r".(\d+)$", d).group(1)),
1971 ),
1972 )
1973 except: # noqa: E722 (we'll look later)
1974 pass
1975 return [f'<option value="{doi}">' for doi in articles]
1978def get_context_with_volumes(journal):
1979 result = model_helpers.get_volumes_in_collection(journal)
1980 volume_count = result["volume_count"]
1981 collections = []
1982 for ancestor in journal.ancestors.all():
1983 item = model_helpers.get_volumes_in_collection(ancestor)
1984 volume_count = max(0, volume_count)
1985 item.update({"journal": ancestor})
1986 collections.append(item)
1988 # add the parent collection to its children list and sort it by date
1989 result.update({"journal": journal})
1990 collections.append(result)
1992 collections = [c for c in collections if c["sorted_issues"]]
1993 collections.sort(
1994 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
1995 reverse=True,
1996 )
1998 context = {
1999 "journal": journal,
2000 "sorted_issues": result["sorted_issues"],
2001 "volume_count": volume_count,
2002 "max_width": result["max_width"],
2003 "collections": collections,
2004 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2005 }
2006 return context
2009class CollectionDetail(
2010 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2011):
2012 model = Collection
2013 slug_field = "pid"
2014 slug_url_kwarg = "pid"
2015 template_name = "ptf/collection_detail.html"
2017 def test_func(self):
2018 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2020 def get(self, request, *args, **kwargs):
2021 self.object = self.get_object(queryset=Collection.objects.all())
2022 return super().get(request, *args, **kwargs)
2024 def get_context_data(self, **kwargs):
2025 context = super().get_context_data(**kwargs)
2026 context["object_list"] = context["object_list"].filter(ctype="issue")
2027 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2028 context.update(get_context_with_volumes(self.object))
2030 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2031 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2032 context["issue_to_appear"] = Container.objects.filter(
2033 pid=context["issue_to_appear_pid"]
2034 ).exists()
2035 try:
2036 latest_error = history_models.HistoryEvent.objects.get_last_unsolved_error(
2037 self.object.pid,
2038 strict=True,
2039 )
2040 except history_models.HistoryEvent.DoesNotExist as _:
2041 pass
2042 else:
2043 message = latest_error.data["message"]
2044 i = message.find(" - ")
2045 latest_exception = message[:i]
2046 latest_error_message = message[i + 3 :]
2047 context["latest_exception"] = latest_exception
2048 context["latest_exception_date"] = latest_error.created_on
2049 context["latest_exception_type"] = latest_error.type
2050 context["latest_error_message"] = latest_error_message
2051 return context
2053 def get_queryset(self):
2054 query = self.object.content.all()
2056 for ancestor in self.object.ancestors.all():
2057 query |= ancestor.content.all()
2059 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2062class ContainerEditView(FormView):
2063 template_name = "container_form.html"
2064 form_class = ContainerForm
2066 def get_success_url(self):
2067 if self.kwargs["pid"]:
2068 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2069 return reverse("mersenne_dashboard/published_articles")
2071 def set_success_message(self): # pylint: disable=no-self-use
2072 messages.success(self.request, "Le fascicule a été modifié")
2074 def get_form_kwargs(self):
2075 kwargs = super().get_form_kwargs()
2076 if "pid" not in self.kwargs:
2077 self.kwargs["pid"] = None
2078 if "colid" not in self.kwargs:
2079 self.kwargs["colid"] = None
2080 if "data" in kwargs and "colid" in kwargs["data"]:
2081 # colid is passed as a hidden param in the form.
2082 # It is used when you submit a new container
2083 self.kwargs["colid"] = kwargs["data"]["colid"]
2085 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2086 self.kwargs["pid"]
2087 )
2088 return kwargs
2090 def get_context_data(self, **kwargs):
2091 context = super().get_context_data(**kwargs)
2093 context["pid"] = self.kwargs["pid"]
2094 context["colid"] = self.kwargs["colid"]
2095 context["container"] = self.kwargs["container"]
2097 context["edit_container"] = context["pid"] is not None
2098 context["name"] = resolve(self.request.path_info).url_name
2100 return context
2102 def form_valid(self, form):
2103 new_pid = form.cleaned_data.get("pid")
2104 new_title = form.cleaned_data.get("title")
2105 new_trans_title = form.cleaned_data.get("trans_title")
2106 new_publisher = form.cleaned_data.get("publisher")
2107 new_year = form.cleaned_data.get("year")
2108 new_volume = form.cleaned_data.get("volume")
2109 new_number = form.cleaned_data.get("number")
2111 collection = None
2112 issue = self.kwargs["container"]
2113 if issue is not None:
2114 collection = issue.my_collection
2115 elif self.kwargs["colid"] is not None:
2116 if "CR" in self.kwargs["colid"]:
2117 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2118 else:
2119 collection = model_helpers.get_collection(self.kwargs["colid"])
2121 if collection is None:
2122 raise ValueError("Collection for " + new_pid + " does not exist")
2124 # Icon
2125 new_icon_location = ""
2126 if "icon" in self.request.FILES:
2127 filename = os.path.basename(self.request.FILES["icon"].name)
2128 file_extension = filename.split(".")[1]
2130 icon_filename = resolver.get_disk_location(
2131 settings.MERSENNE_TEST_DATA_FOLDER,
2132 collection.pid,
2133 file_extension,
2134 new_pid,
2135 None,
2136 True,
2137 )
2139 with open(icon_filename, "wb+") as destination:
2140 for chunk in self.request.FILES["icon"].chunks():
2141 destination.write(chunk)
2143 folder = resolver.get_relative_folder(collection.pid, new_pid)
2144 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2145 name = resolve(self.request.path_info).url_name
2146 if name == "special_issue_create":
2147 self.kwargs["name"] = name
2148 if self.kwargs["container"]:
2149 # Edit Issue
2150 issue = self.kwargs["container"]
2151 if issue is None:
2152 raise ValueError(self.kwargs["pid"] + " does not exist")
2154 issue.pid = new_pid
2155 issue.title_tex = issue.title_html = new_title
2156 issue.title_xml = build_title_xml(
2157 title=new_title,
2158 lang=issue.lang,
2159 title_type="issue-title",
2160 )
2162 trans_lang = ""
2163 if issue.trans_lang != "" and issue.trans_lang != "und":
2164 trans_lang = issue.trans_lang
2165 elif new_trans_title != "":
2166 trans_lang = "fr" if issue.lang == "en" else "en"
2168 issue.titles = []
2169 if trans_lang != "" and new_trans_title != "":
2170 title_xml = build_title_xml(
2171 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2172 )
2173 title = create_titledata(
2174 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2175 )
2176 issue.titles.append(title)
2178 if trans_lang != "" and new_trans_title != "":
2179 title_xml = build_title_xml(
2180 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2181 )
2183 trans_title = Title(
2184 resource=issue,
2185 lang=trans_lang,
2186 type="main",
2187 title_html=new_trans_title,
2188 title_xml=title_xml,
2189 )
2190 trans_title.save()
2191 issue.year = new_year
2192 issue.volume = new_volume
2193 issue.volume_int = make_int(new_volume)
2194 issue.number = new_number
2195 issue.number_int = make_int(new_number)
2196 issue.save()
2197 else:
2198 xissue = create_issuedata()
2200 xissue.ctype = "issue"
2201 xissue.pid = new_pid
2202 xissue.lang = "en"
2203 xissue.title_tex = new_title
2204 xissue.title_html = new_title
2205 xissue.title_xml = build_title_xml(
2206 title=new_title, lang=xissue.lang, title_type="issue-title"
2207 )
2209 if new_trans_title != "":
2210 trans_lang = "fr"
2211 title_xml = build_title_xml(
2212 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2213 )
2214 title = create_titledata(
2215 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2216 )
2217 issue.titles = [title]
2219 xissue.year = new_year
2220 xissue.volume = new_volume
2221 xissue.number = new_number
2222 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2224 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2225 cmd.add_collection(collection)
2226 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2227 issue = cmd.do()
2229 self.kwargs["pid"] = new_pid
2231 # Add objects related to the article: contribs, datastream, counts...
2232 params = {
2233 "icon_location": new_icon_location,
2234 }
2235 cmd = ptf_cmds.updateContainerPtfCmd(params)
2236 cmd.set_resource(issue)
2237 cmd.do()
2239 publisher = model_helpers.get_publisher(new_publisher)
2240 if not publisher:
2241 xpub = create_publisherdata()
2242 xpub.name = new_publisher
2243 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2244 issue.my_publisher = publisher
2245 issue.save()
2247 self.set_success_message()
2249 return super().form_valid(form)
2252# class ArticleEditView(FormView):
2253# template_name = 'article_form.html'
2254# form_class = ArticleForm
2255#
2256# def get_success_url(self):
2257# if self.kwargs['pid']:
2258# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2259# return reverse('mersenne_dashboard/published_articles')
2260#
2261# def set_success_message(self): # pylint: disable=no-self-use
2262# messages.success(self.request, "L'article a été modifié")
2263#
2264# def get_form_kwargs(self):
2265# kwargs = super(ArticleEditView, self).get_form_kwargs()
2266#
2267# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2268# # Article creation: pid is None
2269# self.kwargs['pid'] = None
2270# if 'issue_id' not in self.kwargs:
2271# # Article edit: issue_id is not passed
2272# self.kwargs['issue_id'] = None
2273# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2274# # colid is passed as a hidden param in the form.
2275# # It is used when you submit a new container
2276# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2277#
2278# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2279# return kwargs
2280#
2281# def get_context_data(self, **kwargs):
2282# context = super(ArticleEditView, self).get_context_data(**kwargs)
2283#
2284# context['pid'] = self.kwargs['pid']
2285# context['issue_id'] = self.kwargs['issue_id']
2286# context['article'] = self.kwargs['article']
2287#
2288# context['edit_article'] = context['pid'] is not None
2289#
2290# article = context['article']
2291# if article:
2292# context['author_contributions'] = article.get_author_contributions()
2293# context['kwds_fr'] = None
2294# context['kwds_en'] = None
2295# kwd_gps = article.get_non_msc_kwds()
2296# for kwd_gp in kwd_gps:
2297# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2298# if kwd_gp.value_xml:
2299# kwd_ = types.SimpleNamespace()
2300# kwd_.value = kwd_gp.value_tex
2301# context['kwd_unstructured_fr'] = kwd_
2302# context['kwds_fr'] = kwd_gp.kwd_set.all()
2303# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2304# if kwd_gp.value_xml:
2305# kwd_ = types.SimpleNamespace()
2306# kwd_.value = kwd_gp.value_tex
2307# context['kwd_unstructured_en'] = kwd_
2308# context['kwds_en'] = kwd_gp.kwd_set.all()
2309#
2310# # Article creation: init pid
2311# if context['issue_id'] and context['pid'] is None:
2312# issue = model_helpers.get_container(context['issue_id'])
2313# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2314#
2315# return context
2316#
2317# def form_valid(self, form):
2318#
2319# new_pid = form.cleaned_data.get('pid')
2320# new_title = form.cleaned_data.get('title')
2321# new_fpage = form.cleaned_data.get('fpage')
2322# new_lpage = form.cleaned_data.get('lpage')
2323# new_page_range = form.cleaned_data.get('page_range')
2324# new_page_count = form.cleaned_data.get('page_count')
2325# new_coi_statement = form.cleaned_data.get('coi_statement')
2326# new_show_body = form.cleaned_data.get('show_body')
2327# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2328#
2329# # TODO support MathML
2330# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2331# # We need to pass trans_title to get_title_xml
2332# # Meanwhile, ignore new_title_xml
2333# new_title_xml = jats_parser.get_title_xml(new_title)
2334# new_title_html = new_title
2335#
2336# authors_count = int(self.request.POST.get('authors_count', "0"))
2337# i = 1
2338# new_authors = []
2339# old_author_contributions = []
2340# if self.kwargs['article']:
2341# old_author_contributions = self.kwargs['article'].get_author_contributions()
2342#
2343# while authors_count > 0:
2344# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2345#
2346# if prefix is not None:
2347# addresses = []
2348# if len(old_author_contributions) >= i:
2349# old_author_contribution = old_author_contributions[i - 1]
2350# addresses = [contrib_address.address for contrib_address in
2351# old_author_contribution.get_addresses()]
2352#
2353# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2354# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2355# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2356# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2357# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2358# deceased_before_publication = deceased == 'on'
2359# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2360# equal_contrib = equal_contrib == 'on'
2361# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2362# corresponding = corresponding == 'on'
2363# email = self.request.POST.get('email-' + str(i), None)
2364#
2365# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2366# params['deceased_before_publication'] = deceased_before_publication
2367# params['equal_contrib'] = equal_contrib
2368# params['corresponding'] = corresponding
2369# params['addresses'] = addresses
2370# params['email'] = email
2371#
2372# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2373#
2374# new_authors.append(params)
2375#
2376# authors_count -= 1
2377# i += 1
2378#
2379# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2380# i = 1
2381# new_kwds_fr = []
2382# while kwds_fr_count > 0:
2383# value = self.request.POST.get('kwd-fr-' + str(i), None)
2384# new_kwds_fr.append(value)
2385# kwds_fr_count -= 1
2386# i += 1
2387# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2388#
2389# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2390# i = 1
2391# new_kwds_en = []
2392# while kwds_en_count > 0:
2393# value = self.request.POST.get('kwd-en-' + str(i), None)
2394# new_kwds_en.append(value)
2395# kwds_en_count -= 1
2396# i += 1
2397# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2398#
2399# if self.kwargs['article']:
2400# # Edit article
2401# container = self.kwargs['article'].my_container
2402# else:
2403# # New article
2404# container = model_helpers.get_container(self.kwargs['issue_id'])
2405#
2406# if container is None:
2407# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2408#
2409# collection = container.my_collection
2410#
2411# # Copy PDF file & extract full text
2412# body = ''
2413# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2414# collection.pid,
2415# "pdf",
2416# container.pid,
2417# new_pid,
2418# True)
2419# if 'pdf' in self.request.FILES:
2420# with open(pdf_filename, 'wb+') as destination:
2421# for chunk in self.request.FILES['pdf'].chunks():
2422# destination.write(chunk)
2423#
2424# # Extract full text from the PDF
2425# body = utils.pdf_to_text(pdf_filename)
2426#
2427# # Icon
2428# new_icon_location = ''
2429# if 'icon' in self.request.FILES:
2430# filename = os.path.basename(self.request.FILES['icon'].name)
2431# file_extension = filename.split('.')[1]
2432#
2433# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2434# collection.pid,
2435# file_extension,
2436# container.pid,
2437# new_pid,
2438# True)
2439#
2440# with open(icon_filename, 'wb+') as destination:
2441# for chunk in self.request.FILES['icon'].chunks():
2442# destination.write(chunk)
2443#
2444# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2445# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2446#
2447# if self.kwargs['article']:
2448# # Edit article
2449# article = self.kwargs['article']
2450# article.fpage = new_fpage
2451# article.lpage = new_lpage
2452# article.page_range = new_page_range
2453# article.coi_statement = new_coi_statement
2454# article.show_body = new_show_body
2455# article.do_not_publish = new_do_not_publish
2456# article.save()
2457#
2458# else:
2459# # New article
2460# params = {
2461# 'pid': new_pid,
2462# 'title_xml': new_title_xml,
2463# 'title_html': new_title_html,
2464# 'title_tex': new_title,
2465# 'fpage': new_fpage,
2466# 'lpage': new_lpage,
2467# 'page_range': new_page_range,
2468# 'seq': container.article_set.count() + 1,
2469# 'body': body,
2470# 'coi_statement': new_coi_statement,
2471# 'show_body': new_show_body,
2472# 'do_not_publish': new_do_not_publish
2473# }
2474#
2475# xarticle = create_articledata()
2476# xarticle.pid = new_pid
2477# xarticle.title_xml = new_title_xml
2478# xarticle.title_html = new_title_html
2479# xarticle.title_tex = new_title
2480# xarticle.fpage = new_fpage
2481# xarticle.lpage = new_lpage
2482# xarticle.page_range = new_page_range
2483# xarticle.seq = container.article_set.count() + 1
2484# xarticle.body = body
2485# xarticle.coi_statement = new_coi_statement
2486# params['xobj'] = xarticle
2487#
2488# cmd = ptf_cmds.addArticlePtfCmd(params)
2489# cmd.set_container(container)
2490# cmd.add_collection(container.my_collection)
2491# article = cmd.do()
2492#
2493# self.kwargs['pid'] = new_pid
2494#
2495# # Add objects related to the article: contribs, datastream, counts...
2496# params = {
2497# # 'title_xml': new_title_xml,
2498# # 'title_html': new_title_html,
2499# # 'title_tex': new_title,
2500# 'authors': new_authors,
2501# 'page_count': new_page_count,
2502# 'icon_location': new_icon_location,
2503# 'body': body,
2504# 'use_kwds': True,
2505# 'kwds_fr': new_kwds_fr,
2506# 'kwds_en': new_kwds_en,
2507# 'kwd_uns_fr': new_kwd_uns_fr,
2508# 'kwd_uns_en': new_kwd_uns_en
2509# }
2510# cmd = ptf_cmds.updateArticlePtfCmd(params)
2511# cmd.set_article(article)
2512# cmd.do()
2513#
2514# self.set_success_message()
2515#
2516# return super(ArticleEditView, self).form_valid(form)
2519@require_http_methods(["POST"])
2520def do_not_publish_article(request, *args, **kwargs):
2521 next = request.headers.get("referer")
2523 pid = kwargs.get("pid", "")
2525 article = model_helpers.get_article(pid)
2526 if article:
2527 article.do_not_publish = not article.do_not_publish
2528 article.save()
2529 else:
2530 raise Http404
2532 return HttpResponseRedirect(next)
2535@require_http_methods(["POST"])
2536def show_article_body(request, *args, **kwargs):
2537 next = request.headers.get("referer")
2539 pid = kwargs.get("pid", "")
2541 article = model_helpers.get_article(pid)
2542 if article:
2543 article.show_body = not article.show_body
2544 article.save()
2545 else:
2546 raise Http404
2548 return HttpResponseRedirect(next)
2551class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2552 """
2553 API to get/post article metadata
2554 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2555 """
2557 def __init__(self, *args, **kwargs):
2558 """
2559 we define here what fields we want in the form
2560 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2561 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2562 super().__init__(*args, **kwargs)
2563 self.fields_to_update = [
2564 "lang",
2565 "atype",
2566 "contributors",
2567 "abstracts",
2568 "kwds",
2569 "titles",
2570 "trans_title_html",
2571 "trans_title_xml",
2572 "trans_title_tex",
2573 "streams",
2574 ]
2575 self.article_container_pid = ""
2577 def save_data(self, data_article):
2578 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2579 # The icons are not preserved since we can add/edit/delete them in VueJs
2580 params = {
2581 "pid": data_article.pid,
2582 "export_folder": settings.MERSENNE_TMP_FOLDER,
2583 "export_all": True,
2584 "with_binary_files": False,
2585 }
2586 ptf_cmds.exportExtraDataPtfCmd(params).do()
2588 def restore_data(self, article):
2589 ptf_cmds.importExtraDataPtfCmd(
2590 {
2591 "pid": article.pid,
2592 "import_folder": settings.MERSENNE_TMP_FOLDER,
2593 }
2594 ).do()
2596 def get(self, request, *args, **kwargs):
2597 data = super().get(request, *args, **kwargs)
2598 return data
2600 def post(self, request, *args, **kwargs):
2601 response = super().post(request, *args, **kwargs)
2602 if response["message"] == "OK":
2603 return redirect(
2604 "api-edit-article",
2605 colid=kwargs.get("colid", ""),
2606 containerPid=kwargs.get("containerPid"),
2607 doi=kwargs.get("doi", ""),
2608 )
2609 else:
2610 raise Http404
2613class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2614 template_name = "article_form.html"
2616 def get_success_url(self):
2617 if self.kwargs["doi"]:
2618 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2619 return reverse("mersenne_dashboard/published_articles")
2621 def get_context_data(self, **kwargs):
2622 context = super().get_context_data(**kwargs)
2623 if "doi" in self.kwargs:
2624 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"])
2625 context["pid"] = context["article"].pid
2627 return context
2630class ArticleDeleteView(View):
2631 def get(self, request, *args, **kwargs):
2632 pid = self.kwargs.get("pid", None)
2633 article = get_object_or_404(Article, pid=pid)
2635 try:
2636 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2637 article.undeploy(mersenneSite)
2639 cmd = ptf_cmds.addArticlePtfCmd(
2640 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2641 )
2642 cmd.set_container(article.my_container)
2643 cmd.set_object_to_be_deleted(article)
2644 cmd.undo()
2645 except Exception as exception:
2646 return HttpResponseServerError(exception)
2648 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2649 return JsonResponse(data)
2652def get_messages_in_queue():
2653 app = Celery("ptf-tools")
2654 # tasks = list(current_app.tasks)
2655 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2656 print(tasks)
2657 # i = app.control.inspect()
2659 with app.connection_or_acquire() as conn:
2660 remaining = conn.default_channel.queue_declare(queue="celery", passive=True).message_count
2661 return remaining
2664class FailedTasksListView(ListView):
2665 model = TaskResult
2666 queryset = TaskResult.objects.filter(
2667 status="FAILURE",
2668 task_name="ptf_tools.tasks.archive_numdam_issue",
2669 )
2672class FailedTasksDeleteView(DeleteView):
2673 model = TaskResult
2674 success_url = reverse_lazy("tasks-failed")
2677class FailedTasksRetryView(SingleObjectMixin, RedirectView):
2678 model = TaskResult
2680 @staticmethod
2681 def retry_task(task):
2682 colid, pid = (arg.strip("'") for arg in task.task_args.strip("()").split(", "))
2683 archive_numdam_issue.delay(colid, pid)
2684 task.delete()
2686 def get_redirect_url(self, *args, **kwargs):
2687 self.retry_task(self.get_object())
2688 return reverse("tasks-failed")
2691class NumdamView(TemplateView, history_views.HistoryContextMixin):
2692 template_name = "numdam.html"
2694 def get_context_data(self, **kwargs):
2695 context = super().get_context_data(**kwargs)
2697 context["objs"] = ResourceInNumdam.objects.all()
2699 pre_issues = []
2700 prod_issues = []
2701 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2702 try:
2703 response = requests.get(url)
2704 if response.status_code == 200:
2705 data = response.json()
2706 if "issues" in data:
2707 pre_issues = data["issues"]
2708 except Exception:
2709 pass
2711 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2712 response = requests.get(url)
2713 if response.status_code == 200:
2714 data = response.json()
2715 if "issues" in data:
2716 prod_issues = data["issues"]
2718 new = sorted(list(set(pre_issues).difference(prod_issues)))
2719 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2720 grouped = [
2721 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2722 ]
2723 grouped_removed = [
2724 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2725 ]
2726 context["added_issues"] = grouped
2727 context["removed_issues"] = grouped_removed
2729 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2730 return context
2733class TasksProgressView(View):
2734 def get(self, *args, **kwargs):
2735 task_name = self.kwargs.get("task", "archive_numdam_issue")
2736 successes = TaskResult.objects.filter(
2737 task_name=f"ptf_tools.tasks.{task_name}", status="SUCCESS"
2738 ).count()
2739 fails = TaskResult.objects.filter(
2740 task_name=f"ptf_tools.tasks.{task_name}", status="FAILURE"
2741 ).count()
2742 last_task = (
2743 TaskResult.objects.filter(
2744 task_name=f"ptf_tools.tasks.{task_name}",
2745 status="SUCCESS",
2746 )
2747 .order_by("-date_done")
2748 .first()
2749 )
2750 if last_task:
2751 last_task = " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
2752 remaining = get_messages_in_queue()
2753 all = successes + remaining
2754 progress = int(successes * 100 / all) if all else 0
2755 error_rate = int(fails * 100 / all) if all else 0
2756 status = "consuming_queue" if (successes or fails) and not progress == 100 else "polling"
2757 data = {
2758 "status": status,
2759 "progress": progress,
2760 "total": all,
2761 "remaining": remaining,
2762 "successes": successes,
2763 "fails": fails,
2764 "error_rate": error_rate,
2765 "last_task": last_task,
2766 }
2767 return JsonResponse(data)
2770class TasksView(TemplateView):
2771 template_name = "tasks.html"
2773 def get_context_data(self, **kwargs):
2774 context = super().get_context_data(**kwargs)
2775 context["tasks"] = TaskResult.objects.all()
2776 return context
2779class NumdamArchiveView(RedirectView):
2780 @staticmethod
2781 def reset_task_results():
2782 TaskResult.objects.all().delete()
2784 def get_redirect_url(self, *args, **kwargs):
2785 self.colid = kwargs["colid"]
2787 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2788 return Http404
2790 # we make sure archiving is not already running
2791 if not get_messages_in_queue():
2792 self.reset_task_results()
2793 response = requests.get(f"{settings.NUMDAM_URL}/api-all-collections/")
2794 if response.status_code == 200:
2795 data = sorted(response.json()["collections"])
2797 if self.colid != "ALL" and self.colid not in data:
2798 return Http404
2800 colids = [self.colid] if self.colid != "ALL" else data
2802 with open(
2803 os.path.join(settings.LOG_DIR, "archive.log"), "w", encoding="utf-8"
2804 ) as file_:
2805 file_.write("Archive " + " ".join([colid for colid in colids]) + "\n")
2807 for colid in colids:
2808 if colid not in settings.MERSENNE_COLLECTIONS:
2809 archive_numdam_collection.delay(colid)
2810 return reverse("numdam")
2813class DeployAllNumdamAPIView(View):
2814 def internal_do(self, *args, **kwargs):
2815 pids = []
2817 for obj in ResourceInNumdam.objects.all():
2818 pids.append(obj.pid)
2820 return pids
2822 def get(self, request, *args, **kwargs):
2823 try:
2824 pids, status, message = history_views.execute_and_record_func(
2825 "deploy", "numdam", "numdam", self.internal_do, "numdam"
2826 )
2827 except Exception as exception:
2828 return HttpResponseServerError(exception)
2830 data = {"message": message, "ids": pids, "status": status}
2831 return JsonResponse(data)
2834class NumdamDeleteAPIView(View):
2835 def get(self, request, *args, **kwargs):
2836 pid = self.kwargs.get("pid", None)
2838 try:
2839 obj = ResourceInNumdam.objects.get(pid=pid)
2840 obj.delete()
2841 except Exception as exception:
2842 return HttpResponseServerError(exception)
2844 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
2845 return JsonResponse(data)
2848class ExtIdApiDetail(View):
2849 def get(self, request, *args, **kwargs):
2850 extid = get_object_or_404(
2851 ExtId,
2852 resource__pid=kwargs["pid"],
2853 id_type=kwargs["what"],
2854 )
2855 return JsonResponse(
2856 {
2857 "pk": extid.pk,
2858 "href": extid.get_href(),
2859 "fetch": reverse(
2860 "api-fetch-id",
2861 args=(
2862 extid.resource.pk,
2863 extid.id_value,
2864 extid.id_type,
2865 "extid",
2866 ),
2867 ),
2868 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
2869 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
2870 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
2871 "delete": reverse("update-extid", args=(extid.pk, "delete")),
2872 "is_valid": extid.checked,
2873 }
2874 )
2877class ExtIdFormTemplate(TemplateView):
2878 template_name = "common/externalid_form.html"
2880 def get_context_data(self, **kwargs):
2881 context = super().get_context_data(**kwargs)
2882 context["sequence"] = kwargs["sequence"]
2883 return context
2886class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
2887 def get_context_data(self, **kwargs):
2888 context = super().get_context_data(**kwargs)
2889 context["helper"] = PtfFormHelper
2890 return context
2892 def get_success_url(self):
2893 self.post_process()
2894 return self.object.bibitem.resource.get_absolute_url()
2896 def post_process(self):
2897 cmd = xml_cmds.updateBibitemCitationXmlCmd()
2898 cmd.set_bibitem(self.object.bibitem)
2899 cmd.do()
2900 model_helpers.post_resource_updated(self.object.bibitem.resource)
2903class BibItemIdCreate(BibItemIdFormView, CreateView):
2904 model = BibItemId
2905 form_class = BibItemIdForm
2907 def get_context_data(self, **kwargs):
2908 context = super().get_context_data(**kwargs)
2909 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
2910 return context
2912 def get_initial(self):
2913 initial = super().get_initial()
2914 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
2915 return initial
2917 def form_valid(self, form):
2918 form.instance.checked = False
2919 return super().form_valid(form)
2922class BibItemIdUpdate(BibItemIdFormView, UpdateView):
2923 model = BibItemId
2924 form_class = BibItemIdForm
2926 def get_context_data(self, **kwargs):
2927 context = super().get_context_data(**kwargs)
2928 context["bibitem"] = self.object.bibitem
2929 return context
2932class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
2933 def get_context_data(self, **kwargs):
2934 context = super().get_context_data(**kwargs)
2935 context["helper"] = PtfFormHelper
2936 return context
2938 def get_success_url(self):
2939 self.post_process()
2940 return self.object.resource.get_absolute_url()
2942 def post_process(self):
2943 model_helpers.post_resource_updated(self.object.resource)
2946class ExtIdCreate(ExtIdFormView, CreateView):
2947 model = ExtId
2948 form_class = ExtIdForm
2950 def get_context_data(self, **kwargs):
2951 context = super().get_context_data(**kwargs)
2952 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
2953 return context
2955 def get_initial(self):
2956 initial = super().get_initial()
2957 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
2958 return initial
2960 def form_valid(self, form):
2961 form.instance.checked = False
2962 return super().form_valid(form)
2965class ExtIdUpdate(ExtIdFormView, UpdateView):
2966 model = ExtId
2967 form_class = ExtIdForm
2969 def get_context_data(self, **kwargs):
2970 context = super().get_context_data(**kwargs)
2971 context["resource"] = self.object.resource
2972 return context
2975class BibItemIdApiDetail(View):
2976 def get(self, request, *args, **kwargs):
2977 bibitemid = get_object_or_404(
2978 BibItemId,
2979 bibitem__resource__pid=kwargs["pid"],
2980 bibitem__sequence=kwargs["seq"],
2981 id_type=kwargs["what"],
2982 )
2983 return JsonResponse(
2984 {
2985 "pk": bibitemid.pk,
2986 "href": bibitemid.get_href(),
2987 "fetch": reverse(
2988 "api-fetch-id",
2989 args=(
2990 bibitemid.bibitem.pk,
2991 bibitemid.id_value,
2992 bibitemid.id_type,
2993 "bibitemid",
2994 ),
2995 ),
2996 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
2997 "uncheck": reverse(
2998 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
2999 ),
3000 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
3001 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
3002 "is_valid": bibitemid.checked,
3003 }
3004 )
3007class UpdateTexmfZipAPIView(View):
3008 def get(self, request, *args, **kwargs):
3009 def copy_zip_files(src_folder, dest_folder):
3010 os.makedirs(dest_folder, exist_ok=True)
3012 zip_files = [
3013 os.path.join(src_folder, f)
3014 for f in os.listdir(src_folder)
3015 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3016 ]
3017 for zip_file in zip_files:
3018 resolver.copy_file(zip_file, dest_folder)
3020 # Exceptions: specific zip/gz files
3021 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3022 resolver.copy_file(zip_file, dest_folder)
3024 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3025 resolver.copy_file(zip_file, dest_folder)
3027 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3028 resolver.copy_file(gz_file, dest_folder)
3030 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3032 dest_folder = os.path.join(
3033 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3034 )
3036 try:
3037 copy_zip_files(src_folder, dest_folder)
3038 except Exception as exception:
3039 return HttpResponseServerError(exception)
3041 try:
3042 dest_folder = os.path.join(
3043 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3044 )
3045 copy_zip_files(src_folder, dest_folder)
3046 except Exception as exception:
3047 return HttpResponseServerError(exception)
3049 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3050 return JsonResponse(data)
3053class TestView(TemplateView):
3054 template_name = "mersenne.html"
3056 def get_context_data(self, **kwargs):
3057 super().get_context_data(**kwargs)
3058 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True)
3059 model_data_converter.db_to_issue_data(issue)
3062class TrammelArchiveView(RedirectView):
3063 @staticmethod
3064 def reset_task_results():
3065 TaskResult.objects.all().delete()
3067 def get_redirect_url(self, *args, **kwargs):
3068 self.colid = kwargs["colid"]
3069 self.mathdoc_archive = settings.MATHDOC_ARCHIVE_FOLDER
3070 self.binary_files_folder = settings.MERSENNE_PROD_DATA_FOLDER
3071 # Make sure archiving is not already running
3072 if not get_messages_in_queue():
3073 self.reset_task_results()
3074 if "progress/" in self.colid:
3075 self.colid = self.colid.replace("progress/", "")
3076 if "/progress" in self.colid:
3077 self.colid = self.colid.replace("/progress", "")
3079 if self.colid != "ALL" and self.colid not in settings.MERSENNE_COLLECTIONS:
3080 return Http404
3082 colids = [self.colid] if self.colid != "ALL" else settings.MERSENNE_COLLECTIONS
3084 with open(
3085 os.path.join(settings.LOG_DIR, "archive.log"), "w", encoding="utf-8"
3086 ) as file_:
3087 file_.write("Archive " + " ".join([colid for colid in colids]) + "\n")
3089 for colid in colids:
3090 archive_trammel_collection.delay(
3091 colid, self.mathdoc_archive, self.binary_files_folder
3092 )
3094 if self.colid == "ALL":
3095 return reverse("home")
3096 else:
3097 return reverse("collection-detail", kwargs={"pid": self.colid})
3100class TrammelTasksProgressView(View):
3101 def get(self, request, *args, **kwargs):
3102 """
3103 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3104 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3105 """
3106 task_name = self.kwargs.get("task", "archive_numdam_issue")
3108 def get_event_data():
3109 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3111 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3112 remaining_messages = get_messages_in_queue()
3114 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3115 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3116 failed_tasks = all_tasks.filter(status="FAILURE")
3118 all_tasks_count = all_tasks.count()
3119 success_count = successed_tasks.count()
3120 fail_count = failed_tasks.count()
3122 all_count = all_tasks_count + remaining_messages
3123 remaining_count = all_count - success_count - fail_count
3125 success_rate = int(success_count * 100 / all_count) if all_count else 0
3126 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3127 status = "consuming_queue" if remaining_count != 0 else "polling"
3129 last_task = successed_tasks.first()
3130 last_task = (
3131 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3132 if last_task
3133 else ""
3134 )
3136 # SSE event format
3137 event_data = {
3138 "status": status,
3139 "success_rate": success_rate,
3140 "error_rate": error_rate,
3141 "all_count": all_count,
3142 "remaining_count": remaining_count,
3143 "success_count": success_count,
3144 "fail_count": fail_count,
3145 "last_task": last_task,
3146 }
3148 return event_data
3150 def stream_response(data):
3151 # Send initial response headers
3152 yield f"data: {json.dumps(data)}\n\n"
3154 data = get_event_data()
3155 format = request.GET.get("format", "stream")
3156 if format == "json":
3157 response = JsonResponse(data)
3158 else:
3159 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3160 return response
3163class TrammelFailedTasksListView(ListView):
3164 model = TaskResult
3165 queryset = TaskResult.objects.filter(
3166 status="FAILURE",
3167 task_name="ptf_tools.tasks.archive_trammel_resource",
3168 )