Coverage for src/ptf_tools/views/base_views.py: 18%
1624 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-23 12:08 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-23 12:08 +0000
1import io
2import json
3import os
4import re
5from datetime import datetime
6from itertools import groupby
8import jsonpickle
9import requests
10from allauth.account.signals import user_signed_up
11from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin
12from celery import Celery, current_app
13from django.conf import settings
14from django.contrib import messages
15from django.contrib.auth.mixins import UserPassesTestMixin
16from django.db.models import Q
17from django.http import (
18 Http404,
19 HttpRequest,
20 HttpResponse,
21 HttpResponseRedirect,
22 HttpResponseServerError,
23 JsonResponse,
24)
25from django.shortcuts import get_object_or_404, redirect, render
26from django.urls import resolve, reverse, reverse_lazy
27from django.utils import timezone
28from django.views.decorators.http import require_http_methods
29from django.views.generic import ListView, TemplateView, View
30from django.views.generic.base import RedirectView
31from django.views.generic.detail import SingleObjectMixin
32from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
33from django_celery_results.models import TaskResult
34from extra_views import (
35 CreateWithInlinesView,
36 InlineFormSetFactory,
37 NamedFormsetsMixin,
38 UpdateWithInlinesView,
39)
40from ptf import model_data_converter, model_helpers, tex, utils
41from ptf.cmds import ptf_cmds, xml_cmds
42from ptf.cmds.base_cmds import make_int
43from ptf.cmds.xml.jats.builder.issue import build_title_xml
44from ptf.cmds.xml.xml_utils import replace_html_entities
45from ptf.display import resolver
46from ptf.exceptions import DOIException, PDFException, ServerUnderMaintenance
47from ptf.model_data import create_issuedata, create_publisherdata, create_titledata
48from ptf.models import (
49 Abstract,
50 Article,
51 BibItem,
52 BibItemId,
53 Collection,
54 Container,
55 ExtId,
56 ExtLink,
57 Resource,
58 ResourceId,
59 Title,
60)
61from ptf.views import ArticleEditFormWithVueAPIView
62from pubmed.views import recordPubmed
63from requests import Timeout
64from task.runner import run_task
65from task.tasks.archiving_tasks import ArchiveResourceTask
67from comments_moderation.utils import get_comments_for_home, is_comment_moderator
68from history import models as history_models
69from history import views as history_views
70from history.utils import get_gap, get_history_last_event_by, get_last_unsolved_error
71from ptf_tools.doaj import doaj_pid_register
72from ptf_tools.doi import checkDOI, recordDOI
73from ptf_tools.forms import (
74 BibItemIdForm,
75 CollectionForm,
76 ContainerForm,
77 DiffContainerForm,
78 ExtIdForm,
79 ExtLinkForm,
80 FormSetHelper,
81 ImportArticleForm,
82 ImportContainerForm,
83 PtfFormHelper,
84 PtfLargeModalFormHelper,
85 PtfModalFormHelper,
86 RegisterPubmedForm,
87 ResourceIdForm,
88 get_article_choices,
89)
90from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos
91from ptf_tools.models import ResourceInNumdam
92from ptf_tools.signals import update_user_from_invite
93from ptf_tools.tasks import (
94 ArchiveNumdamCollectionTask,
95 ArchiveNumdamIssueTask,
96)
97from ptf_tools.templatetags.tools_helpers import get_authorized_collections
98from ptf_tools.utils import is_authorized_editor
101def view_404(request: HttpRequest):
102 """
103 Dummy view raising HTTP 404 exception.
104 """
105 raise Http404
108def check_collection(collection, server_url, server_type):
109 """
110 Check if a collection exists on a serveur (test/prod)
111 and upload the collection (XML, image) if necessary
112 """
114 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid})
115 response = requests.get(url, verify=False)
116 # First, upload the collection XML
117 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do()
118 body = xml.encode("utf8")
120 url = server_url + reverse("upload-serials")
121 if response.status_code == 200:
122 # PUT http verb is used for update
123 response = requests.put(url, data=body, verify=False)
124 else:
125 # POST http verb is used for creation
126 response = requests.post(url, data=body, verify=False)
128 # Second, copy the collection images
129 # There is no need to copy files for the test server
130 # Files were already copied in /mersenne_test_data during the ptf_tools import
131 # We only need to copy files from /mersenne_test_data to
132 # /mersenne_prod_data during an upload to prod
133 if server_type == "website":
134 resolver.copy_binary_files(
135 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
136 )
137 elif server_type == "numdam":
138 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
139 if collection.pid in settings.NUMDAM_COLLECTIONS:
140 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
142 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT)
145def check_lock():
146 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE)
149def load_cedrics_article_choices(request):
150 colid = request.GET.get("colid")
151 issue = request.GET.get("issue")
152 article_choices = get_article_choices(colid, issue)
153 return render(
154 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices}
155 )
158class ImportCedricsArticleFormView(FormView):
159 template_name = "import_article.html"
160 form_class = ImportArticleForm
162 def dispatch(self, request, *args, **kwargs):
163 self.colid = self.kwargs["colid"]
164 return super().dispatch(request, *args, **kwargs)
166 def get_success_url(self):
167 if self.colid:
168 return reverse("collection-detail", kwargs={"pid": self.colid})
169 return "/"
171 def get_context_data(self, **kwargs):
172 context = super().get_context_data(**kwargs)
173 context["colid"] = self.colid
174 context["helper"] = PtfModalFormHelper
175 return context
177 def get_form_kwargs(self):
178 kwargs = super().get_form_kwargs()
179 kwargs["colid"] = self.colid
180 return kwargs
182 def form_valid(self, form):
183 self.issue = form.cleaned_data["issue"]
184 self.article = form.cleaned_data["article"]
185 return super().form_valid(form)
187 def import_cedrics_article(self, *args, **kwargs):
188 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd(
189 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid}
190 )
191 cmd.do()
193 def post(self, request, *args, **kwargs):
194 self.colid = self.kwargs.get("colid", None)
195 issue = request.POST["issue"]
196 self.article_pid = request.POST["article"]
197 self.issue_pid = os.path.basename(os.path.dirname(issue))
199 import_args = [self]
200 import_kwargs = {}
202 try:
203 _, status, message = history_views.execute_and_record_func(
204 "import",
205 f"{self.issue_pid} / {self.article_pid}",
206 self.colid,
207 self.import_cedrics_article,
208 "",
209 False,
210 None,
211 None,
212 *import_args,
213 **import_kwargs,
214 )
216 messages.success(
217 self.request, f"L'article {self.article_pid} a été importé avec succès"
218 )
220 except Exception as exception:
221 messages.error(
222 self.request,
223 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}",
224 )
226 return redirect(self.get_success_url())
229class ImportCedricsIssueView(FormView):
230 template_name = "import_container.html"
231 form_class = ImportContainerForm
233 def dispatch(self, request, *args, **kwargs):
234 self.colid = self.kwargs["colid"]
235 self.to_appear = self.request.GET.get("to_appear", False)
236 return super().dispatch(request, *args, **kwargs)
238 def get_success_url(self):
239 if self.filename:
240 return reverse(
241 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename}
242 )
243 return "/"
245 def get_context_data(self, **kwargs):
246 context = super().get_context_data(**kwargs)
247 context["colid"] = self.colid
248 context["helper"] = PtfModalFormHelper
249 return context
251 def get_form_kwargs(self):
252 kwargs = super().get_form_kwargs()
253 kwargs["colid"] = self.colid
254 kwargs["to_appear"] = self.to_appear
255 return kwargs
257 def form_valid(self, form):
258 self.filename = form.cleaned_data["filename"].split("/")[-1]
259 return super().form_valid(form)
262class DiffCedricsIssueView(FormView):
263 template_name = "diff_container_form.html"
264 form_class = DiffContainerForm
265 diffs = None
266 xissue = None
267 xissue_encoded = None
269 def get_success_url(self):
270 return reverse("collection-detail", kwargs={"pid": self.colid})
272 def dispatch(self, request, *args, **kwargs):
273 self.colid = self.kwargs["colid"]
274 # self.filename = self.kwargs['filename']
275 return super().dispatch(request, *args, **kwargs)
277 def get(self, request, *args, **kwargs):
278 self.filename = request.GET["filename"]
279 self.remove_mail = request.GET.get("remove_email", "off")
280 self.remove_date_prod = request.GET.get("remove_date_prod", "off")
281 self.remove_email = self.remove_mail == "on"
282 self.remove_date_prod = self.remove_date_prod == "on"
284 try:
285 result, status, message = history_views.execute_and_record_func(
286 "import",
287 os.path.basename(self.filename),
288 self.colid,
289 self.diff_cedrics_issue,
290 "",
291 True,
292 )
293 except Exception as exception:
294 pid = self.filename.split("/")[-1]
295 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}")
296 return HttpResponseRedirect(self.get_success_url())
298 no_conflict = result[0]
299 self.diffs = result[1]
300 self.xissue = result[2]
302 if no_conflict:
303 # Proceed with the import
304 self.form_valid(self.get_form())
305 return redirect(self.get_success_url())
306 else:
307 # Display the diff template
308 self.xissue_encoded = jsonpickle.encode(self.xissue)
310 return super().get(request, *args, **kwargs)
312 def post(self, request, *args, **kwargs):
313 self.filename = request.POST["filename"]
314 data = request.POST["xissue_encoded"]
315 self.xissue = jsonpickle.decode(data)
317 return super().post(request, *args, **kwargs)
319 def get_context_data(self, **kwargs):
320 context = super().get_context_data(**kwargs)
321 context["colid"] = self.colid
322 context["diff"] = self.diffs
323 context["filename"] = self.filename
324 context["xissue_encoded"] = self.xissue_encoded
325 return context
327 def get_form_kwargs(self):
328 kwargs = super().get_form_kwargs()
329 kwargs["colid"] = self.colid
330 return kwargs
332 def diff_cedrics_issue(self, *args, **kwargs):
333 params = {
334 "colid": self.colid,
335 "input_file": self.filename,
336 "remove_email": self.remove_mail,
337 "remove_date_prod": self.remove_date_prod,
338 "diff_only": True,
339 }
341 if settings.IMPORT_CEDRICS_DIRECTLY:
342 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
343 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS
344 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
345 else:
346 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
348 result = cmd.do()
349 if len(cmd.warnings) > 0 and self.request.user.is_superuser:
350 messages.warning(
351 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings
352 )
354 return result
356 def import_cedrics_issue(self, *args, **kwargs):
357 # modify xissue with data_issue if params to override
358 if "import_choice" in kwargs and kwargs["import_choice"] == "1":
359 issue = model_helpers.get_container(self.xissue.pid)
360 if issue:
361 data_issue = model_data_converter.db_to_issue_data(issue)
362 for xarticle in self.xissue.articles:
363 filter_articles = [
364 article for article in data_issue.articles if article.doi == xarticle.doi
365 ]
366 if len(filter_articles) > 0:
367 db_article = filter_articles[0]
368 xarticle.coi_statement = db_article.coi_statement
369 xarticle.kwds = db_article.kwds
370 xarticle.contrib_groups = db_article.contrib_groups
372 params = {
373 "colid": self.colid,
374 "xissue": self.xissue,
375 "input_file": self.filename,
376 }
378 if settings.IMPORT_CEDRICS_DIRECTLY:
379 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS
380 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS
381 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params)
382 else:
383 cmd = xml_cmds.importCedricsIssueXmlCmd(params)
385 cmd.do()
387 def form_valid(self, form):
388 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1":
389 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]}
390 else:
391 import_kwargs = {}
392 import_args = [self]
394 try:
395 _, status, message = history_views.execute_and_record_func(
396 "import",
397 self.xissue.pid,
398 self.kwargs["colid"],
399 self.import_cedrics_issue,
400 "",
401 False,
402 None,
403 None,
404 *import_args,
405 **import_kwargs,
406 )
407 except Exception as exception:
408 messages.error(
409 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception)
410 )
411 return super().form_invalid(form)
413 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès")
414 return super().form_valid(form)
417class BibtexAPIView(View):
418 def get(self, request, *args, **kwargs):
419 pid = self.kwargs.get("pid", None)
420 all_bibtex = ""
421 if pid:
422 article = model_helpers.get_article(pid)
423 if article:
424 for bibitem in article.bibitem_set.all():
425 bibtex_array = bibitem.get_bibtex()
426 last = len(bibtex_array)
427 i = 1
428 for bibtex in bibtex_array:
429 if i > 1 and i < last:
430 all_bibtex += " "
431 all_bibtex += bibtex + "\n"
432 i += 1
434 data = {"bibtex": all_bibtex}
435 return JsonResponse(data)
438class MatchingAPIView(View):
439 def get(self, request, *args, **kwargs):
440 pid = self.kwargs.get("pid", None)
442 url = settings.MATCHING_URL
443 headers = {"Content-Type": "application/xml"}
445 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do()
447 if settings.DEBUG:
448 print("Issue exported to /tmp/issue.xml")
449 f = open("/tmp/issue.xml", "w")
450 f.write(body.encode("utf8"))
451 f.close()
453 r = requests.post(url, data=body.encode("utf8"), headers=headers)
454 body = r.text.encode("utf8")
455 data = {"status": r.status_code, "message": body[:1000]}
457 if settings.DEBUG:
458 print("Matching received, new issue exported to /tmp/issue1.xml")
459 f = open("/tmp/issue1.xml", "w")
460 text = body
461 f.write(text)
462 f.close()
464 resource = model_helpers.get_resource(pid)
465 obj = resource.cast()
466 colid = obj.get_collection().pid
468 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/"
470 cmd = xml_cmds.addOrUpdateIssueXmlCmd(
471 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder}
472 )
473 cmd.do()
475 print("Matching finished")
476 return JsonResponse(data)
479class ImportAllAPIView(View):
480 def internal_do(self, *args, **kwargs):
481 pid = self.kwargs.get("pid", None)
483 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid)
484 if not os.path.isdir(root_folder):
485 raise ValueError(root_folder + " does not exist")
487 resource = model_helpers.get_resource(pid)
488 if not resource:
489 file = os.path.join(root_folder, pid + ".xml")
490 body = utils.get_file_content_in_utf8(file)
491 journals = xml_cmds.addCollectionsXmlCmd(
492 {
493 "body": body,
494 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
495 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
496 }
497 ).do()
498 if not journals:
499 raise ValueError(file + " does not contain a collection")
500 resource = journals[0]
501 # resolver.copy_binary_files(
502 # resource,
503 # settings.MATHDOC_ARCHIVE_FOLDER,
504 # settings.MERSENNE_TEST_DATA_FOLDER)
506 obj = resource.cast()
508 if obj.classname != "Collection":
509 raise ValueError(pid + " does not contain a collection")
511 cmd = xml_cmds.collectEntireCollectionXmlCmd(
512 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER}
513 )
514 pids = cmd.do()
516 return pids
518 def get(self, request, *args, **kwargs):
519 pid = self.kwargs.get("pid", None)
521 try:
522 pids, status, message = history_views.execute_and_record_func(
523 "import", pid, pid, self.internal_do
524 )
525 except Timeout as exception:
526 return HttpResponse(exception, status=408)
527 except Exception as exception:
528 return HttpResponseServerError(exception)
530 data = {"message": message, "ids": pids, "status": status}
531 return JsonResponse(data)
534class DeployAllAPIView(View):
535 def internal_do(self, *args, **kwargs):
536 pid = self.kwargs.get("pid", None)
537 site = self.kwargs.get("site", None)
539 pids = []
541 collection = model_helpers.get_collection(pid)
542 if not collection:
543 raise RuntimeError(pid + " does not exist")
545 if site == "numdam":
546 server_url = settings.NUMDAM_PRE_URL
547 elif site != "ptf_tools":
548 server_url = getattr(collection, site)()
549 if not server_url:
550 raise RuntimeError("The collection has no " + site)
552 if site != "ptf_tools":
553 # check if the collection exists on the server
554 # if not, check_collection will upload the collection (XML,
555 # image...)
556 check_collection(collection, server_url, site)
558 for issue in collection.content.all():
559 if site != "website" or (site == "website" and issue.are_all_articles_published()):
560 pids.append(issue.pid)
562 return pids
564 def get(self, request, *args, **kwargs):
565 pid = self.kwargs.get("pid", None)
566 site = self.kwargs.get("site", None)
568 try:
569 pids, status, message = history_views.execute_and_record_func(
570 "deploy", pid, pid, self.internal_do, site
571 )
572 except Timeout as exception:
573 return HttpResponse(exception, status=408)
574 except Exception as exception:
575 return HttpResponseServerError(exception)
577 data = {"message": message, "ids": pids, "status": status}
578 return JsonResponse(data)
581class AddIssuePDFView(View):
582 def __init(self, *args, **kwargs):
583 super().__init__(*args, **kwargs)
584 self.pid = None
585 self.issue = None
586 self.collection = None
587 self.site = "test_website"
589 def post_to_site(self, url):
590 response = requests.post(url, verify=False)
591 status = response.status_code
592 if not (199 < status < 205):
593 messages.error(self.request, response.text)
594 if status == 503:
595 raise ServerUnderMaintenance(response.text)
596 else:
597 raise RuntimeError(response.text)
599 def internal_do(self, *args, **kwargs):
600 """
601 Called by history_views.execute_and_record_func to do the actual job.
602 """
604 issue_pid = self.issue.pid
605 colid = self.collection.pid
607 if self.site == "website":
608 # Copy the PDF from the test to the production folder
609 resolver.copy_binary_files(
610 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
611 )
612 else:
613 # Copy the PDF from the cedram to the test folder
614 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid)
615 from_path = os.path.join(from_folder, issue_pid + ".pdf")
616 if not os.path.isfile(from_path):
617 raise Http404(f"{from_path} does not exist")
619 to_path = resolver.get_disk_location(
620 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid
621 )
622 resolver.copy_file(from_path, to_path)
624 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid})
626 if self.site == "test_website":
627 # Post to ptf-tools: it will add a Datastream to the issue
628 absolute_url = self.request.build_absolute_uri(url)
629 self.post_to_site(absolute_url)
631 server_url = getattr(self.collection, self.site)()
632 absolute_url = server_url + url
633 # Post to the test or production website
634 self.post_to_site(absolute_url)
636 def get(self, request, *args, **kwargs):
637 """
638 Send an issue PDF to the test or production website
639 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website'
640 :param args:
641 :param kwargs:
642 :return:
643 """
644 if check_lock():
645 m = "Trammel is under maintenance. Please try again later."
646 messages.error(self.request, m)
647 return JsonResponse({"message": m, "status": 503})
649 self.pid = self.kwargs.get("pid", None)
650 self.site = self.kwargs.get("site", "test_website")
652 self.issue = model_helpers.get_container(self.pid)
653 if not self.issue:
654 raise Http404(f"{self.pid} does not exist")
655 self.collection = self.issue.get_top_collection()
657 try:
658 pids, status, message = history_views.execute_and_record_func(
659 "deploy",
660 self.pid,
661 self.collection.pid,
662 self.internal_do,
663 f"add issue PDF to {self.site}",
664 )
666 except Timeout as exception:
667 return HttpResponse(exception, status=408)
668 except Exception as exception:
669 return HttpResponseServerError(exception)
671 data = {"message": message, "status": status}
672 return JsonResponse(data)
675class ArchiveAllAPIView(View):
676 """
677 - archive le xml de la collection ainsi que les binaires liés
678 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS
679 @return array of issues pid
680 """
682 def internal_do(self, *args, **kwargs):
683 collection = kwargs["collection"]
684 pids = []
685 colid = collection.pid
687 logfile = os.path.join(settings.LOG_DIR, "archive.log")
688 if os.path.isfile(logfile):
689 os.remove(logfile)
691 ptf_cmds.exportPtfCmd(
692 {
693 "pid": colid,
694 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
695 "with_binary_files": True,
696 "for_archive": True,
697 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
698 }
699 ).do()
701 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls")
702 if os.path.isfile(cedramcls):
703 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex")
704 resolver.create_folder(dest_folder)
705 resolver.copy_file(cedramcls, dest_folder)
707 for issue in collection.content.all():
708 qs = issue.article_set.filter(
709 date_online_first__isnull=True, date_published__isnull=True
710 )
711 if qs.count() == 0:
712 pids.append(issue.pid)
714 return pids
716 def get(self, request, *args, **kwargs):
717 pid = self.kwargs.get("pid", None)
719 collection = model_helpers.get_collection(pid)
720 if not collection:
721 return HttpResponse(f"{pid} does not exist", status=400)
723 dict_ = {"collection": collection}
724 args_ = [self]
726 try:
727 pids, status, message = history_views.execute_and_record_func(
728 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_
729 )
730 except Timeout as exception:
731 return HttpResponse(exception, status=408)
732 except Exception as exception:
733 return HttpResponseServerError(exception)
735 data = {"message": message, "ids": pids, "status": status}
736 return JsonResponse(data)
739class CreateAllDjvuAPIView(View):
740 def internal_do(self, *args, **kwargs):
741 issue = kwargs["issue"]
742 pids = [issue.pid]
744 for article in issue.article_set.all():
745 pids.append(article.pid)
747 return pids
749 def get(self, request, *args, **kwargs):
750 pid = self.kwargs.get("pid", None)
751 issue = model_helpers.get_container(pid)
752 if not issue:
753 raise Http404(f"{pid} does not exist")
755 try:
756 dict_ = {"issue": issue}
757 args_ = [self]
759 pids, status, message = history_views.execute_and_record_func(
760 "numdam",
761 pid,
762 issue.get_collection().pid,
763 self.internal_do,
764 "",
765 False,
766 None,
767 None,
768 *args_,
769 **dict_,
770 )
771 except Exception as exception:
772 return HttpResponseServerError(exception)
774 data = {"message": message, "ids": pids, "status": status}
775 return JsonResponse(data)
778class ImportJatsContainerAPIView(View):
779 def internal_do(self, *args, **kwargs):
780 pid = self.kwargs.get("pid", None)
781 colid = self.kwargs.get("colid", None)
783 if pid and colid:
784 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid)
786 cmd = xml_cmds.addOrUpdateContainerXmlCmd(
787 {
788 "body": body,
789 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER,
790 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
791 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER,
792 }
793 )
794 container = cmd.do()
795 if len(cmd.warnings) > 0:
796 messages.warning(
797 self.request,
798 message="Balises non parsées lors de l'import : %s" % cmd.warnings,
799 )
801 if not container:
802 raise RuntimeError("Error: the container " + pid + " was not imported")
804 # resolver.copy_binary_files(
805 # container,
806 # settings.MATHDOC_ARCHIVE_FOLDER,
807 # settings.MERSENNE_TEST_DATA_FOLDER)
808 #
809 # for article in container.article_set.all():
810 # resolver.copy_binary_files(
811 # article,
812 # settings.MATHDOC_ARCHIVE_FOLDER,
813 # settings.MERSENNE_TEST_DATA_FOLDER)
814 else:
815 raise RuntimeError("colid or pid are not defined")
817 def get(self, request, *args, **kwargs):
818 pid = self.kwargs.get("pid", None)
819 colid = self.kwargs.get("colid", None)
821 try:
822 _, status, message = history_views.execute_and_record_func(
823 "import", pid, colid, self.internal_do
824 )
825 except Timeout as exception:
826 return HttpResponse(exception, status=408)
827 except Exception as exception:
828 return HttpResponseServerError(exception)
830 data = {"message": message, "status": status}
831 return JsonResponse(data)
834class DeployCollectionAPIView(View):
835 # Update collection.xml on a site (with its images)
837 def internal_do(self, *args, **kwargs):
838 colid = self.kwargs.get("colid", None)
839 site = self.kwargs.get("site", None)
841 collection = model_helpers.get_collection(colid)
842 if not collection:
843 raise RuntimeError(f"{colid} does not exist")
845 if site == "numdam":
846 server_url = settings.NUMDAM_PRE_URL
847 else:
848 server_url = getattr(collection, site)()
849 if not server_url:
850 raise RuntimeError(f"The collection has no {site}")
852 # check_collection creates or updates the collection (XML, image...)
853 check_collection(collection, server_url, site)
855 def get(self, request, *args, **kwargs):
856 colid = self.kwargs.get("colid", None)
857 site = self.kwargs.get("site", None)
859 try:
860 _, status, message = history_views.execute_and_record_func(
861 "deploy", colid, colid, self.internal_do, site
862 )
863 except Timeout as exception:
864 return HttpResponse(exception, status=408)
865 except Exception as exception:
866 return HttpResponseServerError(exception)
868 data = {"message": message, "status": status}
869 return JsonResponse(data)
872class DeployJatsResourceAPIView(View):
873 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel)
875 def internal_do(self, *args, **kwargs):
876 pid = self.kwargs.get("pid", None)
877 colid = self.kwargs.get("colid", None)
878 site = self.kwargs.get("site", None)
880 if site == "ptf_tools":
881 raise RuntimeError("Do not choose to deploy on PTF Tools")
882 if check_lock():
883 msg = "Trammel is under maintenance. Please try again later."
884 messages.error(self.request, msg)
885 return JsonResponse({"messages": msg, "status": 503})
887 resource = model_helpers.get_resource(pid)
888 if not resource:
889 raise RuntimeError(f"{pid} does not exist")
891 obj = resource.cast()
892 article = None
893 if obj.classname == "Article":
894 article = obj
895 container = article.my_container
896 articles_to_deploy = [article]
897 else:
898 container = obj
899 articles_to_deploy = container.article_set.exclude(do_not_publish=True)
901 if site == "website" and article is not None and article.do_not_publish:
902 raise RuntimeError(f"{pid} is marked as Do not publish")
903 if site == "numdam" and article is not None:
904 raise RuntimeError("You can only deploy issues to Numdam")
906 collection = container.get_top_collection()
907 colid = collection.pid
908 djvu_exception = None
910 if site == "numdam":
911 server_url = settings.NUMDAM_PRE_URL
912 ResourceInNumdam.objects.get_or_create(pid=container.pid)
914 # 06/12/2022: DjVu are no longer added with Mersenne articles
915 # Add Djvu (before exporting the XML)
916 if False and int(container.year) < 2020:
917 for art in container.article_set.all():
918 try:
919 cmd = ptf_cmds.addDjvuPtfCmd()
920 cmd.set_resource(art)
921 cmd.do()
922 except Exception as e:
923 # Djvu are optional.
924 # Allow the deployment, but record the exception in the history
925 djvu_exception = e
926 else:
927 server_url = getattr(collection, site)()
928 if not server_url:
929 raise RuntimeError(f"The collection has no {site}")
931 # check if the collection exists on the server
932 # if not, check_collection will upload the collection (XML,
933 # image...)
934 if article is None:
935 check_collection(collection, server_url, site)
937 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_:
938 # Create/update deployed date and published date on all container articles
939 if site == "website":
940 file_.write(
941 "Create/Update deployed_date and date_published on all articles for {}\n".format(
942 pid
943 )
944 )
946 # create date_published on articles without date_published (ou date_online_first pour le volume 0)
947 cmd = ptf_cmds.publishResourcePtfCmd()
948 cmd.set_resource(resource)
949 updated_articles = cmd.do()
951 tex.create_frontpage(colid, container, updated_articles, test=False)
953 mersenneSite = model_helpers.get_site_mersenne(colid)
954 # create or update deployed_date on container and articles
955 model_helpers.update_deployed_date(obj, mersenneSite, None, file_)
957 for art in articles_to_deploy:
958 if art.doi and (art.date_published or art.date_online_first):
959 if art.my_container.year is None:
960 art.my_container.year = datetime.now().strftime("%Y")
961 # BUG ? update the container but no save() ?
963 file_.write(
964 "Publication date of {} : Online First: {}, Published: {}\n".format(
965 art.pid, art.date_online_first, art.date_published
966 )
967 )
969 if article is None:
970 resolver.copy_binary_files(
971 container,
972 settings.MERSENNE_TEST_DATA_FOLDER,
973 settings.MERSENNE_PROD_DATA_FOLDER,
974 )
976 for art in articles_to_deploy:
977 resolver.copy_binary_files(
978 art,
979 settings.MERSENNE_TEST_DATA_FOLDER,
980 settings.MERSENNE_PROD_DATA_FOLDER,
981 )
983 elif site == "test_website":
984 # create date_pre_published on articles without date_pre_published
985 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True})
986 cmd.set_resource(resource)
987 updated_articles = cmd.do()
989 tex.create_frontpage(colid, container, updated_articles)
991 export_to_website = site == "website"
993 if article is None:
994 with_djvu = site == "numdam"
995 xml = ptf_cmds.exportPtfCmd(
996 {
997 "pid": pid,
998 "with_djvu": with_djvu,
999 "export_to_website": export_to_website,
1000 }
1001 ).do()
1002 body = xml.encode("utf8")
1004 if container.ctype == "issue" or container.ctype.startswith("issue_special"):
1005 url = server_url + reverse("issue_upload")
1006 else:
1007 url = server_url + reverse("book_upload")
1009 # verify=False: ignore TLS certificate
1010 response = requests.post(url, data=body, verify=False)
1011 # response = requests.post(url, files=files, verify=False)
1012 else:
1013 xml = ptf_cmds.exportPtfCmd(
1014 {
1015 "pid": pid,
1016 "with_djvu": False,
1017 "article_standalone": True,
1018 "collection_pid": collection.pid,
1019 "export_to_website": export_to_website,
1020 "export_folder": settings.LOG_DIR,
1021 }
1022 ).do()
1023 # Unlike containers that send their XML as the body of the POST request,
1024 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img)
1025 xml_file = io.StringIO(xml)
1026 files = {"xml": xml_file}
1028 url = server_url + reverse(
1029 "article_in_issue_upload", kwargs={"pid": container.pid}
1030 )
1031 # verify=False: ignore TLS certificate
1032 header = {}
1033 response = requests.post(url, headers=header, files=files, verify=False)
1035 status = response.status_code
1037 if 199 < status < 205:
1038 # There is no need to copy files for the test server
1039 # Files were already copied in /mersenne_test_data during the ptf_tools import
1040 # We only need to copy files from /mersenne_test_data to
1041 # /mersenne_prod_data during an upload to prod
1042 if site == "website":
1043 # TODO mettre ici le record doi pour un issue publié
1044 if container.doi:
1045 recordDOI(container)
1047 for art in articles_to_deploy:
1048 # record DOI automatically when deploying in prod
1050 if art.doi and art.allow_crossref():
1051 recordDOI(art)
1053 if colid == "CRBIOL":
1054 recordPubmed(
1055 art, force_update=False, updated_articles=updated_articles
1056 )
1058 if colid == "PCJ":
1059 self.update_pcj_editor(updated_articles)
1061 # Archive the container or the article
1062 if article is None:
1063 run_task(
1064 ArchiveResourceTask,
1065 colid=colid,
1066 pid=pid,
1067 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1068 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1069 )
1071 else:
1072 run_task(
1073 ArchiveResourceTask,
1074 colid=colid,
1075 pid=pid,
1076 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER,
1077 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER,
1078 article_doi=article.doi,
1079 )
1080 # cmd = ptf_cmds.archiveIssuePtfCmd({
1081 # "pid": pid,
1082 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1083 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER})
1084 # cmd.set_article(article) # set_article allows archiving only the article
1085 # cmd.do()
1087 elif site == "numdam":
1088 from_folder = settings.MERSENNE_PROD_DATA_FOLDER
1089 if colid in settings.NUMDAM_COLLECTIONS:
1090 from_folder = settings.MERSENNE_TEST_DATA_FOLDER
1092 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT)
1093 for article in container.article_set.all():
1094 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT)
1096 elif status == 503:
1097 raise ServerUnderMaintenance(response.text)
1098 else:
1099 raise RuntimeError(response.text)
1101 if djvu_exception:
1102 raise djvu_exception
1104 def get(self, request, *args, **kwargs):
1105 pid = self.kwargs.get("pid", None)
1106 colid = self.kwargs.get("colid", None)
1107 site = self.kwargs.get("site", None)
1109 try:
1110 _, status, message = history_views.execute_and_record_func(
1111 "deploy", pid, colid, self.internal_do, site
1112 )
1113 except Timeout as exception:
1114 return HttpResponse(exception, status=408)
1115 except Exception as exception:
1116 return HttpResponseServerError(exception)
1118 data = {"message": message, "status": status}
1119 return JsonResponse(data)
1121 def update_pcj_editor(self, updated_articles):
1122 for article in updated_articles:
1123 data = {
1124 "date_published": article.date_published.strftime("%Y-%m-%d"),
1125 "article_number": article.article_number,
1126 }
1127 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/"
1128 requests.post(url, json=data, verify=False)
1131class DeployTranslatedArticleAPIView(CsrfExemptMixin, View):
1132 article = None
1134 def internal_do(self, *args, **kwargs):
1135 lang = self.kwargs.get("lang", None)
1137 translation = None
1138 for trans_article in self.article.translations.all():
1139 if trans_article.lang == lang:
1140 translation = trans_article
1142 if translation is None:
1143 raise RuntimeError(f"{self.article.doi} does not exist in {lang}")
1145 collection = self.article.get_top_collection()
1146 colid = collection.pid
1147 container = self.article.my_container
1149 if translation.date_published is None:
1150 # Add date posted
1151 cmd = ptf_cmds.publishResourcePtfCmd()
1152 cmd.set_resource(translation)
1153 updated_articles = cmd.do()
1155 # Recompile PDF to add the date posted
1156 try:
1157 tex.create_frontpage(colid, container, updated_articles, test=False, lang=lang)
1158 except Exception:
1159 raise PDFException(
1160 "Unable to compile the article PDF. Please contact the centre Mersenne"
1161 )
1163 # Unlike regular articles, binary files of translations need to be copied before uploading the XML.
1164 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk
1165 resolver.copy_binary_files(
1166 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER
1167 )
1169 # Deploy in prod
1170 xml = ptf_cmds.exportPtfCmd(
1171 {
1172 "pid": self.article.pid,
1173 "with_djvu": False,
1174 "article_standalone": True,
1175 "collection_pid": colid,
1176 "export_to_website": True,
1177 "export_folder": settings.LOG_DIR,
1178 }
1179 ).do()
1180 xml_file = io.StringIO(xml)
1181 files = {"xml": xml_file}
1183 server_url = getattr(collection, "website")()
1184 if not server_url:
1185 raise RuntimeError("The collection has no website")
1186 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid})
1187 header = {}
1189 try:
1190 response = requests.post(
1191 url, headers=header, files=files, verify=False
1192 ) # verify: ignore TLS certificate
1193 status = response.status_code
1194 except requests.exceptions.ConnectionError:
1195 raise ServerUnderMaintenance(
1196 "The journal is under maintenance. Please try again later."
1197 )
1199 # Register translation in Crossref
1200 if 199 < status < 205:
1201 if self.article.allow_crossref():
1202 try:
1203 recordDOI(translation)
1204 except Exception:
1205 raise DOIException(
1206 "Error while recording the DOI. Please contact the centre Mersenne"
1207 )
1209 def get(self, request, *args, **kwargs):
1210 doi = kwargs.get("doi", None)
1211 self.article = model_helpers.get_article_by_doi(doi)
1212 if self.article is None:
1213 raise Http404(f"{doi} does not exist")
1215 try:
1216 _, status, message = history_views.execute_and_record_func(
1217 "deploy",
1218 self.article.pid,
1219 self.article.get_top_collection().pid,
1220 self.internal_do,
1221 "website",
1222 )
1223 except Timeout as exception:
1224 return HttpResponse(exception, status=408)
1225 except Exception as exception:
1226 return HttpResponseServerError(exception)
1228 data = {"message": message, "status": status}
1229 return JsonResponse(data)
1232class DeleteJatsIssueAPIView(View):
1233 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book*
1234 def get(self, request, *args, **kwargs):
1235 pid = self.kwargs.get("pid", None)
1236 colid = self.kwargs.get("colid", None)
1237 site = self.kwargs.get("site", None)
1238 message = "Le volume a bien été supprimé"
1239 status = 200
1241 issue = model_helpers.get_container(pid)
1242 if not issue:
1243 raise Http404(f"{pid} does not exist")
1244 try:
1245 mersenneSite = model_helpers.get_site_mersenne(colid)
1247 if site == "ptf_tools":
1248 if issue.is_deployed(mersenneSite):
1249 issue.undeploy(mersenneSite)
1250 for article in issue.article_set.all():
1251 article.undeploy(mersenneSite)
1253 p = model_helpers.get_provider("mathdoc-id")
1255 cmd = ptf_cmds.addContainerPtfCmd(
1256 {
1257 "pid": issue.pid,
1258 "ctype": "issue",
1259 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER,
1260 }
1261 )
1262 cmd.set_provider(p)
1263 cmd.add_collection(issue.get_collection())
1264 cmd.set_object_to_be_deleted(issue)
1265 cmd.undo()
1267 else:
1268 if site == "numdam":
1269 server_url = settings.NUMDAM_PRE_URL
1270 else:
1271 collection = issue.get_collection()
1272 server_url = getattr(collection, site)()
1274 if not server_url:
1275 message = "The collection has no " + site
1276 status = 500
1277 else:
1278 url = server_url + reverse("issue_delete", kwargs={"pid": pid})
1279 response = requests.delete(url, verify=False)
1280 status = response.status_code
1282 if status == 404:
1283 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur"
1284 elif status > 204:
1285 body = response.text.encode("utf8")
1286 message = body[:1000]
1287 else:
1288 status = 200
1289 # unpublish issue in collection site (site_register.json)
1290 if site == "website":
1291 if issue.is_deployed(mersenneSite):
1292 issue.undeploy(mersenneSite)
1293 for article in issue.article_set.all():
1294 article.undeploy(mersenneSite)
1295 # delete article binary files
1296 folder = article.get_relative_folder()
1297 resolver.delete_object_folder(
1298 folder,
1299 to_folder=settings.MERSENNE_PROD_DATA_FORLDER,
1300 )
1301 # delete issue binary files
1302 folder = issue.get_relative_folder()
1303 resolver.delete_object_folder(
1304 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER
1305 )
1307 except Timeout as exception:
1308 return HttpResponse(exception, status=408)
1309 except Exception as exception:
1310 return HttpResponseServerError(exception)
1312 data = {"message": message, "status": status}
1313 return JsonResponse(data)
1316class ArchiveIssueAPIView(View):
1317 def get(self, request, *args, **kwargs):
1318 try:
1319 pid = kwargs["pid"]
1320 colid = kwargs["colid"]
1321 except IndexError:
1322 raise Http404
1324 try:
1325 cmd = ptf_cmds.archiveIssuePtfCmd(
1326 {
1327 "pid": pid,
1328 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER,
1329 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER,
1330 }
1331 )
1332 result_, status, message = history_views.execute_and_record_func(
1333 "archive", pid, colid, cmd.do
1334 )
1335 except Exception as exception:
1336 return HttpResponseServerError(exception)
1338 data = {"message": message, "status": 200}
1339 return JsonResponse(data)
1342class CreateDjvuAPIView(View):
1343 def internal_do(self, *args, **kwargs):
1344 pid = self.kwargs.get("pid", None)
1346 resource = model_helpers.get_resource(pid)
1347 cmd = ptf_cmds.addDjvuPtfCmd()
1348 cmd.set_resource(resource)
1349 cmd.do()
1351 def get(self, request, *args, **kwargs):
1352 pid = self.kwargs.get("pid", None)
1353 colid = pid.split("_")[0]
1355 try:
1356 _, status, message = history_views.execute_and_record_func(
1357 "numdam", pid, colid, self.internal_do
1358 )
1359 except Exception as exception:
1360 return HttpResponseServerError(exception)
1362 data = {"message": message, "status": status}
1363 return JsonResponse(data)
1366class PTFToolsHomeView(LoginRequiredMixin, View):
1367 """
1368 Home Page.
1369 - Admin & staff -> Render blank home.html
1370 - User with unique authorized collection -> Redirect to collection details page
1371 - User with multiple authorized collections -> Render home.html with data
1372 - Comment moderator -> Comments dashboard
1373 - Others -> 404 response
1374 """
1376 def get(self, request, *args, **kwargs) -> HttpResponse:
1377 # Staff or user with authorized collections
1378 if request.user.is_staff or request.user.is_superuser:
1379 return render(request, "home.html")
1381 colids = get_authorized_collections(request.user)
1382 is_mod = is_comment_moderator(request.user)
1384 # The user has no rights
1385 if not (colids or is_mod):
1386 raise Http404("No collections associated with your account.")
1387 # Comment moderator only
1388 elif not colids:
1389 return HttpResponseRedirect(reverse("comment_list"))
1391 # User with unique collection -> Redirect to collection detail page
1392 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False):
1393 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]}))
1395 # User with multiple authorized collections - Special home
1396 context = {}
1397 context["overview"] = True
1399 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html")
1400 all_collections = {c["pid"]: c for c in all_collections}
1402 # Comments summary
1403 try:
1404 error, comments_data = get_comments_for_home(request.user)
1405 except AttributeError:
1406 error, comments_data = True, {}
1408 context["comment_server_ok"] = False
1410 if not error:
1411 context["comment_server_ok"] = True
1412 if comments_data:
1413 for col_id, comment_nb in comments_data.items():
1414 if col_id.upper() in all_collections: 1414 ↛ 1413line 1414 didn't jump to line 1413 because the condition on line 1414 was always true
1415 all_collections[col_id.upper()]["pending_comments"] = comment_nb
1417 # TODO: Translations summary
1418 context["translation_server_ok"] = False
1420 # Sort the collections according to the number of pending comments
1421 context["collections"] = sorted(
1422 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True
1423 )
1425 return render(request, "home.html", context)
1428class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin):
1429 columns = 5
1431 def get_common_context_data(self, **kwargs):
1432 context = super().get_context_data(**kwargs)
1433 now = timezone.now()
1434 curyear = now.year
1435 years = range(curyear - self.columns + 1, curyear + 1)
1437 context["collections"] = settings.MERSENNE_COLLECTIONS
1438 context["containers_to_be_published"] = []
1439 context["last_col_events"] = []
1441 event = get_history_last_event_by("clockss", "ALL")
1442 clockss_gap = get_gap(now, event)
1444 context["years"] = years
1445 context["clockss_gap"] = clockss_gap
1447 return context
1449 def calculate_articles_and_pages(self, pid, years):
1450 data_by_year = []
1451 total_articles = [0] * len(years)
1452 total_pages = [0] * len(years)
1454 for year in years:
1455 articles = self.get_articles_for_year(pid, year)
1456 articles_count = articles.count()
1457 page_count = sum(article.get_article_page_count() for article in articles)
1459 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1460 total_articles[year - years[0]] += articles_count
1461 total_pages[year - years[0]] += page_count
1463 return data_by_year, total_articles, total_pages
1465 def get_articles_for_year(self, pid, year):
1466 return Article.objects.filter(
1467 Q(my_container__my_collection__pid=pid)
1468 & (
1469 Q(date_published__year=year, date_online_first__isnull=True)
1470 | Q(date_online_first__year=year)
1471 )
1472 ).prefetch_related("resourcecount_set")
1475class PublishedArticlesDashboardView(BaseMersenneDashboardView):
1476 template_name = "dashboard/published_articles.html"
1478 def get_context_data(self, **kwargs):
1479 context = self.get_common_context_data(**kwargs)
1480 years = context["years"]
1482 published_articles = []
1483 total_published_articles = [
1484 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1485 ]
1487 for pid in settings.MERSENNE_COLLECTIONS:
1488 if pid != "MERSENNE":
1489 articles_data, total_articles, total_pages = self.calculate_articles_and_pages(
1490 pid, years
1491 )
1492 published_articles.append({"pid": pid, "years": articles_data})
1494 for i, year in enumerate(years):
1495 total_published_articles[i]["total_articles"] += total_articles[i]
1496 total_published_articles[i]["total_pages"] += total_pages[i]
1498 context["published_articles"] = published_articles
1499 context["total_published_articles"] = total_published_articles
1501 return context
1504class CreatedVolumesDashboardView(BaseMersenneDashboardView):
1505 template_name = "dashboard/created_volumes.html"
1507 def get_context_data(self, **kwargs):
1508 context = self.get_common_context_data(**kwargs)
1509 years = context["years"]
1511 created_volumes = []
1512 total_created_volumes = [
1513 {"year": year, "total_articles": 0, "total_pages": 0} for year in years
1514 ]
1516 for pid in settings.MERSENNE_COLLECTIONS:
1517 if pid != "MERSENNE":
1518 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages(
1519 pid, years
1520 )
1521 created_volumes.append({"pid": pid, "years": volumes_data})
1523 for i, year in enumerate(years):
1524 total_created_volumes[i]["total_articles"] += total_articles[i]
1525 total_created_volumes[i]["total_pages"] += total_pages[i]
1527 context["created_volumes"] = created_volumes
1528 context["total_created_volumes"] = total_created_volumes
1530 return context
1532 def calculate_volumes_and_pages(self, pid, years):
1533 data_by_year = []
1534 total_articles = [0] * len(years)
1535 total_pages = [0] * len(years)
1537 for year in years:
1538 issues = Container.objects.filter(my_collection__pid=pid, year=year)
1539 articles_count = 0
1540 page_count = 0
1542 for issue in issues:
1543 articles = issue.article_set.filter(
1544 Q(date_published__isnull=False) | Q(date_online_first__isnull=False)
1545 ).prefetch_related("resourcecount_set")
1547 articles_count += articles.count()
1548 page_count += sum(article.get_article_page_count() for article in articles)
1550 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count})
1551 total_articles[year - years[0]] += articles_count
1552 total_pages[year - years[0]] += page_count
1554 return data_by_year, total_articles, total_pages
1557class ReferencingChoice(View):
1558 def post(self, request, *args, **kwargs):
1559 if request.POST.get("optSite") == "ads":
1560 return redirect(
1561 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")})
1562 )
1563 elif request.POST.get("optSite") == "wos":
1564 comp = ReferencingCheckerWos()
1565 journal = comp.make_journal(request.POST.get("selectCol"))
1566 if journal is None:
1567 return render(
1568 request,
1569 "dashboard/referencing.html",
1570 {
1571 "error": "Collection not found",
1572 "colid": request.POST.get("selectCol"),
1573 "optSite": request.POST.get("optSite"),
1574 },
1575 )
1576 return render(
1577 request,
1578 "dashboard/referencing.html",
1579 {
1580 "journal": journal,
1581 "colid": request.POST.get("selectCol"),
1582 "optSite": request.POST.get("optSite"),
1583 },
1584 )
1587class ReferencingWosFileView(View):
1588 template_name = "dashboard/referencing.html"
1590 def post(self, request, *args, **kwargs):
1591 colid = request.POST["colid"]
1592 if request.FILES.get("risfile") is None:
1593 message = "No file uploaded"
1594 return render(
1595 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"}
1596 )
1597 uploaded_file = request.FILES["risfile"]
1598 comp = ReferencingCheckerWos()
1599 journal = comp.check_references(colid, uploaded_file)
1600 return render(request, self.template_name, {"journal": journal})
1603class ReferencingDashboardView(BaseMersenneDashboardView):
1604 template_name = "dashboard/referencing.html"
1606 def get(self, request, *args, **kwargs):
1607 colid = self.kwargs.get("colid", None)
1608 comp = ReferencingCheckerAds()
1609 journal = comp.check_references(colid)
1610 return render(request, self.template_name, {"journal": journal})
1613class BaseCollectionView(TemplateView):
1614 def get_context_data(self, **kwargs):
1615 context = super().get_context_data(**kwargs)
1616 aid = context.get("aid")
1617 year = context.get("year")
1619 if aid and year:
1620 context["collection"] = self.get_collection(aid, year)
1622 return context
1624 def get_collection(self, aid, year):
1625 """Method to be overridden by subclasses to fetch the appropriate collection"""
1626 raise NotImplementedError("Subclasses must implement get_collection method")
1629class ArticleListView(BaseCollectionView):
1630 template_name = "collection-list.html"
1632 def get_collection(self, aid, year):
1633 return Article.objects.filter(
1634 Q(my_container__my_collection__pid=aid)
1635 & (
1636 Q(date_published__year=year, date_online_first__isnull=True)
1637 | Q(date_online_first__year=year)
1638 )
1639 ).prefetch_related("resourcecount_set")
1642class VolumeListView(BaseCollectionView):
1643 template_name = "collection-list.html"
1645 def get_collection(self, aid, year):
1646 return Article.objects.filter(
1647 Q(my_container__my_collection__pid=aid, my_container__year=year)
1648 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
1649 ).prefetch_related("resourcecount_set")
1652class DOAJResourceRegisterView(View):
1653 def get(self, request, *args, **kwargs):
1654 pid = kwargs.get("pid", None)
1655 resource = model_helpers.get_resource(pid)
1656 if resource is None:
1657 raise Http404
1659 try:
1660 data = {}
1661 doaj_meta, response = doaj_pid_register(pid)
1662 if response is None:
1663 return HttpResponse(status=204)
1664 elif doaj_meta and 200 <= response.status_code <= 299:
1665 data.update(doaj_meta)
1666 else:
1667 return HttpResponse(status=response.status_code, reason=response.text)
1668 except Timeout as exception:
1669 return HttpResponse(exception, status=408)
1670 except Exception as exception:
1671 return HttpResponseServerError(exception)
1672 return JsonResponse(data)
1675class CROSSREFResourceRegisterView(View):
1676 def get(self, request, *args, **kwargs):
1677 pid = kwargs.get("pid", None)
1678 # option force for registering doi of articles without date_published (ex; TSG from Numdam)
1679 force = kwargs.get("force", None)
1680 if not request.user.is_superuser:
1681 force = None
1683 resource = model_helpers.get_resource(pid)
1684 if resource is None:
1685 raise Http404
1687 resource = resource.cast()
1688 meth = getattr(self, "recordDOI" + resource.classname)
1689 try:
1690 data = meth(resource, force)
1691 except Timeout as exception:
1692 return HttpResponse(exception, status=408)
1693 except Exception as exception:
1694 return HttpResponseServerError(exception)
1695 return JsonResponse(data)
1697 def recordDOIArticle(self, article, force=None):
1698 result = {"status": 404}
1699 if (
1700 article.doi
1701 and not article.do_not_publish
1702 and (article.date_published or article.date_online_first or force == "force")
1703 ):
1704 if article.my_container.year is None: # or article.my_container.year == '0':
1705 article.my_container.year = datetime.now().strftime("%Y")
1706 result = recordDOI(article)
1707 return result
1709 def recordDOICollection(self, collection, force=None):
1710 return recordDOI(collection)
1712 def recordDOIContainer(self, container, force=None):
1713 data = {"status": 200, "message": "tout va bien"}
1715 if container.ctype == "issue":
1716 if container.doi:
1717 result = recordDOI(container)
1718 if result["status"] != 200:
1719 return result
1720 if force == "force":
1721 articles = container.article_set.exclude(
1722 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True
1723 )
1724 else:
1725 articles = container.article_set.exclude(
1726 doi__isnull=True,
1727 do_not_publish=True,
1728 date_published__isnull=True,
1729 date_online_first__isnull=True,
1730 )
1732 for article in articles:
1733 result = self.recordDOIArticle(article, force)
1734 if result["status"] != 200:
1735 data = result
1736 else:
1737 return recordDOI(container)
1738 return data
1741class CROSSREFResourceCheckStatusView(View):
1742 def get(self, request, *args, **kwargs):
1743 pid = kwargs.get("pid", None)
1744 resource = model_helpers.get_resource(pid)
1745 if resource is None:
1746 raise Http404
1747 resource = resource.cast()
1748 meth = getattr(self, "checkDOI" + resource.classname)
1749 try:
1750 meth(resource)
1751 except Timeout as exception:
1752 return HttpResponse(exception, status=408)
1753 except Exception as exception:
1754 return HttpResponseServerError(exception)
1756 data = {"status": 200, "message": "tout va bien"}
1757 return JsonResponse(data)
1759 def checkDOIArticle(self, article):
1760 if article.my_container.year is None or article.my_container.year == "0":
1761 article.my_container.year = datetime.now().strftime("%Y")
1762 checkDOI(article)
1764 def checkDOICollection(self, collection):
1765 checkDOI(collection)
1767 def checkDOIContainer(self, container):
1768 if container.doi is not None:
1769 checkDOI(container)
1770 for article in container.article_set.all():
1771 self.checkDOIArticle(article)
1774class RegisterPubmedFormView(FormView):
1775 template_name = "record_pubmed_dialog.html"
1776 form_class = RegisterPubmedForm
1778 def get_context_data(self, **kwargs):
1779 context = super().get_context_data(**kwargs)
1780 context["pid"] = self.kwargs["pid"]
1781 context["helper"] = PtfLargeModalFormHelper
1782 return context
1785class RegisterPubmedView(View):
1786 def get(self, request, *args, **kwargs):
1787 pid = kwargs.get("pid", None)
1788 update_article = self.request.GET.get("update_article", "on") == "on"
1790 article = model_helpers.get_article(pid)
1791 if article is None:
1792 raise Http404
1793 try:
1794 recordPubmed(article, update_article)
1795 except Exception as exception:
1796 messages.error("Unable to register the article in PubMed")
1797 return HttpResponseServerError(exception)
1799 return HttpResponseRedirect(
1800 reverse("issue-items", kwargs={"pid": article.my_container.pid})
1801 )
1804class PTFToolsContainerView(TemplateView):
1805 template_name = ""
1807 def get_context_data(self, **kwargs):
1808 context = super().get_context_data(**kwargs)
1810 container = model_helpers.get_container(self.kwargs.get("pid"))
1811 if container is None:
1812 raise Http404
1813 citing_articles = container.citations()
1814 source = self.request.GET.get("source", None)
1815 if container.ctype.startswith("book"):
1816 book_parts = (
1817 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq")
1818 )
1819 references = False
1820 if container.ctype == "book-monograph":
1821 # on regarde si il y a au moins une bibliographie
1822 for art in container.article_set.all():
1823 if art.bibitem_set.count() > 0:
1824 references = True
1825 context.update(
1826 {
1827 "book": container,
1828 "book_parts": list(book_parts),
1829 "source": source,
1830 "citing_articles": citing_articles,
1831 "references": references,
1832 "test_website": container.get_top_collection()
1833 .extlink_set.get(rel="test_website")
1834 .location,
1835 "prod_website": container.get_top_collection()
1836 .extlink_set.get(rel="website")
1837 .location,
1838 }
1839 )
1840 self.template_name = "book-toc.html"
1841 else:
1842 articles = container.article_set.all().order_by("seq")
1843 for article in articles:
1844 try:
1845 last_match = (
1846 history_models.HistoryEvent.objects.filter(
1847 pid=article.pid,
1848 type="matching",
1849 )
1850 .only("created_on")
1851 .latest("created_on")
1852 )
1853 except history_models.HistoryEvent.DoesNotExist as _:
1854 article.last_match = None
1855 else:
1856 article.last_match = last_match.created_on
1858 # article1 = articles.first()
1859 # date = article1.deployed_date()
1860 # TODO next_issue, previous_issue
1862 # check DOI est maintenant une commande à part
1863 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI
1864 # articlesWithStatus = []
1865 # for article in articles:
1866 # checkDOIExistence(article)
1867 # articlesWithStatus.append(article)
1869 test_location = prod_location = ""
1870 qs = container.get_top_collection().extlink_set.filter(rel="test_website")
1871 if qs:
1872 test_location = qs.first().location
1873 qs = container.get_top_collection().extlink_set.filter(rel="website")
1874 if qs:
1875 prod_location = qs.first().location
1876 context.update(
1877 {
1878 "issue": container,
1879 "articles": articles,
1880 "source": source,
1881 "citing_articles": citing_articles,
1882 "test_website": test_location,
1883 "prod_website": prod_location,
1884 }
1885 )
1886 self.template_name = "issue-items.html"
1888 context["allow_crossref"] = container.allow_crossref()
1889 context["coltype"] = container.my_collection.coltype
1890 return context
1893class ExtLinkInline(InlineFormSetFactory):
1894 model = ExtLink
1895 form_class = ExtLinkForm
1896 factory_kwargs = {"extra": 0}
1899class ResourceIdInline(InlineFormSetFactory):
1900 model = ResourceId
1901 form_class = ResourceIdForm
1902 factory_kwargs = {"extra": 0}
1905class IssueDetailAPIView(View):
1906 def get(self, request, *args, **kwargs):
1907 issue = get_object_or_404(Container, pid=kwargs["pid"])
1908 deployed_date = issue.deployed_date()
1909 result = {
1910 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M")
1911 if deployed_date
1912 else None,
1913 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"),
1914 "all_doi_are_registered": issue.all_doi_are_registered(),
1915 "registered_in_doaj": issue.registered_in_doaj(),
1916 "doi": issue.my_collection.doi,
1917 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(),
1918 }
1919 try:
1920 latest = get_last_unsolved_error(pid=issue.pid, strict=False)
1921 except history_models.HistoryEvent.DoesNotExist as _:
1922 pass
1923 else:
1924 result["latest"] = latest.data["message"]
1925 result["latest_target"] = latest.data.get("target", "")
1926 result["latest_date"] = timezone.localtime(latest.created_on).strftime(
1927 "%Y-%m-%d %H:%M"
1928 )
1930 result["latest_type"] = latest.type.capitalize()
1931 for event_type in ["matching", "edit", "deploy", "archive", "import"]:
1932 try:
1933 result[event_type] = timezone.localtime(
1934 history_models.HistoryEvent.objects.filter(
1935 type=event_type,
1936 status="OK",
1937 pid__startswith=issue.pid,
1938 )
1939 .latest("created_on")
1940 .created_on
1941 ).strftime("%Y-%m-%d %H:%M")
1942 except history_models.HistoryEvent.DoesNotExist as _:
1943 result[event_type] = ""
1944 return JsonResponse(result)
1947class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View):
1948 model = Collection
1949 form_class = CollectionForm
1950 inlines = [ResourceIdInline, ExtLinkInline]
1951 inlines_names = ["resource_ids_form", "ext_links_form"]
1953 def get_context_data(self, **kwargs):
1954 context = super().get_context_data(**kwargs)
1955 context["helper"] = PtfFormHelper
1956 context["formset_helper"] = FormSetHelper
1957 return context
1959 def add_description(self, collection, description, lang, seq):
1960 if description:
1961 la = Abstract(
1962 resource=collection,
1963 tag="description",
1964 lang=lang,
1965 seq=seq,
1966 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>',
1967 value_html=description,
1968 value_tex=description,
1969 )
1970 la.save()
1972 def form_valid(self, form):
1973 if form.instance.abbrev:
1974 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>"
1975 else:
1976 form.instance.title_xml = (
1977 f"<title-group><title>{form.instance.title_tex}</title></title-group>"
1978 )
1980 form.instance.title_html = form.instance.title_tex
1981 form.instance.title_sort = form.instance.title_tex
1982 result = super().form_valid(form)
1984 collection = self.object
1985 collection.abstract_set.all().delete()
1987 seq = 1
1988 description = form.cleaned_data["description_en"]
1989 if description:
1990 self.add_description(collection, description, "en", seq)
1991 seq += 1
1992 description = form.cleaned_data["description_fr"]
1993 if description:
1994 self.add_description(collection, description, "fr", seq)
1996 return result
1998 def get_success_url(self):
1999 messages.success(self.request, "La Collection a été modifiée avec succès")
2000 return reverse("collection-detail", kwargs={"pid": self.object.pid})
2003class CollectionCreate(CollectionFormView, CreateWithInlinesView):
2004 """
2005 Warning : Not yet finished
2006 Automatic site membership creation is still missing
2007 """
2010class CollectionUpdate(CollectionFormView, UpdateWithInlinesView):
2011 slug_field = "pid"
2012 slug_url_kwarg = "pid"
2015def suggest_load_journal_dois(colid):
2016 articles = (
2017 Article.objects.filter(my_container__my_collection__pid=colid)
2018 .filter(doi__isnull=False)
2019 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False))
2020 .values_list("doi", flat=True)
2021 )
2023 try:
2024 articles = sorted(
2025 articles,
2026 key=lambda d: (
2027 re.search(r"([a-zA-Z]+).\d+$", d).group(1),
2028 int(re.search(r".(\d+)$", d).group(1)),
2029 ),
2030 )
2031 except: # noqa: E722 (we'll look later)
2032 pass
2033 return [f'<option value="{doi}">' for doi in articles]
2036def get_context_with_volumes(journal):
2037 result = model_helpers.get_volumes_in_collection(journal)
2038 volume_count = result["volume_count"]
2039 collections = []
2040 for ancestor in journal.ancestors.all():
2041 item = model_helpers.get_volumes_in_collection(ancestor)
2042 volume_count = max(0, volume_count)
2043 item.update({"journal": ancestor})
2044 collections.append(item)
2046 # add the parent collection to its children list and sort it by date
2047 result.update({"journal": journal})
2048 collections.append(result)
2050 collections = [c for c in collections if c["sorted_issues"]]
2051 collections.sort(
2052 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"],
2053 reverse=True,
2054 )
2056 context = {
2057 "journal": journal,
2058 "sorted_issues": result["sorted_issues"],
2059 "volume_count": volume_count,
2060 "max_width": result["max_width"],
2061 "collections": collections,
2062 "choices": "\n".join(suggest_load_journal_dois(journal.pid)),
2063 }
2064 return context
2067class CollectionDetail(
2068 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin
2069):
2070 model = Collection
2071 slug_field = "pid"
2072 slug_url_kwarg = "pid"
2073 template_name = "ptf/collection_detail.html"
2075 def test_func(self):
2076 return is_authorized_editor(self.request.user, self.kwargs.get("pid"))
2078 def get(self, request, *args, **kwargs):
2079 self.object = self.get_object(queryset=Collection.objects.all())
2080 return super().get(request, *args, **kwargs)
2082 def get_context_data(self, **kwargs):
2083 context = super().get_context_data(**kwargs)
2084 context["object_list"] = context["object_list"].filter(
2085 Q(ctype="issue") | Q(ctype="book-lecture-notes")
2086 )
2087 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS
2088 context.update(get_context_with_volumes(self.object))
2090 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS:
2091 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid]
2092 context["issue_to_appear"] = Container.objects.filter(
2093 pid=context["issue_to_appear_pid"]
2094 ).exists()
2095 try:
2096 latest_error = history_models.HistoryEvent.objects.filter(
2097 status="ERROR", col=self.object
2098 ).latest("created_on")
2099 except history_models.HistoryEvent.DoesNotExist as _:
2100 pass
2101 else:
2102 message = latest_error.data["message"]
2103 i = message.find(" - ")
2104 latest_exception = message[:i]
2105 latest_error_message = message[i + 3 :]
2106 context["latest_exception"] = latest_exception
2107 context["latest_exception_date"] = latest_error.created_on
2108 context["latest_exception_type"] = latest_error.type
2109 context["latest_error_message"] = latest_error_message
2111 archive_in_error = history_models.HistoryEvent.objects.filter(
2112 status="ERROR", col=self.object, type="archive"
2113 ).exists()
2115 context["archive_in_error"] = archive_in_error
2117 return context
2119 def get_queryset(self):
2120 query = self.object.content.all()
2122 for ancestor in self.object.ancestors.all():
2123 query |= ancestor.content.all()
2125 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int")
2128class ContainerEditView(FormView):
2129 template_name = "container_form.html"
2130 form_class = ContainerForm
2132 def get_success_url(self):
2133 if self.kwargs["pid"]:
2134 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]})
2135 return reverse("mersenne_dashboard/published_articles")
2137 def set_success_message(self): # pylint: disable=no-self-use
2138 messages.success(self.request, "Le fascicule a été modifié")
2140 def get_form_kwargs(self):
2141 kwargs = super().get_form_kwargs()
2142 if "pid" not in self.kwargs:
2143 self.kwargs["pid"] = None
2144 if "colid" not in self.kwargs:
2145 self.kwargs["colid"] = None
2146 if "data" in kwargs and "colid" in kwargs["data"]:
2147 # colid is passed as a hidden param in the form.
2148 # It is used when you submit a new container
2149 self.kwargs["colid"] = kwargs["data"]["colid"]
2151 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container(
2152 self.kwargs["pid"]
2153 )
2154 return kwargs
2156 def get_context_data(self, **kwargs):
2157 context = super().get_context_data(**kwargs)
2159 context["pid"] = self.kwargs["pid"]
2160 context["colid"] = self.kwargs["colid"]
2161 context["container"] = self.kwargs["container"]
2163 context["edit_container"] = context["pid"] is not None
2164 context["name"] = resolve(self.request.path_info).url_name
2166 return context
2168 def form_valid(self, form):
2169 new_pid = form.cleaned_data.get("pid")
2170 new_title = form.cleaned_data.get("title")
2171 new_trans_title = form.cleaned_data.get("trans_title")
2172 new_publisher = form.cleaned_data.get("publisher")
2173 new_year = form.cleaned_data.get("year")
2174 new_volume = form.cleaned_data.get("volume")
2175 new_number = form.cleaned_data.get("number")
2177 collection = None
2178 issue = self.kwargs["container"]
2179 if issue is not None:
2180 collection = issue.my_collection
2181 elif self.kwargs["colid"] is not None:
2182 if "CR" in self.kwargs["colid"]:
2183 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False)
2184 else:
2185 collection = model_helpers.get_collection(self.kwargs["colid"])
2187 if collection is None:
2188 raise ValueError("Collection for " + new_pid + " does not exist")
2190 # Icon
2191 new_icon_location = ""
2192 if "icon" in self.request.FILES:
2193 filename = os.path.basename(self.request.FILES["icon"].name)
2194 file_extension = filename.split(".")[1]
2196 icon_filename = resolver.get_disk_location(
2197 settings.MERSENNE_TEST_DATA_FOLDER,
2198 collection.pid,
2199 file_extension,
2200 new_pid,
2201 None,
2202 True,
2203 )
2205 with open(icon_filename, "wb+") as destination:
2206 for chunk in self.request.FILES["icon"].chunks():
2207 destination.write(chunk)
2209 folder = resolver.get_relative_folder(collection.pid, new_pid)
2210 new_icon_location = os.path.join(folder, new_pid + "." + file_extension)
2211 name = resolve(self.request.path_info).url_name
2212 if name == "special_issue_create":
2213 self.kwargs["name"] = name
2214 if self.kwargs["container"]:
2215 # Edit Issue
2216 issue = self.kwargs["container"]
2217 if issue is None:
2218 raise ValueError(self.kwargs["pid"] + " does not exist")
2220 issue.pid = new_pid
2221 issue.title_tex = issue.title_html = new_title
2222 issue.title_xml = build_title_xml(
2223 title=new_title,
2224 lang=issue.lang,
2225 title_type="issue-title",
2226 )
2228 trans_lang = ""
2229 if issue.trans_lang != "" and issue.trans_lang != "und":
2230 trans_lang = issue.trans_lang
2231 elif new_trans_title != "":
2232 trans_lang = "fr" if issue.lang == "en" else "en"
2233 issue.trans_lang = trans_lang
2235 if trans_lang != "" and new_trans_title != "":
2236 issue.trans_title_html = ""
2237 issue.trans_title_tex = ""
2238 title_xml = build_title_xml(
2239 title=new_trans_title, lang=trans_lang, title_type="issue-title"
2240 )
2241 try:
2242 trans_title_object = Title.objects.get(resource=issue, lang=trans_lang)
2243 trans_title_object.title_html = new_trans_title
2244 trans_title_object.title_xml = title_xml
2245 trans_title_object.save()
2246 except Title.DoesNotExist:
2247 trans_title = Title(
2248 resource=issue,
2249 lang=trans_lang,
2250 type="main",
2251 title_html=new_trans_title,
2252 title_xml=title_xml,
2253 )
2254 trans_title.save()
2255 issue.year = new_year
2256 issue.volume = new_volume
2257 issue.volume_int = make_int(new_volume)
2258 issue.number = new_number
2259 issue.number_int = make_int(new_number)
2260 issue.save()
2261 else:
2262 xissue = create_issuedata()
2264 xissue.ctype = "issue"
2265 xissue.pid = new_pid
2266 xissue.lang = "en"
2267 xissue.title_tex = new_title
2268 xissue.title_html = new_title
2269 xissue.title_xml = build_title_xml(
2270 title=new_title, lang=xissue.lang, title_type="issue-title"
2271 )
2273 if new_trans_title != "":
2274 trans_lang = "fr"
2275 title_xml = build_title_xml(
2276 title=new_trans_title, lang=trans_lang, title_type="trans-title"
2277 )
2278 title = create_titledata(
2279 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml
2280 )
2281 issue.titles = [title]
2283 xissue.year = new_year
2284 xissue.volume = new_volume
2285 xissue.number = new_number
2286 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2288 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue})
2289 cmd.add_collection(collection)
2290 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc"))
2291 issue = cmd.do()
2293 self.kwargs["pid"] = new_pid
2295 # Add objects related to the article: contribs, datastream, counts...
2296 params = {
2297 "icon_location": new_icon_location,
2298 }
2299 cmd = ptf_cmds.updateContainerPtfCmd(params)
2300 cmd.set_resource(issue)
2301 cmd.do()
2303 publisher = model_helpers.get_publisher(new_publisher)
2304 if not publisher:
2305 xpub = create_publisherdata()
2306 xpub.name = new_publisher
2307 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do()
2308 issue.my_publisher = publisher
2309 issue.save()
2311 self.set_success_message()
2313 return super().form_valid(form)
2316# class ArticleEditView(FormView):
2317# template_name = 'article_form.html'
2318# form_class = ArticleForm
2319#
2320# def get_success_url(self):
2321# if self.kwargs['pid']:
2322# return reverse('article', kwargs={'aid': self.kwargs['pid']})
2323# return reverse('mersenne_dashboard/published_articles')
2324#
2325# def set_success_message(self): # pylint: disable=no-self-use
2326# messages.success(self.request, "L'article a été modifié")
2327#
2328# def get_form_kwargs(self):
2329# kwargs = super(ArticleEditView, self).get_form_kwargs()
2330#
2331# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None':
2332# # Article creation: pid is None
2333# self.kwargs['pid'] = None
2334# if 'issue_id' not in self.kwargs:
2335# # Article edit: issue_id is not passed
2336# self.kwargs['issue_id'] = None
2337# if 'data' in kwargs and 'issue_id' in kwargs['data']:
2338# # colid is passed as a hidden param in the form.
2339# # It is used when you submit a new container
2340# self.kwargs['issue_id'] = kwargs['data']['issue_id']
2341#
2342# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid'])
2343# return kwargs
2344#
2345# def get_context_data(self, **kwargs):
2346# context = super(ArticleEditView, self).get_context_data(**kwargs)
2347#
2348# context['pid'] = self.kwargs['pid']
2349# context['issue_id'] = self.kwargs['issue_id']
2350# context['article'] = self.kwargs['article']
2351#
2352# context['edit_article'] = context['pid'] is not None
2353#
2354# article = context['article']
2355# if article:
2356# context['author_contributions'] = article.get_author_contributions()
2357# context['kwds_fr'] = None
2358# context['kwds_en'] = None
2359# kwd_gps = article.get_non_msc_kwds()
2360# for kwd_gp in kwd_gps:
2361# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'):
2362# if kwd_gp.value_xml:
2363# kwd_ = types.SimpleNamespace()
2364# kwd_.value = kwd_gp.value_tex
2365# context['kwd_unstructured_fr'] = kwd_
2366# context['kwds_fr'] = kwd_gp.kwd_set.all()
2367# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'):
2368# if kwd_gp.value_xml:
2369# kwd_ = types.SimpleNamespace()
2370# kwd_.value = kwd_gp.value_tex
2371# context['kwd_unstructured_en'] = kwd_
2372# context['kwds_en'] = kwd_gp.kwd_set.all()
2373#
2374# # Article creation: init pid
2375# if context['issue_id'] and context['pid'] is None:
2376# issue = model_helpers.get_container(context['issue_id'])
2377# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0'
2378#
2379# return context
2380#
2381# def form_valid(self, form):
2382#
2383# new_pid = form.cleaned_data.get('pid')
2384# new_title = form.cleaned_data.get('title')
2385# new_fpage = form.cleaned_data.get('fpage')
2386# new_lpage = form.cleaned_data.get('lpage')
2387# new_page_range = form.cleaned_data.get('page_range')
2388# new_page_count = form.cleaned_data.get('page_count')
2389# new_coi_statement = form.cleaned_data.get('coi_statement')
2390# new_show_body = form.cleaned_data.get('show_body')
2391# new_do_not_publish = form.cleaned_data.get('do_not_publish')
2392#
2393# # TODO support MathML
2394# # 27/10/2020: title_xml embeds the trans_title_group in JATS.
2395# # We need to pass trans_title to get_title_xml
2396# # Meanwhile, ignore new_title_xml
2397# new_title_xml = jats_parser.get_title_xml(new_title)
2398# new_title_html = new_title
2399#
2400# authors_count = int(self.request.POST.get('authors_count', "0"))
2401# i = 1
2402# new_authors = []
2403# old_author_contributions = []
2404# if self.kwargs['article']:
2405# old_author_contributions = self.kwargs['article'].get_author_contributions()
2406#
2407# while authors_count > 0:
2408# prefix = self.request.POST.get('contrib-p-' + str(i), None)
2409#
2410# if prefix is not None:
2411# addresses = []
2412# if len(old_author_contributions) >= i:
2413# old_author_contribution = old_author_contributions[i - 1]
2414# addresses = [contrib_address.address for contrib_address in
2415# old_author_contribution.get_addresses()]
2416#
2417# first_name = self.request.POST.get('contrib-f-' + str(i), None)
2418# last_name = self.request.POST.get('contrib-l-' + str(i), None)
2419# suffix = self.request.POST.get('contrib-s-' + str(i), None)
2420# orcid = self.request.POST.get('contrib-o-' + str(i), None)
2421# deceased = self.request.POST.get('contrib-d-' + str(i), None)
2422# deceased_before_publication = deceased == 'on'
2423# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None)
2424# equal_contrib = equal_contrib == 'on'
2425# corresponding = self.request.POST.get('corresponding-' + str(i), None)
2426# corresponding = corresponding == 'on'
2427# email = self.request.POST.get('email-' + str(i), None)
2428#
2429# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid)
2430# params['deceased_before_publication'] = deceased_before_publication
2431# params['equal_contrib'] = equal_contrib
2432# params['corresponding'] = corresponding
2433# params['addresses'] = addresses
2434# params['email'] = email
2435#
2436# params['contrib_xml'] = xml_utils.get_contrib_xml(params)
2437#
2438# new_authors.append(params)
2439#
2440# authors_count -= 1
2441# i += 1
2442#
2443# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0"))
2444# i = 1
2445# new_kwds_fr = []
2446# while kwds_fr_count > 0:
2447# value = self.request.POST.get('kwd-fr-' + str(i), None)
2448# new_kwds_fr.append(value)
2449# kwds_fr_count -= 1
2450# i += 1
2451# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None)
2452#
2453# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0"))
2454# i = 1
2455# new_kwds_en = []
2456# while kwds_en_count > 0:
2457# value = self.request.POST.get('kwd-en-' + str(i), None)
2458# new_kwds_en.append(value)
2459# kwds_en_count -= 1
2460# i += 1
2461# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None)
2462#
2463# if self.kwargs['article']:
2464# # Edit article
2465# container = self.kwargs['article'].my_container
2466# else:
2467# # New article
2468# container = model_helpers.get_container(self.kwargs['issue_id'])
2469#
2470# if container is None:
2471# raise ValueError(self.kwargs['issue_id'] + " does not exist")
2472#
2473# collection = container.my_collection
2474#
2475# # Copy PDF file & extract full text
2476# body = ''
2477# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2478# collection.pid,
2479# "pdf",
2480# container.pid,
2481# new_pid,
2482# True)
2483# if 'pdf' in self.request.FILES:
2484# with open(pdf_filename, 'wb+') as destination:
2485# for chunk in self.request.FILES['pdf'].chunks():
2486# destination.write(chunk)
2487#
2488# # Extract full text from the PDF
2489# body = utils.pdf_to_text(pdf_filename)
2490#
2491# # Icon
2492# new_icon_location = ''
2493# if 'icon' in self.request.FILES:
2494# filename = os.path.basename(self.request.FILES['icon'].name)
2495# file_extension = filename.split('.')[1]
2496#
2497# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER,
2498# collection.pid,
2499# file_extension,
2500# container.pid,
2501# new_pid,
2502# True)
2503#
2504# with open(icon_filename, 'wb+') as destination:
2505# for chunk in self.request.FILES['icon'].chunks():
2506# destination.write(chunk)
2507#
2508# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid)
2509# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension)
2510#
2511# if self.kwargs['article']:
2512# # Edit article
2513# article = self.kwargs['article']
2514# article.fpage = new_fpage
2515# article.lpage = new_lpage
2516# article.page_range = new_page_range
2517# article.coi_statement = new_coi_statement
2518# article.show_body = new_show_body
2519# article.do_not_publish = new_do_not_publish
2520# article.save()
2521#
2522# else:
2523# # New article
2524# params = {
2525# 'pid': new_pid,
2526# 'title_xml': new_title_xml,
2527# 'title_html': new_title_html,
2528# 'title_tex': new_title,
2529# 'fpage': new_fpage,
2530# 'lpage': new_lpage,
2531# 'page_range': new_page_range,
2532# 'seq': container.article_set.count() + 1,
2533# 'body': body,
2534# 'coi_statement': new_coi_statement,
2535# 'show_body': new_show_body,
2536# 'do_not_publish': new_do_not_publish
2537# }
2538#
2539# xarticle = create_articledata()
2540# xarticle.pid = new_pid
2541# xarticle.title_xml = new_title_xml
2542# xarticle.title_html = new_title_html
2543# xarticle.title_tex = new_title
2544# xarticle.fpage = new_fpage
2545# xarticle.lpage = new_lpage
2546# xarticle.page_range = new_page_range
2547# xarticle.seq = container.article_set.count() + 1
2548# xarticle.body = body
2549# xarticle.coi_statement = new_coi_statement
2550# params['xobj'] = xarticle
2551#
2552# cmd = ptf_cmds.addArticlePtfCmd(params)
2553# cmd.set_container(container)
2554# cmd.add_collection(container.my_collection)
2555# article = cmd.do()
2556#
2557# self.kwargs['pid'] = new_pid
2558#
2559# # Add objects related to the article: contribs, datastream, counts...
2560# params = {
2561# # 'title_xml': new_title_xml,
2562# # 'title_html': new_title_html,
2563# # 'title_tex': new_title,
2564# 'authors': new_authors,
2565# 'page_count': new_page_count,
2566# 'icon_location': new_icon_location,
2567# 'body': body,
2568# 'use_kwds': True,
2569# 'kwds_fr': new_kwds_fr,
2570# 'kwds_en': new_kwds_en,
2571# 'kwd_uns_fr': new_kwd_uns_fr,
2572# 'kwd_uns_en': new_kwd_uns_en
2573# }
2574# cmd = ptf_cmds.updateArticlePtfCmd(params)
2575# cmd.set_article(article)
2576# cmd.do()
2577#
2578# self.set_success_message()
2579#
2580# return super(ArticleEditView, self).form_valid(form)
2583@require_http_methods(["POST"])
2584def do_not_publish_article(request, *args, **kwargs):
2585 next = request.headers.get("referer")
2587 pid = kwargs.get("pid", "")
2589 article = model_helpers.get_article(pid)
2590 if article:
2591 article.do_not_publish = not article.do_not_publish
2592 article.save()
2593 else:
2594 raise Http404
2596 return HttpResponseRedirect(next)
2599@require_http_methods(["POST"])
2600def show_article_body(request, *args, **kwargs):
2601 next = request.headers.get("referer")
2603 pid = kwargs.get("pid", "")
2605 article = model_helpers.get_article(pid)
2606 if article:
2607 article.show_body = not article.show_body
2608 article.save()
2609 else:
2610 raise Http404
2612 return HttpResponseRedirect(next)
2615class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView):
2616 """
2617 API to get/post article metadata
2618 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views)
2619 """
2621 def __init__(self, *args, **kwargs):
2622 """
2623 we define here what fields we want in the form
2624 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title
2625 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update"""
2626 super().__init__(*args, **kwargs)
2627 self.fields_to_update = [
2628 "lang",
2629 "atype",
2630 "contributors",
2631 "abstracts",
2632 "kwds",
2633 "titles",
2634 "trans_title_html",
2635 "title_html",
2636 "title_xml",
2637 "streams",
2638 "ext_links",
2639 ]
2640 self.additional_fields = [
2641 "pid",
2642 "doi",
2643 "container_pid",
2644 "pdf",
2645 "illustration",
2646 ]
2647 self.editorial_tools = ["translation", "sidebar", "lang_selection"]
2648 self.article_container_pid = ""
2649 self.back_url = "trammel"
2651 def save_data(self, data_article):
2652 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json
2653 # The icons are not preserved since we can add/edit/delete them in VueJs
2654 params = {
2655 "pid": data_article.pid,
2656 "export_folder": settings.MERSENNE_TMP_FOLDER,
2657 "export_all": True,
2658 "with_binary_files": False,
2659 }
2660 ptf_cmds.exportExtraDataPtfCmd(params).do()
2662 def restore_data(self, article):
2663 ptf_cmds.importExtraDataPtfCmd(
2664 {
2665 "pid": article.pid,
2666 "import_folder": settings.MERSENNE_TMP_FOLDER,
2667 }
2668 ).do()
2670 def get(self, request, *args, **kwargs):
2671 data = super().get(request, *args, **kwargs)
2672 return data
2674 def post(self, request, *args, **kwargs):
2675 response = super().post(request, *args, **kwargs)
2676 if response["message"] == "OK":
2677 return redirect(
2678 "api-edit-article",
2679 colid=kwargs.get("colid", ""),
2680 containerPid=kwargs.get("containerPid"),
2681 doi=kwargs.get("doi", ""),
2682 )
2683 else:
2684 raise Http404
2687class ArticleEditWithVueView(LoginRequiredMixin, TemplateView):
2688 template_name = "article_form.html"
2690 def get_success_url(self):
2691 if self.kwargs["doi"]:
2692 return reverse("article", kwargs={"aid": self.kwargs["doi"]})
2693 return reverse("mersenne_dashboard/published_articles")
2695 def get_context_data(self, **kwargs):
2696 context = super().get_context_data(**kwargs)
2697 if "doi" in self.kwargs:
2698 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"])
2699 context["pid"] = context["article"].pid
2701 return context
2704class ArticleDeleteView(View):
2705 def get(self, request, *args, **kwargs):
2706 pid = self.kwargs.get("pid", None)
2707 article = get_object_or_404(Article, pid=pid)
2709 try:
2710 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid)
2711 article.undeploy(mersenneSite)
2713 cmd = ptf_cmds.addArticlePtfCmd(
2714 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER}
2715 )
2716 cmd.set_container(article.my_container)
2717 cmd.set_object_to_be_deleted(article)
2718 cmd.undo()
2719 except Exception as exception:
2720 return HttpResponseServerError(exception)
2722 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200}
2723 return JsonResponse(data)
2726def get_messages_in_queue():
2727 app = Celery("ptf-tools")
2728 # tasks = list(current_app.tasks)
2729 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery")))
2730 print(tasks)
2731 # i = app.control.inspect()
2733 with app.connection_or_acquire() as conn:
2734 remaining = conn.default_channel.queue_declare(queue="celery", passive=True).message_count
2735 return remaining
2738class FailedTasksListView(ListView):
2739 model = TaskResult
2740 queryset = TaskResult.objects.filter(
2741 status="FAILURE",
2742 task_name="ptf_tools.tasks.archive_numdam_issue",
2743 )
2746class FailedTasksDeleteView(DeleteView):
2747 model = TaskResult
2748 success_url = reverse_lazy("tasks-failed")
2751class FailedTasksRetryView(SingleObjectMixin, RedirectView):
2752 model = TaskResult
2754 @staticmethod
2755 def retry_task(task):
2756 colid, pid = (arg.strip("'") for arg in task.task_args.strip("()").split(", "))
2757 run_task(ArchiveNumdamIssueTask(colid, pid))
2759 task.delete()
2761 def get_redirect_url(self, *args, **kwargs):
2762 self.retry_task(self.get_object())
2763 return reverse("tasks-failed")
2766class NumdamView(TemplateView, history_views.HistoryContextMixin):
2767 template_name = "numdam.html"
2769 def get_context_data(self, **kwargs):
2770 context = super().get_context_data(**kwargs)
2772 context["objs"] = ResourceInNumdam.objects.all()
2774 pre_issues = []
2775 prod_issues = []
2776 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/"
2777 try:
2778 response = requests.get(url)
2779 if response.status_code == 200:
2780 data = response.json()
2781 if "issues" in data:
2782 pre_issues = data["issues"]
2783 except Exception:
2784 pass
2786 url = f"{settings.NUMDAM_URL}/api-all-issues/"
2787 response = requests.get(url)
2788 if response.status_code == 200:
2789 data = response.json()
2790 if "issues" in data:
2791 prod_issues = data["issues"]
2793 new = sorted(list(set(pre_issues).difference(prod_issues)))
2794 removed = sorted(list(set(prod_issues).difference(pre_issues)))
2795 grouped = [
2796 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0])
2797 ]
2798 grouped_removed = [
2799 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0])
2800 ]
2801 context["added_issues"] = grouped
2802 context["removed_issues"] = grouped_removed
2804 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS
2805 return context
2808class TasksProgressView(View):
2809 def get(self, *args, **kwargs):
2810 task_name = self.kwargs.get("task", "archive_numdam_issue")
2811 successes = TaskResult.objects.filter(
2812 task_name=f"ptf_tools.tasks.{task_name}", status="SUCCESS"
2813 ).count()
2814 fails = TaskResult.objects.filter(
2815 task_name=f"ptf_tools.tasks.{task_name}", status="FAILURE"
2816 ).count()
2817 last_task = (
2818 TaskResult.objects.filter(
2819 task_name=f"ptf_tools.tasks.{task_name}",
2820 status="SUCCESS",
2821 )
2822 .order_by("-date_done")
2823 .first()
2824 )
2825 if last_task:
2826 last_task = " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
2827 remaining = get_messages_in_queue()
2828 all = successes + remaining
2829 progress = int(successes * 100 / all) if all else 0
2830 error_rate = int(fails * 100 / all) if all else 0
2831 status = "consuming_queue" if (successes or fails) and not progress == 100 else "polling"
2832 data = {
2833 "status": status,
2834 "progress": progress,
2835 "total": all,
2836 "remaining": remaining,
2837 "successes": successes,
2838 "fails": fails,
2839 "error_rate": error_rate,
2840 "last_task": last_task,
2841 }
2842 return JsonResponse(data)
2845class NumdamArchiveView(RedirectView):
2846 @staticmethod
2847 def reset_task_results():
2848 TaskResult.objects.all().delete()
2850 def get_redirect_url(self, *args, **kwargs):
2851 self.colid = kwargs["colid"]
2853 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS:
2854 return Http404
2856 # we make sure archiving is not already running
2857 if not get_messages_in_queue():
2858 self.reset_task_results()
2859 response = requests.get(f"{settings.NUMDAM_URL}/api-all-collections/")
2860 if response.status_code == 200:
2861 data = sorted(response.json()["collections"])
2863 if self.colid != "ALL" and self.colid not in data:
2864 return Http404
2866 colids = [self.colid] if self.colid != "ALL" else data
2868 with open(
2869 os.path.join(settings.LOG_DIR, "archive.log"), "w", encoding="utf-8"
2870 ) as file_:
2871 file_.write("Archive " + " ".join([colid for colid in colids]) + "\n")
2873 for colid in colids:
2874 if colid not in settings.MERSENNE_COLLECTIONS:
2875 run_task(ArchiveNumdamCollectionTask, colid)
2877 return reverse("numdam")
2880class DeployAllNumdamAPIView(View):
2881 def internal_do(self, *args, **kwargs):
2882 pids = []
2884 for obj in ResourceInNumdam.objects.all():
2885 pids.append(obj.pid)
2887 return pids
2889 def get(self, request, *args, **kwargs):
2890 try:
2891 pids, status, message = history_views.execute_and_record_func(
2892 "deploy", "numdam", "numdam", self.internal_do, "numdam"
2893 )
2894 except Exception as exception:
2895 return HttpResponseServerError(exception)
2897 data = {"message": message, "ids": pids, "status": status}
2898 return JsonResponse(data)
2901class NumdamDeleteAPIView(View):
2902 def get(self, request, *args, **kwargs):
2903 pid = self.kwargs.get("pid", None)
2905 try:
2906 obj = ResourceInNumdam.objects.get(pid=pid)
2907 obj.delete()
2908 except Exception as exception:
2909 return HttpResponseServerError(exception)
2911 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200}
2912 return JsonResponse(data)
2915class ExtIdApiDetail(View):
2916 def get(self, request, *args, **kwargs):
2917 extid = get_object_or_404(
2918 ExtId,
2919 resource__pid=kwargs["pid"],
2920 id_type=kwargs["what"],
2921 )
2922 return JsonResponse(
2923 {
2924 "pk": extid.pk,
2925 "href": extid.get_href(),
2926 "fetch": reverse(
2927 "api-fetch-id",
2928 args=(
2929 extid.resource.pk,
2930 extid.id_value,
2931 extid.id_type,
2932 "extid",
2933 ),
2934 ),
2935 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")),
2936 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")),
2937 "update": reverse("extid-update", kwargs={"pk": extid.pk}),
2938 "delete": reverse("update-extid", args=(extid.pk, "delete")),
2939 "is_valid": extid.checked,
2940 }
2941 )
2944class ExtIdFormTemplate(TemplateView):
2945 template_name = "common/externalid_form.html"
2947 def get_context_data(self, **kwargs):
2948 context = super().get_context_data(**kwargs)
2949 context["sequence"] = kwargs["sequence"]
2950 return context
2953class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
2954 def get_context_data(self, **kwargs):
2955 context = super().get_context_data(**kwargs)
2956 context["helper"] = PtfFormHelper
2957 return context
2959 def get_success_url(self):
2960 self.post_process()
2961 return self.object.bibitem.resource.get_absolute_url()
2963 def post_process(self):
2964 cmd = xml_cmds.updateBibitemCitationXmlCmd()
2965 cmd.set_bibitem(self.object.bibitem)
2966 cmd.do()
2967 model_helpers.post_resource_updated(self.object.bibitem.resource)
2970class BibItemIdCreate(BibItemIdFormView, CreateView):
2971 model = BibItemId
2972 form_class = BibItemIdForm
2974 def get_context_data(self, **kwargs):
2975 context = super().get_context_data(**kwargs)
2976 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
2977 return context
2979 def get_initial(self):
2980 initial = super().get_initial()
2981 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"])
2982 return initial
2984 def form_valid(self, form):
2985 form.instance.checked = False
2986 return super().form_valid(form)
2989class BibItemIdUpdate(BibItemIdFormView, UpdateView):
2990 model = BibItemId
2991 form_class = BibItemIdForm
2993 def get_context_data(self, **kwargs):
2994 context = super().get_context_data(**kwargs)
2995 context["bibitem"] = self.object.bibitem
2996 return context
2999class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View):
3000 def get_context_data(self, **kwargs):
3001 context = super().get_context_data(**kwargs)
3002 context["helper"] = PtfFormHelper
3003 return context
3005 def get_success_url(self):
3006 self.post_process()
3007 return self.object.resource.get_absolute_url()
3009 def post_process(self):
3010 model_helpers.post_resource_updated(self.object.resource)
3013class ExtIdCreate(ExtIdFormView, CreateView):
3014 model = ExtId
3015 form_class = ExtIdForm
3017 def get_context_data(self, **kwargs):
3018 context = super().get_context_data(**kwargs)
3019 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3020 return context
3022 def get_initial(self):
3023 initial = super().get_initial()
3024 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"])
3025 return initial
3027 def form_valid(self, form):
3028 form.instance.checked = False
3029 return super().form_valid(form)
3032class ExtIdUpdate(ExtIdFormView, UpdateView):
3033 model = ExtId
3034 form_class = ExtIdForm
3036 def get_context_data(self, **kwargs):
3037 context = super().get_context_data(**kwargs)
3038 context["resource"] = self.object.resource
3039 return context
3042class BibItemIdApiDetail(View):
3043 def get(self, request, *args, **kwargs):
3044 bibitemid = get_object_or_404(
3045 BibItemId,
3046 bibitem__resource__pid=kwargs["pid"],
3047 bibitem__sequence=kwargs["seq"],
3048 id_type=kwargs["what"],
3049 )
3050 return JsonResponse(
3051 {
3052 "pk": bibitemid.pk,
3053 "href": bibitemid.get_href(),
3054 "fetch": reverse(
3055 "api-fetch-id",
3056 args=(
3057 bibitemid.bibitem.pk,
3058 bibitemid.id_value,
3059 bibitemid.id_type,
3060 "bibitemid",
3061 ),
3062 ),
3063 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")),
3064 "uncheck": reverse(
3065 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive")
3066 ),
3067 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}),
3068 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")),
3069 "is_valid": bibitemid.checked,
3070 }
3071 )
3074class UpdateTexmfZipAPIView(View):
3075 def get(self, request, *args, **kwargs):
3076 def copy_zip_files(src_folder, dest_folder):
3077 os.makedirs(dest_folder, exist_ok=True)
3079 zip_files = [
3080 os.path.join(src_folder, f)
3081 for f in os.listdir(src_folder)
3082 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip")
3083 ]
3084 for zip_file in zip_files:
3085 resolver.copy_file(zip_file, dest_folder)
3087 # Exceptions: specific zip/gz files
3088 zip_file = os.path.join(src_folder, "texmf-bsmf.zip")
3089 resolver.copy_file(zip_file, dest_folder)
3091 zip_file = os.path.join(src_folder, "texmf-cg.zip")
3092 resolver.copy_file(zip_file, dest_folder)
3094 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz")
3095 resolver.copy_file(gz_file, dest_folder)
3097 src_folder = settings.CEDRAM_DISTRIB_FOLDER
3099 dest_folder = os.path.join(
3100 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf"
3101 )
3103 try:
3104 copy_zip_files(src_folder, dest_folder)
3105 except Exception as exception:
3106 return HttpResponseServerError(exception)
3108 try:
3109 dest_folder = os.path.join(
3110 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf"
3111 )
3112 copy_zip_files(src_folder, dest_folder)
3113 except Exception as exception:
3114 return HttpResponseServerError(exception)
3116 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200}
3117 return JsonResponse(data)
3120class TestView(TemplateView):
3121 template_name = "mersenne.html"
3123 def get_context_data(self, **kwargs):
3124 super().get_context_data(**kwargs)
3125 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True)
3126 model_data_converter.db_to_issue_data(issue)
3129class TrammelTasksProgressView(View):
3130 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs):
3131 """
3132 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement
3133 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events)
3134 """
3135 task_name = task
3137 def get_event_data():
3138 # Tasks are typically in the CREATED then SUCCESS or FAILURE state
3140 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet
3141 remaining_messages = get_messages_in_queue()
3143 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}")
3144 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done")
3145 failed_tasks = all_tasks.filter(status="FAILURE")
3147 all_tasks_count = all_tasks.count()
3148 success_count = successed_tasks.count()
3149 fail_count = failed_tasks.count()
3151 all_count = all_tasks_count + remaining_messages
3152 remaining_count = all_count - success_count - fail_count
3154 success_rate = int(success_count * 100 / all_count) if all_count else 0
3155 error_rate = int(fail_count * 100 / all_count) if all_count else 0
3156 status = "consuming_queue" if remaining_count != 0 else "polling"
3158 last_task = successed_tasks.first()
3159 last_task = (
3160 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args])
3161 if last_task
3162 else ""
3163 )
3165 # SSE event format
3166 event_data = {
3167 "status": status,
3168 "success_rate": success_rate,
3169 "error_rate": error_rate,
3170 "all_count": all_count,
3171 "remaining_count": remaining_count,
3172 "success_count": success_count,
3173 "fail_count": fail_count,
3174 "last_task": last_task,
3175 }
3177 return event_data
3179 def stream_response(data):
3180 # Send initial response headers
3181 yield f"data: {json.dumps(data)}\n\n"
3183 data = get_event_data()
3184 format = request.GET.get("format", "stream")
3185 if format == "json":
3186 response = JsonResponse(data)
3187 else:
3188 response = HttpResponse(stream_response(data), content_type="text/event-stream")
3189 return response
3192class TrammelFailedTasksListView(ListView):
3193 model = TaskResult
3194 queryset = TaskResult.objects.filter(
3195 status="FAILURE",
3196 task_name="ptf_tools.tasks.archive_trammel_collection",
3197 )
3200user_signed_up.connect(update_user_from_invite)