Coverage for src/ptf_tools/views/cms_views.py: 48%
884 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-07-30 15:02 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-07-30 15:02 +0000
1import base64
2import json
3import os
4import re
5import shutil
6from datetime import datetime
8import requests
9from ckeditor_uploader.views import ImageUploadView, browse
10from django.conf import settings
11from django.contrib import messages
12from django.contrib.auth.mixins import UserPassesTestMixin
13from django.core.exceptions import PermissionDenied
14from django.core.files import File
15from django.db.models import Q
16from django.forms.models import model_to_dict
17from django.http import (
18 Http404,
19 HttpResponse,
20 HttpResponseBadRequest,
21 HttpResponseRedirect,
22 HttpResponseServerError,
23 JsonResponse,
24)
25from django.shortcuts import get_object_or_404, redirect
26from django.urls import resolve, reverse
27from django.utils import timezone
28from django.utils.safestring import mark_safe
29from django.views.decorators.csrf import csrf_exempt
30from django.views.generic import CreateView, TemplateView, UpdateView, View
31from lxml import etree
32from mersenne_cms.models import (
33 MERSENNE_ID_VIRTUAL_ISSUES,
34 News,
35 Page,
36 get_news_content,
37 get_pages_content,
38 import_news,
39 import_pages,
40)
41from munch import Munch
42from PIL import Image
43from ptf import model_data_converter, model_helpers
44from ptf.cmds import solr_cmds, xml_cmds
45from ptf.cmds.ptf_cmds import base_ptf_cmds
46from ptf.cmds.xml import xml_utils
47from ptf.cmds.xml.ckeditor.ckeditor_parser import CkeditorParser
48from ptf.cmds.xml.jats.builder.issue import build_title_xml, get_abstract_xml
49from ptf.display import resolver
51# from ptf.display import resolver
52from ptf.exceptions import ServerUnderMaintenance
54# from ptf.model_data import ArticleData
55from ptf.model_data import (
56 create_contributor,
57 create_datastream,
58 create_issuedata,
59 create_publisherdata,
60 create_titledata,
61)
63# from ptf.models import ExtLink
64# from ptf.models import ResourceInSpecialIssue
65# from ptf.models import Contribution
66# from ptf.models import Collection
67from ptf.models import (
68 Article,
69 Collection,
70 Container,
71 ContribAddress,
72 ExtLink,
73 GraphicalAbstract,
74 RelatedArticles,
75 RelatedObject,
76)
77from ptf.site_register import SITE_REGISTER
78from ptf.utils import convert_tiff_to_jpg_from_path, get_names
79from requests import Timeout
81from ptf_tools.forms import GraphicalAbstractForm, NewsForm, PageForm, RelatedForm
82from ptf_tools.utils import is_authorized_editor
84from .base_views import check_lock
87def get_media_base_root(colid):
88 """
89 Base folder where media files are stored in Trammel
90 """
91 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]:
92 colid = "CR"
94 return os.path.join(settings.RESOURCES_ROOT, "media", colid)
97def get_media_base_root_in_test(colid):
98 """
99 Base folder where media files are stored in the test website
100 Use the same folder as the Trammel media folder so that no copy is necessary when deploy in test
101 """
102 return get_media_base_root(colid)
105def get_media_base_root_in_prod(colid):
106 """
107 Base folder where media files are stored in the prod website
108 """
109 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]:
110 colid = "CR"
112 return os.path.join(settings.MERSENNE_PROD_DATA_FOLDER, "media", colid)
115def get_media_base_url(colid):
116 path = os.path.join(settings.MEDIA_URL, colid)
118 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]:
119 prefixes = {
120 "CRMECA": "mecanique",
121 "CRBIOL": "biologies",
122 "CRGEOS": "geoscience",
123 "CRCHIM": "chimie",
124 "CRMATH": "mathematique",
125 "CRPHYS": "physique",
126 }
127 path = f"/{prefixes[colid]}{settings.MEDIA_URL}/CR"
129 return path
132def change_ckeditor_storage(colid):
133 """
134 By default, CKEditor stores all the files under 1 folder (MEDIA_ROOT)
135 We want to store the files under a subfolder of @colid
136 To do that we have to
137 - change the URL calling this view to pass the site_id (info used by the Pages to filter the objects)
138 - modify the storage location
139 """
141 from ckeditor_uploader import utils, views
142 from django.core.files.storage import FileSystemStorage
144 storage = FileSystemStorage(
145 location=get_media_base_root(colid), base_url=get_media_base_url(colid)
146 )
148 utils.storage = storage
149 views.storage = storage
152class EditorRequiredMixin(UserPassesTestMixin):
153 def test_func(self):
154 return is_authorized_editor(self.request.user, self.kwargs.get("colid"))
157class CollectionImageUploadView(EditorRequiredMixin, ImageUploadView):
158 """
159 By default, CKEditor stores all the files under 1 folder (MEDIA_ROOT)
160 We want to store the files under a subfolder of @colid
161 To do that we have to
162 - change the URL calling this view to pass the site_id (info used by the Pages to filter the objects)
163 - modify the storage location
164 """
166 def dispatch(self, request, *args, **kwargs):
167 colid = kwargs["colid"]
169 change_ckeditor_storage(colid)
171 return super().dispatch(request, **kwargs)
174class CollectionBrowseView(EditorRequiredMixin, View):
175 def dispatch(self, request, **kwargs):
176 colid = kwargs["colid"]
178 change_ckeditor_storage(colid)
180 return browse(request)
183file_upload_in_collection = csrf_exempt(CollectionImageUploadView.as_view())
184file_browse_in_collection = csrf_exempt(CollectionBrowseView.as_view())
187def deploy_cms(site, collection):
188 colid = collection.pid
189 base_url = getattr(collection, site)()
191 if base_url is None: 191 ↛ 194line 191 didn't jump to line 194 because the condition on line 191 was always true
192 return JsonResponse({"message": "OK"})
194 if site == "website":
195 from_base_path = get_media_base_root_in_test(colid)
196 to_base_path = get_media_base_root_in_prod(colid)
198 for sub_path in ["uploads", "images"]:
199 from_path = os.path.join(from_base_path, sub_path)
200 to_path = os.path.join(to_base_path, sub_path)
201 if os.path.exists(from_path):
202 try:
203 shutil.copytree(from_path, to_path, dirs_exist_ok=True)
204 except OSError as exception:
205 return HttpResponseServerError(f"Error during copy: {exception}")
207 site_id = model_helpers.get_site_id(colid)
208 if model_helpers.get_site_default_language(site_id):
209 from modeltranslation import fields, manager
211 old_ftor = manager.get_language
212 manager.get_language = monkey_get_language_en
213 fields.get_language = monkey_get_language_en
215 pages = get_pages_content(colid)
216 news = get_news_content(colid)
218 manager.get_language = old_ftor
219 fields.get_language = old_ftor
220 else:
221 pages = get_pages_content(colid)
222 news = get_news_content(colid)
224 data = json.dumps({"pages": json.loads(pages), "news": json.loads(news)})
225 url = getattr(collection, site)() + "/import_cms/"
227 try:
228 response = requests.put(url, data=data, verify=False)
230 if response.status_code == 503:
231 e = ServerUnderMaintenance(
232 "The journal test website is under maintenance. Please try again later."
233 )
234 return HttpResponseServerError(e, status=503)
236 except Timeout as exception:
237 return HttpResponse(exception, status=408)
238 except Exception as exception:
239 return HttpResponseServerError(exception)
241 return JsonResponse({"message": "OK"})
244class HandleCMSMixin(EditorRequiredMixin):
245 """
246 Mixin for classes that need to send request to (test) website to import/export CMS content (pages, news)
247 """
249 # def dispatch(self, request, *args, **kwargs):
250 # self.colid = self.kwargs["colid"]
251 # return super().dispatch(request, *args, **kwargs)
253 def init_data(self, kwargs):
254 self.collection = None
256 self.colid = kwargs.get("colid", None)
257 if self.colid:
258 self.collection = model_helpers.get_collection(self.colid)
259 if not self.collection:
260 raise Http404(f"{self.colid} does not exist")
262 test_server_url = self.collection.test_website()
263 if not test_server_url:
264 raise Http404("The collection has no test site")
266 prod_server_url = self.collection.website()
267 if not prod_server_url:
268 raise Http404("The collection has no prod site")
271class GetCMSFromSiteAPIView(HandleCMSMixin, View):
272 """
273 Get the CMS content from the (test) website and save it on disk.
274 It can be used if needed to restore the Trammel content with RestoreCMSAPIView below
275 """
277 def get(self, request, *args, **kwargs):
278 self.init_data(self.kwargs)
280 site = kwargs.get("site", "test_website")
282 try:
283 url = getattr(self.collection, site)() + "/export_cms/"
284 response = requests.get(url, verify=False)
286 # Just to need to save the json on disk
287 # Media files are already saved in MEDIA_ROOT which is equal to
288 # /mersenne_test_data/@colid/media
289 folder = get_media_base_root(self.colid)
290 os.makedirs(folder, exist_ok=True)
291 filename = os.path.join(folder, f"pages_{self.colid}.json")
292 with open(filename, mode="w", encoding="utf-8") as file:
293 file.write(response.content.decode(encoding="utf-8"))
295 except Timeout as exception:
296 return HttpResponse(exception, status=408)
297 except Exception as exception:
298 return HttpResponseServerError(exception)
300 return JsonResponse({"message": "OK", "status": 200})
303def monkey_get_language_en():
304 return "en"
307class RestoreCMSAPIView(HandleCMSMixin, View):
308 """
309 Restore the Trammel CMS content (of a colid) from disk
310 """
312 def get(self, request, *args, **kwargs):
313 self.init_data(self.kwargs)
315 folder = get_media_base_root(self.colid)
316 filename = os.path.join(folder, f"pages_{self.colid}.json")
317 with open(filename, encoding="utf-8") as f:
318 json_data = json.load(f)
320 pages = json_data.get("pages")
322 site_id = model_helpers.get_site_id(self.colid)
323 if model_helpers.get_site_default_language(site_id):
324 from modeltranslation import fields, manager
326 old_ftor = manager.get_language
327 manager.get_language = monkey_get_language_en
328 fields.get_language = monkey_get_language_en
330 import_pages(pages, self.colid)
332 manager.get_language = old_ftor
333 fields.get_language = old_ftor
334 else:
335 import_pages(pages, self.colid)
337 if "news" in json_data:
338 news = json_data.get("news")
339 import_news(news, self.colid)
341 return JsonResponse({"message": "OK", "status": 200})
344class DeployCMSAPIView(HandleCMSMixin, View):
345 def get(self, request, *args, **kwargs):
346 self.init_data(self.kwargs)
348 if check_lock():
349 msg = "Trammel is under maintenance. Please try again later."
350 messages.error(self.request, msg)
351 return JsonResponse({"messages": msg, "status": 503})
353 site = kwargs.get("site", "test_website")
355 response = deploy_cms(site, self.collection)
357 if response.status_code == 503:
358 messages.error(
359 self.request, "The journal website is under maintenance. Please try again later."
360 )
362 return response
365def get_server_urls(collection, site="test_website"):
366 urls = [""]
367 if hasattr(settings, "MERSENNE_DEV_URL"): 367 ↛ 369line 367 didn't jump to line 369 because the condition on line 367 was never true
368 # set RESOURCES_ROOT and apache config accordingly (for instance with "/mersenne_dev_data")
369 url = getattr(collection, "test_website")().split(".fr")
370 urls = [settings.MERSENNE_DEV_URL + url[1] if len(url) == 2 else ""]
371 elif site == "both": 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true
372 urls = [getattr(collection, "test_website")(), getattr(collection, "website")()]
373 elif hasattr(collection, site) and getattr(collection, site)(): 373 ↛ 374line 373 didn't jump to line 374 because the condition on line 373 was never true
374 urls = [getattr(collection, site)()]
375 return urls
378class SuggestDeployView(EditorRequiredMixin, View):
379 def post(self, request, *args, **kwargs):
380 doi = kwargs.get("doi", "")
381 site = kwargs.get("site", "test_website")
382 article = get_object_or_404(Article, doi=doi)
384 obj, created = RelatedArticles.objects.get_or_create(resource=article)
385 form = RelatedForm(request.POST or None, instance=obj)
386 if form.is_valid(): 386 ↛ 403line 386 didn't jump to line 403 because the condition on line 386 was always true
387 data = form.cleaned_data
388 obj.date_modified = timezone.now()
389 form.save()
390 collection = article.my_container.my_collection
391 urls = get_server_urls(collection, site=site)
392 response = requests.models.Response()
393 for url in urls: 393 ↛ 401line 393 didn't jump to line 401 because the loop on line 393 didn't complete
394 url = url + reverse("api-update-suggest", kwargs={"doi": doi})
395 try:
396 response = requests.post(url, data=data, timeout=15)
397 except requests.exceptions.RequestException as e:
398 response.status_code = 503
399 response.reason = e.args[0]
400 break
401 return HttpResponse(status=response.status_code, reason=response.reason)
402 else:
403 return HttpResponseBadRequest()
406def suggest_debug(results, article, message):
407 crop_results = 5
408 if results: 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true
409 dois = []
410 results["docs"] = results["docs"][:crop_results]
411 numFound = f"({len(results['docs'])} sur {results['numFound']} documents)"
412 head = f"Résultats de la recherche automatique {numFound} :\n\n"
413 for item in results["docs"]:
414 doi = item.get("doi")
415 if doi:
416 explain = results["explain"][item["id"]]
417 terms = re.findall(r"([0-9.]+?) = weight\((.+?:.+?) in", explain)
418 terms.sort(key=lambda t: t[0], reverse=True)
419 details = (" + ").join(f"{round(float(s), 1)}:{t}" for s, t in terms)
420 score = f"Score : {round(float(item['score']), 1)} (= {details})\n"
421 url = ""
422 suggest = Article.objects.filter(doi=doi).first()
423 if suggest and suggest.my_container:
424 collection = suggest.my_container.my_collection
425 base_url = collection.website() or ""
426 url = base_url + "/articles/" + doi
427 dois.append((doi, url, score))
429 tail = f"\n\nScore minimum retenu : {results['params']['min_score']}\n\n\n"
430 tail += "Termes principaux utilisés pour la requête "
431 tail = [tail + "(champ:terme recherché | pertinence du terme) :\n"]
432 if results["params"]["mlt.fl"] == "all":
433 tail.append(" * all = body + abstract + title + authors + keywords\n")
434 terms = results["interestingTerms"]
435 terms = [" | ".join((x[0], str(x[1]))) for x in zip(terms[::2], terms[1::2])]
436 tail.extend(reversed(terms))
437 tail.append("\n\nParamètres de la requête :\n")
438 tail.extend([f"{k}: {v} " for k, v in results["params"].items()])
439 return [(head, dois, "\n".join(tail))]
440 else:
441 msg = f"Erreur {message['status']} {message['err']} at {message['url']}"
442 return [(msg, [], "")]
445class SuggestUpdateView(EditorRequiredMixin, TemplateView):
446 template_name = "editorial_tools/suggested.html"
448 def get_context_data(self, **kwargs):
449 doi = kwargs.get("doi", "")
450 article = get_object_or_404(Article, doi=doi)
452 obj, created = RelatedArticles.objects.get_or_create(resource=article)
453 collection = article.my_container.my_collection
454 base_url = collection.website() or ""
455 response = requests.models.Response()
456 try:
457 response = requests.get(base_url + "/mlt/" + doi, timeout=10.0)
458 except requests.exceptions.RequestException as e:
459 response.status_code = 503
460 response.reason = e.args[0]
461 msg = {
462 "url": response.url,
463 "status": response.status_code,
464 "err": response.reason,
465 }
466 results = None
467 if response.status_code == 200: 467 ↛ 468line 467 didn't jump to line 468 because the condition on line 467 was never true
468 results = solr_cmds.auto_suggest_doi(obj, article, response.json())
469 context = super().get_context_data(**kwargs)
470 context["debug"] = suggest_debug(results, article, msg)
471 context["form"] = RelatedForm(instance=obj)
472 context["author"] = "; ".join(get_names(article, "author"))
473 context["citation_base"] = article.get_citation_base().strip(", .")
474 context["article"] = article
475 context["date_modified"] = obj.date_modified
476 context["url"] = base_url + "/articles/" + doi
477 return context
480class EditorialToolsVolumeItemsView(EditorRequiredMixin, TemplateView):
481 template_name = "editorial_tools/volume-items.html"
483 def get_context_data(self, **kwargs):
484 vid = kwargs.get("vid")
485 issues_articles, collection = model_helpers.get_issues_in_volume(vid)
486 context = super().get_context_data(**kwargs)
487 context["issues_articles"] = issues_articles
488 context["collection"] = collection
489 return context
492class EditorialToolsArticleView(EditorRequiredMixin, TemplateView):
493 template_name = "editorial_tools/find-article.html"
495 def get_context_data(self, **kwargs):
496 colid = kwargs.get("colid")
497 doi = kwargs.get("doi")
498 article = get_object_or_404(Article, doi=doi, my_container__my_collection__pid=colid)
500 context = super().get_context_data(**kwargs)
501 context["article"] = article
502 context["citation_base"] = article.get_citation_base().strip(", .")
503 return context
506class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView):
507 template_name = "editorial_tools/graphical-abstract.html"
509 def get_context_data(self, **kwargs):
510 doi = kwargs.get("doi", "")
511 article = get_object_or_404(Article, doi=doi)
513 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
514 context = super().get_context_data(**kwargs)
515 context["author"] = "; ".join(get_names(article, "author"))
516 context["citation_base"] = article.get_citation_base().strip(", .")
517 context["article"] = article
518 context["date_modified"] = obj.date_modified
519 context["form"] = GraphicalAbstractForm(instance=obj)
520 context["graphical_abstract"] = obj.graphical_abstract
521 context["illustration"] = obj.illustration
522 return context
525class GraphicalAbstractDeployView(EditorRequiredMixin, View):
526 def __get_path_and_replace_tiff_file(self, processed_attributes, obj_attribute_file):
527 """
528 Returns the path of the attribute.
529 If the attribute is a tiff file, converts it to jpg, delete the old tiff file, and return the new path of the attribute.
531 Checks if paths have already been processed, to prevent issues related to object mutation.
532 """
533 if obj_attribute_file.name.lower().endswith((".tiff", ".tif")):
534 jpg_path = obj_attribute_file.path.rsplit(".", 1)[0] + ".jpg"
535 if obj_attribute_file.name not in processed_attributes:
536 processed_attributes[obj_attribute_file.path] = obj_attribute_file
537 original_path = obj_attribute_file.path
538 convert_tiff_to_jpg_from_path(original_path)
539 with open(jpg_path, "rb") as fp:
540 obj_attribute_file.save(os.path.basename(jpg_path), File(fp), save=True)
541 os.remove(original_path)
542 return jpg_path
543 return obj_attribute_file.path
545 def post(self, request, *args, **kwargs):
546 doi = kwargs.get("doi", "")
547 site = kwargs.get("site", "both")
548 article = get_object_or_404(Article, doi=doi)
550 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
551 form = GraphicalAbstractForm(request.POST, request.FILES or None, instance=obj)
552 if form.is_valid(): 552 ↛ 584line 552 didn't jump to line 584 because the condition on line 552 was always true
553 obj.date_modified = timezone.now()
554 data = {"date_modified": obj.date_modified}
555 form.save()
556 files = {}
558 processed_attributes = {}
559 for attribute in ("graphical_abstract", "illustration"):
560 obj_attribute_file = getattr(obj, attribute, None)
561 if obj_attribute_file and os.path.exists(obj_attribute_file.path): 561 ↛ 562line 561 didn't jump to line 562 because the condition on line 561 was never true
562 file_path = self.__get_path_and_replace_tiff_file(
563 processed_attributes, obj_attribute_file
564 )
565 with open(file_path, "rb") as fp:
566 files.update({attribute: (obj_attribute_file.name, fp.read())})
568 collection = article.my_container.my_collection
569 urls = get_server_urls(collection, site=site)
570 response = requests.models.Response()
571 for url in urls: 571 ↛ 582line 571 didn't jump to line 582 because the loop on line 571 didn't complete
572 url = url + reverse("api-graphical-abstract", kwargs={"doi": doi})
573 try:
574 if not obj.graphical_abstract and not obj.illustration: 574 ↛ 577line 574 didn't jump to line 577 because the condition on line 574 was always true
575 response = requests.delete(url, data=data, files=files, timeout=15)
576 else:
577 response = requests.post(url, data=data, files=files, timeout=15)
578 except requests.exceptions.RequestException as e:
579 response.status_code = 503
580 response.reason = e.args[0]
581 break
582 return HttpResponse(status=response.status_code, reason=response.reason)
583 else:
584 return HttpResponseBadRequest()
587def parse_content(content):
588 table = re.search(r'(.*?)(<table id="summary".+?</table>)(.*)', content, re.DOTALL)
589 if not table:
590 return {"head": content, "tail": "", "articles": []}
592 articles = []
593 rows = re.findall(r"<tr>.+?</tr>", table.group(2), re.DOTALL)
594 for row in rows:
595 citation = re.search(r'<div href=".*?">(.*?)</div>', row, re.DOTALL)
596 href = re.search(r'href="(.+?)\/?">', row)
597 doi = re.search(r"(10[.].+)", href.group(1)) if href else ""
598 src = re.search(r'<img.+?src="(.+?)"', row)
599 item = {}
600 item["citation"] = citation.group(1) if citation else ""
601 item["doi"] = doi.group(1) if doi else href.group(1) if href else ""
602 item["src"] = src.group(1) if src else ""
603 item["imageName"] = item["src"].split("/")[-1] if item["src"] else ""
604 if item["doi"] or item["src"]:
605 articles.append(item)
606 return {"head": table.group(1), "tail": table.group(3), "articles": articles}
609class VirtualIssueParseView(EditorRequiredMixin, View):
610 def get(self, request, *args, **kwargs):
611 pid = kwargs.get("pid", "")
612 page = get_object_or_404(Page, id=pid)
614 data = {"pid": pid}
615 data["colid"] = kwargs.get("colid", "")
616 journal = model_helpers.get_collection(data["colid"])
617 data["journal_title"] = journal.title_tex.replace(".", "")
618 site_id = model_helpers.get_site_id(data["colid"])
619 data["page"] = model_to_dict(page)
620 pages = Page.objects.filter(site_id=site_id).exclude(id=pid)
621 data["parents"] = [model_to_dict(p, fields=["id", "menu_title"]) for p in pages]
623 content_fr = parse_content(page.content_fr)
624 data["head_fr"] = content_fr["head"]
625 data["tail_fr"] = content_fr["tail"]
627 content_en = parse_content(page.content_en)
628 data["articles"] = content_en["articles"]
629 data["head_en"] = content_en["head"]
630 data["tail_en"] = content_en["tail"]
631 return JsonResponse(data)
634class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView):
635 template_name = "editorial_tools/virtual-issue.html"
637 def get(self, request, *args, **kwargs):
638 pid = kwargs.get("pid", "")
639 get_object_or_404(Page, id=pid)
640 return super().get(request, *args, **kwargs)
643class VirtualIssueCreateView(EditorRequiredMixin, View):
644 def get(self, request, *args, **kwargs):
645 colid = kwargs.get("colid", "")
646 site_id = model_helpers.get_site_id(colid)
647 parent, _ = Page.objects.get_or_create(
648 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES,
649 parent_page=None,
650 site_id=site_id,
651 )
652 page = Page.objects.create(
653 menu_title_en="New virtual issue",
654 menu_title_fr="Nouvelle collection transverse",
655 parent_page=parent,
656 site_id=site_id,
657 state="draft",
658 )
659 kwargs = {"colid": colid, "pid": page.id}
660 return HttpResponseRedirect(reverse("virtual_issue_update", kwargs=kwargs))
663class SpecialIssuesIndex(EditorRequiredMixin, TemplateView):
664 template_name = "editorial_tools/special-issues-index.html"
666 def get_context_data(self, **kwargs):
667 colid = kwargs.get("colid", "")
669 context = super().get_context_data(**kwargs)
670 context["colid"] = colid
671 collection = Collection.objects.get(pid=colid)
672 context["special_issues"] = Container.objects.filter(
673 Q(ctype="issue_special") | Q(ctype="issue_special_img")
674 ).filter(my_collection=collection)
676 context["journal"] = model_helpers.get_collection(colid, sites=False)
677 return context
680class SpecialIssueEditView(EditorRequiredMixin, TemplateView):
681 template_name = "editorial_tools/special-issue-edit.html"
683 def get_context_data(self, **kwargs):
684 context = super().get_context_data(**kwargs)
685 return context
688class VirtualIssuesIndex(EditorRequiredMixin, TemplateView):
689 template_name = "editorial_tools/virtual-issues-index.html"
691 def get_context_data(self, **kwargs):
692 colid = kwargs.get("colid", "")
693 site_id = model_helpers.get_site_id(colid)
694 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES)
695 pages = Page.objects.filter(site_id=site_id, parent_page=vi)
696 context = super().get_context_data(**kwargs)
697 context["journal"] = model_helpers.get_collection(colid)
698 context["pages"] = pages
699 return context
702def get_citation_fr(doi, citation_en):
703 citation_fr = citation_en
704 article = Article.objects.filter(doi=doi).first()
705 if article and article.trans_title_html:
706 trans_title = article.trans_title_html
707 try:
708 citation_fr = re.sub(
709 r'(<a href="https:\/\/doi\.org.*">)([^<]+)',
710 rf"\1{trans_title}",
711 citation_en,
712 )
713 except re.error:
714 pass
715 return citation_fr
718def summary_build(articles, colid):
719 summary_fr = ""
720 summary_en = ""
721 head = '<table id="summary"><tbody>'
722 tail = "</tbody></table>"
723 style = "max-width:180px;max-height:200px"
724 colid_lo = colid.lower()
725 site_domain = SITE_REGISTER[colid_lo]["site_domain"].split("/")
726 site_domain = "/" + site_domain[-1] if len(site_domain) == 2 else ""
728 for article in articles:
729 image_src = article.get("src", "")
730 image_name = article.get("imageName", "")
731 doi = article.get("doi", "")
732 citation_en = article.get("citation", "")
733 if doi or citation_en:
734 row_fr = f'<div href="{doi}">{get_citation_fr(doi, citation_en)}</div>'
735 row_en = f'<div href="{doi}">{citation_en}</div>'
736 if image_src:
737 date = datetime.now().strftime("%Y/%m/%d/")
738 base_url = get_media_base_url(colid)
739 suffix = os.path.join(base_url, "uploads", date)
740 image_url = os.path.join(site_domain, suffix, image_name)
741 image_header = "^data:image/.+;base64,"
742 if re.match(image_header, image_src):
743 image_src = re.sub(image_header, "", image_src)
744 base64_data = base64.b64decode(image_src)
745 base_root = get_media_base_root(colid)
746 path = os.path.join(base_root, "uploads", date)
747 os.makedirs(path, exist_ok=True)
748 with open(path + image_name, "wb") as fp:
749 fp.write(base64_data)
750 im = f'<img src="{image_url}" style="{style}" />'
751 # TODO mettre la vrai valeur pour le SITE_DOMAIN
752 elif settings.SITE_DOMAIN == "http://127.0.0.1:8002":
753 im = f'<img src="{image_src}" style="{style}" />'
754 else:
755 im = f'<img src="{site_domain}{image_src}" style="{style}" />'
756 summary_fr += f"<tr><td>{im}</td><td>{row_fr}</td></tr>"
757 summary_en += f"<tr><td>{im}</td><td>{row_en}</td></tr>"
758 summary_fr = head + summary_fr + tail
759 summary_en = head + summary_en + tail
760 return {"summary_fr": summary_fr, "summary_en": summary_en}
763# @method_decorator([csrf_exempt], name="dispatch")
764class VirtualIssueDeployView(HandleCMSMixin, View):
765 """
766 called by the Virtual.vue VueJS component, when the virtual issue is saved
767 We get data in JSON and we need to update the corresponding Page.
768 The Page is then immediately posted to the test_website.
769 The "Apply the changes to the production website" button is then used to update the (prod) website
770 => See DeployCMSAPIView
771 """
773 def post(self, request, *args, **kwargs):
774 self.init_data(self.kwargs)
775 if check_lock():
776 msg = "Trammel is under maintenance. Please try again later."
777 messages.error(self.request, msg)
778 return JsonResponse({"messages": msg, "status": 503})
780 pid = kwargs.get("pid")
781 colid = self.colid
782 data = json.loads(request.body)
783 summary = summary_build(data["articles"], colid)
784 page = get_object_or_404(Page, id=pid)
785 page.slug = page.slug_fr = page.slug_en = None
786 page.menu_title_fr = data["title_fr"]
787 page.menu_title_en = data["title_en"]
788 page.content_fr = data["head_fr"] + summary["summary_fr"] + data["tail_fr"]
789 page.content_en = data["head_en"] + summary["summary_en"] + data["tail_en"]
790 page.state = data["page"]["state"]
791 page.menu_order = data["page"]["menu_order"]
792 page.parent_page = Page.objects.filter(id=data["page"]["parent_page"]).first()
793 page.save()
795 response = deploy_cms("test_website", self.collection)
796 if response.status_code == 503:
797 messages.error(
798 self.request, "The journal website is under maintenance. Please try again later."
799 )
801 return response # HttpResponse(status=response.status_code, reason=response.reason)
804class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView):
805 template_name = "editorial_tools/special-issue-edit.html"
807 def get_context_data(self, **kwargs):
808 context = super().get_context_data(**kwargs)
809 return context
811 def set_contrib_addresses(self, contrib, contribution):
812 for address in contrib:
813 contrib_address = ContribAddress(contribution=contribution, address=address)
814 contrib_address.save()
816 def delete(self, pid):
817 special_issue = Container.objects.get(pid=pid)
818 cmd = base_ptf_cmds.addContainerPtfCmd()
819 cmd.set_object_to_be_deleted(special_issue)
820 cmd.undo()
822 def get(self, request, *args, **kwargs):
823 pid = kwargs.get("pid", "")
825 data = {"pid": pid}
826 colid = kwargs.get("colid", "")
827 data["colid"] = colid
828 journal = model_helpers.get_collection(colid, sites=False)
829 name = resolve(request.path_info).url_name
830 if name == "special_issue_delete": 830 ↛ 831line 830 didn't jump to line 831 because the condition on line 830 was never true
831 self.delete(pid)
832 return redirect("special_issues_index", data["colid"])
834 data["journal_title"] = journal.title_tex.replace(".", "")
836 if pid != "create":
837 container = get_object_or_404(Container, pid=pid)
838 # TODO: pass the lang and trans_lang as well
839 # In VueJS (Special.vu)e, titleFr = title_html
840 # June 2025: Title objects are added for translated titles
841 # keep using trans_title_html for backward compatibility
842 if container.trans_title_html: 842 ↛ 845line 842 didn't jump to line 845 because the condition on line 842 was always true
843 data["title"] = container.trans_title_html
844 else:
845 for title in container.title_set.all():
846 if title["lang"] == "fr" and title["type"] == "main":
847 data["title"] = title["title_html"]
848 data["doi"] = container.doi
849 data["trans_title"] = container.title_html
850 data["year"] = container.year
851 data["volume"] = container.volume
852 data["articles"] = [
853 {"doi": article.resource_doi, "citation": article.citation}
854 for article in container.resources_in_special_issue.all().order_by("seq")
855 ]
856 if container.ctype == "issue_special_img": 856 ↛ 857line 856 didn't jump to line 857 because the condition on line 856 was never true
857 data["use_resources_icon"] = True
858 else:
859 data["use_resources_icon"] = False
861 contribs = model_data_converter.db_to_contributors(container.contributions)
862 data["contribs"] = contribs
863 abstract_set = container.abstract_set.all()
864 data["head_fr"] = (
865 abstract_set.filter(tag="head_fr").first().value_html
866 if abstract_set.filter(tag="head_fr").exists()
867 else ""
868 )
869 data["head_en"] = (
870 abstract_set.filter(tag="head_en").first().value_html
871 if abstract_set.filter(tag="head_en").exists()
872 else ""
873 )
874 data["tail_fr"] = (
875 abstract_set.filter(tag="tail_fr").first().value_html
876 if abstract_set.filter(tag="tail_fr").exists()
877 else ""
878 )
879 data["tail_en"] = (
880 abstract_set.filter(tag="tail_en").first().value_html
881 if abstract_set.filter(tag="tail_en").exists()
882 else ""
883 )
884 data["editor_bio_en"] = (
885 abstract_set.filter(tag="bio_en").first().value_html
886 if abstract_set.filter(tag="bio_en").exists()
887 else ""
888 )
889 data["editor_bio_fr"] = (
890 abstract_set.filter(tag="bio_fr").first().value_html
891 if abstract_set.filter(tag="bio_fr").exists()
892 else ""
893 )
895 streams = container.datastream_set.all()
896 data["pdf_file_name"] = ""
897 data["edito_file_name"] = ""
898 data["edito_display_name"] = ""
899 for stream in streams: # don't work 899 ↛ 900line 899 didn't jump to line 900 because the loop on line 899 never started
900 if os.path.basename(stream.location).split(".")[0] == data["pid"]:
901 data["pdf_file_name"] = stream.text
902 try:
903 # edito related objects metadata contains both file real name and displayed name in issue summary
904 edito_name_infos = container.relatedobject_set.get(rel="edito").metadata.split(
905 "$$$"
906 )
907 data["edito_file_name"] = edito_name_infos[0]
908 data["edito_display_name"] = edito_name_infos[1]
910 except RelatedObject.DoesNotExist:
911 pass
912 try:
913 container_icon = container.extlink_set.get(rel="icon")
915 data["icon_location"] = container_icon.location
916 except ExtLink.DoesNotExist:
917 data["icon_location"] = ""
918 # try:
919 # special_issue_icon = container.extlink_set.get(rel="icon")
920 # data["special_issue_icon"] = special_issue_icon.location
921 # except ExtLink.DoesNotExist:
922 # data["special_issue_icon"] = None
924 else:
925 data["title"] = ""
926 data["doi"] = None
927 data["trans_title"] = ""
928 data["year"] = ""
929 data["volume"] = ""
930 data["articles"] = []
931 data["contribs"] = []
933 data["head_fr"] = ""
934 data["head_en"] = ""
935 data["tail_fr"] = ""
936 data["tail_en"] = ""
937 data["editor_bio_en"] = ""
938 data["editor_bio_fr"] = ""
939 data["pdf_file_name"] = ""
940 data["edito_file_name"] = ""
941 data["use_resources_icon"] = False
943 return JsonResponse(data)
945 def post(self, request, *args, **kwargs):
946 # le but est de faire un IssueDAta
947 pid = kwargs.get("pid", "")
948 colid = kwargs.get("colid", "")
949 journal = collection = model_helpers.get_collection(colid, sites=False)
950 special_issue = create_issuedata()
951 year = request.POST["year"]
952 # TODO 1: the values should be the tex values, not the html ones
953 # TODO 2: In VueJS, titleFr = title
954 trans_title_html = request.POST["title"]
955 title_html = request.POST["trans_title"]
956 if pid != "create":
957 # TODO: do not use the pk, but the pid in the URLs
958 container = get_object_or_404(Container, pid=pid)
959 lang = container.lang
960 trans_lang = container.trans_lang
961 xpub = create_publisherdata()
962 xpub.name = container.my_publisher.pid
963 special_issue.provider = container.provider
964 special_issue.number = container.number
965 volume = container.volume
966 special_issue_pid = pid
967 special_issue.date_pre_published = container.date_pre_published
968 special_issue.date_published = container.date_published
969 # used for first special issues created withou a proper doi
970 # can be remove when no doi's less special issue existe
971 if not container.doi: 971 ↛ 972line 971 didn't jump to line 972 because the condition on line 971 was never true
972 special_issue.doi = model_helpers.assign_container_doi(colid)
973 else:
974 special_issue.doi = container.doi
975 else:
976 lang = "en"
977 container = None
978 trans_lang = "fr"
979 xpub = create_publisherdata()
980 special_issue.doi = model_helpers.assign_container_doi(colid)
981 volume = ""
982 issues = collection.content.all().order_by("-year")
983 # if cras_issues.exists():
984 same_year_issues = issues.filter(year=int(year))
985 if same_year_issues.exists(): 985 ↛ 987line 985 didn't jump to line 987 because the condition on line 985 was always true
986 volume = same_year_issues.first().volume
987 elif (
988 issues.exists() and colid != "HOUCHES"
989 ): # because we don't want a volume for houches
990 volume = str(int(issues.first().volume) + 1)
991 else:
992 volume = ""
993 # issues = model_helpers.get_volumes_in_collection(collection, get_special_issues=True)
994 # if issues["sorted_issues"]:
995 # volumes = issues["sorted_issues"][0]["volumes"]
996 # for v in volumes:
997 # if v["fyear"] == int(year):
998 # volume = v["volume"]
999 # break
1001 if colid == "HOUCHES": 1001 ↛ 1002line 1001 didn't jump to line 1002 because the condition on line 1001 was never true
1002 xpub.name = "UGA Éditions"
1003 else:
1004 xpub.name = issues.first().my_publisher.pid
1005 # xpub.name = parent_container.my_publisher.pid
1006 special_issue.provider = collection.provider
1008 special_issues = issues.filter(year=year).filter(
1009 Q(ctype="issue_special") | Q(ctype="issue") | Q(ctype="issue_special_img")
1010 )
1011 if special_issues: 1011 ↛ 1021line 1011 didn't jump to line 1021 because the condition on line 1011 was always true
1012 all_special_issues_numbers = [
1013 int(si.number[1:]) for si in special_issues if si.number[1:].isnumeric()
1014 ]
1015 if len(all_special_issues_numbers) > 0:
1016 max_number = max(all_special_issues_numbers)
1017 else:
1018 max_number = 0
1020 else:
1021 max_number = 0
1022 special_issue.number = f"S{max_number + 1}"
1023 special_issue_pid = f"{colid}_{year}__{volume}_{special_issue.number}"
1025 if request.POST["use_resources_icon"] == "true": 1025 ↛ 1026line 1025 didn't jump to line 1026 because the condition on line 1025 was never true
1026 special_issue.ctype = "issue_special_img"
1027 else:
1028 special_issue.ctype = "issue_special"
1030 existing_issue = model_helpers.get_resource(special_issue_pid)
1031 if pid == "create" and existing_issue is not None: 1031 ↛ 1032line 1031 didn't jump to line 1032 because the condition on line 1031 was never true
1032 raise ValueError(f"The special issue with the pid {special_issue_pid} already exists")
1034 special_issue.lang = lang
1035 special_issue.title_html = title_html
1036 special_issue.title_xml = build_title_xml(
1037 title=title_html, lang=lang, title_type="issue-title"
1038 )
1040 special_issue.trans_lang = trans_lang
1041 special_issue.trans_title_html = trans_title_html
1042 title_xml = build_title_xml(
1043 title=trans_title_html, lang=trans_lang, title_type="issue-title"
1044 )
1045 title = create_titledata(
1046 lang=trans_lang, type="main", title_html=trans_title_html, title_xml=title_xml
1047 )
1048 special_issue.titles = [title]
1050 special_issue.year = year
1051 special_issue.volume = volume
1052 special_issue.journal = journal
1053 special_issue.publisher = xpub
1054 special_issue.pid = special_issue_pid
1055 special_issue.last_modified_iso_8601_date_str = datetime.now().strftime(
1056 "%Y-%m-%d %H:%M:%S"
1057 )
1059 articles = []
1060 contribs = []
1061 index = 0
1063 if "nb_articles" in request.POST.keys():
1064 while index < int(request.POST["nb_articles"]):
1065 article = json.loads(request.POST[f"article[{index}]"])
1066 article["citation"] = xml_utils.replace_html_entities(article["citation"])
1067 # if not article["citation"]:
1068 # index += 1
1069 # continue
1070 articles.append(article)
1072 index += 1
1074 special_issue.articles = [Munch(article) for article in articles]
1075 index = 0
1076 # TODO make a function to call to add a contributor
1077 if "nb_contrib" in request.POST.keys():
1078 while index < int(request.POST["nb_contrib"]):
1079 contrib = json.loads(request.POST[f"contrib[{index}]"])
1080 contributor = create_contributor()
1081 contributor["first_name"] = contrib["first_name"]
1082 contributor["last_name"] = contrib["last_name"]
1083 contributor["orcid"] = contrib["orcid"]
1084 contributor["role"] = "editor"
1086 contrib_xml = xml_utils.get_contrib_xml(contrib)
1087 contributor["contrib_xml"] = contrib_xml
1088 contribs.append(Munch(contributor))
1089 index += 1
1090 special_issue.contributors = contribs
1092 # Part of the code that handle forwords and lastwords
1094 xhead_fr, head_fr_xml = self.create_abstract_from_vuejs(
1095 request.POST["head_fr"], "fr", "intro", colid, special_issue_pid
1096 )
1097 xtail_fr, tail_fr_xml = self.create_abstract_from_vuejs(
1098 request.POST["tail_fr"], "fr", "tail", colid, special_issue_pid
1099 )
1100 xhead_en, head_en_xml = self.create_abstract_from_vuejs(
1101 request.POST["head_en"], "en", "intro", colid, special_issue_pid
1102 )
1104 xtail_en, tail_en_xml = self.create_abstract_from_vuejs(
1105 request.POST["tail_en"], "en", "tail", colid, special_issue_pid
1106 )
1108 xeditor_bio_en, editor_bio_en_xml = self.create_abstract_from_vuejs(
1109 request.POST["editor_bio_en"], "en", "bio_en", colid, special_issue_pid
1110 )
1112 xeditor_bio_fr, editor_bio_fr_xml = self.create_abstract_from_vuejs(
1113 request.POST["editor_bio_fr"], "fr", "bio_fr", colid, special_issue_pid
1114 )
1116 abstracts = [
1117 head_fr_xml,
1118 head_en_xml,
1119 tail_fr_xml,
1120 tail_en_xml,
1121 editor_bio_fr_xml,
1122 editor_bio_en_xml,
1123 ]
1124 figures = self.create_related_objects_from_abstract(abstracts, colid, special_issue.pid)
1125 special_issue.related_objects = figures
1126 # TODO can be factorized?
1127 special_issue.abstracts = [
1128 {
1129 "tag": "head_fr",
1130 "lang": "fr",
1131 "value_html": xhead_fr.value_html,
1132 "value_tex": xhead_fr.value_tex,
1133 "value_xml": head_fr_xml,
1134 },
1135 {
1136 "tag": "head_en",
1137 "lang": "en",
1138 "value_html": xhead_en.value_html,
1139 "value_tex": xhead_en.value_tex,
1140 "value_xml": head_en_xml,
1141 },
1142 {
1143 "tag": "tail_fr",
1144 "lang": "fr",
1145 "value_html": xtail_fr.value_html,
1146 "value_tex": xtail_fr.value_tex,
1147 "value_xml": tail_fr_xml,
1148 },
1149 {
1150 "tag": "tail_en",
1151 "lang": "en",
1152 "value_html": xtail_en.value_html,
1153 "value_tex": xtail_en.value_tex,
1154 "value_xml": tail_en_xml,
1155 },
1156 {
1157 "tag": "bio_en",
1158 "lang": "en",
1159 "value_html": xeditor_bio_en.value_html,
1160 "value_tex": xeditor_bio_en.value_tex,
1161 "value_xml": editor_bio_en_xml,
1162 },
1163 {
1164 "tag": "bio_fr",
1165 "lang": "fr",
1166 "value_html": xeditor_bio_fr.value_html,
1167 "value_tex": xeditor_bio_fr.value_tex,
1168 "value_xml": editor_bio_fr_xml,
1169 },
1170 ]
1172 # This part handle pdf files included in special issue. Can be editor of full pdf version
1173 # Both are stored in same directory
1175 pdf_file_path = resolver.get_disk_location(
1176 f"{settings.RESOURCES_ROOT}",
1177 f"{collection.pid}",
1178 "pdf",
1179 special_issue_pid,
1180 article_id=None,
1181 do_create_folder=False,
1182 )
1183 pdf_path = os.path.dirname(pdf_file_path)
1184 if "pdf" in self.request.FILES: 1184 ↛ 1185line 1184 didn't jump to line 1185 because the condition on line 1184 was never true
1185 if os.path.isfile(f"{pdf_path}/{pid}.pdf"):
1186 os.remove(f"{pdf_path}/{pid}.pdf")
1187 if "edito" in self.request.FILES: 1187 ↛ 1188line 1187 didn't jump to line 1188 because the condition on line 1187 was never true
1188 if os.path.isfile(f"{pdf_path}/{pid}_edito.pdf"):
1189 os.remove(f"{pdf_path}/{pid}_edito.pdf")
1191 if request.POST["pdf_name"] != "No file uploaded": 1191 ↛ 1192line 1191 didn't jump to line 1192 because the condition on line 1191 was never true
1192 if "pdf" in self.request.FILES:
1193 pdf_file_name = self.request.FILES["pdf"].name
1194 location = pdf_path + "/" + special_issue_pid + ".pdf"
1195 with open(location, "wb+") as destination:
1196 for chunk in self.request.FILES["pdf"].chunks():
1197 destination.write(chunk)
1199 else:
1200 pdf_file_name = request.POST["pdf_name"]
1201 location = pdf_path + "/" + special_issue_pid + ".pdf"
1203 pdf_stream_data = create_datastream()
1204 pdf_stream_data["location"] = location.replace("/mersenne_test_data/", "")
1205 pdf_stream_data["mimetype"] = "application/pdf"
1206 pdf_stream_data["rel"] = "full-text"
1207 pdf_stream_data["text"] = pdf_file_name
1208 special_issue.streams.append(pdf_stream_data)
1210 if request.POST["edito_name"] != "No file uploaded": 1210 ↛ 1211line 1210 didn't jump to line 1211 because the condition on line 1210 was never true
1211 if "edito" in self.request.FILES:
1212 location = pdf_path + "/" + special_issue_pid + "_edito.pdf"
1213 edito_file_name = self.request.FILES["edito"].name
1214 edito_display_name = request.POST["edito_display_name"]
1215 with open(location, "wb+") as destination:
1216 for chunk in self.request.FILES["edito"].chunks():
1217 destination.write(chunk)
1218 else:
1219 location = pdf_path + "/" + special_issue_pid + "_edito.pdf"
1220 edito_file_name = request.POST["edito_name"]
1221 edito_display_name = request.POST["edito_display_name"]
1223 location = location.replace("/mersenne_test_data/", "")
1224 data = {
1225 "rel": "edito",
1226 "mimetype": "application/pdf",
1227 "location": location,
1228 "base": None,
1229 "metadata": edito_file_name + "$$$" + edito_display_name,
1230 }
1231 special_issue.related_objects.append(data)
1232 # Handle special issue icon. It is stored in same directory that pdf version or edito.
1233 # The icon is linked to special issue as an ExtLink
1234 if "icon" in request.FILES: 1234 ↛ 1235line 1234 didn't jump to line 1235 because the condition on line 1234 was never true
1235 icon_file = request.FILES["icon"]
1236 relative_file_name = resolver.copy_file_obj_to_article_folder(
1237 icon_file,
1238 collection.pid,
1239 special_issue.pid,
1240 special_issue.pid,
1241 )
1242 data = {
1243 "rel": "icon",
1244 "location": relative_file_name,
1245 "base": None,
1246 "seq": 1,
1247 "metadata": "",
1248 }
1249 special_issue.ext_links.append(data)
1250 elif "icon" in request.POST.keys(): 1250 ↛ 1251line 1250 didn't jump to line 1251 because the condition on line 1250 was never true
1251 if request.POST["icon"] != "[object Object]":
1252 icon_file = request.POST["icon"].replace("/icon/", "")
1253 data = {
1254 "rel": "icon",
1255 "location": icon_file,
1256 "base": None,
1257 "seq": 1,
1258 "metadata": "",
1259 }
1260 special_issue.ext_links.append(data)
1262 special_issue = Munch(special_issue.__dict__)
1263 params = {"xissue": special_issue, "use_body": False}
1264 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params)
1265 cmd.do()
1266 # tail_fr_html = xml_utils.replace_html_entities(request.POST["tail_fr"])
1267 # tail_en_html = xml_utils.replace_html_entities(request.POST["tail_en"])
1268 return redirect("special_issue_edit_api", colid, special_issue.pid)
1270 def create_abstract_from_vuejs(self, abstract, lang, position, colid, pid):
1271 abstract_html = xml_utils.replace_html_entities(abstract)
1272 xabstract = CkeditorParser(
1273 html_value=abstract_html, issue_pid=colid, pid=pid, mml_formulas=[]
1274 )
1275 abstract_xml = get_abstract_xml(xabstract.value_xml, lang, position)
1276 return xabstract, abstract_xml
1278 def create_related_objects_from_abstract(self, abstracts, colid, pid):
1279 figures = []
1280 for abstract in abstracts:
1281 abstract_xml = abstract.encode("utf8")
1283 tree = etree.fromstring(abstract_xml)
1285 pics = tree.xpath("//graphic")
1286 for pic in pics: 1286 ↛ 1287line 1286 didn't jump to line 1287 because the loop on line 1286 never started
1287 pic_location = pic.attrib["specific-use"]
1288 basename = os.path.basename(pic.attrib["href"])
1289 ext = basename.split(".")[-1]
1290 base = get_media_base_root(colid)
1291 data_location = os.path.join(
1292 settings.RESOURCES_ROOT, "media", base, "uploads", pic_location, basename
1293 )
1294 # we use related objects to send pics to journal site. Directory where pic is stored in trammel may differ
1295 # from the directory in journal site. So one need to save the pic in same directory that journal's one
1296 # so related objects can go for the correct one
1297 img = Image.open(data_location)
1298 final_data_location = os.path.join(
1299 settings.RESOURCES_ROOT, colid, pid, "src", "figures"
1300 )
1301 if not os.path.isdir(final_data_location):
1302 os.makedirs(final_data_location)
1303 relative_path = os.path.join(colid, pid, "src", "figures", basename)
1304 final_data_location = f"{final_data_location}/{basename}"
1305 img.save(final_data_location)
1306 if ext == "png":
1307 mimetype = "image/png"
1308 else:
1309 mimetype = "image/jpeg"
1310 data = {
1311 "rel": "html-image",
1312 "mimetype": mimetype,
1313 "location": relative_path,
1314 "base": None,
1315 "metadata": "",
1316 }
1317 if data not in figures:
1318 figures.append(data)
1319 return figures
1322class PageIndexView(EditorRequiredMixin, TemplateView):
1323 template_name = "mersenne_cms/page_index.html"
1325 def get_context_data(self, **kwargs):
1326 colid = kwargs.get("colid", "")
1327 site_id = model_helpers.get_site_id(colid)
1328 vi = Page.objects.filter(site_id=site_id, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES).first()
1329 if vi: 1329 ↛ 1330line 1329 didn't jump to line 1330 because the condition on line 1329 was never true
1330 pages = Page.objects.filter(site_id=site_id).exclude(parent_page=vi)
1331 else:
1332 pages = Page.objects.filter(site_id=site_id)
1333 context = super().get_context_data(**kwargs)
1334 context["colid"] = colid
1335 context["journal"] = model_helpers.get_collection(colid)
1336 context["pages"] = pages
1337 context["news"] = News.objects.filter(site_id=site_id)
1338 context["fields_lang"] = "fr" if model_helpers.is_site_fr_only(site_id) else "en"
1339 return context
1342class PageBaseView(HandleCMSMixin, View):
1343 template_name = "mersenne_cms/page_form.html"
1344 model = Page
1345 form_class = PageForm
1347 def dispatch(self, request, *args, **kwargs):
1348 self.colid = self.kwargs["colid"]
1349 self.collection = model_helpers.get_collection(self.colid, sites=False)
1350 self.site_id = model_helpers.get_site_id(self.colid)
1352 return super().dispatch(request, *args, **kwargs)
1354 def get_success_url(self):
1355 return reverse("page_index", kwargs={"colid": self.colid})
1357 def get_context_data(self, **kwargs):
1358 context = super().get_context_data(**kwargs)
1359 context["journal"] = self.collection
1360 return context
1362 def update_test_website(self):
1363 response = deploy_cms("test_website", self.collection)
1364 if response.status_code < 300: 1364 ↛ 1367line 1364 didn't jump to line 1367 because the condition on line 1364 was always true
1365 messages.success(self.request, "The test website has been updated")
1366 else:
1367 text = "ERROR: Unable to update the test website<br/>"
1369 if response.status_code == 503:
1370 text += "The test website is under maintenance. Please try again later.<br/>"
1371 else:
1372 text += f"Please contact the centre Mersenne<br/><br/>Status code: {response.status_code}<br/>"
1373 if hasattr(response, "content") and response.content:
1374 text += f"{response.content.decode()}<br/>"
1375 if hasattr(response, "reason") and response.reason:
1376 text += f"Reason: {response.reason}<br/>"
1377 if hasattr(response, "text") and response.text:
1378 text += f"Details: {response.text}<br/>"
1379 messages.error(self.request, mark_safe(text))
1381 def get_form_kwargs(self):
1382 kwargs = super().get_form_kwargs()
1383 kwargs["site_id"] = self.site_id
1384 kwargs["user"] = self.request.user
1385 return kwargs
1387 def form_valid(self, form):
1388 form.save()
1390 self.update_test_website()
1392 return HttpResponseRedirect(self.get_success_url())
1395# @method_decorator([csrf_exempt], name="dispatch")
1396class PageDeleteView(PageBaseView):
1397 def post(self, request, *args, **kwargs):
1398 colid = kwargs.get("colid", "")
1399 pk = kwargs.get("pk")
1400 page = get_object_or_404(Page, id=pk)
1401 if page.mersenne_id:
1402 raise PermissionDenied
1404 page.delete()
1406 self.update_test_website()
1408 if page.parent_page and page.parent_page.mersenne_id == MERSENNE_ID_VIRTUAL_ISSUES:
1409 return HttpResponseRedirect(reverse("virtual_issues_index", kwargs={"colid": colid}))
1410 else:
1411 return HttpResponseRedirect(reverse("page_index", kwargs={"colid": colid}))
1414class PageCreateView(PageBaseView, CreateView):
1415 def get_context_data(self, **kwargs):
1416 context = super().get_context_data(**kwargs)
1417 context["title"] = "Add a menu page"
1418 return context
1421class PageUpdateView(PageBaseView, UpdateView):
1422 def get_context_data(self, **kwargs):
1423 context = super().get_context_data(**kwargs)
1424 context["title"] = "Edit a menu page"
1425 return context
1428class NewsBaseView(PageBaseView):
1429 template_name = "mersenne_cms/news_form.html"
1430 model = News
1431 form_class = NewsForm
1434class NewsDeleteView(NewsBaseView):
1435 def post(self, request, *args, **kwargs):
1436 pk = kwargs.get("pk")
1437 news = get_object_or_404(News, id=pk)
1439 news.delete()
1441 self.update_test_website()
1443 return HttpResponseRedirect(self.get_success_url())
1446class NewsCreateView(NewsBaseView, CreateView):
1447 def get_context_data(self, **kwargs):
1448 context = super().get_context_data(**kwargs)
1449 context["title"] = "Add a News"
1450 return context
1453class NewsUpdateView(NewsBaseView, UpdateView):
1454 def get_context_data(self, **kwargs):
1455 context = super().get_context_data(**kwargs)
1456 context["title"] = "Edit a News"
1457 return context
1460# def page_create_view(request, colid):
1461# context = {}
1462# if not is_authorized_editor(request.user, colid):
1463# raise PermissionDenied
1464# collection = model_helpers.get_collection(colid)
1465# page = Page(site_id=model_helpers.get_site_id(colid))
1466# form = PageForm(request.POST or None, instance=page)
1467# if form.is_valid():
1468# form.save()
1469# response = deploy_cms("test_website", collection)
1470# if response.status_code < 300:
1471# messages.success(request, "Page created successfully.")
1472# else:
1473# text = f"ERROR: page creation failed<br/>Status code: {response.status_code}<br/>"
1474# if hasattr(response, "reason") and response.reason:
1475# text += f"Reason: {response.reason}<br/>"
1476# if hasattr(response, "text") and response.text:
1477# text += f"Details: {response.text}<br/>"
1478# messages.error(request, mark_safe(text))
1479# kwargs = {"colid": colid, "pid": form.instance.id}
1480# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1481#
1482# context["form"] = form
1483# context["title"] = "Add a menu page"
1484# context["journal"] = collection
1485# return render(request, "mersenne_cms/page_form.html", context)
1488# def page_update_view(request, colid, pid):
1489# context = {}
1490# if not is_authorized_editor(request.user, colid):
1491# raise PermissionDenied
1492#
1493# collection = model_helpers.get_collection(colid)
1494# page = get_object_or_404(Page, id=pid)
1495# form = PageForm(request.POST or None, instance=page)
1496# if form.is_valid():
1497# form.save()
1498# response = deploy_cms("test_website", collection)
1499# if response.status_code < 300:
1500# messages.success(request, "Page updated successfully.")
1501# else:
1502# text = f"ERROR: page update failed<br/>Status code: {response.status_code}<br/>"
1503# if hasattr(response, "reason") and response.reason:
1504# text += f"Reason: {response.reason}<br/>"
1505# if hasattr(response, "text") and response.text:
1506# text += f"Details: {response.text}<br/>"
1507# messages.error(request, mark_safe(text))
1508# kwargs = {"colid": colid, "pid": form.instance.id}
1509# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1510#
1511# context["form"] = form
1512# context["pid"] = pid
1513# context["title"] = "Edit a menu page"
1514# context["journal"] = collection
1515# return render(request, "mersenne_cms/page_form.html", context)