Coverage for src / ptf_tools / views / cms_views.py: 49%
878 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-08-03 13:45 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-08-03 13:45 +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"]
171 change_ckeditor_storage(colid)
173 return super().dispatch(request, **kwargs)
176class CollectionBrowseView(EditorRequiredMixin, View):
177 def dispatch(self, request, **kwargs):
178 colid = kwargs["colid"]
180 change_ckeditor_storage(colid)
182 return browse(request)
185file_upload_in_collection = csrf_exempt(CollectionImageUploadView.as_view())
186file_browse_in_collection = csrf_exempt(CollectionBrowseView.as_view())
189def deploy_cms(site, collection):
190 colid = collection.pid
191 base_url = getattr(collection, site)()
193 if base_url is None: 193 ↛ 196line 193 didn't jump to line 196 because the condition on line 193 was always true
194 return JsonResponse({"message": "OK"})
196 if site == "website":
197 from_base_path = get_media_base_root_in_test(colid)
198 to_base_path = get_media_base_root_in_prod(colid)
200 for sub_path in ["uploads", "images"]:
201 from_path = os.path.join(from_base_path, sub_path)
202 to_path = os.path.join(to_base_path, sub_path)
203 if os.path.exists(from_path):
204 try:
205 shutil.copytree(from_path, to_path, dirs_exist_ok=True)
206 except OSError as exception:
207 return HttpResponseServerError(f"Error during copy: {exception}")
209 site_id = model_helpers.get_site_id(colid)
210 if model_helpers.get_site_default_language(site_id):
211 from modeltranslation import fields, manager
213 old_ftor = manager.get_language
214 manager.get_language = monkey_get_language_en
215 fields.get_language = monkey_get_language_en
217 pages = get_pages_content(colid)
218 news = get_news_content(colid)
220 manager.get_language = old_ftor
221 fields.get_language = old_ftor
222 else:
223 pages = get_pages_content(colid)
224 news = get_news_content(colid)
226 data = json.dumps({"pages": json.loads(pages), "news": json.loads(news)})
227 url = getattr(collection, site)() + "/import_cms/"
229 try:
230 response = requests.put(url, data=data, verify=False)
232 if response.status_code == 503:
233 e = ServerUnderMaintenance(
234 "The journal test website is under maintenance. Please try again later."
235 )
236 return HttpResponseServerError(e, status=503)
238 except Timeout as exception:
239 return HttpResponse(exception, status=408)
240 except Exception as exception:
241 return HttpResponseServerError(exception)
243 return JsonResponse({"message": "OK"})
246class HandleCMSMixin(EditorRequiredMixin):
247 """
248 Mixin for classes that need to send request to (test) website to import/export CMS content (pages, news)
249 """
251 # def dispatch(self, request, *args, **kwargs):
252 # self.colid = self.kwargs["colid"]
253 # return super().dispatch(request, *args, **kwargs)
255 def init_data(self, kwargs):
256 self.collection = None
258 self.colid = kwargs.get("colid", None)
259 if self.colid:
260 self.collection = model_helpers.get_collection(self.colid)
261 if not self.collection:
262 raise Http404(f"{self.colid} does not exist")
264 test_server_url = self.collection.test_website()
265 if not test_server_url:
266 raise Http404("The collection has no test site")
268 prod_server_url = self.collection.website()
269 if not prod_server_url:
270 raise Http404("The collection has no prod site")
273class GetCMSFromSiteAPIView(HandleCMSMixin, View):
274 """
275 Get the CMS content from the (test) website and save it on disk.
276 It can be used if needed to restore the Trammel content with RestoreCMSAPIView below
277 """
279 def get(self, request, *args, **kwargs):
280 self.init_data(self.kwargs)
282 site = kwargs.get("site", "test_website")
284 try:
285 url = getattr(self.collection, site)() + "/export_cms/"
286 response = requests.get(url, verify=False)
288 # Just to need to save the json on disk
289 # Media files are already saved in MEDIA_ROOT which is equal to
290 # /mersenne_test_data/@colid/media
291 folder = get_media_base_root(self.colid)
292 os.makedirs(folder, exist_ok=True)
293 filename = os.path.join(folder, f"pages_{self.colid}.json")
294 with open(filename, mode="w", encoding="utf-8") as file:
295 file.write(response.content.decode(encoding="utf-8"))
297 except Timeout as exception:
298 return HttpResponse(exception, status=408)
299 except Exception as exception:
300 return HttpResponseServerError(exception)
302 return JsonResponse({"message": "OK", "status": 200})
305def monkey_get_language_en():
306 return "en"
309class RestoreCMSAPIView(HandleCMSMixin, View):
310 """
311 Restore the Trammel CMS content (of a colid) from disk
312 """
314 def get(self, request, *args, **kwargs):
315 self.init_data(self.kwargs)
317 folder = get_media_base_root(self.colid)
318 filename = os.path.join(folder, f"pages_{self.colid}.json")
319 with open(filename, encoding="utf-8") as f:
320 json_data = json.load(f)
322 pages = json_data.get("pages")
324 site_id = model_helpers.get_site_id(self.colid)
325 if model_helpers.get_site_default_language(site_id):
326 from modeltranslation import fields, manager
328 old_ftor = manager.get_language
329 manager.get_language = monkey_get_language_en
330 fields.get_language = monkey_get_language_en
332 import_pages(pages, self.colid)
334 manager.get_language = old_ftor
335 fields.get_language = old_ftor
336 else:
337 import_pages(pages, self.colid)
339 if "news" in json_data:
340 news = json_data.get("news")
341 import_news(news, self.colid)
343 return JsonResponse({"message": "OK", "status": 200})
346class DeployCMSAPIView(HandleCMSMixin, View):
347 def get(self, request, *args, **kwargs):
348 self.init_data(self.kwargs)
350 if check_lock():
351 msg = "Trammel is under maintenance. Please try again later."
352 messages.error(self.request, msg)
353 return JsonResponse({"messages": msg, "status": 503})
355 site = kwargs.get("site", "test_website")
357 response = deploy_cms(site, self.collection)
359 if response.status_code == 503:
360 messages.error(
361 self.request, "The journal website is under maintenance. Please try again later."
362 )
364 return response
367def get_server_urls(collection, site="test_website"):
368 urls = [""]
369 if hasattr(settings, "MERSENNE_DEV_URL"): 369 ↛ 371line 369 didn't jump to line 371 because the condition on line 369 was never true
370 # set RESOURCES_ROOT and apache config accordingly (for instance with "/mersenne_dev_data")
371 url = getattr(collection, "test_website")().split(".fr")
372 urls = [settings.MERSENNE_DEV_URL + url[1] if len(url) == 2 else ""]
373 elif site == "both": 373 ↛ 374line 373 didn't jump to line 374 because the condition on line 373 was never true
374 urls = [getattr(collection, "test_website")(), getattr(collection, "website")()]
375 elif hasattr(collection, site) and getattr(collection, site)(): 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true
376 urls = [getattr(collection, site)()]
377 return urls
380class SuggestDeployView(EditorRequiredMixin, View):
381 def post(self, request, *args, **kwargs):
382 doi = kwargs.get("doi", "")
383 site = kwargs.get("site", "test_website")
384 article = get_object_or_404(Article, doi=doi)
386 obj, created = RelatedArticles.objects.get_or_create(resource=article)
387 form = RelatedForm(request.POST or None, instance=obj)
388 if form.is_valid(): 388 ↛ 405line 388 didn't jump to line 405 because the condition on line 388 was always true
389 data = form.cleaned_data
390 obj.date_modified = timezone.now()
391 form.save()
392 collection = article.my_container.my_collection
393 urls = get_server_urls(collection, site=site)
394 response = requests.models.Response()
395 for url in urls: 395 ↛ 403line 395 didn't jump to line 403 because the loop on line 395 didn't complete
396 url = url + reverse("api-update-suggest", kwargs={"doi": doi})
397 try:
398 response = requests.post(url, data=data, timeout=15)
399 except requests.exceptions.RequestException as e:
400 response.status_code = 503
401 response.reason = e.args[0]
402 break
403 return HttpResponse(status=response.status_code, reason=response.reason)
404 else:
405 return HttpResponseBadRequest()
408def suggest_debug(results, article, message):
409 crop_results = 5
410 if results: 410 ↛ 411line 410 didn't jump to line 411 because the condition on line 410 was never true
411 dois = []
412 results["docs"] = results["docs"][:crop_results]
413 numFound = f"({len(results['docs'])} sur {results['numFound']} documents)"
414 head = f"Résultats de la recherche automatique {numFound} :\n\n"
415 for item in results["docs"]:
416 doi = item.get("doi")
417 if doi:
418 explain = results["explain"][item["id"]]
419 terms = re.findall(r"([0-9.]+?) = weight\((.+?:.+?) in", explain)
420 terms.sort(key=lambda t: t[0], reverse=True)
421 details = (" + ").join(f"{round(float(s), 1)}:{t}" for s, t in terms)
422 score = f"Score : {round(float(item['score']), 1)} (= {details})\n"
423 url = ""
424 suggest = Article.objects.filter(doi=doi).first()
425 if suggest and suggest.my_container:
426 collection = suggest.my_container.my_collection
427 base_url = collection.website() or ""
428 url = base_url + "/articles/" + doi
429 dois.append((doi, url, score))
431 tail = f"\n\nScore minimum retenu : {results['params']['min_score']}\n\n\n"
432 tail += "Termes principaux utilisés pour la requête "
433 tail = [tail + "(champ:terme recherché | pertinence du terme) :\n"]
434 if results["params"]["mlt.fl"] == "all":
435 tail.append(" * all = body + abstract + title + authors + keywords\n")
436 terms = results["interestingTerms"]
437 terms = [" | ".join((x[0], str(x[1]))) for x in zip(terms[::2], terms[1::2])]
438 tail.extend(reversed(terms))
439 tail.append("\n\nParamètres de la requête :\n")
440 tail.extend([f"{k}: {v} " for k, v in results["params"].items()])
441 return [(head, dois, "\n".join(tail))]
442 else:
443 msg = f"Erreur {message['status']} {message['err']} at {message['url']}"
444 return [(msg, [], "")]
447class SuggestUpdateView(EditorRequiredMixin, TemplateView):
448 template_name = "editorial_tools/suggested.html"
450 def get_context_data(self, **kwargs):
451 doi = kwargs.get("doi", "")
452 article = get_object_or_404(Article, doi=doi)
454 obj, created = RelatedArticles.objects.get_or_create(resource=article)
455 collection = article.my_container.my_collection
456 base_url = collection.website() or ""
457 response = requests.models.Response()
458 try:
459 response = requests.get(base_url + "/mlt/" + doi, timeout=10.0)
460 except requests.exceptions.RequestException as e:
461 response.status_code = 503
462 response.reason = e.args[0]
463 msg = {
464 "url": response.url,
465 "status": response.status_code,
466 "err": response.reason,
467 }
468 results = None
469 if response.status_code == 200: 469 ↛ 470line 469 didn't jump to line 470 because the condition on line 469 was never true
470 results = solr_cmds.auto_suggest_doi(obj, article, response.json())
471 context = super().get_context_data(**kwargs)
472 context["debug"] = suggest_debug(results, article, msg)
473 context["form"] = RelatedForm(instance=obj)
474 context["author"] = "; ".join(get_names(article, "author"))
475 context["citation_base"] = article.get_citation_base().strip(", .")
476 context["article"] = article
477 context["date_modified"] = obj.date_modified
478 context["url"] = base_url + "/articles/" + doi
479 return context
482class EditorialToolsVolumeItemsView(EditorRequiredMixin, TemplateView):
483 template_name = "editorial_tools/volume-items.html"
485 def get_context_data(self, **kwargs):
486 vid = kwargs.get("vid")
487 issues_articles, collection = model_helpers.get_issues_in_volume(vid)
488 context = super().get_context_data(**kwargs)
489 context["issues_articles"] = issues_articles
490 context["collection"] = collection
491 return context
494class EditorialToolsArticleView(EditorRequiredMixin, TemplateView):
495 template_name = "editorial_tools/find-article.html"
497 def get_context_data(self, **kwargs):
498 colid = kwargs.get("colid")
499 doi = kwargs.get("doi")
500 article = get_object_or_404(Article, doi=doi, my_container__my_collection__pid=colid)
502 context = super().get_context_data(**kwargs)
503 context["article"] = article
504 context["citation_base"] = article.get_citation_base().strip(", .")
505 return context
508class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView):
509 template_name = "editorial_tools/graphical-abstract.html"
511 def get_context_data(self, **kwargs):
512 doi = kwargs.get("doi", "")
513 article = get_object_or_404(Article, doi=doi)
515 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
516 context = super().get_context_data(**kwargs)
517 context["author"] = "; ".join(get_names(article, "author"))
518 context["citation_base"] = article.get_citation_base().strip(", .")
519 context["article"] = article
520 context["date_modified"] = obj.date_modified
521 context["form"] = GraphicalAbstractForm(instance=obj)
522 context["graphical_abstract"] = obj.graphical_abstract
523 context["illustration"] = obj.illustration
524 return context
527class GraphicalAbstractDeployView(EditorRequiredMixin, View):
528 def __get_path_and_replace_tiff_file(self, obj_attribute_file):
529 """
530 Returns the path of the attribute.
531 If the attribute is a tiff file, converts it to jpg, delete the old tiff file, and return the new path of the attribute.
533 Checks if paths have already been processed, to prevent issues related to object mutation.
534 """
535 if obj_attribute_file.name.lower().endswith((".tiff", ".tif")):
536 jpeg_path = ImageManager(obj_attribute_file.path).to_jpeg(delete_original_tiff=True)
537 with open(jpeg_path, "rb") as fp:
538 obj_attribute_file.save(os.path.basename(jpeg_path), File(fp), save=True)
539 return jpeg_path
541 return obj_attribute_file.path
543 def post(self, request, *args, **kwargs):
544 doi = kwargs.get("doi", "")
545 site = kwargs.get("site", "both")
546 article = get_object_or_404(Article, doi=doi)
548 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
549 form = GraphicalAbstractForm(request.POST, request.FILES or None, instance=obj)
550 if form.is_valid(): 550 ↛ 579line 550 didn't jump to line 579 because the condition on line 550 was always true
551 obj.date_modified = timezone.now()
552 data = {"date_modified": obj.date_modified}
553 form.save()
554 files = {}
556 for attribute in ("graphical_abstract", "illustration"):
557 obj_attribute_file = getattr(obj, attribute, None)
558 if obj_attribute_file and os.path.exists(obj_attribute_file.path): 558 ↛ 559line 558 didn't jump to line 559 because the condition on line 558 was never true
559 file_path = self.__get_path_and_replace_tiff_file(obj_attribute_file)
560 with open(file_path, "rb") as fp:
561 files.update({attribute: (obj_attribute_file.name, fp.read())})
563 collection = article.my_container.my_collection
564 urls = get_server_urls(collection, site=site)
565 response = requests.models.Response()
566 for url in urls: 566 ↛ 577line 566 didn't jump to line 577 because the loop on line 566 didn't complete
567 url = url + reverse("api-graphical-abstract", kwargs={"doi": doi})
568 try:
569 if not obj.graphical_abstract and not obj.illustration: 569 ↛ 572line 569 didn't jump to line 572 because the condition on line 569 was always true
570 response = requests.delete(url, data=data, files=files, timeout=15)
571 else:
572 response = requests.post(url, data=data, files=files, timeout=15)
573 except requests.exceptions.RequestException as e:
574 response.status_code = 503
575 response.reason = e.args[0]
576 break
577 return HttpResponse(status=response.status_code, reason=response.reason)
578 else:
579 return HttpResponseBadRequest()
582def parse_content(content):
583 table = re.search(r'(.*?)(<table id="summary".+?</table>)(.*)', content, re.DOTALL)
584 if not table:
585 return {"head": content, "tail": "", "articles": []}
587 articles = []
588 rows = re.findall(r"<tr>.+?</tr>", table.group(2), re.DOTALL)
589 for row in rows:
590 citation = re.search(r'<div href=".*?">(.*?)</div>', row, re.DOTALL)
591 href = re.search(r'href="(.+?)\/?">', row)
592 doi = re.search(r"(10[.].+)", href.group(1)) if href else ""
593 src = re.search(r'<img.+?src="(.+?)"', row)
594 item = {}
595 item["citation"] = citation.group(1) if citation else ""
596 item["doi"] = doi.group(1) if doi else href.group(1) if href else ""
597 item["src"] = src.group(1) if src else ""
598 item["imageName"] = item["src"].split("/")[-1] if item["src"] else ""
599 if item["doi"] or item["src"]:
600 articles.append(item)
601 return {"head": table.group(1), "tail": table.group(3), "articles": articles}
604class VirtualIssueParseView(EditorRequiredMixin, View):
605 def get(self, request, *args, **kwargs):
606 pid = kwargs.get("pid", "")
607 page = get_object_or_404(Page, id=pid)
609 data = {"pid": pid}
610 data["colid"] = kwargs.get("colid", "")
611 journal = model_helpers.get_collection(data["colid"])
612 data["journal_title"] = journal.title_tex.replace(".", "")
613 site_id = model_helpers.get_site_id(data["colid"])
614 data["page"] = model_to_dict(page)
615 pages = Page.objects.filter(site_id=site_id).exclude(id=pid)
616 data["parents"] = [model_to_dict(p, fields=["id", "menu_title"]) for p in pages]
618 content_fr = parse_content(page.content_fr)
619 data["head_fr"] = content_fr["head"]
620 data["tail_fr"] = content_fr["tail"]
622 content_en = parse_content(page.content_en)
623 data["articles"] = content_en["articles"]
624 data["head_en"] = content_en["head"]
625 data["tail_en"] = content_en["tail"]
626 return JsonResponse(data)
629class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView):
630 template_name = "editorial_tools/virtual-issue.html"
632 def get(self, request, *args, **kwargs):
633 pid = kwargs.get("pid", "")
634 get_object_or_404(Page, id=pid)
635 return super().get(request, *args, **kwargs)
638class VirtualIssueCreateView(EditorRequiredMixin, View):
639 def get(self, request, *args, **kwargs):
640 colid = kwargs.get("colid", "")
641 site_id = model_helpers.get_site_id(colid)
642 parent, _ = Page.objects.get_or_create(
643 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES,
644 parent_page=None,
645 site_id=site_id,
646 )
647 page = Page.objects.create(
648 menu_title_en="New virtual issue",
649 menu_title_fr="Nouvelle collection transverse",
650 parent_page=parent,
651 site_id=site_id,
652 state="draft",
653 )
654 kwargs = {"colid": colid, "pid": page.id}
655 return HttpResponseRedirect(reverse("virtual_issue_update", kwargs=kwargs))
658class SpecialIssuesIndex(EditorRequiredMixin, TemplateView):
659 template_name = "editorial_tools/special-issues-index.html"
661 def get_context_data(self, **kwargs):
662 colid = kwargs.get("colid", "")
664 context = super().get_context_data(**kwargs)
665 context["colid"] = colid
666 collection = Collection.objects.get(pid=colid)
667 context["special_issues"] = Container.objects.filter(
668 Q(ctype="issue_special") | Q(ctype="issue_special_img")
669 ).filter(my_collection=collection)
671 context["journal"] = model_helpers.get_collection(colid, sites=False)
672 return context
675class SpecialIssueEditView(EditorRequiredMixin, TemplateView):
676 template_name = "editorial_tools/special-issue-edit.html"
678 def get_context_data(self, **kwargs):
679 context = super().get_context_data(**kwargs)
680 return context
683class VirtualIssuesIndex(EditorRequiredMixin, TemplateView):
684 template_name = "editorial_tools/virtual-issues-index.html"
686 def get_context_data(self, **kwargs):
687 colid = kwargs.get("colid", "")
688 site_id = model_helpers.get_site_id(colid)
689 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES)
690 pages = Page.objects.filter(site_id=site_id, parent_page=vi)
691 context = super().get_context_data(**kwargs)
692 context["journal"] = model_helpers.get_collection(colid)
693 context["pages"] = pages
694 return context
697def get_citation_fr(doi, citation_en):
698 citation_fr = citation_en
699 article = Article.objects.filter(doi=doi).first()
700 if article and article.trans_title_html:
701 trans_title = article.trans_title_html
702 try:
703 citation_fr = re.sub(
704 r'(<a href="https:\/\/doi\.org.*">)([^<]+)',
705 rf"\1{trans_title}",
706 citation_en,
707 )
708 except re.error:
709 pass
710 return citation_fr
713def summary_build(articles, colid):
714 summary_fr = ""
715 summary_en = ""
716 head = '<table id="summary"><tbody>'
717 tail = "</tbody></table>"
718 style = "max-width:180px;max-height:200px"
719 colid_lo = colid.lower()
720 site_domain = SITE_REGISTER[colid_lo]["site_domain"].split("/")
721 site_domain = "/" + site_domain[-1] if len(site_domain) == 2 else ""
723 for article in articles:
724 image_src = article.get("src", "")
725 image_name = article.get("imageName", "")
726 doi = article.get("doi", "")
727 citation_en = article.get("citation", "")
728 if doi or citation_en:
729 row_fr = f'<div href="{doi}">{get_citation_fr(doi, citation_en)}</div>'
730 row_en = f'<div href="{doi}">{citation_en}</div>'
731 if image_src:
732 date = datetime.now().strftime("%Y/%m/%d/")
733 base_url = get_media_base_url(colid)
734 suffix = os.path.join(base_url, "uploads", date)
735 image_url = os.path.join(site_domain, suffix, image_name)
736 image_header = "^data:image/.+;base64,"
737 if re.match(image_header, image_src):
738 image_src = re.sub(image_header, "", image_src)
739 base64_data = base64.b64decode(image_src)
740 base_root = get_media_base_root(colid)
741 path = os.path.join(base_root, "uploads", date)
742 os.makedirs(path, exist_ok=True)
743 with open(path + image_name, "wb") as fp:
744 fp.write(base64_data)
745 im = f'<img src="{image_url}" style="{style}" />'
746 # TODO mettre la vrai valeur pour le SITE_DOMAIN
747 elif settings.SITE_DOMAIN == "http://127.0.0.1:8002":
748 im = f'<img src="{image_src}" style="{style}" />'
749 else:
750 im = f'<img src="{site_domain}{image_src}" style="{style}" />'
751 summary_fr += f"<tr><td>{im}</td><td>{row_fr}</td></tr>"
752 summary_en += f"<tr><td>{im}</td><td>{row_en}</td></tr>"
753 summary_fr = head + summary_fr + tail
754 summary_en = head + summary_en + tail
755 return {"summary_fr": summary_fr, "summary_en": summary_en}
758# @method_decorator([csrf_exempt], name="dispatch")
759class VirtualIssueDeployView(HandleCMSMixin, View):
760 """
761 called by the Virtual.vue VueJS component, when the virtual issue is saved
762 We get data in JSON and we need to update the corresponding Page.
763 The Page is then immediately posted to the test_website.
764 The "Apply changes to production website" button is then used to update the (prod) website
765 => See DeployCMSAPIView
766 """
768 def post(self, request, *args, **kwargs):
769 self.init_data(self.kwargs)
770 if check_lock():
771 msg = "Trammel is under maintenance. Please try again later."
772 messages.error(self.request, msg)
773 return JsonResponse({"messages": msg, "status": 503})
775 pid = kwargs.get("pid")
776 colid = self.colid
777 data = json.loads(request.body)
778 summary = summary_build(data["articles"], colid)
779 page = get_object_or_404(Page, id=pid)
780 page.slug = page.slug_fr = page.slug_en = None
781 page.menu_title_fr = data["title_fr"]
782 page.menu_title_en = data["title_en"]
783 page.content_fr = data["head_fr"] + summary["summary_fr"] + data["tail_fr"]
784 page.content_en = data["head_en"] + summary["summary_en"] + data["tail_en"]
785 page.state = data["page"]["state"]
786 page.menu_order = data["page"]["menu_order"]
787 page.parent_page = Page.objects.filter(id=data["page"]["parent_page"]).first()
788 page.save()
790 response = deploy_cms("test_website", self.collection)
791 if response.status_code == 503:
792 messages.error(
793 self.request, "The journal website is under maintenance. Please try again later."
794 )
796 return response # HttpResponse(status=response.status_code, reason=response.reason)
799class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView):
800 template_name = "editorial_tools/special-issue-edit.html"
802 def get_context_data(self, **kwargs):
803 context = super().get_context_data(**kwargs)
804 return context
806 def set_contrib_addresses(self, contrib, contribution):
807 for address in contrib:
808 contrib_address = ContribAddress(contribution=contribution, address=address)
809 contrib_address.save()
811 def delete(self, pid):
812 special_issue = Container.objects.get(pid=pid)
813 cmd = base_ptf_cmds.addContainerPtfCmd()
814 cmd.set_object_to_be_deleted(special_issue)
815 cmd.undo()
817 def get(self, request, *args, **kwargs):
818 pid = kwargs.get("pid", "")
820 data = {"pid": pid}
821 colid = kwargs.get("colid", "")
822 data["colid"] = colid
823 journal = model_helpers.get_collection(colid, sites=False)
824 name = resolve(request.path_info).url_name
825 if name == "special_issue_delete": 825 ↛ 826line 825 didn't jump to line 826 because the condition on line 825 was never true
826 self.delete(pid)
827 return redirect("special_issues_index", data["colid"])
829 data["journal_title"] = journal.title_tex.replace(".", "")
831 if pid != "create":
832 container = get_object_or_404(Container, pid=pid)
833 # TODO: pass the lang and trans_lang as well
834 # In VueJS (Special.vu)e, titleFr = title_html
835 # June 2025: Title objects are added for translated titles
836 # keep using trans_title_html for backward compatibility
837 translated_title = container.title_set.all().filter(lang="fr", type="main").first()
838 if translated_title: 838 ↛ 840line 838 didn't jump to line 840 because the condition on line 838 was always true
839 data["title"] = translated_title.title_html
840 data["doi"] = container.doi
841 data["trans_title"] = container.title_html
842 data["year"] = container.year_str
843 data["volume"] = container.volume
844 data["articles"] = [
845 {"doi": article.resource_doi, "citation": article.citation}
846 for article in container.resources_in_special_issue.all().order_by("seq")
847 ]
848 if container.ctype == "issue_special_img": 848 ↛ 849line 848 didn't jump to line 849 because the condition on line 848 was never true
849 data["use_resources_icon"] = True
850 else:
851 data["use_resources_icon"] = False
853 contribs = model_data_converter.db_to_contributors(container.contributions)
854 data["contribs"] = contribs
855 abstract_set = container.abstract_set.all()
856 data["head_fr"] = (
857 abstract_set.filter(tag="intro", lang="fr").first().value_html
858 if abstract_set.filter(tag="intro", lang="fr").exists()
859 else ""
860 )
861 data["head_en"] = (
862 abstract_set.filter(tag="intro", lang="en").first().value_html
863 if abstract_set.filter(tag="intro", lang="en").exists()
864 else ""
865 )
866 data["tail_fr"] = (
867 abstract_set.filter(tag="tail", lang="fr").first().value_html
868 if abstract_set.filter(tag="tail", lang="fr").exists()
869 else ""
870 )
871 data["tail_en"] = (
872 abstract_set.filter(tag="tail", lang="en").first().value_html
873 if abstract_set.filter(tag="tail", lang="en").exists()
874 else ""
875 )
876 data["editor_bio_en"] = (
877 abstract_set.filter(tag="bio_en").first().value_html
878 if abstract_set.filter(tag="bio_en").exists()
879 else ""
880 )
881 data["editor_bio_fr"] = (
882 abstract_set.filter(tag="bio_fr").first().value_html
883 if abstract_set.filter(tag="bio_fr").exists()
884 else ""
885 )
887 streams = container.datastream_set.all()
888 data["pdf_file_name"] = ""
889 data["edito_file_name"] = ""
890 data["edito_display_name"] = ""
891 for stream in streams: # don't work 891 ↛ 892line 891 didn't jump to line 892 because the loop on line 891 never started
892 if os.path.basename(stream.location).split(".")[0] == data["pid"]:
893 data["pdf_file_name"] = stream.text
894 try:
895 # edito related objects metadata contains both file real name and displayed name in issue summary
896 edito_name_infos = container.relatedobject_set.get(rel="edito").metadata.split(
897 "$$$"
898 )
899 data["edito_file_name"] = edito_name_infos[0]
900 data["edito_display_name"] = edito_name_infos[1]
902 except RelatedObject.DoesNotExist:
903 pass
904 try:
905 container_icon = container.extlink_set.get(rel="icon")
907 data["icon_location"] = container_icon.location
908 except ExtLink.DoesNotExist:
909 data["icon_location"] = ""
910 # try:
911 # special_issue_icon = container.extlink_set.get(rel="icon")
912 # data["special_issue_icon"] = special_issue_icon.location
913 # except ExtLink.DoesNotExist:
914 # data["special_issue_icon"] = None
916 else:
917 data["title"] = ""
918 data["doi"] = None
919 data["trans_title"] = ""
920 data["year"] = ""
921 data["volume"] = ""
922 data["articles"] = []
923 data["contribs"] = []
925 data["head_fr"] = ""
926 data["head_en"] = ""
927 data["tail_fr"] = ""
928 data["tail_en"] = ""
929 data["editor_bio_en"] = ""
930 data["editor_bio_fr"] = ""
931 data["pdf_file_name"] = ""
932 data["edito_file_name"] = ""
933 data["use_resources_icon"] = False
935 return JsonResponse(data)
937 def post(self, request, *args, **kwargs):
938 # le but est de faire un IssueDAta
939 pid = kwargs.get("pid", "")
940 colid = kwargs.get("colid", "")
941 journal = collection = model_helpers.get_collection(colid, sites=False)
942 special_issue = create_issuedata()
943 year_str = request.POST["year"]
944 years = year_str.split("-")
945 if len(years) > 1: 945 ↛ 946line 945 didn't jump to line 946 because the condition on line 945 was never true
946 fyear, lyear = int(years[0]), int(years[1])
947 else:
948 fyear, lyear = int(years[0]), None
949 # TODO 1: the values should be the tex values, not the html ones
950 # TODO 2: In VueJS, titleFr = title
951 trans_title_html = request.POST["title"]
952 title_html = request.POST["trans_title"]
953 issues = collection.content.all().order_by("-fyear")
954 same_year_issues = issues.filter(fyear=fyear)
955 if same_year_issues.exists(): 955 ↛ 957line 955 didn't jump to line 957 because the condition on line 955 was always true
956 volume = same_year_issues.first().volume
957 elif issues.exists() and colid != "HOUCHES": # because we don't want a volume for houches
958 ref_volume = issues.filter(fyear=2024).first().volume
959 volume = int(ref_volume) + (
960 fyear - 2024
961 ) # 2024 is the ref year for wich we know the volume is 347
962 else:
963 volume = ""
964 if pid != "create":
965 # TODO: do not use the pk, but the pid in the URLs
966 container = get_object_or_404(Container, pid=pid)
967 lang = container.lang
968 trans_title = container.title_set.all().filter(type="main").first()
969 if not trans_title: 969 ↛ 970line 969 didn't jump to line 970 because the condition on line 969 was never true
970 raise ValueError(
971 "Cannot find trans_lang: Container does not have a main title translation"
972 )
973 trans_lang = trans_title.lang
974 xpub = create_publisherdata()
975 xpub.name = container.my_publisher.pub_name
976 special_issue.provider = container.provider
977 special_issue.number = container.number
978 special_issue_pid = pid
979 special_issue.date_pre_published = container.date_pre_published
980 special_issue.date_published = container.date_published
981 # used for first special issues created withou a proper doi
982 # can be remove when no doi's less special issue existe
983 if not container.doi: 983 ↛ 984line 983 didn't jump to line 984 because the condition on line 983 was never true
984 special_issue.doi = model_helpers.assign_container_doi(colid)
985 else:
986 special_issue.doi = container.doi
987 else:
988 lang = "en"
989 container = None
990 trans_lang = "fr"
991 xpub = create_publisherdata()
992 special_issue.doi = model_helpers.assign_container_doi(colid)
994 if colid == "HOUCHES": 994 ↛ 995line 994 didn't jump to line 995 because the condition on line 994 was never true
995 xpub.name = "UGA Éditions"
996 else:
997 xpub.name = issues.first().my_publisher.pub_name
998 special_issue.provider = collection.provider
1000 special_issues = issues.filter(fyear=fyear).filter(
1001 Q(ctype="issue_special") | Q(ctype="issue") | Q(ctype="issue_special_img")
1002 )
1003 if special_issues: 1003 ↛ 1013line 1003 didn't jump to line 1013 because the condition on line 1003 was always true
1004 all_special_issues_numbers = [
1005 int(si.number[1:]) for si in special_issues if si.number[1:].isnumeric()
1006 ]
1007 if len(all_special_issues_numbers) > 0:
1008 max_number = max(all_special_issues_numbers)
1009 else:
1010 max_number = 0
1012 else:
1013 max_number = 0
1014 special_issue.number = f"S{max_number + 1}"
1015 special_issue_pid = f"{colid}_{year_str}__{volume}_{special_issue.number}"
1017 if request.POST["use_resources_icon"] == "true": 1017 ↛ 1018line 1017 didn't jump to line 1018 because the condition on line 1017 was never true
1018 special_issue.ctype = "issue_special_img"
1019 else:
1020 special_issue.ctype = "issue_special"
1022 existing_issue = model_helpers.get_resource(special_issue_pid)
1023 if pid == "create" and existing_issue is not None: 1023 ↛ 1024line 1023 didn't jump to line 1024 because the condition on line 1023 was never true
1024 raise ValueError(f"The special issue with the pid {special_issue_pid} already exists")
1026 special_issue.lang = lang
1027 special_issue.title_html = title_html
1028 special_issue.title_xml = build_title_xml(
1029 title=title_html, lang=lang, title_type="issue-title"
1030 )
1032 title_xml = build_title_xml(
1033 title=trans_title_html, lang=trans_lang, title_type="issue-title"
1034 )
1035 title = create_titledata(
1036 lang=trans_lang, type="main", title_html=trans_title_html, title_xml=title_xml
1037 )
1038 special_issue.titles = [title]
1040 special_issue.fyear = fyear
1041 special_issue.lyear = lyear
1042 special_issue.volume = volume
1043 special_issue.journal = journal
1044 special_issue.publisher = xpub
1045 special_issue.pid = special_issue_pid
1046 special_issue.last_modified_iso_8601_date_str = datetime.now().strftime(
1047 "%Y-%m-%d %H:%M:%S"
1048 )
1050 articles = []
1051 contribs = []
1052 index = 0
1054 if "nb_articles" in request.POST.keys():
1055 while index < int(request.POST["nb_articles"]):
1056 article = json.loads(request.POST[f"article[{index}]"])
1057 article["citation"] = xml_utils.replace_html_entities(article["citation"])
1058 # if not article["citation"]:
1059 # index += 1
1060 # continue
1061 articles.append(article)
1063 index += 1
1065 special_issue.articles = [Munch(article) for article in articles]
1066 index = 0
1067 # TODO make a function to call to add a contributor
1068 if "nb_contrib" in request.POST.keys():
1069 while index < int(request.POST["nb_contrib"]):
1070 contrib = json.loads(request.POST[f"contrib[{index}]"])
1071 contributor = create_contributor()
1072 contributor["first_name"] = contrib["first_name"]
1073 contributor["last_name"] = contrib["last_name"]
1074 contributor["orcid"] = contrib["orcid"]
1075 contributor["role"] = "editor"
1077 contrib_xml = xml_utils.get_contrib_xml(contrib)
1078 contributor["contrib_xml"] = contrib_xml
1079 contribs.append(Munch(contributor))
1080 index += 1
1081 special_issue.contributors = contribs
1083 # Part of the code that handle forwords and lastwords
1085 request_datas = [
1086 {"request_key": "head_fr", "abstract_type": "intro"},
1087 {"request_key": "head_en", "abstract_type": "intro"},
1088 {"request_key": "tail_fr", "abstract_type": "tail"},
1089 {"request_key": "tail_en", "abstract_type": "tail"},
1090 {"request_key": "editor_bio_fr", "abstract_type": "bio_fr"},
1091 {"request_key": "editor_bio_en", "abstract_type": "bio_en"},
1092 ]
1094 special_issue.abstracts = []
1095 abstracts_xml = []
1096 for request_data in request_datas:
1097 lang = request_data["request_key"][-2:]
1098 value_html = request.POST[request_data["request_key"]]
1100 ckeditor_data = build_jats_data_from_html_field(
1101 value_html,
1102 tag="abstract",
1103 text_lang=lang,
1104 resource_lang="en",
1105 field_type=request_data["abstract_type"],
1106 mml_formulas=[],
1107 issue_pid=colid,
1108 pid=special_issue.pid,
1109 )
1111 abstract_data = create_abstract(
1112 tag=request_data["abstract_type"],
1113 lang=lang,
1114 value_html=value_html,
1115 value_tex=ckeditor_data["value_tex"],
1116 value_xml=ckeditor_data["value_xml"],
1117 )
1119 special_issue.abstracts.append(abstract_data)
1120 abstracts_xml.append(ckeditor_data["value_xml"])
1122 figures = self.create_related_objects_from_abstract(
1123 abstracts_xml, colid, special_issue.pid
1124 )
1125 special_issue.related_objects = figures
1127 # This part handle pdf files included in special issue. Can be editor of full pdf version
1128 # Both are stored in same directory
1130 pdf_file_path = resolver.get_disk_location(
1131 f"{settings.RESOURCES_ROOT}",
1132 f"{collection.pid}",
1133 "pdf",
1134 special_issue_pid,
1135 article_id=None,
1136 do_create_folder=False,
1137 )
1138 pdf_path = os.path.dirname(pdf_file_path)
1139 if "pdf" in self.request.FILES: 1139 ↛ 1140line 1139 didn't jump to line 1140 because the condition on line 1139 was never true
1140 if os.path.isfile(f"{pdf_path}/{pid}.pdf"):
1141 os.remove(f"{pdf_path}/{pid}.pdf")
1142 if "edito" in self.request.FILES: 1142 ↛ 1143line 1142 didn't jump to line 1143 because the condition on line 1142 was never true
1143 if os.path.isfile(f"{pdf_path}/{pid}_edito.pdf"):
1144 os.remove(f"{pdf_path}/{pid}_edito.pdf")
1146 if request.POST["pdf_name"] != "No file uploaded": 1146 ↛ 1147line 1146 didn't jump to line 1147 because the condition on line 1146 was never true
1147 if "pdf" in self.request.FILES:
1148 pdf_file = request.FILES["pdf"]
1149 relative_file_name = resolver.copy_file_obj_to_article_folder(
1150 pdf_file,
1151 collection.pid,
1152 special_issue.pid,
1153 special_issue.pid,
1154 )
1155 pdf_file_name = self.request.FILES["pdf"].name
1157 else:
1158 pdf_file_name = request.POST["pdf_name"]
1159 relative_file_name = pdf_path + "/" + special_issue_pid + ".pdf"
1161 pdf_stream_data = create_datastream()
1162 pdf_stream_data["location"] = relative_file_name
1163 pdf_stream_data["mimetype"] = "application/pdf"
1164 pdf_stream_data["rel"] = "full-text"
1165 pdf_stream_data["text"] = pdf_file_name
1166 special_issue.streams.append(pdf_stream_data)
1168 if request.POST["edito_name"] != "No file uploaded": 1168 ↛ 1169line 1168 didn't jump to line 1169 because the condition on line 1168 was never true
1169 if "edito" in self.request.FILES:
1170 edito_file = self.request.FILES["edito"]
1171 relative_file_name = resolver.copy_file_obj_to_article_folder(
1172 edito_file,
1173 collection.pid,
1174 special_issue.pid,
1175 special_issue.pid,
1176 )
1178 edito_file_name = self.request.FILES["edito"].name
1179 edito_display_name = request.POST["edito_display_name"]
1180 else:
1181 relative_file_name = pdf_path + "/" + special_issue_pid + "_edito.pdf"
1182 edito_file_name = request.POST["edito_name"]
1183 edito_display_name = request.POST["edito_display_name"]
1185 data = {
1186 "rel": "edito",
1187 "mimetype": "application/pdf",
1188 "location": relative_file_name,
1189 "base": None,
1190 "metadata": edito_file_name + "$$$" + edito_display_name,
1191 }
1192 special_issue.related_objects.append(data)
1193 # Handle special issue icon. It is stored in same directory that pdf version or edito.
1194 # The icon is linked to special issue as an ExtLink
1195 if "icon" in request.FILES: 1195 ↛ 1196line 1195 didn't jump to line 1196 because the condition on line 1195 was never true
1196 icon_file = request.FILES["icon"]
1197 relative_file_name = resolver.copy_file_obj_to_article_folder(
1198 icon_file,
1199 collection.pid,
1200 special_issue.pid,
1201 special_issue.pid,
1202 )
1203 if ".tif" in relative_file_name:
1204 jpeg_path = ImageManager(
1205 os.path.join(settings.RESOURCES_ROOT, relative_file_name)
1206 ).to_jpeg()
1207 relative_file_name = str(jpeg_path).replace(settings.RESOURCES_ROOT + "/", "")
1208 data = {
1209 "rel": "icon",
1210 "location": relative_file_name,
1211 "base": None,
1212 "seq": 1,
1213 "metadata": "",
1214 }
1215 special_issue.ext_links.append(data)
1216 elif "icon" in request.POST.keys(): 1216 ↛ 1217line 1216 didn't jump to line 1217 because the condition on line 1216 was never true
1217 if request.POST["icon"] != "[object Object]":
1218 icon_file = request.POST["icon"].replace("/icon/", "")
1219 data = {
1220 "rel": "icon",
1221 "location": icon_file,
1222 "base": None,
1223 "seq": 1,
1224 "metadata": "",
1225 }
1226 special_issue.ext_links.append(data)
1228 special_issue = Munch(special_issue.__dict__)
1229 params = {"xissue": special_issue, "use_body": False}
1230 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params)
1231 cmd.do()
1232 return redirect("special_issue_edit_api", colid, special_issue.pid)
1234 def create_related_objects_from_abstract(self, abstracts, colid, pid):
1235 figures = []
1236 for abstract in abstracts:
1237 abstract_xml = abstract.encode("utf8")
1239 tree = etree.fromstring(abstract_xml)
1241 pics = tree.xpath("//graphic")
1242 for pic in pics: 1242 ↛ 1243line 1242 didn't jump to line 1243 because the loop on line 1242 never started
1243 base = None
1244 pic_location = pic.attrib["specific-use"]
1245 basename = os.path.basename(pic.attrib["href"])
1246 ext = basename.split(".")[-1]
1247 base = get_media_base_root(colid)
1248 data_location = os.path.join(
1249 settings.RESOURCES_ROOT, "media", base, "uploads", pic_location, basename
1250 )
1251 # we use related objects to send pics to journal site. Directory where pic is stored in trammel may differ
1252 # from the directory in journal site. So one need to save the pic in same directory that journal's one
1253 # so related objects can go for the correct one
1254 img = Image.open(data_location)
1255 final_data_location = os.path.join(
1256 settings.RESOURCES_ROOT, colid, pid, "src", "figures"
1257 )
1258 if not os.path.isdir(final_data_location):
1259 os.makedirs(final_data_location)
1260 relative_path = os.path.join(colid, pid, "src", "figures", basename)
1261 final_data_location = f"{final_data_location}/{basename}"
1262 img.save(final_data_location)
1263 if ext == "png":
1264 mimetype = "image/png"
1265 else:
1266 mimetype = "image/jpeg"
1267 data = {
1268 "rel": "html-image",
1269 "mimetype": mimetype,
1270 "location": relative_path,
1271 "base": base,
1272 "metadata": "",
1273 }
1274 if data not in figures:
1275 figures.append(data)
1276 return figures
1279class PageIndexView(EditorRequiredMixin, TemplateView):
1280 template_name = "mersenne_cms/page_index.html"
1282 def get_context_data(self, **kwargs):
1283 colid = kwargs.get("colid", "")
1284 site_id = model_helpers.get_site_id(colid)
1285 vi = Page.objects.filter(site_id=site_id, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES).first()
1286 if vi: 1286 ↛ 1287line 1286 didn't jump to line 1287 because the condition on line 1286 was never true
1287 pages = Page.objects.filter(site_id=site_id).exclude(parent_page=vi)
1288 else:
1289 pages = Page.objects.filter(site_id=site_id)
1290 context = super().get_context_data(**kwargs)
1291 context["colid"] = colid
1292 context["journal"] = model_helpers.get_collection(colid)
1293 context["pages"] = pages
1294 context["news"] = News.objects.filter(site_id=site_id)
1295 context["fields_lang"] = "fr" if model_helpers.is_site_fr_only(site_id) else "en"
1296 return context
1299class PageBaseView(HandleCMSMixin, View):
1300 template_name = "mersenne_cms/page_form.html"
1301 model = Page
1302 form_class = PageForm
1304 def dispatch(self, request, *args, **kwargs):
1305 self.colid = self.kwargs["colid"]
1306 self.collection = model_helpers.get_collection(self.colid, sites=False)
1307 self.site_id = model_helpers.get_site_id(self.colid)
1309 return super().dispatch(request, *args, **kwargs)
1311 def get_success_url(self):
1312 return reverse("page_index", kwargs={"colid": self.colid})
1314 def get_context_data(self, **kwargs):
1315 context = super().get_context_data(**kwargs)
1316 context["journal"] = self.collection
1317 return context
1319 def update_test_website(self):
1320 response = deploy_cms("test_website", self.collection)
1321 if response.status_code < 300: 1321 ↛ 1324line 1321 didn't jump to line 1324 because the condition on line 1321 was always true
1322 messages.success(self.request, "The test website has been updated")
1323 else:
1324 text = "ERROR: Unable to update the test website<br/>"
1326 if response.status_code == 503:
1327 text += "The test website is under maintenance. Please try again later.<br/>"
1328 else:
1329 text += f"Please contact the centre Mersenne<br/><br/>Status code: {response.status_code}<br/>"
1330 if hasattr(response, "content") and response.content:
1331 text += f"{response.content.decode()}<br/>"
1332 if hasattr(response, "reason") and response.reason:
1333 text += f"Reason: {response.reason}<br/>"
1334 if hasattr(response, "text") and response.text:
1335 text += f"Details: {response.text}<br/>"
1336 messages.error(self.request, mark_safe(text))
1338 def get_form_kwargs(self):
1339 kwargs = super().get_form_kwargs()
1340 kwargs["site_id"] = self.site_id
1341 kwargs["user"] = self.request.user
1342 return kwargs
1344 def form_valid(self, form):
1345 form.save()
1347 self.update_test_website()
1349 return HttpResponseRedirect(self.get_success_url())
1352# @method_decorator([csrf_exempt], name="dispatch")
1353class PageDeleteView(PageBaseView):
1354 def post(self, request, *args, **kwargs):
1355 colid = kwargs.get("colid", "")
1356 pk = kwargs.get("pk")
1357 page = get_object_or_404(Page, id=pk)
1358 if page.mersenne_id:
1359 raise PermissionDenied
1361 page.delete()
1363 self.update_test_website()
1365 if page.parent_page and page.parent_page.mersenne_id == MERSENNE_ID_VIRTUAL_ISSUES:
1366 return HttpResponseRedirect(reverse("virtual_issues_index", kwargs={"colid": colid}))
1367 else:
1368 return HttpResponseRedirect(reverse("page_index", kwargs={"colid": colid}))
1371class PageCreateView(PageBaseView, CreateView):
1372 def get_context_data(self, **kwargs):
1373 context = super().get_context_data(**kwargs)
1374 context["title"] = "Add a menu page"
1375 return context
1378class PageUpdateView(PageBaseView, UpdateView):
1379 def get_context_data(self, **kwargs):
1380 context = super().get_context_data(**kwargs)
1381 context["title"] = "Edit a menu page"
1382 return context
1385class NewsBaseView(PageBaseView):
1386 template_name = "mersenne_cms/news_form.html"
1387 model = News
1388 form_class = NewsForm
1391class NewsDeleteView(NewsBaseView):
1392 def post(self, request, *args, **kwargs):
1393 pk = kwargs.get("pk")
1394 news = get_object_or_404(News, id=pk)
1396 news.delete()
1398 self.update_test_website()
1400 return HttpResponseRedirect(self.get_success_url())
1403class NewsCreateView(NewsBaseView, CreateView):
1404 def get_context_data(self, **kwargs):
1405 context = super().get_context_data(**kwargs)
1406 context["title"] = "Add a News"
1407 return context
1410class NewsUpdateView(NewsBaseView, UpdateView):
1411 def get_context_data(self, **kwargs):
1412 context = super().get_context_data(**kwargs)
1413 context["title"] = "Edit a News"
1414 return context
1417# def page_create_view(request, colid):
1418# context = {}
1419# if not is_authorized_editor(request.user, colid):
1420# raise PermissionDenied
1421# collection = model_helpers.get_collection(colid)
1422# page = Page(site_id=model_helpers.get_site_id(colid))
1423# form = PageForm(request.POST or None, instance=page)
1424# if form.is_valid():
1425# form.save()
1426# response = deploy_cms("test_website", collection)
1427# if response.status_code < 300:
1428# messages.success(request, "Page created successfully.")
1429# else:
1430# text = f"ERROR: page creation failed<br/>Status code: {response.status_code}<br/>"
1431# if hasattr(response, "reason") and response.reason:
1432# text += f"Reason: {response.reason}<br/>"
1433# if hasattr(response, "text") and response.text:
1434# text += f"Details: {response.text}<br/>"
1435# messages.error(request, mark_safe(text))
1436# kwargs = {"colid": colid, "pid": form.instance.id}
1437# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1438#
1439# context["form"] = form
1440# context["title"] = "Add a menu page"
1441# context["journal"] = collection
1442# return render(request, "mersenne_cms/page_form.html", context)
1445# def page_update_view(request, colid, pid):
1446# context = {}
1447# if not is_authorized_editor(request.user, colid):
1448# raise PermissionDenied
1449#
1450# collection = model_helpers.get_collection(colid)
1451# page = get_object_or_404(Page, id=pid)
1452# form = PageForm(request.POST or None, instance=page)
1453# if form.is_valid():
1454# form.save()
1455# response = deploy_cms("test_website", collection)
1456# if response.status_code < 300:
1457# messages.success(request, "Page updated successfully.")
1458# else:
1459# text = f"ERROR: page update failed<br/>Status code: {response.status_code}<br/>"
1460# if hasattr(response, "reason") and response.reason:
1461# text += f"Reason: {response.reason}<br/>"
1462# if hasattr(response, "text") and response.text:
1463# text += f"Details: {response.text}<br/>"
1464# messages.error(request, mark_safe(text))
1465# kwargs = {"colid": colid, "pid": form.instance.id}
1466# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1467#
1468# context["form"] = form
1469# context["pid"] = pid
1470# context["title"] = "Edit a menu page"
1471# context["journal"] = collection
1472# return render(request, "mersenne_cms/page_form.html", context)