Coverage for src / ptf_tools / views / cms_views.py: 49%
879 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-09-18 08:07 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-09-18 08:07 +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.utils import build_jats_data_from_html_field
48from ptf.cmds.xml.jats.builder.issue import build_title_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_abstract,
57 create_contributor,
58 create_datastream,
59 create_issuedata,
60 create_publisherdata,
61 create_titledata,
62)
64# from ptf.models import ExtLink
65# from ptf.models import ResourceInSpecialIssue
66# from ptf.models import Contribution
67# from ptf.models import Collection
68from ptf.models import (
69 Article,
70 Collection,
71 Container,
72 ContribAddress,
73 ExtLink,
74 GraphicalAbstract,
75 RelatedArticles,
76 RelatedObject,
77)
78from ptf.site_register import SITE_REGISTER
79from ptf.utils import ImageManager, get_names
80from requests import Timeout
82from ptf_tools.forms import GraphicalAbstractForm, NewsForm, PageForm, RelatedForm
83from ptf_tools.utils import is_authorized_editor
85from .base_views import check_lock
88def get_media_base_root(colid):
89 """
90 Base folder where media files are stored in Trammel
91 """
92 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]:
93 colid = "CR"
95 return os.path.join(settings.RESOURCES_ROOT, "media", colid)
98def get_media_base_root_in_test(colid):
99 """
100 Base folder where media files are stored in the test website
101 Use the same folder as the Trammel media folder so that no copy is necessary when deploy in test
102 """
103 return get_media_base_root(colid)
106def get_media_base_root_in_prod(colid):
107 """
108 Base folder where media files are stored in the prod website
109 """
110 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]:
111 colid = "CR"
113 return os.path.join(settings.MERSENNE_PROD_DATA_FOLDER, "media", colid)
116def get_media_base_url(colid):
117 path = os.path.join(settings.MEDIA_URL, colid)
119 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]:
120 prefixes = {
121 "CRMECA": "mecanique",
122 "CRBIOL": "biologies",
123 "CRGEOS": "geoscience",
124 "CRCHIM": "chimie",
125 "CRMATH": "mathematique",
126 "CRPHYS": "physique",
127 }
128 path = os.path.join(settings.MEDIA_URL, "CR")
129 path = f"/{prefixes[colid]}{path}"
131 return path
134def change_ckeditor_storage(colid):
135 """
136 By default, CKEditor stores all the files under 1 folder (MEDIA_ROOT)
137 We want to store the files under a subfolder of @colid
138 To do that we have to
139 - change the URL calling this view to pass the site_id (info used by the Pages to filter the objects)
140 - modify the storage location
141 """
143 from ckeditor_uploader import utils, views
144 from django.core.files.storage import FileSystemStorage
146 storage = FileSystemStorage(
147 location=get_media_base_root(colid), base_url=get_media_base_url(colid)
148 )
150 utils.storage = storage
151 views.storage = storage
154class EditorRequiredMixin(UserPassesTestMixin):
155 def test_func(self):
156 return is_authorized_editor(self.request.user, self.kwargs.get("colid"))
159class CollectionImageUploadView(EditorRequiredMixin, ImageUploadView):
160 """
161 By default, CKEditor stores all the files under 1 folder (MEDIA_ROOT)
162 We want to store the files under a subfolder of @colid
163 To do that we have to
164 - change the URL calling this view to pass the site_id (info used by the Pages to filter the objects)
165 - modify the storage location
166 """
168 def dispatch(self, request, *args, **kwargs):
169 colid = kwargs["colid"]
170 colid = colid.replace("/", "")
172 change_ckeditor_storage(colid)
174 return super().dispatch(request, **kwargs)
177class CollectionBrowseView(EditorRequiredMixin, View):
178 def dispatch(self, request, **kwargs):
179 colid = kwargs["colid"]
181 change_ckeditor_storage(colid)
183 return browse(request)
186file_upload_in_collection = csrf_exempt(CollectionImageUploadView.as_view())
187file_browse_in_collection = csrf_exempt(CollectionBrowseView.as_view())
190def deploy_cms(site, collection):
191 colid = collection.pid
192 base_url = getattr(collection, site)()
194 if base_url is None: 194 ↛ 197line 194 didn't jump to line 197 because the condition on line 194 was always true
195 return JsonResponse({"message": "OK"})
197 if site == "website":
198 from_base_path = get_media_base_root_in_test(colid)
199 to_base_path = get_media_base_root_in_prod(colid)
201 for sub_path in ["uploads", "images"]:
202 from_path = os.path.join(from_base_path, sub_path)
203 to_path = os.path.join(to_base_path, sub_path)
204 if os.path.exists(from_path):
205 try:
206 shutil.copytree(from_path, to_path, dirs_exist_ok=True)
207 except OSError as exception:
208 return HttpResponseServerError(f"Error during copy: {exception}")
210 site_id = model_helpers.get_site_id(colid)
211 if model_helpers.get_site_default_language(site_id):
212 from modeltranslation import fields, manager
214 old_ftor = manager.get_language
215 manager.get_language = monkey_get_language_en
216 fields.get_language = monkey_get_language_en
218 pages = get_pages_content(colid)
219 news = get_news_content(colid)
221 manager.get_language = old_ftor
222 fields.get_language = old_ftor
223 else:
224 pages = get_pages_content(colid)
225 news = get_news_content(colid)
227 data = json.dumps({"pages": json.loads(pages), "news": json.loads(news)})
228 url = getattr(collection, site)() + "/import_cms/"
230 try:
231 response = requests.put(url, data=data, verify=False)
233 if response.status_code == 503:
234 e = ServerUnderMaintenance(
235 "The journal test website is under maintenance. Please try again later."
236 )
237 return HttpResponseServerError(e, status=503)
239 except Timeout as exception:
240 return HttpResponse(exception, status=408)
241 except Exception as exception:
242 return HttpResponseServerError(exception)
244 return JsonResponse({"message": "OK"})
247class HandleCMSMixin(EditorRequiredMixin):
248 """
249 Mixin for classes that need to send request to (test) website to import/export CMS content (pages, news)
250 """
252 # def dispatch(self, request, *args, **kwargs):
253 # self.colid = self.kwargs["colid"]
254 # return super().dispatch(request, *args, **kwargs)
256 def init_data(self, kwargs):
257 self.collection = None
259 self.colid = kwargs.get("colid", None)
260 if self.colid:
261 self.collection = model_helpers.get_collection(self.colid)
262 if not self.collection:
263 raise Http404(f"{self.colid} does not exist")
265 test_server_url = self.collection.test_website()
266 if not test_server_url:
267 raise Http404("The collection has no test site")
269 prod_server_url = self.collection.website()
270 if not prod_server_url:
271 raise Http404("The collection has no prod site")
274class GetCMSFromSiteAPIView(HandleCMSMixin, View):
275 """
276 Get the CMS content from the (test) website and save it on disk.
277 It can be used if needed to restore the Trammel content with RestoreCMSAPIView below
278 """
280 def get(self, request, *args, **kwargs):
281 self.init_data(self.kwargs)
283 site = kwargs.get("site", "test_website")
285 try:
286 url = getattr(self.collection, site)() + "/export_cms/"
287 response = requests.get(url, verify=False)
289 # Just to need to save the json on disk
290 # Media files are already saved in MEDIA_ROOT which is equal to
291 # /mersenne_test_data/@colid/media
292 folder = get_media_base_root(self.colid)
293 os.makedirs(folder, exist_ok=True)
294 filename = os.path.join(folder, f"pages_{self.colid}.json")
295 with open(filename, mode="w", encoding="utf-8") as file:
296 file.write(response.content.decode(encoding="utf-8"))
298 except Timeout as exception:
299 return HttpResponse(exception, status=408)
300 except Exception as exception:
301 return HttpResponseServerError(exception)
303 return JsonResponse({"message": "OK", "status": 200})
306def monkey_get_language_en():
307 return "en"
310class RestoreCMSAPIView(HandleCMSMixin, View):
311 """
312 Restore the Trammel CMS content (of a colid) from disk
313 """
315 def get(self, request, *args, **kwargs):
316 self.init_data(self.kwargs)
318 folder = get_media_base_root(self.colid)
319 filename = os.path.join(folder, f"pages_{self.colid}.json")
320 with open(filename, encoding="utf-8") as f:
321 json_data = json.load(f)
323 pages = json_data.get("pages")
325 site_id = model_helpers.get_site_id(self.colid)
326 if model_helpers.get_site_default_language(site_id):
327 from modeltranslation import fields, manager
329 old_ftor = manager.get_language
330 manager.get_language = monkey_get_language_en
331 fields.get_language = monkey_get_language_en
333 import_pages(pages, self.colid)
335 manager.get_language = old_ftor
336 fields.get_language = old_ftor
337 else:
338 import_pages(pages, self.colid)
340 if "news" in json_data:
341 news = json_data.get("news")
342 import_news(news, self.colid)
344 return JsonResponse({"message": "OK", "status": 200})
347class DeployCMSAPIView(HandleCMSMixin, View):
348 def get(self, request, *args, **kwargs):
349 self.init_data(self.kwargs)
351 if check_lock():
352 msg = "Trammel is under maintenance. Please try again later."
353 messages.error(self.request, msg)
354 return JsonResponse({"messages": msg, "status": 503})
356 site = kwargs.get("site", "test_website")
358 response = deploy_cms(site, self.collection)
360 if response.status_code == 503:
361 messages.error(
362 self.request, "The journal website is under maintenance. Please try again later."
363 )
365 return response
368def get_server_urls(collection, site="test_website"):
369 urls = [""]
370 if hasattr(settings, "MERSENNE_DEV_URL"): 370 ↛ 372line 370 didn't jump to line 372 because the condition on line 370 was never true
371 # set RESOURCES_ROOT and apache config accordingly (for instance with "/mersenne_dev_data")
372 url = getattr(collection, "test_website")().split(".fr")
373 urls = [settings.MERSENNE_DEV_URL + url[1] if len(url) == 2 else ""]
374 elif site == "both": 374 ↛ 375line 374 didn't jump to line 375 because the condition on line 374 was never true
375 urls = [getattr(collection, "test_website")(), getattr(collection, "website")()]
376 elif hasattr(collection, site) and getattr(collection, site)(): 376 ↛ 377line 376 didn't jump to line 377 because the condition on line 376 was never true
377 urls = [getattr(collection, site)()]
378 return urls
381class SuggestDeployView(EditorRequiredMixin, View):
382 def post(self, request, *args, **kwargs):
383 doi = kwargs.get("doi", "")
384 site = kwargs.get("site", "test_website")
385 article = get_object_or_404(Article, doi=doi)
387 obj, created = RelatedArticles.objects.get_or_create(resource=article)
388 form = RelatedForm(request.POST or None, instance=obj)
389 if form.is_valid(): 389 ↛ 406line 389 didn't jump to line 406 because the condition on line 389 was always true
390 data = form.cleaned_data
391 obj.date_modified = timezone.now()
392 form.save()
393 collection = article.my_container.my_collection
394 urls = get_server_urls(collection, site=site)
395 response = requests.models.Response()
396 for url in urls: 396 ↛ 404line 396 didn't jump to line 404 because the loop on line 396 didn't complete
397 url = url + reverse("api-update-suggest", kwargs={"doi": doi})
398 try:
399 response = requests.post(url, data=data, timeout=15)
400 except requests.exceptions.RequestException as e:
401 response.status_code = 503
402 response.reason = e.args[0]
403 break
404 return HttpResponse(status=response.status_code, reason=response.reason)
405 else:
406 return HttpResponseBadRequest()
409def suggest_debug(results, article, message):
410 crop_results = 5
411 if results: 411 ↛ 412line 411 didn't jump to line 412 because the condition on line 411 was never true
412 dois = []
413 results["docs"] = results["docs"][:crop_results]
414 numFound = f"({len(results['docs'])} sur {results['numFound']} documents)"
415 head = f"Résultats de la recherche automatique {numFound} :\n\n"
416 for item in results["docs"]:
417 doi = item.get("doi")
418 if doi:
419 explain = results["explain"][item["id"]]
420 terms = re.findall(r"([0-9.]+?) = weight\((.+?:.+?) in", explain)
421 terms.sort(key=lambda t: t[0], reverse=True)
422 details = (" + ").join(f"{round(float(s), 1)}:{t}" for s, t in terms)
423 score = f"Score : {round(float(item['score']), 1)} (= {details})\n"
424 url = ""
425 suggest = Article.objects.filter(doi=doi).first()
426 if suggest and suggest.my_container:
427 collection = suggest.my_container.my_collection
428 base_url = collection.website() or ""
429 url = base_url + "/articles/" + doi
430 dois.append((doi, url, score))
432 tail = f"\n\nScore minimum retenu : {results['params']['min_score']}\n\n\n"
433 tail += "Termes principaux utilisés pour la requête "
434 tail = [tail + "(champ:terme recherché | pertinence du terme) :\n"]
435 if results["params"]["mlt.fl"] == "all":
436 tail.append(" * all = body + abstract + title + authors + keywords\n")
437 terms = results["interestingTerms"]
438 terms = [" | ".join((x[0], str(x[1]))) for x in zip(terms[::2], terms[1::2])]
439 tail.extend(reversed(terms))
440 tail.append("\n\nParamètres de la requête :\n")
441 tail.extend([f"{k}: {v} " for k, v in results["params"].items()])
442 return [(head, dois, "\n".join(tail))]
443 else:
444 msg = f"Erreur {message['status']} {message['err']} at {message['url']}"
445 return [(msg, [], "")]
448class SuggestUpdateView(EditorRequiredMixin, TemplateView):
449 template_name = "editorial_tools/suggested.html"
451 def get_context_data(self, **kwargs):
452 doi = kwargs.get("doi", "")
453 article = get_object_or_404(Article, doi=doi)
455 obj, created = RelatedArticles.objects.get_or_create(resource=article)
456 collection = article.my_container.my_collection
457 base_url = collection.website() or ""
458 response = requests.models.Response()
459 try:
460 response = requests.get(base_url + "/mlt/" + doi, timeout=10.0)
461 except requests.exceptions.RequestException as e:
462 response.status_code = 503
463 response.reason = e.args[0]
464 msg = {
465 "url": response.url,
466 "status": response.status_code,
467 "err": response.reason,
468 }
469 results = None
470 if response.status_code == 200: 470 ↛ 471line 470 didn't jump to line 471 because the condition on line 470 was never true
471 results = solr_cmds.auto_suggest_doi(obj, article, response.json())
472 context = super().get_context_data(**kwargs)
473 context["debug"] = suggest_debug(results, article, msg)
474 context["form"] = RelatedForm(instance=obj)
475 context["author"] = "; ".join(get_names(article, "author"))
476 context["citation_base"] = article.get_citation_base().strip(", .")
477 context["article"] = article
478 context["date_modified"] = obj.date_modified
479 context["url"] = base_url + "/articles/" + doi
480 return context
483class EditorialToolsVolumeItemsView(EditorRequiredMixin, TemplateView):
484 template_name = "editorial_tools/volume-items.html"
486 def get_context_data(self, **kwargs):
487 vid = kwargs.get("vid")
488 issues_articles, collection = model_helpers.get_issues_in_volume(vid)
489 context = super().get_context_data(**kwargs)
490 context["issues_articles"] = issues_articles
491 context["collection"] = collection
492 return context
495class EditorialToolsArticleView(EditorRequiredMixin, TemplateView):
496 template_name = "editorial_tools/find-article.html"
498 def get_context_data(self, **kwargs):
499 colid = kwargs.get("colid")
500 doi = kwargs.get("doi")
501 article = get_object_or_404(Article, doi=doi, my_container__my_collection__pid=colid)
503 context = super().get_context_data(**kwargs)
504 context["article"] = article
505 context["citation_base"] = article.get_citation_base().strip(", .")
506 return context
509class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView):
510 template_name = "editorial_tools/graphical-abstract.html"
512 def get_context_data(self, **kwargs):
513 doi = kwargs.get("doi", "")
514 article = get_object_or_404(Article, doi=doi)
516 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
517 context = super().get_context_data(**kwargs)
518 context["author"] = "; ".join(get_names(article, "author"))
519 context["citation_base"] = article.get_citation_base().strip(", .")
520 context["article"] = article
521 context["date_modified"] = obj.date_modified
522 context["form"] = GraphicalAbstractForm(instance=obj)
523 context["graphical_abstract"] = obj.graphical_abstract
524 context["illustration"] = obj.illustration
525 return context
528class GraphicalAbstractDeployView(EditorRequiredMixin, View):
529 def __get_path_and_replace_tiff_file(self, obj_attribute_file):
530 """
531 Returns the path of the attribute.
532 If the attribute is a tiff file, converts it to jpg, delete the old tiff file, and return the new path of the attribute.
534 Checks if paths have already been processed, to prevent issues related to object mutation.
535 """
536 if obj_attribute_file.name.lower().endswith((".tiff", ".tif")):
537 jpeg_path = ImageManager(obj_attribute_file.path).to_jpeg(delete_original_tiff=True)
538 with open(jpeg_path, "rb") as fp:
539 obj_attribute_file.save(os.path.basename(jpeg_path), File(fp), save=True)
540 return jpeg_path
542 return obj_attribute_file.path
544 def post(self, request, *args, **kwargs):
545 doi = kwargs.get("doi", "")
546 site = kwargs.get("site", "both")
547 article = get_object_or_404(Article, doi=doi)
549 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
550 form = GraphicalAbstractForm(request.POST, request.FILES or None, instance=obj)
551 if form.is_valid(): 551 ↛ 580line 551 didn't jump to line 580 because the condition on line 551 was always true
552 obj.date_modified = timezone.now()
553 data = {"date_modified": obj.date_modified}
554 form.save()
555 files = {}
557 for attribute in ("graphical_abstract", "illustration"):
558 obj_attribute_file = getattr(obj, attribute, None)
559 if obj_attribute_file and os.path.exists(obj_attribute_file.path): 559 ↛ 560line 559 didn't jump to line 560 because the condition on line 559 was never true
560 file_path = self.__get_path_and_replace_tiff_file(obj_attribute_file)
561 with open(file_path, "rb") as fp:
562 files.update({attribute: (obj_attribute_file.name, fp.read())})
564 collection = article.my_container.my_collection
565 urls = get_server_urls(collection, site=site)
566 response = requests.models.Response()
567 for url in urls: 567 ↛ 578line 567 didn't jump to line 578 because the loop on line 567 didn't complete
568 url = url + reverse("api-graphical-abstract", kwargs={"doi": doi})
569 try:
570 if not obj.graphical_abstract and not obj.illustration: 570 ↛ 573line 570 didn't jump to line 573 because the condition on line 570 was always true
571 response = requests.delete(url, data=data, files=files, timeout=15)
572 else:
573 response = requests.post(url, data=data, files=files, timeout=15)
574 except requests.exceptions.RequestException as e:
575 response.status_code = 503
576 response.reason = e.args[0]
577 break
578 return HttpResponse(status=response.status_code, reason=response.reason)
579 else:
580 return HttpResponseBadRequest()
583def parse_content(content):
584 table = re.search(r'(.*?)(<table id="summary".+?</table>)(.*)', content, re.DOTALL)
585 if not table:
586 return {"head": content, "tail": "", "articles": []}
588 articles = []
589 rows = re.findall(r"<tr>.+?</tr>", table.group(2), re.DOTALL)
590 for row in rows:
591 citation = re.search(r'<div href=".*?">(.*?)</div>', row, re.DOTALL)
592 href = re.search(r'href="(.+?)\/?">', row)
593 doi = re.search(r"(10[.].+)", href.group(1)) if href else ""
594 src = re.search(r'<img.+?src="(.+?)"', row)
595 item = {}
596 item["citation"] = citation.group(1) if citation else ""
597 item["doi"] = doi.group(1) if doi else href.group(1) if href else ""
598 item["src"] = src.group(1) if src else ""
599 item["imageName"] = item["src"].split("/")[-1] if item["src"] else ""
600 if item["doi"] or item["src"]:
601 articles.append(item)
602 return {"head": table.group(1), "tail": table.group(3), "articles": articles}
605class VirtualIssueParseView(EditorRequiredMixin, View):
606 def get(self, request, *args, **kwargs):
607 pid = kwargs.get("pid", "")
608 page = get_object_or_404(Page, id=pid)
610 data = {"pid": pid}
611 data["colid"] = kwargs.get("colid", "")
612 journal = model_helpers.get_collection(data["colid"])
613 data["journal_title"] = journal.title_tex.replace(".", "")
614 site_id = model_helpers.get_site_id(data["colid"])
615 data["page"] = model_to_dict(page)
616 pages = Page.objects.filter(site_id=site_id).exclude(id=pid)
617 data["parents"] = [model_to_dict(p, fields=["id", "menu_title"]) for p in pages]
619 content_fr = parse_content(page.content_fr)
620 data["head_fr"] = content_fr["head"]
621 data["tail_fr"] = content_fr["tail"]
623 content_en = parse_content(page.content_en)
624 data["articles"] = content_en["articles"]
625 data["head_en"] = content_en["head"]
626 data["tail_en"] = content_en["tail"]
627 return JsonResponse(data)
630class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView):
631 template_name = "editorial_tools/virtual-issue.html"
633 def get(self, request, *args, **kwargs):
634 pid = kwargs.get("pid", "")
635 get_object_or_404(Page, id=pid)
636 return super().get(request, *args, **kwargs)
639class VirtualIssueCreateView(EditorRequiredMixin, View):
640 def get(self, request, *args, **kwargs):
641 colid = kwargs.get("colid", "")
642 site_id = model_helpers.get_site_id(colid)
643 parent, _ = Page.objects.get_or_create(
644 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES,
645 parent_page=None,
646 site_id=site_id,
647 )
648 page = Page.objects.create(
649 menu_title_en="New virtual issue",
650 menu_title_fr="Nouvelle collection transverse",
651 parent_page=parent,
652 site_id=site_id,
653 state="draft",
654 )
655 kwargs = {"colid": colid, "pid": page.id}
656 return HttpResponseRedirect(reverse("virtual_issue_update", kwargs=kwargs))
659class SpecialIssuesIndex(EditorRequiredMixin, TemplateView):
660 template_name = "editorial_tools/special-issues-index.html"
662 def get_context_data(self, **kwargs):
663 colid = kwargs.get("colid", "")
665 context = super().get_context_data(**kwargs)
666 context["colid"] = colid
667 collection = Collection.objects.get(pid=colid)
668 context["special_issues"] = Container.objects.filter(
669 Q(ctype="issue_special") | Q(ctype="issue_special_img")
670 ).filter(my_collection=collection)
672 context["journal"] = model_helpers.get_collection(colid, sites=False)
673 return context
676class SpecialIssueEditView(EditorRequiredMixin, TemplateView):
677 template_name = "editorial_tools/special-issue-edit.html"
679 def get_context_data(self, **kwargs):
680 context = super().get_context_data(**kwargs)
681 return context
684class VirtualIssuesIndex(EditorRequiredMixin, TemplateView):
685 template_name = "editorial_tools/virtual-issues-index.html"
687 def get_context_data(self, **kwargs):
688 colid = kwargs.get("colid", "")
689 site_id = model_helpers.get_site_id(colid)
690 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES)
691 pages = Page.objects.filter(site_id=site_id, parent_page=vi)
692 context = super().get_context_data(**kwargs)
693 context["journal"] = model_helpers.get_collection(colid)
694 context["pages"] = pages
695 return context
698def get_citation_fr(doi, citation_en):
699 citation_fr = citation_en
700 article = Article.objects.filter(doi=doi).first()
701 if article and article.trans_title_html:
702 trans_title = article.trans_title_html
703 try:
704 citation_fr = re.sub(
705 r'(<a href="https:\/\/doi\.org.*">)([^<]+)',
706 rf"\1{trans_title}",
707 citation_en,
708 )
709 except re.error:
710 pass
711 return citation_fr
714def summary_build(articles, colid):
715 summary_fr = ""
716 summary_en = ""
717 head = '<table id="summary"><tbody>'
718 tail = "</tbody></table>"
719 style = "max-width:180px;max-height:200px"
720 colid_lo = colid.lower()
721 site_domain = SITE_REGISTER[colid_lo]["site_domain"].split("/")
722 site_domain = "/" + site_domain[-1] if len(site_domain) == 2 else ""
724 for article in articles:
725 image_src = article.get("src", "")
726 image_name = article.get("imageName", "")
727 doi = article.get("doi", "")
728 citation_en = article.get("citation", "")
729 if doi or citation_en:
730 row_fr = f'<div href="{doi}">{get_citation_fr(doi, citation_en)}</div>'
731 row_en = f'<div href="{doi}">{citation_en}</div>'
732 if image_src:
733 date = datetime.now().strftime("%Y/%m/%d/")
734 base_url = get_media_base_url(colid)
735 suffix = os.path.join(base_url, "uploads", date)
736 image_url = os.path.join(site_domain, suffix, image_name)
737 image_header = "^data:image/.+;base64,"
738 if re.match(image_header, image_src):
739 image_src = re.sub(image_header, "", image_src)
740 base64_data = base64.b64decode(image_src)
741 base_root = get_media_base_root(colid)
742 path = os.path.join(base_root, "uploads", date)
743 os.makedirs(path, exist_ok=True)
744 with open(path + image_name, "wb") as fp:
745 fp.write(base64_data)
746 im = f'<img src="{image_url}" style="{style}" />'
747 # TODO mettre la vrai valeur pour le SITE_DOMAIN
748 elif settings.SITE_DOMAIN == "http://127.0.0.1:8002":
749 im = f'<img src="{image_src}" style="{style}" />'
750 else:
751 im = f'<img src="{site_domain}{image_src}" style="{style}" />'
752 summary_fr += f"<tr><td>{im}</td><td>{row_fr}</td></tr>"
753 summary_en += f"<tr><td>{im}</td><td>{row_en}</td></tr>"
754 summary_fr = head + summary_fr + tail
755 summary_en = head + summary_en + tail
756 return {"summary_fr": summary_fr, "summary_en": summary_en}
759# @method_decorator([csrf_exempt], name="dispatch")
760class VirtualIssueDeployView(HandleCMSMixin, View):
761 """
762 called by the Virtual.vue VueJS component, when the virtual issue is saved
763 We get data in JSON and we need to update the corresponding Page.
764 The Page is then immediately posted to the test_website.
765 The "Apply changes to production website" button is then used to update the (prod) website
766 => See DeployCMSAPIView
767 """
769 def post(self, request, *args, **kwargs):
770 self.init_data(self.kwargs)
771 if check_lock():
772 msg = "Trammel is under maintenance. Please try again later."
773 messages.error(self.request, msg)
774 return JsonResponse({"messages": msg, "status": 503})
776 pid = kwargs.get("pid")
777 colid = self.colid
778 data = json.loads(request.body)
779 summary = summary_build(data["articles"], colid)
780 page = get_object_or_404(Page, id=pid)
781 page.slug = page.slug_fr = page.slug_en = None
782 page.menu_title_fr = data["title_fr"]
783 page.menu_title_en = data["title_en"]
784 page.content_fr = data["head_fr"] + summary["summary_fr"] + data["tail_fr"]
785 page.content_en = data["head_en"] + summary["summary_en"] + data["tail_en"]
786 page.state = data["page"]["state"]
787 page.menu_order = data["page"]["menu_order"]
788 page.parent_page = Page.objects.filter(id=data["page"]["parent_page"]).first()
789 page.save()
791 response = deploy_cms("test_website", self.collection)
792 if response.status_code == 503:
793 messages.error(
794 self.request, "The journal website is under maintenance. Please try again later."
795 )
797 return response # HttpResponse(status=response.status_code, reason=response.reason)
800class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView):
801 template_name = "editorial_tools/special-issue-edit.html"
803 def get_context_data(self, **kwargs):
804 context = super().get_context_data(**kwargs)
805 return context
807 def set_contrib_addresses(self, contrib, contribution):
808 for address in contrib:
809 contrib_address = ContribAddress(contribution=contribution, address=address)
810 contrib_address.save()
812 def delete(self, pid):
813 special_issue = Container.objects.get(pid=pid)
814 cmd = base_ptf_cmds.addContainerPtfCmd()
815 cmd.set_object_to_be_deleted(special_issue)
816 cmd.undo()
818 def get(self, request, *args, **kwargs):
819 pid = kwargs.get("pid", "")
821 data = {"pid": pid}
822 colid = kwargs.get("colid", "")
823 data["colid"] = colid
824 journal = model_helpers.get_collection(colid, sites=False)
825 name = resolve(request.path_info).url_name
826 if name == "special_issue_delete": 826 ↛ 827line 826 didn't jump to line 827 because the condition on line 826 was never true
827 self.delete(pid)
828 return redirect("special_issues_index", data["colid"])
830 data["journal_title"] = journal.title_tex.replace(".", "")
832 if pid != "create":
833 container = get_object_or_404(Container, pid=pid)
834 # TODO: pass the lang and trans_lang as well
835 # In VueJS (Special.vu)e, titleFr = title_html
836 # June 2025: Title objects are added for translated titles
837 # keep using trans_title_html for backward compatibility
838 translated_title = container.title_set.all().filter(lang="fr", type="main").first()
839 if translated_title: 839 ↛ 841line 839 didn't jump to line 841 because the condition on line 839 was always true
840 data["title"] = translated_title.title_html
841 data["doi"] = container.doi
842 data["trans_title"] = container.title_html
843 data["year"] = container.year_str
844 data["volume"] = container.volume
845 data["articles"] = [
846 {"doi": article.resource_doi, "citation": article.citation}
847 for article in container.resources_in_special_issue.all().order_by("seq")
848 ]
849 if container.ctype == "issue_special_img": 849 ↛ 850line 849 didn't jump to line 850 because the condition on line 849 was never true
850 data["use_resources_icon"] = True
851 else:
852 data["use_resources_icon"] = False
854 contribs = model_data_converter.db_to_contributors(container.contributions)
855 data["contribs"] = contribs
856 abstract_set = container.abstract_set.all()
857 data["head_fr"] = (
858 abstract_set.filter(tag="intro", lang="fr").first().value_html
859 if abstract_set.filter(tag="intro", lang="fr").exists()
860 else ""
861 )
862 data["head_en"] = (
863 abstract_set.filter(tag="intro", lang="en").first().value_html
864 if abstract_set.filter(tag="intro", lang="en").exists()
865 else ""
866 )
867 data["tail_fr"] = (
868 abstract_set.filter(tag="tail", lang="fr").first().value_html
869 if abstract_set.filter(tag="tail", lang="fr").exists()
870 else ""
871 )
872 data["tail_en"] = (
873 abstract_set.filter(tag="tail", lang="en").first().value_html
874 if abstract_set.filter(tag="tail", lang="en").exists()
875 else ""
876 )
877 data["editor_bio_en"] = (
878 abstract_set.filter(tag="bio_en").first().value_html
879 if abstract_set.filter(tag="bio_en").exists()
880 else ""
881 )
882 data["editor_bio_fr"] = (
883 abstract_set.filter(tag="bio_fr").first().value_html
884 if abstract_set.filter(tag="bio_fr").exists()
885 else ""
886 )
888 streams = container.datastream_set.all()
889 data["pdf_file_name"] = ""
890 data["edito_file_name"] = ""
891 data["edito_display_name"] = ""
892 for stream in streams: # don't work 892 ↛ 893line 892 didn't jump to line 893 because the loop on line 892 never started
893 if os.path.basename(stream.location).split(".")[0] == data["pid"]:
894 data["pdf_file_name"] = stream.text
895 try:
896 # edito related objects metadata contains both file real name and displayed name in issue summary
897 edito_name_infos = container.relatedobject_set.get(rel="edito").metadata.split(
898 "$$$"
899 )
900 data["edito_file_name"] = edito_name_infos[0]
901 data["edito_display_name"] = edito_name_infos[1]
903 except RelatedObject.DoesNotExist:
904 pass
905 try:
906 container_icon = container.extlink_set.get(rel="icon")
908 data["icon_location"] = container_icon.location
909 except ExtLink.DoesNotExist:
910 data["icon_location"] = ""
911 # try:
912 # special_issue_icon = container.extlink_set.get(rel="icon")
913 # data["special_issue_icon"] = special_issue_icon.location
914 # except ExtLink.DoesNotExist:
915 # data["special_issue_icon"] = None
917 else:
918 data["title"] = ""
919 data["doi"] = None
920 data["trans_title"] = ""
921 data["year"] = ""
922 data["volume"] = ""
923 data["articles"] = []
924 data["contribs"] = []
926 data["head_fr"] = ""
927 data["head_en"] = ""
928 data["tail_fr"] = ""
929 data["tail_en"] = ""
930 data["editor_bio_en"] = ""
931 data["editor_bio_fr"] = ""
932 data["pdf_file_name"] = ""
933 data["edito_file_name"] = ""
934 data["use_resources_icon"] = False
936 return JsonResponse(data)
938 def post(self, request, *args, **kwargs):
939 # le but est de faire un IssueDAta
940 pid = kwargs.get("pid", "")
941 colid = kwargs.get("colid", "")
942 journal = collection = model_helpers.get_collection(colid, sites=False)
943 special_issue = create_issuedata()
944 year_str = request.POST["year"]
945 years = year_str.split("-")
946 if len(years) > 1: 946 ↛ 947line 946 didn't jump to line 947 because the condition on line 946 was never true
947 fyear, lyear = int(years[0]), int(years[1])
948 else:
949 fyear, lyear = int(years[0]), None
950 # TODO 1: the values should be the tex values, not the html ones
951 # TODO 2: In VueJS, titleFr = title
952 trans_title_html = request.POST["title"]
953 title_html = request.POST["trans_title"]
954 issues = collection.content.all().order_by("-fyear")
955 same_year_issues = issues.filter(fyear=fyear)
956 if same_year_issues.exists(): 956 ↛ 958line 956 didn't jump to line 958 because the condition on line 956 was always true
957 volume = same_year_issues.first().volume
958 elif issues.exists() and colid != "HOUCHES": # because we don't want a volume for houches
959 ref_volume = issues.filter(fyear=2024).first().volume
960 volume = int(ref_volume) + (
961 fyear - 2024
962 ) # 2024 is the ref year for wich we know the volume is 347
963 else:
964 volume = ""
965 if pid != "create":
966 # TODO: do not use the pk, but the pid in the URLs
967 container = get_object_or_404(Container, pid=pid)
968 lang = container.lang
969 trans_title = container.title_set.all().filter(type="main").first()
970 if not trans_title: 970 ↛ 971line 970 didn't jump to line 971 because the condition on line 970 was never true
971 raise ValueError(
972 "Cannot find trans_lang: Container does not have a main title translation"
973 )
974 trans_lang = trans_title.lang
975 xpub = create_publisherdata()
976 xpub.name = container.my_publisher.pub_name
977 special_issue.provider = container.provider
978 special_issue.number = container.number
979 special_issue_pid = pid
980 special_issue.date_pre_published = container.date_pre_published
981 special_issue.date_published = container.date_published
982 # used for first special issues created withou a proper doi
983 # can be remove when no doi's less special issue existe
984 if not container.doi: 984 ↛ 985line 984 didn't jump to line 985 because the condition on line 984 was never true
985 special_issue.doi = model_helpers.assign_container_doi(colid)
986 else:
987 special_issue.doi = container.doi
988 else:
989 lang = "en"
990 container = None
991 trans_lang = "fr"
992 xpub = create_publisherdata()
993 special_issue.doi = model_helpers.assign_container_doi(colid)
995 if colid == "HOUCHES": 995 ↛ 996line 995 didn't jump to line 996 because the condition on line 995 was never true
996 xpub.name = "UGA Éditions"
997 else:
998 xpub.name = issues.first().my_publisher.pub_name
999 special_issue.provider = collection.provider
1001 special_issues = issues.filter(fyear=fyear).filter(
1002 Q(ctype="issue_special") | Q(ctype="issue") | Q(ctype="issue_special_img")
1003 )
1004 if special_issues: 1004 ↛ 1014line 1004 didn't jump to line 1014 because the condition on line 1004 was always true
1005 all_special_issues_numbers = [
1006 int(si.number[1:]) for si in special_issues if si.number[1:].isnumeric()
1007 ]
1008 if len(all_special_issues_numbers) > 0:
1009 max_number = max(all_special_issues_numbers)
1010 else:
1011 max_number = 0
1013 else:
1014 max_number = 0
1015 special_issue.number = f"S{max_number + 1}"
1016 special_issue_pid = f"{colid}_{year_str}__{volume}_{special_issue.number}"
1018 if request.POST["use_resources_icon"] == "true": 1018 ↛ 1019line 1018 didn't jump to line 1019 because the condition on line 1018 was never true
1019 special_issue.ctype = "issue_special_img"
1020 else:
1021 special_issue.ctype = "issue_special"
1023 existing_issue = model_helpers.get_resource(special_issue_pid)
1024 if pid == "create" and existing_issue is not None: 1024 ↛ 1025line 1024 didn't jump to line 1025 because the condition on line 1024 was never true
1025 raise ValueError(f"The special issue with the pid {special_issue_pid} already exists")
1027 special_issue.lang = lang
1028 special_issue.title_html = title_html
1029 special_issue.title_xml = build_title_xml(
1030 title=title_html, lang=lang, title_type="issue-title"
1031 )
1033 title_xml = build_title_xml(
1034 title=trans_title_html, lang=trans_lang, title_type="issue-title"
1035 )
1036 title = create_titledata(
1037 lang=trans_lang, type="main", title_html=trans_title_html, title_xml=title_xml
1038 )
1039 special_issue.titles = [title]
1041 special_issue.fyear = fyear
1042 special_issue.lyear = lyear
1043 special_issue.volume = volume
1044 special_issue.journal = journal
1045 special_issue.publisher = xpub
1046 special_issue.pid = special_issue_pid
1047 special_issue.last_modified_iso_8601_date_str = datetime.now().strftime(
1048 "%Y-%m-%d %H:%M:%S"
1049 )
1051 articles = []
1052 contribs = []
1053 index = 0
1055 if "nb_articles" in request.POST.keys():
1056 while index < int(request.POST["nb_articles"]):
1057 article = json.loads(request.POST[f"article[{index}]"])
1058 article["citation"] = xml_utils.replace_html_entities(article["citation"])
1059 # if not article["citation"]:
1060 # index += 1
1061 # continue
1062 articles.append(article)
1064 index += 1
1066 special_issue.articles = [Munch(article) for article in articles]
1067 index = 0
1068 # TODO make a function to call to add a contributor
1069 if "nb_contrib" in request.POST.keys():
1070 while index < int(request.POST["nb_contrib"]):
1071 contrib = json.loads(request.POST[f"contrib[{index}]"])
1072 contributor = create_contributor()
1073 contributor["first_name"] = contrib["first_name"]
1074 contributor["last_name"] = contrib["last_name"]
1075 contributor["orcid"] = contrib["orcid"]
1076 contributor["role"] = "editor"
1078 contrib_xml = xml_utils.get_contrib_xml(contrib)
1079 contributor["contrib_xml"] = contrib_xml
1080 contribs.append(Munch(contributor))
1081 index += 1
1082 special_issue.contributors = contribs
1084 # Part of the code that handle forwords and lastwords
1086 request_datas = [
1087 {"request_key": "head_fr", "abstract_type": "intro"},
1088 {"request_key": "head_en", "abstract_type": "intro"},
1089 {"request_key": "tail_fr", "abstract_type": "tail"},
1090 {"request_key": "tail_en", "abstract_type": "tail"},
1091 {"request_key": "editor_bio_fr", "abstract_type": "bio_fr"},
1092 {"request_key": "editor_bio_en", "abstract_type": "bio_en"},
1093 ]
1095 special_issue.abstracts = []
1096 abstracts_xml = []
1097 for request_data in request_datas:
1098 lang = request_data["request_key"][-2:]
1099 value_html = request.POST[request_data["request_key"]]
1101 ckeditor_data = build_jats_data_from_html_field(
1102 value_html,
1103 tag="abstract",
1104 text_lang=lang,
1105 resource_lang="en",
1106 field_type=request_data["abstract_type"],
1107 mml_formulas=[],
1108 issue_pid=colid,
1109 pid=special_issue.pid,
1110 )
1112 abstract_data = create_abstract(
1113 tag=request_data["abstract_type"],
1114 lang=lang,
1115 value_html=value_html,
1116 value_tex=ckeditor_data["value_tex"],
1117 value_xml=ckeditor_data["value_xml"],
1118 )
1120 special_issue.abstracts.append(abstract_data)
1121 abstracts_xml.append(ckeditor_data["value_xml"])
1123 figures = self.create_related_objects_from_abstract(
1124 abstracts_xml, colid, special_issue.pid
1125 )
1126 special_issue.related_objects = figures
1128 # This part handle pdf files included in special issue. Can be editor of full pdf version
1129 # Both are stored in same directory
1131 pdf_file_path = resolver.get_disk_location(
1132 f"{settings.RESOURCES_ROOT}",
1133 f"{collection.pid}",
1134 "pdf",
1135 special_issue_pid,
1136 article_id=None,
1137 do_create_folder=False,
1138 )
1139 pdf_path = os.path.dirname(pdf_file_path)
1140 if "pdf" in self.request.FILES: 1140 ↛ 1141line 1140 didn't jump to line 1141 because the condition on line 1140 was never true
1141 if os.path.isfile(f"{pdf_path}/{pid}.pdf"):
1142 os.remove(f"{pdf_path}/{pid}.pdf")
1143 if "edito" in self.request.FILES: 1143 ↛ 1144line 1143 didn't jump to line 1144 because the condition on line 1143 was never true
1144 if os.path.isfile(f"{pdf_path}/{pid}_edito.pdf"):
1145 os.remove(f"{pdf_path}/{pid}_edito.pdf")
1147 if request.POST["pdf_name"] != "No file uploaded": 1147 ↛ 1148line 1147 didn't jump to line 1148 because the condition on line 1147 was never true
1148 if "pdf" in self.request.FILES:
1149 pdf_file = request.FILES["pdf"]
1150 relative_file_name = resolver.copy_file_obj_to_article_folder(
1151 pdf_file,
1152 collection.pid,
1153 special_issue.pid,
1154 special_issue.pid,
1155 )
1156 pdf_file_name = self.request.FILES["pdf"].name
1158 else:
1159 pdf_file_name = request.POST["pdf_name"]
1160 relative_file_name = pdf_path + "/" + special_issue_pid + ".pdf"
1162 pdf_stream_data = create_datastream()
1163 pdf_stream_data["location"] = relative_file_name
1164 pdf_stream_data["mimetype"] = "application/pdf"
1165 pdf_stream_data["rel"] = "full-text"
1166 pdf_stream_data["text"] = pdf_file_name
1167 special_issue.streams.append(pdf_stream_data)
1169 if request.POST["edito_name"] != "No file uploaded": 1169 ↛ 1170line 1169 didn't jump to line 1170 because the condition on line 1169 was never true
1170 if "edito" in self.request.FILES:
1171 edito_file = self.request.FILES["edito"]
1172 relative_file_name = resolver.copy_file_obj_to_article_folder(
1173 edito_file,
1174 collection.pid,
1175 special_issue.pid,
1176 special_issue.pid,
1177 )
1179 edito_file_name = self.request.FILES["edito"].name
1180 edito_display_name = request.POST["edito_display_name"]
1181 else:
1182 relative_file_name = pdf_path + "/" + special_issue_pid + "_edito.pdf"
1183 edito_file_name = request.POST["edito_name"]
1184 edito_display_name = request.POST["edito_display_name"]
1186 data = {
1187 "rel": "edito",
1188 "mimetype": "application/pdf",
1189 "location": relative_file_name,
1190 "base": None,
1191 "metadata": edito_file_name + "$$$" + edito_display_name,
1192 }
1193 special_issue.related_objects.append(data)
1194 # Handle special issue icon. It is stored in same directory that pdf version or edito.
1195 # The icon is linked to special issue as an ExtLink
1196 if "icon" in request.FILES: 1196 ↛ 1197line 1196 didn't jump to line 1197 because the condition on line 1196 was never true
1197 icon_file = request.FILES["icon"]
1198 relative_file_name = resolver.copy_file_obj_to_article_folder(
1199 icon_file,
1200 collection.pid,
1201 special_issue.pid,
1202 special_issue.pid,
1203 )
1204 if ".tif" in relative_file_name:
1205 jpeg_path = ImageManager(
1206 os.path.join(settings.RESOURCES_ROOT, relative_file_name)
1207 ).to_jpeg()
1208 relative_file_name = str(jpeg_path).replace(settings.RESOURCES_ROOT + "/", "")
1209 data = {
1210 "rel": "icon",
1211 "location": relative_file_name,
1212 "base": None,
1213 "seq": 1,
1214 "metadata": "",
1215 }
1216 special_issue.ext_links.append(data)
1217 elif "icon" in request.POST.keys(): 1217 ↛ 1218line 1217 didn't jump to line 1218 because the condition on line 1217 was never true
1218 if request.POST["icon"] != "[object Object]":
1219 icon_file = request.POST["icon"].replace("/icon/", "")
1220 data = {
1221 "rel": "icon",
1222 "location": icon_file,
1223 "base": None,
1224 "seq": 1,
1225 "metadata": "",
1226 }
1227 special_issue.ext_links.append(data)
1229 special_issue = Munch(special_issue.__dict__)
1230 params = {"xissue": special_issue, "use_body": False}
1231 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params)
1232 cmd.do()
1233 return redirect("special_issue_edit_api", colid, special_issue.pid)
1235 def create_related_objects_from_abstract(self, abstracts, colid, pid):
1236 figures = []
1237 for abstract in abstracts:
1238 abstract_xml = abstract.encode("utf8")
1240 tree = etree.fromstring(abstract_xml)
1242 pics = tree.xpath("//graphic")
1243 for pic in pics: 1243 ↛ 1244line 1243 didn't jump to line 1244 because the loop on line 1243 never started
1244 base = None
1245 pic_location = pic.attrib["specific-use"]
1246 basename = os.path.basename(pic.attrib["href"])
1247 ext = basename.split(".")[-1]
1248 base = get_media_base_root(colid)
1249 data_location = os.path.join(
1250 settings.RESOURCES_ROOT, "media", base, "uploads", pic_location, basename
1251 )
1252 # we use related objects to send pics to journal site. Directory where pic is stored in trammel may differ
1253 # from the directory in journal site. So one need to save the pic in same directory that journal's one
1254 # so related objects can go for the correct one
1255 img = Image.open(data_location)
1256 final_data_location = os.path.join(
1257 settings.RESOURCES_ROOT, colid, pid, "src", "figures"
1258 )
1259 if not os.path.isdir(final_data_location):
1260 os.makedirs(final_data_location)
1261 relative_path = os.path.join(colid, pid, "src", "figures", basename)
1262 final_data_location = f"{final_data_location}/{basename}"
1263 img.save(final_data_location)
1264 if ext == "png":
1265 mimetype = "image/png"
1266 else:
1267 mimetype = "image/jpeg"
1268 data = {
1269 "rel": "html-image",
1270 "mimetype": mimetype,
1271 "location": relative_path,
1272 "base": base,
1273 "metadata": "",
1274 }
1275 if data not in figures:
1276 figures.append(data)
1277 return figures
1280class PageIndexView(EditorRequiredMixin, TemplateView):
1281 template_name = "mersenne_cms/page_index.html"
1283 def get_context_data(self, **kwargs):
1284 colid = kwargs.get("colid", "")
1285 site_id = model_helpers.get_site_id(colid)
1286 vi = Page.objects.filter(site_id=site_id, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES).first()
1287 if vi: 1287 ↛ 1288line 1287 didn't jump to line 1288 because the condition on line 1287 was never true
1288 pages = Page.objects.filter(site_id=site_id).exclude(parent_page=vi)
1289 else:
1290 pages = Page.objects.filter(site_id=site_id)
1291 context = super().get_context_data(**kwargs)
1292 context["colid"] = colid
1293 context["journal"] = model_helpers.get_collection(colid)
1294 context["pages"] = pages
1295 context["news"] = News.objects.filter(site_id=site_id)
1296 context["fields_lang"] = "fr" if model_helpers.is_site_fr_only(site_id) else "en"
1297 return context
1300class PageBaseView(HandleCMSMixin, View):
1301 template_name = "mersenne_cms/page_form.html"
1302 model = Page
1303 form_class = PageForm
1305 def dispatch(self, request, *args, **kwargs):
1306 self.colid = self.kwargs["colid"]
1307 self.collection = model_helpers.get_collection(self.colid, sites=False)
1308 self.site_id = model_helpers.get_site_id(self.colid)
1310 return super().dispatch(request, *args, **kwargs)
1312 def get_success_url(self):
1313 return reverse("page_index", kwargs={"colid": self.colid})
1315 def get_context_data(self, **kwargs):
1316 context = super().get_context_data(**kwargs)
1317 context["journal"] = self.collection
1318 return context
1320 def update_test_website(self):
1321 response = deploy_cms("test_website", self.collection)
1322 if response.status_code < 300: 1322 ↛ 1325line 1322 didn't jump to line 1325 because the condition on line 1322 was always true
1323 messages.success(self.request, "The test website has been updated")
1324 else:
1325 text = "ERROR: Unable to update the test website<br/>"
1327 if response.status_code == 503:
1328 text += "The test website is under maintenance. Please try again later.<br/>"
1329 else:
1330 text += f"Please contact the centre Mersenne<br/><br/>Status code: {response.status_code}<br/>"
1331 if hasattr(response, "content") and response.content:
1332 text += f"{response.content.decode()}<br/>"
1333 if hasattr(response, "reason") and response.reason:
1334 text += f"Reason: {response.reason}<br/>"
1335 if hasattr(response, "text") and response.text:
1336 text += f"Details: {response.text}<br/>"
1337 messages.error(self.request, mark_safe(text))
1339 def get_form_kwargs(self):
1340 kwargs = super().get_form_kwargs()
1341 kwargs["site_id"] = self.site_id
1342 kwargs["user"] = self.request.user
1343 return kwargs
1345 def form_valid(self, form):
1346 form.save()
1348 self.update_test_website()
1350 return HttpResponseRedirect(self.get_success_url())
1353# @method_decorator([csrf_exempt], name="dispatch")
1354class PageDeleteView(PageBaseView):
1355 def post(self, request, *args, **kwargs):
1356 colid = kwargs.get("colid", "")
1357 pk = kwargs.get("pk")
1358 page = get_object_or_404(Page, id=pk)
1359 if page.mersenne_id:
1360 raise PermissionDenied
1362 page.delete()
1364 self.update_test_website()
1366 if page.parent_page and page.parent_page.mersenne_id == MERSENNE_ID_VIRTUAL_ISSUES:
1367 return HttpResponseRedirect(reverse("virtual_issues_index", kwargs={"colid": colid}))
1368 else:
1369 return HttpResponseRedirect(reverse("page_index", kwargs={"colid": colid}))
1372class PageCreateView(PageBaseView, CreateView):
1373 def get_context_data(self, **kwargs):
1374 context = super().get_context_data(**kwargs)
1375 context["title"] = "Add a menu page"
1376 return context
1379class PageUpdateView(PageBaseView, UpdateView):
1380 def get_context_data(self, **kwargs):
1381 context = super().get_context_data(**kwargs)
1382 context["title"] = "Edit a menu page"
1383 return context
1386class NewsBaseView(PageBaseView):
1387 template_name = "mersenne_cms/news_form.html"
1388 model = News
1389 form_class = NewsForm
1392class NewsDeleteView(NewsBaseView):
1393 def post(self, request, *args, **kwargs):
1394 pk = kwargs.get("pk")
1395 news = get_object_or_404(News, id=pk)
1397 news.delete()
1399 self.update_test_website()
1401 return HttpResponseRedirect(self.get_success_url())
1404class NewsCreateView(NewsBaseView, CreateView):
1405 def get_context_data(self, **kwargs):
1406 context = super().get_context_data(**kwargs)
1407 context["title"] = "Add a News"
1408 return context
1411class NewsUpdateView(NewsBaseView, UpdateView):
1412 def get_context_data(self, **kwargs):
1413 context = super().get_context_data(**kwargs)
1414 context["title"] = "Edit a News"
1415 return context
1418# def page_create_view(request, colid):
1419# context = {}
1420# if not is_authorized_editor(request.user, colid):
1421# raise PermissionDenied
1422# collection = model_helpers.get_collection(colid)
1423# page = Page(site_id=model_helpers.get_site_id(colid))
1424# form = PageForm(request.POST or None, instance=page)
1425# if form.is_valid():
1426# form.save()
1427# response = deploy_cms("test_website", collection)
1428# if response.status_code < 300:
1429# messages.success(request, "Page created successfully.")
1430# else:
1431# text = f"ERROR: page creation failed<br/>Status code: {response.status_code}<br/>"
1432# if hasattr(response, "reason") and response.reason:
1433# text += f"Reason: {response.reason}<br/>"
1434# if hasattr(response, "text") and response.text:
1435# text += f"Details: {response.text}<br/>"
1436# messages.error(request, mark_safe(text))
1437# kwargs = {"colid": colid, "pid": form.instance.id}
1438# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1439#
1440# context["form"] = form
1441# context["title"] = "Add a menu page"
1442# context["journal"] = collection
1443# return render(request, "mersenne_cms/page_form.html", context)
1446# def page_update_view(request, colid, pid):
1447# context = {}
1448# if not is_authorized_editor(request.user, colid):
1449# raise PermissionDenied
1450#
1451# collection = model_helpers.get_collection(colid)
1452# page = get_object_or_404(Page, id=pid)
1453# form = PageForm(request.POST or None, instance=page)
1454# if form.is_valid():
1455# form.save()
1456# response = deploy_cms("test_website", collection)
1457# if response.status_code < 300:
1458# messages.success(request, "Page updated successfully.")
1459# else:
1460# text = f"ERROR: page update failed<br/>Status code: {response.status_code}<br/>"
1461# if hasattr(response, "reason") and response.reason:
1462# text += f"Reason: {response.reason}<br/>"
1463# if hasattr(response, "text") and response.text:
1464# text += f"Details: {response.text}<br/>"
1465# messages.error(request, mark_safe(text))
1466# kwargs = {"colid": colid, "pid": form.instance.id}
1467# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1468#
1469# context["form"] = form
1470# context["pid"] = pid
1471# context["title"] = "Edit a menu page"
1472# context["journal"] = collection
1473# return render(request, "mersenne_cms/page_form.html", context)