Coverage for sites/ptf_tools/ptf_tools/views/base_views.py: 20%
1640 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-11-04 17:46 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2024-11-04 17:46 +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
11from braces.views import LoginRequiredMixin
12from braces.views import StaffuserRequiredMixin
13from celery import Celery
14from celery import current_app
15from django_celery_results.models import TaskResult
16from extra_views import CreateWithInlinesView
17from extra_views import InlineFormSetFactory
18from extra_views import NamedFormsetsMixin
19from extra_views import UpdateWithInlinesView
20from requests import Timeout
22from django.conf import settings
23from django.contrib import messages
24from django.contrib.auth.mixins import UserPassesTestMixin
25from django.db.models import Q
26from django.http import Http404
27from django.http import HttpRequest
28from django.http import HttpResponse
29from django.http import HttpResponseRedirect
30from django.http import HttpResponseServerError
31from django.http import JsonResponse
32from django.shortcuts import get_object_or_404
33from django.shortcuts import redirect
34from django.shortcuts import render
35from django.urls import resolve
36from django.urls import reverse
37from django.urls import reverse_lazy
38from django.utils import timezone
39from django.views.decorators.http import require_http_methods
40from django.views.generic import ListView
41from django.views.generic import TemplateView
42from django.views.generic import View
43from django.views.generic.base import RedirectView
44from django.views.generic.detail import SingleObjectMixin
45from django.views.generic.edit import CreateView
46from django.views.generic.edit import DeleteView
47from django.views.generic.edit import FormView
48from django.views.generic.edit import UpdateView
50from comments_moderation.utils import get_comments_for_home
51from comments_moderation.utils import is_comment_moderator
52from history import models as history_models
53from history import views as history_views
54from ptf import model_data_converter
55from ptf import model_helpers
56from ptf import tex
57from ptf import utils
58from ptf.cmds import ptf_cmds
59from ptf.cmds import xml_cmds
60from ptf.cmds.base_cmds import make_int
61from ptf.cmds.xml.jats.builder.issue import get_issue_title_xml
62from ptf.cmds.xml.xml_utils import replace_html_entities
63from ptf.display import resolver
64from ptf.exceptions import DOIException
65from ptf.exceptions import PDFException
66from ptf.exceptions import ServerUnderMaintenance
67from ptf.model_data import create_issuedata
68from ptf.model_data import create_publisherdata
69from ptf.models import Abstract
70from ptf.models import Article
71from ptf.models import BibItem
72from ptf.models import BibItemId
73from ptf.models import Collection
74from ptf.models import Container
75from ptf.models import ExtId
76from ptf.models import ExtLink
77from ptf.models import Resource
78from ptf.models import ResourceId
79from ptf.views import ArticleEditAPIView
80from ptf_tools.doaj import doaj_pid_register
81from ptf_tools.doi import get_or_create_doibatch
82from ptf_tools.doi import recordDOI
83from ptf_tools.forms import BibItemIdForm
84from ptf_tools.forms import CollectionForm
85from ptf_tools.forms import ContainerForm
86from ptf_tools.forms import DiffContainerForm
87from ptf_tools.forms import ExtIdForm
88from ptf_tools.forms import ExtLinkForm
89from ptf_tools.forms import FormSetHelper
90from ptf_tools.forms import ImportArticleForm
91from ptf_tools.forms import ImportContainerForm
92from ptf_tools.forms import PtfFormHelper
93from ptf_tools.forms import PtfLargeModalFormHelper
94from ptf_tools.forms import PtfModalFormHelper
95from ptf_tools.forms import RegisterPubmedForm
96from ptf_tools.forms import ResourceIdForm
97from ptf_tools.forms import get_article_choices
98from ptf_tools.models import ResourceInNumdam
99from ptf_tools.tasks import archive_numdam_collection
100from ptf_tools.tasks import archive_numdam_issue
101from ptf_tools.tasks import archive_trammel_collection
102from ptf_tools.tasks import archive_trammel_resource
103from ptf_tools.templatetags.tools_helpers import get_authorized_collections
104from ptf_tools.utils import is_authorized_editor
105from pubmed.views import recordPubmed
108def view_404(request: HttpRequest):
109 """
110 Dummy view raising HTTP 404 exception.
111 """
112 raise Http404
115def check_collection(collection, server_url, server_type):
116 """
117 Check if a collection exists on a serveur (test/prod)
118 and upload the collection (XML, image) if necessary
119 """
121 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
122 response = requests.get(url, verify=False)
123 # First, upload the collection XML
124 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
125 body = xml.encode("utf8")
127 url = server_url + reverse("upload-serials")
128 if response.status_code == 200:
129 # PUT http verb is used for update
130 response = requests.put(url, data=body, verify=False)
131 else:
132 # POST http verb is used for creation
133 response = requests.post(url, data=body, verify=False)
135 # Second, copy the collection images
136 # There is no need to copy files for the test server
137 # Files were already copied in /mersenne_test_data during the ptf_tools import
138 # We only need to copy files from /mersenne_test_data to
139 # /mersenne_prod_data during an upload to prod
140 if server_type == "website":
141 resolver.copy_binary_files(
142 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
143 )
144 elif server_type == "numdam":
145 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
146 if collection.pid in settings.NUMDAM_COLLECTIONS:
147 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
149 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
152def check_lock():
153 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
156def load_cedrics_article_choices(request):
157 colid = request.GET.get("colid")
158 issue = request.GET.get("issue")
159 article_choices = get_article_choices(colid, issue)
160 return render(
161 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
162 )
165class ImportCedricsArticleFormView(FormView):
166 template_name = "import_article.html"
167 form_class = ImportArticleForm
169 def dispatch(self, request, *args, **kwargs):
170 self.colid = self.kwargs["colid"]
171 return super().dispatch(request, *args, **kwargs)
173 def get_success_url(self):
174 if self.colid:
175 return reverse("collection-detail", kwargs={"pid": self.colid})
176 return "/"
178 def get_context_data(self, **kwargs):
179 context = super().get_context_data(**kwargs)
180 context["colid"] = self.colid
181 context["helper"] = PtfModalFormHelper
182 return context
184 def get_form_kwargs(self):
185 kwargs = super().get_form_kwargs()
186 kwargs["colid"] = self.colid
187 return kwargs
189 def form_valid(self, form):
190 self.issue = form.cleaned_data["issue"]
191 self.article = form.cleaned_data["article"]
192 return super().form_valid(form)
194 def import_cedrics_article(self, *args, **kwargs):
195 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
196 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
197 )
198 cmd.do()
200 def post(self, request, *args, **kwargs):
201 self.colid = self.kwargs.get("colid", None)
202 issue = request.POST["issue"]
203 self.article_pid = request.POST["article"]
204 self.issue_pid = os.path.basename(os.path.dirname(issue))
206 import_args = [self]
207 import_kwargs = {}
209 try:
210 _, status, message = history_views.execute_and_record_func(
211 "import",
212 f"{self.issue_pid} / {self.article_pid}",
213 self.colid,
214 self.import_cedrics_article,
215 "",
216 False,
217 *import_args,
218 **import_kwargs,
219 )
221 messages.success(
222 self.request, f"L'article {self.article_pid} a été importé avec succès"
223 )
225 except Exception as exception:
226 messages.error(
227 self.request,
228 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}",
229 )
231 return redirect(self.get_success_url())
234class ImportCedricsIssueView(FormView):
235 template_name = "import_container.html"
236 form_class = ImportContainerForm
238 def dispatch(self, request, *args, **kwargs):
239 self.colid = self.kwargs["colid"]
240 self.to_appear = self.request.GET.get("to_appear", False)
241 return super().dispatch(request, *args, **kwargs)
243 def get_success_url(self):
244 if self.filename:
245 return reverse(
246 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
247 )
248 return "/"
250 def get_context_data(self, **kwargs):
251 context = super().get_context_data(**kwargs)
252 context["colid"] = self.colid
253 context["helper"] = PtfModalFormHelper
254 return context
256 def get_form_kwargs(self):
257 kwargs = super().get_form_kwargs()
258 kwargs["colid"] = self.colid
259 kwargs["to_appear"] = self.to_appear
260 return kwargs
262 def form_valid(self, form):
263 self.filename = form.cleaned_data["filename"].split("/")[-1]
264 return super().form_valid(form)
267class DiffCedricsIssueView(FormView):
268 template_name = "diff_container_form.html"
269 form_class = DiffContainerForm
270 diffs = None
271 xissue = None
272 xissue_encoded = None
274 def get_success_url(self):
275 return reverse("collection-detail", kwargs={"pid": self.colid})
277 def dispatch(self, request, *args, **kwargs):
278 self.colid = self.kwargs["colid"]
279 # self.filename = self.kwargs['filename']
280 return super().dispatch(request, *args, **kwargs)
282 def get(self, request, *args, **kwargs):
283 self.filename = request.GET["filename"]
284 self.remove_mail = request.GET["remove_email"]
285 self.remove_date_prod = request.GET["remove_date_prod"]
287 try:
288 result, status, message = history_views.execute_and_record_func(
289 "import",
290 os.path.basename(self.filename),
291 self.colid,
292 self.diff_cedrics_issue,
293 "",
294 True,
295 )
296 except Exception as exception:
297 pid = self.filename.split("/")[-1]
298 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
299 return HttpResponseRedirect(self.get_success_url())
301 no_conflict = result[0]
302 self.diffs = result[1]
303 self.xissue = result[2]
305 if no_conflict:
306 # Proceed with the import
307 self.form_valid(self.get_form())
308 return redirect(self.get_success_url())
309 else:
310 # Display the diff template
311 self.xissue_encoded = jsonpickle.encode(self.xissue)
313 return super().get(request, *args, **kwargs)
315 def post(self, request, *args, **kwargs):
316 self.filename = request.POST["filename"]
317 data = request.POST["xissue_encoded"]
318 self.xissue = jsonpickle.decode(data)
320 return super().post(request, *args, **kwargs)
322 def get_context_data(self, **kwargs):
323 context = super().get_context_data(**kwargs)
324 context["colid"] = self.colid
325 context["diff"] = self.diffs
326 context["filename"] = self.filename
327 context["xissue_encoded"] = self.xissue_encoded
328 return context
330 def get_form_kwargs(self):
331 kwargs = super().get_form_kwargs()
332 kwargs["colid"] = self.colid
333 return kwargs
335 def diff_cedrics_issue(self, *args, **kwargs):
336 params = {
337 "colid": self.colid,
338 "input_file": self.filename,
339 "remove_email": self.remove_mail,
340 "remove_date_prod": self.remove_date_prod,
341 "diff_only": True,
342 }
344 if settings.IMPORT_CEDRICS_DIRECTLY:
345 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
346 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
347 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
348 else:
349 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
351 result = cmd.do()
352 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
353 messages.warning(
354 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings
355 )
357 return result
359 def import_cedrics_issue(self, *args, **kwargs):
360 # modify xissue with data_issue if params to override
361 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
362 issue = model_helpers.get_container(self.xissue.pid)
363 if issue:
364 data_issue = model_data_converter.db_to_issue_data(issue)
365 for xarticle in self.xissue.articles:
366 filter_articles = [
367 article for article in data_issue.articles if article.doi == xarticle.doi
368 ]
369 if len(filter_articles) > 0:
370 db_article = filter_articles[0]
371 xarticle.coi_statement = db_article.coi_statement
372 xarticle.kwds = db_article.kwds
373 xarticle.contrib_groups = db_article.contrib_groups
375 params = {
376 "colid": self.colid,
377 "xissue": self.xissue,
378 "input_file": self.filename,
379 }
381 if settings.IMPORT_CEDRICS_DIRECTLY:
382 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
383 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
384 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
385 else:
386 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
388 cmd.do()
390 def form_valid(self, form):
391 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
392 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
393 else:
394 import_kwargs = {}
395 import_args = [self]
397 try:
398 _, status, message = history_views.execute_and_record_func(
399 "import",
400 self.xissue.pid,
401 self.kwargs["colid"],
402 self.import_cedrics_issue,
403 "",
404 False,
405 *import_args,
406 **import_kwargs,
407 )
408 except Exception as exception:
409 messages.error(
410 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
411 )
412 return super().form_invalid(form)
414 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
415 return super().form_valid(form)
418class BibtexAPIView(View):
419 def get(self, request, *args, **kwargs):
420 pid = self.kwargs.get("pid", None)
421 all_bibtex = ""
422 if pid:
423 article = model_helpers.get_article(pid)
424 if article:
425 for bibitem in article.bibitem_set.all():
426 bibtex_array = bibitem.get_bibtex()
427 last = len(bibtex_array)
428 i = 1
429 for bibtex in bibtex_array:
430 if i > 1 and i < last:
431 all_bibtex += " "
432 all_bibtex += bibtex + "\n"
433 i += 1
435 data = {"bibtex": all_bibtex}
436 return JsonResponse(data)
439class MatchingAPIView(View):
440 def get(self, request, *args, **kwargs):
441 pid = self.kwargs.get("pid", None)
443 url = settings.MATCHING_URL
444 headers = {"Content-Type": "application/xml"}
446 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
448 if settings.DEBUG:
449 print("Issue exported to /tmp/issue.xml")
450 f = open("/tmp/issue.xml", "w")
451 f.write(body.encode("utf8"))
452 f.close()
454 r = requests.post(url, data=body.encode("utf8"), headers=headers)
455 body = r.text.encode("utf8")
456 data = {"status": r.status_code, "message": body[:1000]}
458 if settings.DEBUG:
459 print("Matching received, new issue exported to /tmp/issue1.xml")
460 f = open("/tmp/issue1.xml", "w")
461 text = body
462 f.write(text)
463 f.close()
465 resource = model_helpers.get_resource(pid)
466 obj = resource.cast()
467 colid = obj.get_collection().pid
469 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
471 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
472 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
473 )
474 cmd.do()
476 print("Matching finished")
477 return JsonResponse(data)
480class ImportAllAPIView(View):
481 def internal_do(self, *args, **kwargs):
482 pid = self.kwargs.get("pid", None)
484 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
485 if not os.path.isdir(root_folder):
486 raise ValueError(root_folder + " does not exist")
488 resource = model_helpers.get_resource(pid)
489 if not resource:
490 file = os.path.join(root_folder, pid + ".xml")
491 body = utils.get_file_content_in_utf8(file)
492 journals = xml_cmds.addCollectionsXmlCmd(
493 {
494 "body": body,
495 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
496 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
497 }
498 ).do()
499 if not journals:
500 raise ValueError(file + " does not contain a collection")
501 resource = journals[0]
502 # resolver.copy_binary_files(
503 # resource,
504 # settings.MATHDOC_ARCHIVE_FOLDER,
505 # settings.MERSENNE_TEST_DATA_FOLDER)
507 obj = resource.cast()
509 if obj.classname != "Collection":
510 raise ValueError(pid + " does not contain a collection")
512 cmd = xml_cmds.collectEntireCollectionXmlCmd(
513 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
514 )
515 pids = cmd.do()
517 return pids
519 def get(self, request, *args, **kwargs):
520 pid = self.kwargs.get("pid", None)
522 try:
523 pids, status, message = history_views.execute_and_record_func(
524 "import", pid, pid, self.internal_do
525 )
526 except Timeout as exception:
527 return HttpResponse(exception, status=408)
528 except Exception as exception:
529 return HttpResponseServerError(exception)
531 data = {"message": message, "ids": pids, "status": status}
532 return JsonResponse(data)
535class DeployAllAPIView(View):
536 def internal_do(self, *args, **kwargs):
537 pid = self.kwargs.get("pid", None)
538 site = self.kwargs.get("site", None)
540 pids = []
542 collection = model_helpers.get_collection(pid)
543 if not collection:
544 raise RuntimeError(pid + " does not exist")
546 if site == "numdam":
547 server_url = settings.NUMDAM_PRE_URL
548 elif site != "ptf_tools":
549 server_url = getattr(collection, site)()
550 if not server_url:
551 raise RuntimeError("The collection has no " + site)
553 if site != "ptf_tools":
554 # check if the collection exists on the server
555 # if not, check_collection will upload the collection (XML,
556 # image...)
557 check_collection(collection, server_url, site)
559 for issue in collection.content.all():
560 if site != "website" or (site == "website" and issue.are_all_articles_published()):
561 pids.append(issue.pid)
563 return pids
565 def get(self, request, *args, **kwargs):
566 pid = self.kwargs.get("pid", None)
567 site = self.kwargs.get("site", None)
569 try:
570 pids, status, message = history_views.execute_and_record_func(
571 "deploy", pid, pid, self.internal_do, site
572 )
573 except Timeout as exception:
574 return HttpResponse(exception, status=408)
575 except Exception as exception:
576 return HttpResponseServerError(exception)
578 data = {"message": message, "ids": pids, "status": status}
579 return JsonResponse(data)
582class AddIssuePDFView(View):
583 def __init(self, *args, **kwargs):
584 super().__init__(*args, **kwargs)
585 self.pid = None
586 self.issue = None
587 self.collection = None
588 self.site = "test_website"
590 def post_to_site(self, url):
591 response = requests.post(url, verify=False)
592 status = response.status_code
593 if not (199 < status < 205):
594 messages.error(self.request, response.text)
595 if status == 503:
596 raise ServerUnderMaintenance(response.text)
597 else:
598 raise RuntimeError(response.text)
600 def internal_do(self, *args, **kwargs):
601 """
602 Called by history_views.execute_and_record_func to do the actual job.
603 """
605 issue_pid = self.issue.pid
606 colid = self.collection.pid
608 if self.site == "website":
609 # Copy the PDF from the test to the production folder
610 resolver.copy_binary_files(
611 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
612 )
613 else:
614 # Copy the PDF from the cedram to the test folder
615 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
616 from_path = os.path.join(from_folder, issue_pid + ".pdf")
617 if not os.path.isfile(from_path):
618 raise Http404(f"{from_path} does not exist")
620 to_path = resolver.get_disk_location(
621 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
622 )
623 resolver.copy_file(from_path, to_path)
625 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
627 if self.site == "test_website":
628 # Post to ptf-tools: it will add a Datastream to the issue
629 absolute_url = self.request.build_absolute_uri(url)
630 self.post_to_site(absolute_url)
632 server_url = getattr(self.collection, self.site)()
633 absolute_url = server_url + url
634 # Post to the test or production website
635 self.post_to_site(absolute_url)
637 def get(self, request, *args, **kwargs):
638 """
639 Send an issue PDF to the test or production website
640 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
641 :param args:
642 :param kwargs:
643 :return:
644 """
645 if check_lock():
646 m = "Trammel is under maintenance. Please try again later."
647 messages.error(self.request, m)
648 return JsonResponse({"message": m, "status": 503})
650 self.pid = self.kwargs.get("pid", None)
651 self.site = self.kwargs.get("site", "test_website")
653 self.issue = model_helpers.get_container(self.pid)
654 if not self.issue:
655 raise Http404(f"{self.pid} does not exist")
656 self.collection = self.issue.get_top_collection()
658 try:
659 pids, status, message = history_views.execute_and_record_func(
660 "deploy",
661 self.pid,
662 self.collection.pid,
663 self.internal_do,
664 f"add issue PDF to {self.site}",
665 )
667 except Timeout as exception:
668 return HttpResponse(exception, status=408)
669 except Exception as exception:
670 return HttpResponseServerError(exception)
672 data = {"message": message, "status": status}
673 return JsonResponse(data)
676class ArchiveAllAPIView(View):
677 """
678 - archive le xml de la collection ainsi que les binaires liés
679 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
680 @return array of issues pid
681 """
683 def internal_do(self, *args, **kwargs):
684 collection = kwargs["collection"]
685 pids = []
686 colid = collection.pid
688 logfile = os.path.join(settings.LOG_DIR, "archive.log")
689 if os.path.isfile(logfile):
690 os.remove(logfile)
692 ptf_cmds.exportPtfCmd(
693 {
694 "pid": colid,
695 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
696 "with_binary_files": True,
697 "for_archive": True,
698 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
699 }
700 ).do()
702 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
703 if os.path.isfile(cedramcls):
704 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
705 resolver.create_folder(dest_folder)
706 resolver.copy_file(cedramcls, dest_folder)
708 for issue in collection.content.all():
709 qs = issue.article_set.filter(
710 date_online_first__isnull=True, date_published__isnull=True
711 )
712 if qs.count() == 0:
713 pids.append(issue.pid)
715 return pids
717 def get(self, request, *args, **kwargs):
718 pid = self.kwargs.get("pid", None)
720 collection = model_helpers.get_collection(pid)
721 if not collection:
722 return HttpResponse(f"{pid} does not exist", status=400)
724 dict_ = {"collection": collection}
725 args_ = [self]
727 try:
728 pids, status, message = history_views.execute_and_record_func(
729 "archive", pid, pid, self.internal_do, "", False, *args_, **dict_
730 )
731 except Timeout as exception:
732 return HttpResponse(exception, status=408)
733 except Exception as exception:
734 return HttpResponseServerError(exception)
736 data = {"message": message, "ids": pids, "status": status}
737 return JsonResponse(data)
740class CreateAllDjvuAPIView(View):
741 def internal_do(self, *args, **kwargs):
742 issue = kwargs["issue"]
743 pids = [issue.pid]
745 for article in issue.article_set.all():
746 pids.append(article.pid)
748 return pids
750 def get(self, request, *args, **kwargs):
751 pid = self.kwargs.get("pid", None)
752 issue = model_helpers.get_container(pid)
753 if not issue:
754 raise Http404(f"{pid} does not exist")
756 try:
757 dict_ = {"issue": issue}
758 args_ = [self]
760 pids, status, message = history_views.execute_and_record_func(
761 "numdam",
762 pid,
763 issue.get_collection().pid,
764 self.internal_do,
765 "",
766 False,
767 *args_,
768 **dict_,
769 )
770 except Exception as exception:
771 return HttpResponseServerError(exception)
773 data = {"message": message, "ids": pids, "status": status}
774 return JsonResponse(data)
777class ImportJatsContainerAPIView(View):
778 def internal_do(self, *args, **kwargs):
779 pid = self.kwargs.get("pid", None)
780 colid = self.kwargs.get("colid", None)
782 if pid and colid:
783 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
785 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
786 {
787 "body": body,
788 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
789 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
790 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
791 }
792 )
793 container = cmd.do()
794 if len(cmd.warnings) > 0:
795 messages.warning(
796 self.request,
797 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
798 )
800 if not container:
801 raise RuntimeError("Error: the container " + pid + " was not imported")
803 # resolver.copy_binary_files(
804 # container,
805 # settings.MATHDOC_ARCHIVE_FOLDER,
806 # settings.MERSENNE_TEST_DATA_FOLDER)
807 #
808 # for article in container.article_set.all():
809 # resolver.copy_binary_files(
810 # article,
811 # settings.MATHDOC_ARCHIVE_FOLDER,
812 # settings.MERSENNE_TEST_DATA_FOLDER)
813 else:
814 raise RuntimeError("colid or pid are not defined")
816 def get(self, request, *args, **kwargs):
817 pid = self.kwargs.get("pid", None)
818 colid = self.kwargs.get("colid", None)
820 try:
821 _, status, message = history_views.execute_and_record_func(
822 "import", pid, colid, self.internal_do
823 )
824 except Timeout as exception:
825 return HttpResponse(exception, status=408)
826 except Exception as exception:
827 return HttpResponseServerError(exception)
829 data = {"message": message, "status": status}
830 return JsonResponse(data)
833class DeployCollectionAPIView(View):
834 # Update collection.xml on a site (with its images)
836 def internal_do(self, *args, **kwargs):
837 colid = self.kwargs.get("colid", None)
838 site = self.kwargs.get("site", None)
840 collection = model_helpers.get_collection(colid)
841 if not collection:
842 raise RuntimeError(f"{colid} does not exist")
844 if site == "numdam":
845 server_url = settings.NUMDAM_PRE_URL
846 else:
847 server_url = getattr(collection, site)()
848 if not server_url:
849 raise RuntimeError(f"The collection has no {site}")
851 # check_collection creates or updates the collection (XML, image...)
852 check_collection(collection, server_url, site)
854 def get(self, request, *args, **kwargs):
855 colid = self.kwargs.get("colid", None)
856 site = self.kwargs.get("site", None)
858 try:
859 _, status, message = history_views.execute_and_record_func(
860 "deploy", colid, colid, self.internal_do, site
861 )
862 except Timeout as exception:
863 return HttpResponse(exception, status=408)
864 except Exception as exception:
865 return HttpResponseServerError(exception)
867 data = {"message": message, "status": status}
868 return JsonResponse(data)
871class DeployJatsResourceAPIView(View):
872 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
874 def internal_do(self, *args, **kwargs):
875 pid = self.kwargs.get("pid", None)
876 colid = self.kwargs.get("colid", None)
877 site = self.kwargs.get("site", None)
879 if site == "ptf_tools":
880 raise RuntimeError("Do not choose to deploy on PTF Tools")
882 resource = model_helpers.get_resource(pid)
883 if not resource:
884 raise RuntimeError(f"{pid} does not exist")
886 obj = resource.cast()
887 article = None
888 if obj.classname == "Article":
889 article = obj
890 container = article.my_container
891 articles_to_deploy = [article]
892 else:
893 container = obj
894 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
896 if site == "website" and article is not None and article.do_not_publish:
897 raise RuntimeError(f"{pid} is marked as Do not publish")
898 if site == "numdam" and article is not None:
899 raise RuntimeError("You can only deploy issues to Numdam")
901 collection = container.get_top_collection()
902 colid = collection.pid
903 djvu_exception = None
905 if site == "numdam":
906 server_url = settings.NUMDAM_PRE_URL
907 ResourceInNumdam.objects.get_or_create(pid=container.pid)
909 # 06/12/2022: DjVu are no longer added with Mersenne articles
910 # Add Djvu (before exporting the XML)
911 if False and int(container.year) < 2020:
912 for art in container.article_set.all():
913 try:
914 cmd = ptf_cmds.addDjvuPtfCmd()
915 cmd.set_resource(art)
916 cmd.do()
917 except Exception as e:
918 # Djvu are optional.
919 # Allow the deployment, but record the exception in the history
920 djvu_exception = e
921 else:
922 server_url = getattr(collection, site)()
923 if not server_url:
924 raise RuntimeError(f"The collection has no {site}")
926 # check if the collection exists on the server
927 # if not, check_collection will upload the collection (XML,
928 # image...)
929 if article is None:
930 check_collection(collection, server_url, site)
932 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
933 # Create/update deployed date and published date on all container articles
934 if site == "website":
935 file_.write(
936 "Create/Update deployed_date and date_published on all articles for {}\n".format(
937 pid
938 )
939 )
941 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
942 cmd = ptf_cmds.publishResourcePtfCmd()
943 cmd.set_resource(resource)
944 updated_articles = cmd.do()
946 tex.create_frontpage(colid, container, updated_articles, test=False)
948 mersenneSite = model_helpers.get_site_mersenne(colid)
949 # create or update deployed_date on container and articles
950 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
952 for art in articles_to_deploy:
953 if art.doi and (art.date_published or art.date_online_first):
954 if art.my_container.year is None:
955 art.my_container.year = datetime.now().strftime("%Y")
956 # BUG ? update the container but no save() ?
958 file_.write(
959 "Publication date of {} : Online First: {}, Published: {}\n".format(
960 art.pid, art.date_online_first, art.date_published
961 )
962 )
964 if article is None:
965 resolver.copy_binary_files(
966 container,
967 settings.MERSENNE_TEST_DATA_FOLDER,
968 settings.MERSENNE_PROD_DATA_FOLDER,
969 )
971 for art in articles_to_deploy:
972 resolver.copy_binary_files(
973 art,
974 settings.MERSENNE_TEST_DATA_FOLDER,
975 settings.MERSENNE_PROD_DATA_FOLDER,
976 )
978 elif site == "test_website":
979 # create date_pre_published on articles without date_pre_published
980 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
981 cmd.set_resource(resource)
982 updated_articles = cmd.do()
984 tex.create_frontpage(colid, container, updated_articles)
986 export_to_website = site == "website"
988 if article is None:
989 with_djvu = site == "numdam"
990 # if obj.ctype == 'issue_special':
991 #
992 xml = ptf_cmds.exportPtfCmd(
993 {
994 "pid": pid,
995 "with_djvu": with_djvu,
996 "export_to_website": export_to_website,
997 }
998 ).do()
999 body = xml.encode("utf8")
1001 if container.ctype == "issue" or container.ctype == "issue_special":
1002 url = server_url + reverse("issue_upload")
1003 else:
1004 url = server_url + reverse("book_upload")
1006 # verify=False: ignore TLS certificate
1007 response = requests.post(url, data=body, verify=False)
1008 else:
1009 xml = ptf_cmds.exportPtfCmd(
1010 {
1011 "pid": pid,
1012 "with_djvu": False,
1013 "article_standalone": True,
1014 "collection_pid": collection.pid,
1015 "export_to_website": export_to_website,
1016 "export_folder": settings.LOG_DIR,
1017 }
1018 ).do()
1019 # Unlike containers that send their XML as the body of the POST request,
1020 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1021 xml_file = io.StringIO(xml)
1022 files = {"xml": xml_file}
1024 url = server_url + reverse(
1025 "article_in_issue_upload", kwargs={"pid": container.pid}
1026 )
1027 # verify=False: ignore TLS certificate
1028 header = {}
1029 response = requests.post(url, headers=header, files=files, verify=False)
1031 status = response.status_code
1033 if 199 < status < 205:
1034 # There is no need to copy files for the test server
1035 # Files were already copied in /mersenne_test_data during the ptf_tools import
1036 # We only need to copy files from /mersenne_test_data to
1037 # /mersenne_prod_data during an upload to prod
1038 if site == "website":
1039 for art in articles_to_deploy:
1040 # record DOI automatically when deploying in prod
1042 if art.doi and art.allow_crossref():
1043 recordDOI(art)
1045 if colid == "CRBIOL":
1046 recordPubmed(
1047 art, force_update=False, updated_articles=updated_articles
1048 )
1050 if colid == "PCJ":
1051 self.update_pcj_editor(updated_articles)
1053 # Archive the container or the article
1054 if article is None:
1055 archive_trammel_resource.delay(
1056 colid=colid,
1057 pid=pid,
1058 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1059 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1060 )
1061 else:
1062 archive_trammel_resource.delay(
1063 colid=colid,
1064 pid=pid,
1065 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1066 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1067 article_doi=article.doi,
1068 )
1069 # cmd = ptf_cmds.archiveIssuePtfCmd({
1070 # "pid": pid,
1071 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1072 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1073 # cmd.set_article(article) # set_article allows archiving only the article
1074 # cmd.do()
1076 elif site == "numdam":
1077 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1078 if colid in settings.NUMDAM_COLLECTIONS:
1079 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1081 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1082 for article in container.article_set.all():
1083 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1085 elif status == 503:
1086 raise ServerUnderMaintenance(response.text)
1087 else:
1088 raise RuntimeError(response.text)
1090 if djvu_exception:
1091 raise djvu_exception
1093 def get(self, request, *args, **kwargs):
1094 pid = self.kwargs.get("pid", None)
1095 colid = self.kwargs.get("colid", None)
1096 site = self.kwargs.get("site", None)
1098 try:
1099 _, status, message = history_views.execute_and_record_func(
1100 "deploy", pid, colid, self.internal_do, site
1101 )
1102 except Timeout as exception:
1103 return HttpResponse(exception, status=408)
1104 except Exception as exception:
1105 return HttpResponseServerError(exception)
1107 data = {"message": message, "status": status}
1108 return JsonResponse(data)
1110 def update_pcj_editor(self, updated_articles):
1111 for article in updated_articles:
1112 data = {
1113 "date_published": article.date_published.strftime("%Y-%m-%d"),
1114 "article_number": article.article_number,
1115 }
1116 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1117 requests.post(url, json=data, verify=False)
1120class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1121 article = None
1123 def internal_do(self, *args, **kwargs):
1124 lang = self.kwargs.get("lang", None)
1126 translation = None
1127 for trans_article in self.article.translations.all():
1128 if trans_article.lang == lang:
1129 translation = trans_article
1131 if translation is None:
1132 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1134 collection = self.article.get_top_collection()
1135 colid = collection.pid
1136 container = self.article.my_container
1138 if translation.date_published is None:
1139 # Add date posted
1140 cmd = ptf_cmds.publishResourcePtfCmd()
1141 cmd.set_resource(translation)
1142 updated_articles = cmd.do()
1144 # Recompile PDF to add the date posted
1145 try:
1146 tex.create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1147 except Exception:
1148 raise PDFException(
1149 "Unable to compile the article PDF. Please contact the centre Mersenne"
1150 )
1152 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1153 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1154 resolver.copy_binary_files(
1155 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1156 )
1158 # Deploy in prod
1159 xml = ptf_cmds.exportPtfCmd(
1160 {
1161 "pid": self.article.pid,
1162 "with_djvu": False,
1163 "article_standalone": True,
1164 "collection_pid": colid,
1165 "export_to_website": True,
1166 "export_folder": settings.LOG_DIR,
1167 }
1168 ).do()
1169 xml_file = io.StringIO(xml)
1170 files = {"xml": xml_file}
1172 server_url = getattr(collection, "website")()
1173 if not server_url:
1174 raise RuntimeError("The collection has no website")
1175 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1176 header = {}
1178 try:
1179 response = requests.post(
1180 url, headers=header, files=files, verify=False
1181 ) # verify: ignore TLS certificate
1182 status = response.status_code
1183 except requests.exceptions.ConnectionError:
1184 raise ServerUnderMaintenance(
1185 "The journal is under maintenance. Please try again later."
1186 )
1188 # Register translation in Crossref
1189 if 199 < status < 205:
1190 if self.article.allow_crossref():
1191 try:
1192 recordDOI(translation)
1193 except Exception:
1194 raise DOIException(
1195 "Error while recording the DOI. Please contact the centre Mersenne"
1196 )
1198 def get(self, request, *args, **kwargs):
1199 doi = kwargs.get("doi", None)
1200 self.article = model_helpers.get_article_by_doi(doi)
1201 if self.article is None:
1202 raise Http404(f"{doi} does not exist")
1204 try:
1205 _, status, message = history_views.execute_and_record_func(
1206 "deploy",
1207 self.article.pid,
1208 self.article.get_top_collection().pid,
1209 self.internal_do,
1210 "website",
1211 )
1212 except Timeout as exception:
1213 return HttpResponse(exception, status=408)
1214 except Exception as exception:
1215 return HttpResponseServerError(exception)
1217 data = {"message": message, "status": status}
1218 return JsonResponse(data)
1221class DeleteJatsIssueAPIView(View):
1222 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1223 def get(self, request, *args, **kwargs):
1224 pid = self.kwargs.get("pid", None)
1225 colid = self.kwargs.get("colid", None)
1226 site = self.kwargs.get("site", None)
1227 message = "Le volume a bien été supprimé"
1228 status = 200
1230 issue = model_helpers.get_container(pid)
1231 if not issue:
1232 raise Http404(f"{pid} does not exist")
1233 try:
1234 mersenneSite = model_helpers.get_site_mersenne(colid)
1236 if site == "ptf_tools":
1237 if issue.is_deployed(mersenneSite):
1238 issue.undeploy(mersenneSite)
1239 for article in issue.article_set.all():
1240 article.undeploy(mersenneSite)
1242 p = model_helpers.get_provider("mathdoc-id")
1244 cmd = ptf_cmds.addContainerPtfCmd(
1245 {
1246 "pid": issue.pid,
1247 "ctype": "issue",
1248 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1249 }
1250 )
1251 cmd.set_provider(p)
1252 cmd.add_collection(issue.get_collection())
1253 cmd.set_object_to_be_deleted(issue)
1254 cmd.undo()
1256 else:
1257 if site == "numdam":
1258 server_url = settings.NUMDAM_PRE_URL
1259 else:
1260 collection = issue.get_collection()
1261 server_url = getattr(collection, site)()
1263 if not server_url:
1264 message = "The collection has no " + site
1265 status = 500
1266 else:
1267 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1268 response = requests.delete(url, verify=False)
1269 status = response.status_code
1271 if status == 404:
1272 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1273 elif status > 204:
1274 body = response.text.encode("utf8")
1275 message = body[:1000]
1276 else:
1277 status = 200
1278 # unpublish issue in collection site (site_register.json)
1279 if site == "website":
1280 if issue.is_deployed(mersenneSite):
1281 issue.undeploy(mersenneSite)
1282 for article in issue.article_set.all():
1283 article.undeploy(mersenneSite)
1284 # delete article binary files
1285 folder = article.get_relative_folder()
1286 resolver.delete_object_folder(
1287 folder,
1288 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1289 )
1290 # delete issue binary files
1291 folder = issue.get_relative_folder()
1292 resolver.delete_object_folder(
1293 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1294 )
1296 except Timeout as exception:
1297 return HttpResponse(exception, status=408)
1298 except Exception as exception:
1299 return HttpResponseServerError(exception)
1301 data = {"message": message, "status": status}
1302 return JsonResponse(data)
1305class ArchiveIssueAPIView(View):
1306 def get(self, request, *args, **kwargs):
1307 try:
1308 pid = kwargs["pid"]
1309 colid = kwargs["colid"]
1310 except IndexError:
1311 raise Http404
1313 try:
1314 cmd = ptf_cmds.archiveIssuePtfCmd(
1315 {
1316 "pid": pid,
1317 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1318 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1319 }
1320 )
1321 result_, status, message = history_views.execute_and_record_func(
1322 "archive", pid, colid, cmd.do
1323 )
1324 except Exception as exception:
1325 return HttpResponseServerError(exception)
1327 data = {"message": message, "status": 200}
1328 return JsonResponse(data)
1331class CreateDjvuAPIView(View):
1332 def internal_do(self, *args, **kwargs):
1333 pid = self.kwargs.get("pid", None)
1335 resource = model_helpers.get_resource(pid)
1336 cmd = ptf_cmds.addDjvuPtfCmd()
1337 cmd.set_resource(resource)
1338 cmd.do()
1340 def get(self, request, *args, **kwargs):
1341 pid = self.kwargs.get("pid", None)
1342 colid = pid.split("_")[0]
1344 try:
1345 _, status, message = history_views.execute_and_record_func(
1346 "numdam", pid, colid, self.internal_do
1347 )
1348 except Exception as exception:
1349 return HttpResponseServerError(exception)
1351 data = {"message": message, "status": status}
1352 return JsonResponse(data)
1355class PTFToolsHomeView(LoginRequiredMixin, View):
1356 """
1357 Home Page.
1358 - Admin & staff -> Render blank home.html
1359 - User with unique authorized collection -> Redirect to collection details page
1360 - User with multiple authorized collections -> Render home.html with data
1361 - Comment moderator -> Comments dashboard
1362 - Others -> 404 response
1363 """
1365 def get(self, request, *args, **kwargs) -> HttpResponse:
1366 # Staff or user with authorized collections
1367 if request.user.is_staff or request.user.is_superuser:
1368 return render(request, "home.html")
1370 colids = get_authorized_collections(request.user)
1371 is_mod = is_comment_moderator(request.user)
1373 # The user has no rights
1374 if not (colids or is_mod):
1375 raise Http404("No collections associated with your account.")
1376 # Comment moderator only
1377 elif not colids:
1378 return HttpResponseRedirect(reverse("comment_list"))
1380 # User with unique collection -> Redirect to collection detail page
1381 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1382 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1384 # User with multiple authorized collections - Special home
1385 context = {}
1386 context["overview"] = True
1388 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1389 all_collections = {c["pid"]: c for c in all_collections}
1391 # Comments summary
1392 try:
1393 error, comments_data = get_comments_for_home(request.user)
1394 except AttributeError:
1395 error, comments_data = True, {}
1397 context["comment_server_ok"] = False
1399 if not error:
1400 context["comment_server_ok"] = True
1401 if comments_data:
1402 for col_id, comment_nb in comments_data.items():
1403 if col_id.upper() in all_collections: 1403 ↛ 1402line 1403 didn't jump to line 1402, because the condition on line 1403 was never false
1404 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1406 # TODO: Translations summary
1407 context["translation_server_ok"] = False
1409 # Sort the collections according to the number of pending comments
1410 context["collections"] = sorted(
1411 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1412 )
1414 return render(request, "home.html", context)
1417class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1418 columns = 5
1420 def get_common_context_data(self, **kwargs):
1421 context = super().get_context_data(**kwargs)
1422 now = timezone.now()
1423 curyear = now.year
1424 years = range(curyear - self.columns + 1, curyear + 1)
1426 context["collections"] = settings.MERSENNE_COLLECTIONS
1427 context["containers_to_be_published"] = []
1428 context["last_col_events"] = []
1430 event = history_models.get_history_last_event_by("clockss", "ALL")
1431 clockss_gap = history_models.get_gap(now, event)
1433 context["years"] = years
1434 context["clockss_gap"] = clockss_gap
1436 return context
1438 def calculate_articles_and_pages(self, pid, years):
1439 data_by_year = []
1440 total_articles = [0] * len(years)
1441 total_pages = [0] * len(years)
1443 for year in years:
1444 articles = self.get_articles_for_year(pid, year)
1445 articles_count = articles.count()
1446 page_count = sum(article.get_article_page_count() for article in articles)
1448 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1449 total_articles[year - years[0]] += articles_count
1450 total_pages[year - years[0]] += page_count
1452 return data_by_year, total_articles, total_pages
1454 def get_articles_for_year(self, pid, year):
1455 return Article.objects.filter(
1456 Q(my_container__my_collection__pid=pid)
1457 & (
1458 Q(date_published__year=year, date_online_first__isnull=True)
1459 | Q(date_online_first__year=year)
1460 )
1461 ).prefetch_related("resourcecount_set")
1464class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1465 template_name = "dashboard/published_articles.html"
1467 def get_context_data(self, **kwargs):
1468 context = self.get_common_context_data(**kwargs)
1469 years = context["years"]
1471 published_articles = []
1472 total_published_articles = [
1473 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1474 ]
1476 for pid in settings.MERSENNE_COLLECTIONS:
1477 if pid != "MERSENNE":
1478 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1479 pid, years
1480 )
1481 published_articles.append({"pid": pid, "years": articles_data})
1483 for i, year in enumerate(years):
1484 total_published_articles[i]["total_articles"] += total_articles[i]
1485 total_published_articles[i]["total_pages"] += total_pages[i]
1487 context["published_articles"] = published_articles
1488 context["total_published_articles"] = total_published_articles
1490 return context
1493class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1494 template_name = "dashboard/created_volumes.html"
1496 def get_context_data(self, **kwargs):
1497 context = self.get_common_context_data(**kwargs)
1498 years = context["years"]
1500 created_volumes = []
1501 total_created_volumes = [
1502 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1503 ]
1505 for pid in settings.MERSENNE_COLLECTIONS:
1506 if pid != "MERSENNE":
1507 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1508 pid, years
1509 )
1510 created_volumes.append({"pid": pid, "years": volumes_data})
1512 for i, year in enumerate(years):
1513 total_created_volumes[i]["total_articles"] += total_articles[i]
1514 total_created_volumes[i]["total_pages"] += total_pages[i]
1516 context["created_volumes"] = created_volumes
1517 context["total_created_volumes"] = total_created_volumes
1519 return context
1521 def calculate_volumes_and_pages(self, pid, years):
1522 data_by_year = []
1523 total_articles = [0] * len(years)
1524 total_pages = [0] * len(years)
1526 for year in years:
1527 issues = Container.objects.filter(my_collection__pid=pid, year=year)
1528 articles_count = 0
1529 page_count = 0
1531 for issue in issues:
1532 articles = issue.article_set.filter(
1533 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1534 ).prefetch_related("resourcecount_set")
1536 articles_count += articles.count()
1537 page_count += sum(article.get_article_page_count() for article in articles)
1539 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1540 total_articles[year - years[0]] += articles_count
1541 total_pages[year - years[0]] += page_count
1543 return data_by_year, total_articles, total_pages
1546class BaseCollectionView(TemplateView):
1547 def get_context_data(self, **kwargs):
1548 context = super().get_context_data(**kwargs)
1549 aid = context.get("aid")
1550 year = context.get("year")
1552 if aid and year:
1553 context["collection"] = self.get_collection(aid, year)
1555 return context
1557 def get_collection(self, aid, year):
1558 """Method to be overridden by subclasses to fetch the appropriate collection"""
1559 raise NotImplementedError("Subclasses must implement get_collection method")
1562class ArticleListView(BaseCollectionView):
1563 template_name = "collection-list.html"
1565 def get_collection(self, aid, year):
1566 return Article.objects.filter(
1567 Q(my_container__my_collection__pid=aid)
1568 & (
1569 Q(date_published__year=year, date_online_first__isnull=True)
1570 | Q(date_online_first__year=year)
1571 )
1572 ).prefetch_related("resourcecount_set")
1575class VolumeListView(BaseCollectionView):
1576 template_name = "collection-list.html"
1578 def get_collection(self, aid, year):
1579 return Article.objects.filter(
1580 Q(my_container__my_collection__pid=aid, my_container__year=year)
1581 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1582 ).prefetch_related("resourcecount_set")
1585class DOAJResourceRegisterView(View):
1586 def get(self, request, *args, **kwargs):
1587 pid = kwargs.get("pid", None)
1588 resource = model_helpers.get_resource(pid)
1589 if resource is None:
1590 raise Http404
1592 try:
1593 data = {}
1594 doaj_meta, response = doaj_pid_register(pid)
1595 if response is None:
1596 return HttpResponse(status=204)
1597 elif doaj_meta and 200 <= response.status_code <= 299:
1598 data.update(doaj_meta)
1599 else:
1600 return HttpResponse(status=response.status_code, reason=response.text)
1601 except Timeout as exception:
1602 return HttpResponse(exception, status=408)
1603 except Exception as exception:
1604 return HttpResponseServerError(exception)
1605 return JsonResponse(data)
1608class CROSSREFResourceRegisterView(View):
1609 def get(self, request, *args, **kwargs):
1610 pid = kwargs.get("pid", None)
1611 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1612 force = kwargs.get("force", None)
1613 if not request.user.is_superuser:
1614 force = None
1616 resource = model_helpers.get_resource(pid)
1617 if resource is None:
1618 raise Http404
1620 resource = resource.cast()
1621 meth = getattr(self, "recordDOI" + resource.classname)
1622 try:
1623 data = meth(resource, force)
1624 except Timeout as exception:
1625 return HttpResponse(exception, status=408)
1626 except Exception as exception:
1627 return HttpResponseServerError(exception)
1628 return JsonResponse(data)
1630 def recordDOIArticle(self, article, force=None):
1631 result = {"status": 404}
1632 if (
1633 article.doi
1634 and not article.do_not_publish
1635 and (article.date_published or article.date_online_first or force == "force")
1636 ):
1637 if article.my_container.year is None: # or article.my_container.year == '0':
1638 article.my_container.year = datetime.now().strftime("%Y")
1639 result = recordDOI(article)
1640 return result
1642 def recordDOICollection(self, collection, force=None):
1643 return recordDOI(collection)
1645 def recordDOIContainer(self, container, force=None):
1646 data = {"status": 200, "message": "tout va bien"}
1648 if container.ctype == "issue":
1649 if container.doi:
1650 result = recordDOI(container)
1651 if result["status"] != 200:
1652 return result
1653 if force == "force":
1654 articles = container.article_set.exclude(
1655 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1656 )
1657 else:
1658 articles = container.article_set.exclude(
1659 doi__isnull=True,
1660 do_not_publish=True,
1661 date_published__isnull=True,
1662 date_online_first__isnull=True,
1663 )
1665 for article in articles:
1666 result = self.recordDOIArticle(article, force)
1667 if result["status"] != 200:
1668 data = result
1669 else:
1670 return recordDOI(container)
1671 return data
1674class CROSSREFResourceCheckStatusView(View):
1675 def get(self, request, *args, **kwargs):
1676 pid = kwargs.get("pid", None)
1677 resource = model_helpers.get_resource(pid)
1678 if resource is None:
1679 raise Http404
1680 resource = resource.cast()
1681 meth = getattr(self, "checkDOI" + resource.classname)
1682 try:
1683 meth(resource)
1684 except Timeout as exception:
1685 return HttpResponse(exception, status=408)
1686 except Exception as exception:
1687 return HttpResponseServerError(exception)
1689 data = {"status": 200, "message": "tout va bien"}
1690 return JsonResponse(data)
1692 def checkDOIArticle(self, article):
1693 if article.my_container.year is None or article.my_container.year == "0":
1694 article.my_container.year = datetime.now().strftime("%Y")
1695 get_or_create_doibatch(article)
1697 def checkDOICollection(self, collection):
1698 get_or_create_doibatch(collection)
1700 def checkDOIContainer(self, container):
1701 if container.doi is not None:
1702 get_or_create_doibatch(container)
1703 for article in container.article_set.all():
1704 self.checkDOIArticle(article)
1707class RegisterPubmedFormView(FormView):
1708 template_name = "record_pubmed_dialog.html"
1709 form_class = RegisterPubmedForm
1711 def get_context_data(self, **kwargs):
1712 context = super().get_context_data(**kwargs)
1713 context["pid"] = self.kwargs["pid"]
1714 context["helper"] = PtfLargeModalFormHelper
1715 return context
1718class RegisterPubmedView(View):
1719 def get(self, request, *args, **kwargs):
1720 pid = kwargs.get("pid", None)
1721 update_article = self.request.GET.get("update_article", "on") == "on"
1723 article = model_helpers.get_article(pid)
1724 if article is None:
1725 raise Http404
1726 try:
1727 recordPubmed(article, update_article)
1728 except Exception as exception:
1729 messages.error("Unable to register the article in PubMed")
1730 return HttpResponseServerError(exception)
1732 return HttpResponseRedirect(
1733 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1734 )
1737class PTFToolsContainerView(TemplateView):
1738 template_name = ""
1740 def get_context_data(self, **kwargs):
1741 context = super().get_context_data(**kwargs)
1743 container = model_helpers.get_container(self.kwargs.get("pid"))
1744 if container is None:
1745 raise Http404
1746 citing_articles = container.citations()
1747 source = self.request.GET.get("source", None)
1748 if container.ctype.startswith("book"):
1749 book_parts = (
1750 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1751 )
1752 references = False
1753 if container.ctype == "book-monograph":
1754 # on regarde si il y a au moins une bibliographie
1755 for art in container.article_set.all():
1756 if art.bibitem_set.count() > 0:
1757 references = True
1758 context.update(
1759 {
1760 "book": container,
1761 "book_parts": list(book_parts),
1762 "source": source,
1763 "citing_articles": citing_articles,
1764 "references": references,
1765 "test_website": container.get_top_collection()
1766 .extlink_set.get(rel="test_website")
1767 .location,
1768 "prod_website": container.get_top_collection()
1769 .extlink_set.get(rel="website")
1770 .location,
1771 }
1772 )
1773 self.template_name = "book-toc.html"
1774 else:
1775 articles = container.article_set.all().order_by("seq")
1776 for article in articles:
1777 try:
1778 last_match = (
1779 history_models.HistoryEvent.objects.filter(
1780 pid=article.pid,
1781 type="matching",
1782 )
1783 .only("created_on")
1784 .latest("created_on")
1785 )
1786 except history_models.HistoryEvent.DoesNotExist as _:
1787 article.last_match = None
1788 else:
1789 article.last_match = last_match.created_on
1791 # article1 = articles.first()
1792 # date = article1.deployed_date()
1793 # TODO next_issue, previous_issue
1795 # check DOI est maintenant une commande à part
1796 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
1797 # articlesWithStatus = []
1798 # for article in articles:
1799 # get_or_create_doibatch(article)
1800 # articlesWithStatus.append(article)
1802 test_location = prod_location = ""
1803 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
1804 if qs:
1805 test_location = qs.first().location
1806 qs = container.get_top_collection().extlink_set.filter(rel="website")
1807 if qs:
1808 prod_location = qs.first().location
1809 context.update(
1810 {
1811 "issue": container,
1812 "articles": articles,
1813 "source": source,
1814 "citing_articles": citing_articles,
1815 "test_website": test_location,
1816 "prod_website": prod_location,
1817 }
1818 )
1819 self.template_name = "issue-items.html"
1821 context["allow_crossref"] = container.allow_crossref()
1822 context["coltype"] = container.my_collection.coltype
1823 return context
1826class ExtLinkInline(InlineFormSetFactory):
1827 model = ExtLink
1828 form_class = ExtLinkForm
1829 factory_kwargs = {"extra": 0}
1832class ResourceIdInline(InlineFormSetFactory):
1833 model = ResourceId
1834 form_class = ResourceIdForm
1835 factory_kwargs = {"extra": 0}
1838class IssueDetailAPIView(View):
1839 def get(self, request, *args, **kwargs):
1840 issue = get_object_or_404(Container, pid=kwargs["pid"])
1841 deployed_date = issue.deployed_date()
1842 result = {
1843 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
1844 if deployed_date
1845 else None,
1846 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
1847 "all_doi_are_registered": issue.all_doi_are_registered(),
1848 "registered_in_doaj": issue.registered_in_doaj(),
1849 "doi": issue.my_collection.doi,
1850 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
1851 }
1852 try:
1853 latest = history_models.HistoryEvent.objects.get_last_unsolved_error(
1854 pid=issue.pid, strict=False
1855 )
1856 except history_models.HistoryEvent.DoesNotExist as _:
1857 pass
1858 else:
1859 result["latest"] = latest.data["message"]
1860 result["latest_target"] = latest.data.get("target", "")
1861 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
1862 "%Y-%m-%d %H:%M"
1863 )
1865 result["latest_type"] = latest.type.capitalize()
1866 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
1867 try:
1868 result[event_type] = timezone.localtime(
1869 history_models.HistoryEvent.objects.filter(
1870 type=event_type,
1871 status="OK",
1872 pid__startswith=issue.pid,
1873 )
1874 .latest("created_on")
1875 .created_on
1876 ).strftime("%Y-%m-%d %H:%M")
1877 except history_models.HistoryEvent.DoesNotExist as _:
1878 result[event_type] = ""
1879 return JsonResponse(result)
1882class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
1883 model = Collection
1884 form_class = CollectionForm
1885 inlines = [ResourceIdInline, ExtLinkInline]
1886 inlines_names = ["resource_ids_form", "ext_links_form"]
1888 def get_context_data(self, **kwargs):
1889 context = super().get_context_data(**kwargs)
1890 context["helper"] = PtfFormHelper
1891 context["formset_helper"] = FormSetHelper
1892 return context
1894 def add_description(self, collection, description, lang, seq):
1895 if description:
1896 la = Abstract(
1897 resource=collection,
1898 tag="description",
1899 lang=lang,
1900 seq=seq,
1901 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
1902 value_html=description,
1903 value_tex=description,
1904 )
1905 la.save()
1907 def form_valid(self, form):
1908 if form.instance.abbrev:
1909 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
1910 else:
1911 form.instance.title_xml = (
1912 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
1913 )
1915 form.instance.title_html = form.instance.title_tex
1916 form.instance.title_sort = form.instance.title_tex
1917 result = super().form_valid(form)
1919 collection = self.object
1920 collection.abstract_set.all().delete()
1922 seq = 1
1923 description = form.cleaned_data["description_en"]
1924 if description:
1925 self.add_description(collection, description, "en", seq)
1926 seq += 1
1927 description = form.cleaned_data["description_fr"]
1928 if description:
1929 self.add_description(collection, description, "fr", seq)
1931 return result
1933 def get_success_url(self):
1934 messages.success(self.request, "La Collection a été modifiée avec succès")
1935 return reverse("collection-detail", kwargs={"pid": self.object.pid})
1938class CollectionCreate(CollectionFormView, CreateWithInlinesView):
1939 """
1940 Warning : Not yet finished
1941 Automatic site membership creation is still missing
1942 """
1945class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
1946 slug_field = "pid"
1947 slug_url_kwarg = "pid"
1950def suggest_load_journal_dois(colid):
1951 articles = (
1952 Article.objects.filter(my_container__my_collection__pid=colid)
1953 .filter(doi__isnull=False)
1954 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1955 .values_list("doi", flat=True)
1956 )
1958 try:
1959 articles = sorted(
1960 articles,
1961 key=lambda d: (
1962 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
1963 int(re.search(r".(\d+)$", d).group(1)),
1964 ),
1965 )
1966 except:
1967 pass
1968 return [f'<option value="{doi}">' for doi in articles]
1971def get_context_with_volumes(journal):
1972 result = model_helpers.get_volumes_in_collection(journal)
1973 volume_count = result["volume_count"]
1974 collections = []
1975 for ancestor in journal.ancestors.all():
1976 item = model_helpers.get_volumes_in_collection(ancestor)
1977 volume_count = max(0, volume_count)
1978 item.update({"journal": ancestor})
1979 collections.append(item)
1981 # add the parent collection to its children list and sort it by date
1982 result.update({"journal": journal})
1983 collections.append(result)
1985 collections = [c for c in collections if c["sorted_issues"]]
1986 collections.sort(
1987 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
1988 reverse=True,
1989 )
1991 context = {
1992 "journal": journal,
1993 "sorted_issues": result["sorted_issues"],
1994 "volume_count": volume_count,
1995 "max_width": result["max_width"],
1996 "collections": collections,
1997 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
1998 }
1999 return context
2002class CollectionDetail(
2003 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2004):
2005 model = Collection
2006 slug_field = "pid"
2007 slug_url_kwarg = "pid"
2008 template_name = "ptf/collection_detail.html"
2010 def test_func(self):
2011 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2013 def get(self, request, *args, **kwargs):
2014 self.object = self.get_object(queryset=Collection.objects.all())
2015 return super().get(request, *args, **kwargs)
2017 def get_context_data(self, **kwargs):
2018 context = super().get_context_data(**kwargs)
2019 context["object_list"] = context["object_list"].filter(ctype="issue")
2020 context.update(get_context_with_volumes(self.object))
2022 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2023 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2024 context["issue_to_appear"] = Container.objects.filter(
2025 pid=context["issue_to_appear_pid"]
2026 ).exists()
2027 try:
2028 latest_error = history_models.HistoryEvent.objects.get_last_unsolved_error(
2029 self.object.pid,
2030 strict=True,
2031 )
2032 except history_models.HistoryEvent.DoesNotExist as _:
2033 pass
2034 else:
2035 message = latest_error.data["message"]
2036 i = message.find(" - ")
2037 latest_exception = message[:i]
2038 latest_error_message = message[i + 3 :]
2039 context["latest_exception"] = latest_exception
2040 context["latest_exception_date"] = latest_error.created_on
2041 context["latest_exception_type"] = latest_error.type
2042 context["latest_error_message"] = latest_error_message
2043 return context
2045 def get_queryset(self):
2046 query = self.object.content.all()
2048 for ancestor in self.object.ancestors.all():
2049 query |= ancestor.content.all()
2051 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2054class ContainerEditView(FormView):
2055 template_name = "container_form.html"
2056 form_class = ContainerForm
2058 def get_success_url(self):
2059 if self.kwargs["name"] == "special_issue_create":
2060 return reverse("special_issues_index", kwargs={"colid": self.kwargs["colid"]})
2061 if self.kwargs["pid"]:
2062 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2063 return reverse("mersenne_dashboard/published_articles")
2065 def set_success_message(self): # pylint: disable=no-self-use
2066 messages.success(self.request, "Le fascicule a été modifié")
2068 def get_form_kwargs(self):
2069 kwargs = super().get_form_kwargs()
2070 if "pid" not in self.kwargs:
2071 self.kwargs["pid"] = None
2072 if "colid" not in self.kwargs:
2073 self.kwargs["colid"] = None
2074 if "data" in kwargs and "colid" in kwargs["data"]:
2075 # colid is passed as a hidden param in the form.
2076 # It is used when you submit a new container
2077 self.kwargs["colid"] = kwargs["data"]["colid"]
2079 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2080 self.kwargs["pid"]
2081 )
2082 return kwargs
2084 def get_context_data(self, **kwargs):
2085 context = super().get_context_data(**kwargs)
2087 context["pid"] = self.kwargs["pid"]
2088 context["colid"] = self.kwargs["colid"]
2089 context["container"] = self.kwargs["container"]
2091 context["edit_container"] = context["pid"] is not None
2092 context["name"] = resolve(self.request.path_info).url_name
2094 return context
2096 def form_valid(self, form):
2097 new_pid = form.cleaned_data.get("pid")
2098 new_title = form.cleaned_data.get("title")
2099 new_trans_title = form.cleaned_data.get("trans_title")
2100 new_publisher = form.cleaned_data.get("publisher")
2101 new_year = form.cleaned_data.get("year")
2102 new_volume = form.cleaned_data.get("volume")
2103 new_number = form.cleaned_data.get("number")
2105 collection = None
2106 issue = self.kwargs["container"]
2107 if issue is not None:
2108 collection = issue.my_collection
2109 elif self.kwargs["colid"] is not None:
2110 if "CR" in self.kwargs["colid"]:
2111 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2112 else:
2113 collection = model_helpers.get_collection(self.kwargs["colid"])
2115 if collection is None:
2116 raise ValueError("Collection for " + new_pid + " does not exist")
2118 # Icon
2119 new_icon_location = ""
2120 if "icon" in self.request.FILES:
2121 filename = os.path.basename(self.request.FILES["icon"].name)
2122 file_extension = filename.split(".")[1]
2124 icon_filename = resolver.get_disk_location(
2125 settings.MERSENNE_TEST_DATA_FOLDER,
2126 collection.pid,
2127 file_extension,
2128 new_pid,
2129 None,
2130 True,
2131 )
2133 with open(icon_filename, "wb+") as destination:
2134 for chunk in self.request.FILES["icon"].chunks():
2135 destination.write(chunk)
2137 folder = resolver.get_relative_folder(collection.pid, new_pid)
2138 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2139 name = resolve(self.request.path_info).url_name
2140 if name == "special_issue_create":
2141 self.kwargs["name"] = name
2142 if self.kwargs["container"]:
2143 # Edit Issue
2144 issue = self.kwargs["container"]
2145 if issue is None:
2146 raise ValueError(self.kwargs["pid"] + " does not exist")
2148 if new_trans_title and (not issue.trans_lang or issue.trans_lang == "und"):
2149 issue.trans_lang = "fr" if issue.lang == "en" else "en"
2151 issue.pid = new_pid
2152 issue.title_tex = issue.title_html = new_title
2153 issue.trans_title_tex = issue.trans_title_html = new_trans_title
2154 issue.title_xml = get_issue_title_xml(
2155 new_title, issue.lang, new_trans_title, issue.trans_lang
2156 )
2157 issue.year = new_year
2158 issue.volume = new_volume
2159 issue.volume_int = make_int(new_volume)
2160 issue.number = new_number
2161 issue.number_int = make_int(new_number)
2162 issue.save()
2163 else:
2164 xissue = create_issuedata()
2166 if name == "special_issue_create":
2167 xissue.ctype = "issue_special"
2168 if not new_year:
2169 new_year = datetime.now().year
2170 else:
2171 xissue.ctype = "issue"
2172 xissue.pid = new_pid
2173 # TODO: add lang + trans_lang
2174 xissue.title_tex = new_title
2175 xissue.title_html = new_title
2176 xissue.trans_title_tex = new_trans_title
2177 xissue.title_xml = get_issue_title_xml(new_title)
2179 # new_title, new_trans_title, issue.trans_lang
2180 # )
2181 xissue.year = new_year
2182 xissue.volume = new_volume
2183 xissue.number = new_number
2184 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2186 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2187 cmd.add_collection(collection)
2188 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2189 issue = cmd.do()
2191 self.kwargs["pid"] = new_pid
2193 # Add objects related to the article: contribs, datastream, counts...
2194 params = {
2195 "icon_location": new_icon_location,
2196 }
2197 cmd = ptf_cmds.updateContainerPtfCmd(params)
2198 cmd.set_resource(issue)
2199 cmd.do()
2201 publisher = model_helpers.get_publisher(new_publisher)
2202 if not publisher:
2203 xpub = create_publisherdata()
2204 xpub.name = new_publisher
2205 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2206 issue.my_publisher = publisher
2207 issue.save()
2209 self.set_success_message()
2211 return super().form_valid(form)
2214# class ArticleEditView(FormView):
2215# template_name = 'article_form.html'
2216# form_class = ArticleForm
2217#
2218# def get_success_url(self):
2219# if self.kwargs['pid']:
2220# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2221# return reverse('mersenne_dashboard/published_articles')
2222#
2223# def set_success_message(self): # pylint: disable=no-self-use
2224# messages.success(self.request, "L'article a été modifié")
2225#
2226# def get_form_kwargs(self):
2227# kwargs = super(ArticleEditView, self).get_form_kwargs()
2228#
2229# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2230# # Article creation: pid is None
2231# self.kwargs['pid'] = None
2232# if 'issue_id' not in self.kwargs:
2233# # Article edit: issue_id is not passed
2234# self.kwargs['issue_id'] = None
2235# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2236# # colid is passed as a hidden param in the form.
2237# # It is used when you submit a new container
2238# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2239#
2240# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2241# return kwargs
2242#
2243# def get_context_data(self, **kwargs):
2244# context = super(ArticleEditView, self).get_context_data(**kwargs)
2245#
2246# context['pid'] = self.kwargs['pid']
2247# context['issue_id'] = self.kwargs['issue_id']
2248# context['article'] = self.kwargs['article']
2249#
2250# context['edit_article'] = context['pid'] is not None
2251#
2252# article = context['article']
2253# if article:
2254# context['author_contributions'] = article.get_author_contributions()
2255# context['kwds_fr'] = None
2256# context['kwds_en'] = None
2257# kwd_gps = article.get_non_msc_kwds()
2258# for kwd_gp in kwd_gps:
2259# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2260# if kwd_gp.value_xml:
2261# kwd_ = types.SimpleNamespace()
2262# kwd_.value = kwd_gp.value_tex
2263# context['kwd_unstructured_fr'] = kwd_
2264# context['kwds_fr'] = kwd_gp.kwd_set.all()
2265# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2266# if kwd_gp.value_xml:
2267# kwd_ = types.SimpleNamespace()
2268# kwd_.value = kwd_gp.value_tex
2269# context['kwd_unstructured_en'] = kwd_
2270# context['kwds_en'] = kwd_gp.kwd_set.all()
2271#
2272# # Article creation: init pid
2273# if context['issue_id'] and context['pid'] is None:
2274# issue = model_helpers.get_container(context['issue_id'])
2275# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2276#
2277# return context
2278#
2279# def form_valid(self, form):
2280#
2281# new_pid = form.cleaned_data.get('pid')
2282# new_title = form.cleaned_data.get('title')
2283# new_fpage = form.cleaned_data.get('fpage')
2284# new_lpage = form.cleaned_data.get('lpage')
2285# new_page_range = form.cleaned_data.get('page_range')
2286# new_page_count = form.cleaned_data.get('page_count')
2287# new_coi_statement = form.cleaned_data.get('coi_statement')
2288# new_show_body = form.cleaned_data.get('show_body')
2289# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2290#
2291# # TODO support MathML
2292# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2293# # We need to pass trans_title to get_title_xml
2294# # Meanwhile, ignore new_title_xml
2295# new_title_xml = jats_parser.get_title_xml(new_title)
2296# new_title_html = new_title
2297#
2298# authors_count = int(self.request.POST.get('authors_count', "0"))
2299# i = 1
2300# new_authors = []
2301# old_author_contributions = []
2302# if self.kwargs['article']:
2303# old_author_contributions = self.kwargs['article'].get_author_contributions()
2304#
2305# while authors_count > 0:
2306# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2307#
2308# if prefix is not None:
2309# addresses = []
2310# if len(old_author_contributions) >= i:
2311# old_author_contribution = old_author_contributions[i - 1]
2312# addresses = [contrib_address.address for contrib_address in
2313# old_author_contribution.get_addresses()]
2314#
2315# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2316# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2317# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2318# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2319# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2320# deceased_before_publication = deceased == 'on'
2321# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2322# equal_contrib = equal_contrib == 'on'
2323# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2324# corresponding = corresponding == 'on'
2325# email = self.request.POST.get('email-' + str(i), None)
2326#
2327# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2328# params['deceased_before_publication'] = deceased_before_publication
2329# params['equal_contrib'] = equal_contrib
2330# params['corresponding'] = corresponding
2331# params['addresses'] = addresses
2332# params['email'] = email
2333#
2334# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2335#
2336# new_authors.append(params)
2337#
2338# authors_count -= 1
2339# i += 1
2340#
2341# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2342# i = 1
2343# new_kwds_fr = []
2344# while kwds_fr_count > 0:
2345# value = self.request.POST.get('kwd-fr-' + str(i), None)
2346# new_kwds_fr.append(value)
2347# kwds_fr_count -= 1
2348# i += 1
2349# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2350#
2351# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2352# i = 1
2353# new_kwds_en = []
2354# while kwds_en_count > 0:
2355# value = self.request.POST.get('kwd-en-' + str(i), None)
2356# new_kwds_en.append(value)
2357# kwds_en_count -= 1
2358# i += 1
2359# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2360#
2361# if self.kwargs['article']:
2362# # Edit article
2363# container = self.kwargs['article'].my_container
2364# else:
2365# # New article
2366# container = model_helpers.get_container(self.kwargs['issue_id'])
2367#
2368# if container is None:
2369# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2370#
2371# collection = container.my_collection
2372#
2373# # Copy PDF file & extract full text
2374# body = ''
2375# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2376# collection.pid,
2377# "pdf",
2378# container.pid,
2379# new_pid,
2380# True)
2381# if 'pdf' in self.request.FILES:
2382# with open(pdf_filename, 'wb+') as destination:
2383# for chunk in self.request.FILES['pdf'].chunks():
2384# destination.write(chunk)
2385#
2386# # Extract full text from the PDF
2387# body = utils.pdf_to_text(pdf_filename)
2388#
2389# # Icon
2390# new_icon_location = ''
2391# if 'icon' in self.request.FILES:
2392# filename = os.path.basename(self.request.FILES['icon'].name)
2393# file_extension = filename.split('.')[1]
2394#
2395# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2396# collection.pid,
2397# file_extension,
2398# container.pid,
2399# new_pid,
2400# True)
2401#
2402# with open(icon_filename, 'wb+') as destination:
2403# for chunk in self.request.FILES['icon'].chunks():
2404# destination.write(chunk)
2405#
2406# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2407# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2408#
2409# if self.kwargs['article']:
2410# # Edit article
2411# article = self.kwargs['article']
2412# article.fpage = new_fpage
2413# article.lpage = new_lpage
2414# article.page_range = new_page_range
2415# article.coi_statement = new_coi_statement
2416# article.show_body = new_show_body
2417# article.do_not_publish = new_do_not_publish
2418# article.save()
2419#
2420# else:
2421# # New article
2422# params = {
2423# 'pid': new_pid,
2424# 'title_xml': new_title_xml,
2425# 'title_html': new_title_html,
2426# 'title_tex': new_title,
2427# 'fpage': new_fpage,
2428# 'lpage': new_lpage,
2429# 'page_range': new_page_range,
2430# 'seq': container.article_set.count() + 1,
2431# 'body': body,
2432# 'coi_statement': new_coi_statement,
2433# 'show_body': new_show_body,
2434# 'do_not_publish': new_do_not_publish
2435# }
2436#
2437# xarticle = create_articledata()
2438# xarticle.pid = new_pid
2439# xarticle.title_xml = new_title_xml
2440# xarticle.title_html = new_title_html
2441# xarticle.title_tex = new_title
2442# xarticle.fpage = new_fpage
2443# xarticle.lpage = new_lpage
2444# xarticle.page_range = new_page_range
2445# xarticle.seq = container.article_set.count() + 1
2446# xarticle.body = body
2447# xarticle.coi_statement = new_coi_statement
2448# params['xobj'] = xarticle
2449#
2450# cmd = ptf_cmds.addArticlePtfCmd(params)
2451# cmd.set_container(container)
2452# cmd.add_collection(container.my_collection)
2453# article = cmd.do()
2454#
2455# self.kwargs['pid'] = new_pid
2456#
2457# # Add objects related to the article: contribs, datastream, counts...
2458# params = {
2459# # 'title_xml': new_title_xml,
2460# # 'title_html': new_title_html,
2461# # 'title_tex': new_title,
2462# 'authors': new_authors,
2463# 'page_count': new_page_count,
2464# 'icon_location': new_icon_location,
2465# 'body': body,
2466# 'use_kwds': True,
2467# 'kwds_fr': new_kwds_fr,
2468# 'kwds_en': new_kwds_en,
2469# 'kwd_uns_fr': new_kwd_uns_fr,
2470# 'kwd_uns_en': new_kwd_uns_en
2471# }
2472# cmd = ptf_cmds.updateArticlePtfCmd(params)
2473# cmd.set_article(article)
2474# cmd.do()
2475#
2476# self.set_success_message()
2477#
2478# return super(ArticleEditView, self).form_valid(form)
2481@require_http_methods(["POST"])
2482def do_not_publish_article(request, *args, **kwargs):
2483 next = request.headers.get("referer")
2485 pid = kwargs.get("pid", "")
2487 article = model_helpers.get_article(pid)
2488 if article:
2489 article.do_not_publish = not article.do_not_publish
2490 article.save()
2491 else:
2492 raise Http404
2494 return HttpResponseRedirect(next)
2497@require_http_methods(["POST"])
2498def show_article_body(request, *args, **kwargs):
2499 next = request.headers.get("referer")
2501 pid = kwargs.get("pid", "")
2503 article = model_helpers.get_article(pid)
2504 if article:
2505 article.show_body = not article.show_body
2506 article.save()
2507 else:
2508 raise Http404
2510 return HttpResponseRedirect(next)
2513class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditAPIView):
2514 """
2515 API to get/post article metadata
2516 The class is derived from ArticleEditAPIView (see ptf.views)
2517 """
2519 def __init__(self, *args, **kwargs):
2520 super().__init__(*args, **kwargs)
2521 self.fields_to_update = [
2522 "lang",
2523 "title_xml",
2524 "title_tex",
2525 "title_html",
2526 "trans_lang",
2527 "trans_title_html",
2528 "trans_title_tex",
2529 "trans_title_xml",
2530 "atype",
2531 "contributors",
2532 "abstracts",
2533 "subjs",
2534 "kwds",
2535 "ext_links",
2536 ]
2538 def convert_data_for_editor(self, data_article):
2539 super().convert_data_for_editor(data_article)
2540 data_article.is_staff = self.request.user.is_staff
2542 def save_data(self, data_article):
2543 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2544 # The icons are not preserved since we can add/edit/delete them in VueJs
2545 params = {
2546 "pid": data_article.pid,
2547 "export_folder": settings.MERSENNE_TMP_FOLDER,
2548 "export_all": True,
2549 "with_binary_files": False,
2550 }
2551 ptf_cmds.exportExtraDataPtfCmd(params).do()
2553 def restore_data(self, article):
2554 ptf_cmds.importExtraDataPtfCmd(
2555 {
2556 "pid": article.pid,
2557 "import_folder": settings.MERSENNE_TMP_FOLDER,
2558 }
2559 ).do()
2562class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2563 template_name = "article_form.html"
2565 def get_success_url(self):
2566 if self.kwargs["doi"]:
2567 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2568 return reverse("mersenne_dashboard/published_articles")
2570 def get_context_data(self, **kwargs):
2571 context = super().get_context_data(**kwargs)
2572 if "doi" in self.kwargs:
2573 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"])
2574 context["pid"] = context["article"].pid
2576 return context
2579class ArticleDeleteView(View):
2580 def get(self, request, *args, **kwargs):
2581 pid = self.kwargs.get("pid", None)
2582 article = get_object_or_404(Article, pid=pid)
2584 try:
2585 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2586 article.undeploy(mersenneSite)
2588 cmd = ptf_cmds.addArticlePtfCmd(
2589 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2590 )
2591 cmd.set_container(article.my_container)
2592 cmd.set_object_to_be_deleted(article)
2593 cmd.undo()
2594 except Exception as exception:
2595 return HttpResponseServerError(exception)
2597 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2598 return JsonResponse(data)
2601def get_messages_in_queue():
2602 app = Celery("ptf-tools")
2603 # tasks = list(current_app.tasks)
2604 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2605 print(tasks)
2606 # i = app.control.inspect()
2608 with app.connection_or_acquire() as conn:
2609 remaining = conn.default_channel.queue_declare(queue="celery", passive=True).message_count
2610 return remaining
2613class FailedTasksListView(ListView):
2614 model = TaskResult
2615 queryset = TaskResult.objects.filter(
2616 status="FAILURE",
2617 task_name="ptf_tools.tasks.archive_numdam_issue",
2618 )
2621class FailedTasksDeleteView(DeleteView):
2622 model = TaskResult
2623 success_url = reverse_lazy("tasks-failed")
2626class FailedTasksRetryView(SingleObjectMixin, RedirectView):
2627 model = TaskResult
2629 @staticmethod
2630 def retry_task(task):
2631 colid, pid = (arg.strip("'") for arg in task.task_args.strip("()").split(", "))
2632 archive_numdam_issue.delay(colid, pid)
2633 task.delete()
2635 def get_redirect_url(self, *args, **kwargs):
2636 self.retry_task(self.get_object())
2637 return reverse("tasks-failed")
2640class NumdamView(TemplateView, history_views.HistoryContextMixin):
2641 template_name = "numdam.html"
2643 def get_context_data(self, **kwargs):
2644 context = super().get_context_data(**kwargs)
2646 context["objs"] = ResourceInNumdam.objects.all()
2648 pre_issues = []
2649 prod_issues = []
2650 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2651 try:
2652 response = requests.get(url)
2653 if response.status_code == 200:
2654 data = response.json()
2655 if "issues" in data:
2656 pre_issues = data["issues"]
2657 except Exception:
2658 pass
2660 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2661 response = requests.get(url)
2662 if response.status_code == 200:
2663 data = response.json()
2664 if "issues" in data:
2665 prod_issues = data["issues"]
2667 new = sorted(list(set(pre_issues).difference(prod_issues)))
2668 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2669 grouped = [
2670 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2671 ]
2672 grouped_removed = [
2673 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2674 ]
2675 context["added_issues"] = grouped
2676 context["removed_issues"] = grouped_removed
2678 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2679 return context
2682class TasksProgressView(View):
2683 def get(self, *args, **kwargs):
2684 task_name = self.kwargs.get("task", "archive_numdam_issue")
2685 successes = TaskResult.objects.filter(
2686 task_name=f"ptf_tools.tasks.{task_name}", status="SUCCESS"
2687 ).count()
2688 fails = TaskResult.objects.filter(
2689 task_name=f"ptf_tools.tasks.{task_name}", status="FAILURE"
2690 ).count()
2691 last_task = (
2692 TaskResult.objects.filter(
2693 task_name=f"ptf_tools.tasks.{task_name}",
2694 status="SUCCESS",
2695 )
2696 .order_by("-date_done")
2697 .first()
2698 )
2699 if last_task:
2700 last_task = " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
2701 remaining = get_messages_in_queue()
2702 all = successes + remaining
2703 progress = int(successes * 100 / all) if all else 0
2704 error_rate = int(fails * 100 / all) if all else 0
2705 status = "consuming_queue" if (successes or fails) and not progress == 100 else "polling"
2706 data = {
2707 "status": status,
2708 "progress": progress,
2709 "total": all,
2710 "remaining": remaining,
2711 "successes": successes,
2712 "fails": fails,
2713 "error_rate": error_rate,
2714 "last_task": last_task,
2715 }
2716 return JsonResponse(data)
2719class TasksView(TemplateView):
2720 template_name = "tasks.html"
2722 def get_context_data(self, **kwargs):
2723 context = super().get_context_data(**kwargs)
2724 context["tasks"] = TaskResult.objects.all()
2725 return context
2728class NumdamArchiveView(RedirectView):
2729 @staticmethod
2730 def reset_task_results():
2731 TaskResult.objects.all().delete()
2733 def get_redirect_url(self, *args, **kwargs):
2734 self.colid = kwargs["colid"]
2736 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2737 return Http404
2739 # we make sure archiving is not already running
2740 if not get_messages_in_queue():
2741 self.reset_task_results()
2742 response = requests.get(f"{settings.NUMDAM_URL}/api-all-collections/")
2743 if response.status_code == 200:
2744 data = sorted(response.json()["collections"])
2746 if self.colid != "ALL" and self.colid not in data:
2747 return Http404
2749 colids = [self.colid] if self.colid != "ALL" else data
2751 with open(
2752 os.path.join(settings.LOG_DIR, "archive.log"), "w", encoding="utf-8"
2753 ) as file_:
2754 file_.write("Archive " + " ".join([colid for colid in colids]) + "\n")
2756 for colid in colids:
2757 if colid not in settings.MERSENNE_COLLECTIONS:
2758 archive_numdam_collection.delay(colid)
2759 return reverse("numdam")
2762class DeployAllNumdamAPIView(View):
2763 def internal_do(self, *args, **kwargs):
2764 pids = []
2766 for obj in ResourceInNumdam.objects.all():
2767 pids.append(obj.pid)
2769 return pids
2771 def get(self, request, *args, **kwargs):
2772 try:
2773 pids, status, message = history_views.execute_and_record_func(
2774 "deploy", "numdam", "numdam", self.internal_do, "numdam"
2775 )
2776 except Exception as exception:
2777 return HttpResponseServerError(exception)
2779 data = {"message": message, "ids": pids, "status": status}
2780 return JsonResponse(data)
2783class NumdamDeleteAPIView(View):
2784 def get(self, request, *args, **kwargs):
2785 pid = self.kwargs.get("pid", None)
2787 try:
2788 obj = ResourceInNumdam.objects.get(pid=pid)
2789 obj.delete()
2790 except Exception as exception:
2791 return HttpResponseServerError(exception)
2793 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
2794 return JsonResponse(data)
2797class ExtIdApiDetail(View):
2798 def get(self, request, *args, **kwargs):
2799 extid = get_object_or_404(
2800 ExtId,
2801 resource__pid=kwargs["pid"],
2802 id_type=kwargs["what"],
2803 )
2804 return JsonResponse(
2805 {
2806 "pk": extid.pk,
2807 "href": extid.get_href(),
2808 "fetch": reverse(
2809 "api-fetch-id",
2810 args=(
2811 extid.resource.pk,
2812 extid.id_value,
2813 extid.id_type,
2814 "extid",
2815 ),
2816 ),
2817 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
2818 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
2819 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
2820 "delete": reverse("update-extid", args=(extid.pk, "delete")),
2821 "is_valid": extid.checked,
2822 }
2823 )
2826class ExtIdFormTemplate(TemplateView):
2827 template_name = "common/externalid_form.html"
2829 def get_context_data(self, **kwargs):
2830 context = super().get_context_data(**kwargs)
2831 context["sequence"] = kwargs["sequence"]
2832 return context
2835class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
2836 def get_context_data(self, **kwargs):
2837 context = super().get_context_data(**kwargs)
2838 context["helper"] = PtfFormHelper
2839 return context
2841 def get_success_url(self):
2842 self.post_process()
2843 return self.object.bibitem.resource.get_absolute_url()
2845 def post_process(self):
2846 cmd = xml_cmds.updateBibitemCitationXmlCmd()
2847 cmd.set_bibitem(self.object.bibitem)
2848 cmd.do()
2849 model_helpers.post_resource_updated(self.object.bibitem.resource)
2852class BibItemIdCreate(BibItemIdFormView, CreateView):
2853 model = BibItemId
2854 form_class = BibItemIdForm
2856 def get_context_data(self, **kwargs):
2857 context = super().get_context_data(**kwargs)
2858 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
2859 return context
2861 def get_initial(self):
2862 initial = super().get_initial()
2863 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
2864 return initial
2866 def form_valid(self, form):
2867 form.instance.checked = False
2868 return super().form_valid(form)
2871class BibItemIdUpdate(BibItemIdFormView, UpdateView):
2872 model = BibItemId
2873 form_class = BibItemIdForm
2875 def get_context_data(self, **kwargs):
2876 context = super().get_context_data(**kwargs)
2877 context["bibitem"] = self.object.bibitem
2878 return context
2881class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
2882 def get_context_data(self, **kwargs):
2883 context = super().get_context_data(**kwargs)
2884 context["helper"] = PtfFormHelper
2885 return context
2887 def get_success_url(self):
2888 self.post_process()
2889 return self.object.resource.get_absolute_url()
2891 def post_process(self):
2892 model_helpers.post_resource_updated(self.object.resource)
2895class ExtIdCreate(ExtIdFormView, CreateView):
2896 model = ExtId
2897 form_class = ExtIdForm
2899 def get_context_data(self, **kwargs):
2900 context = super().get_context_data(**kwargs)
2901 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
2902 return context
2904 def get_initial(self):
2905 initial = super().get_initial()
2906 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
2907 return initial
2909 def form_valid(self, form):
2910 form.instance.checked = False
2911 return super().form_valid(form)
2914class ExtIdUpdate(ExtIdFormView, UpdateView):
2915 model = ExtId
2916 form_class = ExtIdForm
2918 def get_context_data(self, **kwargs):
2919 context = super().get_context_data(**kwargs)
2920 context["resource"] = self.object.resource
2921 return context
2924class BibItemIdApiDetail(View):
2925 def get(self, request, *args, **kwargs):
2926 bibitemid = get_object_or_404(
2927 BibItemId,
2928 bibitem__resource__pid=kwargs["pid"],
2929 bibitem__sequence=kwargs["seq"],
2930 id_type=kwargs["what"],
2931 )
2932 return JsonResponse(
2933 {
2934 "pk": bibitemid.pk,
2935 "href": bibitemid.get_href(),
2936 "fetch": reverse(
2937 "api-fetch-id",
2938 args=(
2939 bibitemid.bibitem.pk,
2940 bibitemid.id_value,
2941 bibitemid.id_type,
2942 "bibitemid",
2943 ),
2944 ),
2945 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
2946 "uncheck": reverse(
2947 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
2948 ),
2949 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
2950 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
2951 "is_valid": bibitemid.checked,
2952 }
2953 )
2956class UpdateTexmfZipAPIView(View):
2957 def get(self, request, *args, **kwargs):
2958 def copy_zip_files(src_folder, dest_folder):
2959 os.makedirs(dest_folder, exist_ok=True)
2961 zip_files = [
2962 os.path.join(src_folder, f)
2963 for f in os.listdir(src_folder)
2964 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
2965 ]
2966 for zip_file in zip_files:
2967 resolver.copy_file(zip_file, dest_folder)
2969 # Exceptions: specific zip/gz files
2970 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
2971 resolver.copy_file(zip_file, dest_folder)
2973 zip_file = os.path.join(src_folder, "texmf-cg.zip")
2974 resolver.copy_file(zip_file, dest_folder)
2976 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
2977 resolver.copy_file(gz_file, dest_folder)
2979 src_folder = settings.CEDRAM_DISTRIB_FOLDER
2981 dest_folder = os.path.join(
2982 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
2983 )
2985 try:
2986 copy_zip_files(src_folder, dest_folder)
2987 except Exception as exception:
2988 return HttpResponseServerError(exception)
2990 try:
2991 dest_folder = os.path.join(
2992 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
2993 )
2994 copy_zip_files(src_folder, dest_folder)
2995 except Exception as exception:
2996 return HttpResponseServerError(exception)
2998 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
2999 return JsonResponse(data)
3002class TestView(TemplateView):
3003 template_name = "mersenne.html"
3005 def get_context_data(self, **kwargs):
3006 super().get_context_data(**kwargs)
3007 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True)
3008 model_data_converter.db_to_issue_data(issue)
3011class TrammelArchiveView(RedirectView):
3012 @staticmethod
3013 def reset_task_results():
3014 TaskResult.objects.all().delete()
3016 def get_redirect_url(self, *args, **kwargs):
3017 self.colid = kwargs["colid"]
3018 self.mathdoc_archive = settings.MATHDOC_ARCHIVE_FOLDER
3019 self.binary_files_folder = settings.MERSENNE_PROD_DATA_FOLDER
3020 # Make sure archiving is not already running
3021 if not get_messages_in_queue():
3022 self.reset_task_results()
3023 if "progress/" in self.colid:
3024 self.colid = self.colid.replace("progress/", "")
3025 if "/progress" in self.colid:
3026 self.colid = self.colid.replace("/progress", "")
3028 if self.colid != "ALL" and self.colid not in settings.MERSENNE_COLLECTIONS:
3029 return Http404
3031 colids = [self.colid] if self.colid != "ALL" else settings.MERSENNE_COLLECTIONS
3033 with open(
3034 os.path.join(settings.LOG_DIR, "archive.log"), "w", encoding="utf-8"
3035 ) as file_:
3036 file_.write("Archive " + " ".join([colid for colid in colids]) + "\n")
3038 for colid in colids:
3039 archive_trammel_collection.delay(
3040 colid, self.mathdoc_archive, self.binary_files_folder
3041 )
3043 if self.colid == "ALL":
3044 return reverse("home")
3045 else:
3046 return reverse("collection-detail", kwargs={"pid": self.colid})
3049class TrammelTasksProgressView(View):
3050 def get(self, request, *args, **kwargs):
3051 """
3052 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3053 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3054 """
3055 task_name = self.kwargs.get("task", "archive_numdam_issue")
3057 def get_event_data():
3058 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3060 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3061 remaining_messages = get_messages_in_queue()
3063 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3064 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3065 failed_tasks = all_tasks.filter(status="FAILURE")
3067 all_tasks_count = all_tasks.count()
3068 success_count = successed_tasks.count()
3069 fail_count = failed_tasks.count()
3071 all_count = all_tasks_count + remaining_messages
3072 remaining_count = all_count - success_count - fail_count
3074 success_rate = int(success_count * 100 / all_count) if all_count else 0
3075 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3076 status = "consuming_queue" if remaining_count != 0 else "polling"
3078 last_task = successed_tasks.first()
3079 last_task = (
3080 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3081 if last_task
3082 else ""
3083 )
3085 # SSE event format
3086 event_data = {
3087 "status": status,
3088 "success_rate": success_rate,
3089 "error_rate": error_rate,
3090 "all_count": all_count,
3091 "remaining_count": remaining_count,
3092 "success_count": success_count,
3093 "fail_count": fail_count,
3094 "last_task": last_task,
3095 }
3097 return event_data
3099 def stream_response(data):
3100 # Send initial response headers
3101 yield f"data: {json.dumps(data)}\n\n"
3103 data = get_event_data()
3104 format = request.GET.get("format", "stream")
3105 if format == "json":
3106 response = JsonResponse(data)
3107 else:
3108 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3109 return response
3112class TrammelFailedTasksListView(ListView):
3113 model = TaskResult
3114 queryset = TaskResult.objects.filter(
3115 status="FAILURE",
3116 task_name="ptf_tools.tasks.archive_trammel_resource",
3117 )