Coverage for src / ptf_tools / views / cms_views.py: 48%
877 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-04-23 13:02 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-04-23 13:02 +0000
1import base64
2import json
3import os
4import re
5import shutil
6from datetime import datetime
8import requests
9from ckeditor_uploader.views import ImageUploadView, browse
10from django.conf import settings
11from django.contrib import messages
12from django.contrib.auth.mixins import UserPassesTestMixin
13from django.core.exceptions import PermissionDenied
14from django.core.files import File
15from django.db.models import Q
16from django.forms.models import model_to_dict
17from django.http import (
18 Http404,
19 HttpResponse,
20 HttpResponseBadRequest,
21 HttpResponseRedirect,
22 HttpResponseServerError,
23 JsonResponse,
24)
25from django.shortcuts import get_object_or_404, redirect
26from django.urls import resolve, reverse
27from django.utils import timezone
28from django.utils.safestring import mark_safe
29from django.views.decorators.csrf import csrf_exempt
30from django.views.generic import CreateView, TemplateView, UpdateView, View
31from lxml import etree
32from mersenne_cms.models import (
33 MERSENNE_ID_VIRTUAL_ISSUES,
34 News,
35 Page,
36 get_news_content,
37 get_pages_content,
38 import_news,
39 import_pages,
40)
41from munch import Munch
42from PIL import Image
43from ptf import model_data_converter, model_helpers
44from ptf.cmds import solr_cmds, xml_cmds
45from ptf.cmds.ptf_cmds import base_ptf_cmds
46from ptf.cmds.xml import xml_utils
47from ptf.cmds.xml.ckeditor.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 = f"/{prefixes[colid]}{settings.MEDIA_URL}/CR"
130 return path
133def change_ckeditor_storage(colid):
134 """
135 By default, CKEditor stores all the files under 1 folder (MEDIA_ROOT)
136 We want to store the files under a subfolder of @colid
137 To do that we have to
138 - change the URL calling this view to pass the site_id (info used by the Pages to filter the objects)
139 - modify the storage location
140 """
142 from ckeditor_uploader import utils, views
143 from django.core.files.storage import FileSystemStorage
145 storage = FileSystemStorage(
146 location=get_media_base_root(colid), base_url=get_media_base_url(colid)
147 )
149 utils.storage = storage
150 views.storage = storage
153class EditorRequiredMixin(UserPassesTestMixin):
154 def test_func(self):
155 return is_authorized_editor(self.request.user, self.kwargs.get("colid"))
158class CollectionImageUploadView(EditorRequiredMixin, ImageUploadView):
159 """
160 By default, CKEditor stores all the files under 1 folder (MEDIA_ROOT)
161 We want to store the files under a subfolder of @colid
162 To do that we have to
163 - change the URL calling this view to pass the site_id (info used by the Pages to filter the objects)
164 - modify the storage location
165 """
167 def dispatch(self, request, *args, **kwargs):
168 colid = kwargs["colid"]
170 change_ckeditor_storage(colid)
172 return super().dispatch(request, **kwargs)
175class CollectionBrowseView(EditorRequiredMixin, View):
176 def dispatch(self, request, **kwargs):
177 colid = kwargs["colid"]
179 change_ckeditor_storage(colid)
181 return browse(request)
184file_upload_in_collection = csrf_exempt(CollectionImageUploadView.as_view())
185file_browse_in_collection = csrf_exempt(CollectionBrowseView.as_view())
188def deploy_cms(site, collection):
189 colid = collection.pid
190 base_url = getattr(collection, site)()
192 if base_url is None: 192 ↛ 195line 192 didn't jump to line 195 because the condition on line 192 was always true
193 return JsonResponse({"message": "OK"})
195 if site == "website":
196 from_base_path = get_media_base_root_in_test(colid)
197 to_base_path = get_media_base_root_in_prod(colid)
199 for sub_path in ["uploads", "images"]:
200 from_path = os.path.join(from_base_path, sub_path)
201 to_path = os.path.join(to_base_path, sub_path)
202 if os.path.exists(from_path):
203 try:
204 shutil.copytree(from_path, to_path, dirs_exist_ok=True)
205 except OSError as exception:
206 return HttpResponseServerError(f"Error during copy: {exception}")
208 site_id = model_helpers.get_site_id(colid)
209 if model_helpers.get_site_default_language(site_id):
210 from modeltranslation import fields, manager
212 old_ftor = manager.get_language
213 manager.get_language = monkey_get_language_en
214 fields.get_language = monkey_get_language_en
216 pages = get_pages_content(colid)
217 news = get_news_content(colid)
219 manager.get_language = old_ftor
220 fields.get_language = old_ftor
221 else:
222 pages = get_pages_content(colid)
223 news = get_news_content(colid)
225 data = json.dumps({"pages": json.loads(pages), "news": json.loads(news)})
226 url = getattr(collection, site)() + "/import_cms/"
228 try:
229 response = requests.put(url, data=data, verify=False)
231 if response.status_code == 503:
232 e = ServerUnderMaintenance(
233 "The journal test website is under maintenance. Please try again later."
234 )
235 return HttpResponseServerError(e, status=503)
237 except Timeout as exception:
238 return HttpResponse(exception, status=408)
239 except Exception as exception:
240 return HttpResponseServerError(exception)
242 return JsonResponse({"message": "OK"})
245class HandleCMSMixin(EditorRequiredMixin):
246 """
247 Mixin for classes that need to send request to (test) website to import/export CMS content (pages, news)
248 """
250 # def dispatch(self, request, *args, **kwargs):
251 # self.colid = self.kwargs["colid"]
252 # return super().dispatch(request, *args, **kwargs)
254 def init_data(self, kwargs):
255 self.collection = None
257 self.colid = kwargs.get("colid", None)
258 if self.colid:
259 self.collection = model_helpers.get_collection(self.colid)
260 if not self.collection:
261 raise Http404(f"{self.colid} does not exist")
263 test_server_url = self.collection.test_website()
264 if not test_server_url:
265 raise Http404("The collection has no test site")
267 prod_server_url = self.collection.website()
268 if not prod_server_url:
269 raise Http404("The collection has no prod site")
272class GetCMSFromSiteAPIView(HandleCMSMixin, View):
273 """
274 Get the CMS content from the (test) website and save it on disk.
275 It can be used if needed to restore the Trammel content with RestoreCMSAPIView below
276 """
278 def get(self, request, *args, **kwargs):
279 self.init_data(self.kwargs)
281 site = kwargs.get("site", "test_website")
283 try:
284 url = getattr(self.collection, site)() + "/export_cms/"
285 response = requests.get(url, verify=False)
287 # Just to need to save the json on disk
288 # Media files are already saved in MEDIA_ROOT which is equal to
289 # /mersenne_test_data/@colid/media
290 folder = get_media_base_root(self.colid)
291 os.makedirs(folder, exist_ok=True)
292 filename = os.path.join(folder, f"pages_{self.colid}.json")
293 with open(filename, mode="w", encoding="utf-8") as file:
294 file.write(response.content.decode(encoding="utf-8"))
296 except Timeout as exception:
297 return HttpResponse(exception, status=408)
298 except Exception as exception:
299 return HttpResponseServerError(exception)
301 return JsonResponse({"message": "OK", "status": 200})
304def monkey_get_language_en():
305 return "en"
308class RestoreCMSAPIView(HandleCMSMixin, View):
309 """
310 Restore the Trammel CMS content (of a colid) from disk
311 """
313 def get(self, request, *args, **kwargs):
314 self.init_data(self.kwargs)
316 folder = get_media_base_root(self.colid)
317 filename = os.path.join(folder, f"pages_{self.colid}.json")
318 with open(filename, encoding="utf-8") as f:
319 json_data = json.load(f)
321 pages = json_data.get("pages")
323 site_id = model_helpers.get_site_id(self.colid)
324 if model_helpers.get_site_default_language(site_id):
325 from modeltranslation import fields, manager
327 old_ftor = manager.get_language
328 manager.get_language = monkey_get_language_en
329 fields.get_language = monkey_get_language_en
331 import_pages(pages, self.colid)
333 manager.get_language = old_ftor
334 fields.get_language = old_ftor
335 else:
336 import_pages(pages, self.colid)
338 if "news" in json_data:
339 news = json_data.get("news")
340 import_news(news, self.colid)
342 return JsonResponse({"message": "OK", "status": 200})
345class DeployCMSAPIView(HandleCMSMixin, View):
346 def get(self, request, *args, **kwargs):
347 self.init_data(self.kwargs)
349 if check_lock():
350 msg = "Trammel is under maintenance. Please try again later."
351 messages.error(self.request, msg)
352 return JsonResponse({"messages": msg, "status": 503})
354 site = kwargs.get("site", "test_website")
356 response = deploy_cms(site, self.collection)
358 if response.status_code == 503:
359 messages.error(
360 self.request, "The journal website is under maintenance. Please try again later."
361 )
363 return response
366def get_server_urls(collection, site="test_website"):
367 urls = [""]
368 if hasattr(settings, "MERSENNE_DEV_URL"): 368 ↛ 370line 368 didn't jump to line 370 because the condition on line 368 was never true
369 # set RESOURCES_ROOT and apache config accordingly (for instance with "/mersenne_dev_data")
370 url = getattr(collection, "test_website")().split(".fr")
371 urls = [settings.MERSENNE_DEV_URL + url[1] if len(url) == 2 else ""]
372 elif site == "both": 372 ↛ 373line 372 didn't jump to line 373 because the condition on line 372 was never true
373 urls = [getattr(collection, "test_website")(), getattr(collection, "website")()]
374 elif hasattr(collection, site) and getattr(collection, site)(): 374 ↛ 375line 374 didn't jump to line 375 because the condition on line 374 was never true
375 urls = [getattr(collection, site)()]
376 return urls
379class SuggestDeployView(EditorRequiredMixin, View):
380 def post(self, request, *args, **kwargs):
381 doi = kwargs.get("doi", "")
382 site = kwargs.get("site", "test_website")
383 article = get_object_or_404(Article, doi=doi)
385 obj, created = RelatedArticles.objects.get_or_create(resource=article)
386 form = RelatedForm(request.POST or None, instance=obj)
387 if form.is_valid(): 387 ↛ 404line 387 didn't jump to line 404 because the condition on line 387 was always true
388 data = form.cleaned_data
389 obj.date_modified = timezone.now()
390 form.save()
391 collection = article.my_container.my_collection
392 urls = get_server_urls(collection, site=site)
393 response = requests.models.Response()
394 for url in urls: 394 ↛ 402line 394 didn't jump to line 402 because the loop on line 394 didn't complete
395 url = url + reverse("api-update-suggest", kwargs={"doi": doi})
396 try:
397 response = requests.post(url, data=data, timeout=15)
398 except requests.exceptions.RequestException as e:
399 response.status_code = 503
400 response.reason = e.args[0]
401 break
402 return HttpResponse(status=response.status_code, reason=response.reason)
403 else:
404 return HttpResponseBadRequest()
407def suggest_debug(results, article, message):
408 crop_results = 5
409 if results: 409 ↛ 410line 409 didn't jump to line 410 because the condition on line 409 was never true
410 dois = []
411 results["docs"] = results["docs"][:crop_results]
412 numFound = f"({len(results['docs'])} sur {results['numFound']} documents)"
413 head = f"Résultats de la recherche automatique {numFound} :\n\n"
414 for item in results["docs"]:
415 doi = item.get("doi")
416 if doi:
417 explain = results["explain"][item["id"]]
418 terms = re.findall(r"([0-9.]+?) = weight\((.+?:.+?) in", explain)
419 terms.sort(key=lambda t: t[0], reverse=True)
420 details = (" + ").join(f"{round(float(s), 1)}:{t}" for s, t in terms)
421 score = f"Score : {round(float(item['score']), 1)} (= {details})\n"
422 url = ""
423 suggest = Article.objects.filter(doi=doi).first()
424 if suggest and suggest.my_container:
425 collection = suggest.my_container.my_collection
426 base_url = collection.website() or ""
427 url = base_url + "/articles/" + doi
428 dois.append((doi, url, score))
430 tail = f"\n\nScore minimum retenu : {results['params']['min_score']}\n\n\n"
431 tail += "Termes principaux utilisés pour la requête "
432 tail = [tail + "(champ:terme recherché | pertinence du terme) :\n"]
433 if results["params"]["mlt.fl"] == "all":
434 tail.append(" * all = body + abstract + title + authors + keywords\n")
435 terms = results["interestingTerms"]
436 terms = [" | ".join((x[0], str(x[1]))) for x in zip(terms[::2], terms[1::2])]
437 tail.extend(reversed(terms))
438 tail.append("\n\nParamètres de la requête :\n")
439 tail.extend([f"{k}: {v} " for k, v in results["params"].items()])
440 return [(head, dois, "\n".join(tail))]
441 else:
442 msg = f"Erreur {message['status']} {message['err']} at {message['url']}"
443 return [(msg, [], "")]
446class SuggestUpdateView(EditorRequiredMixin, TemplateView):
447 template_name = "editorial_tools/suggested.html"
449 def get_context_data(self, **kwargs):
450 doi = kwargs.get("doi", "")
451 article = get_object_or_404(Article, doi=doi)
453 obj, created = RelatedArticles.objects.get_or_create(resource=article)
454 collection = article.my_container.my_collection
455 base_url = collection.website() or ""
456 response = requests.models.Response()
457 try:
458 response = requests.get(base_url + "/mlt/" + doi, timeout=10.0)
459 except requests.exceptions.RequestException as e:
460 response.status_code = 503
461 response.reason = e.args[0]
462 msg = {
463 "url": response.url,
464 "status": response.status_code,
465 "err": response.reason,
466 }
467 results = None
468 if response.status_code == 200: 468 ↛ 469line 468 didn't jump to line 469 because the condition on line 468 was never true
469 results = solr_cmds.auto_suggest_doi(obj, article, response.json())
470 context = super().get_context_data(**kwargs)
471 context["debug"] = suggest_debug(results, article, msg)
472 context["form"] = RelatedForm(instance=obj)
473 context["author"] = "; ".join(get_names(article, "author"))
474 context["citation_base"] = article.get_citation_base().strip(", .")
475 context["article"] = article
476 context["date_modified"] = obj.date_modified
477 context["url"] = base_url + "/articles/" + doi
478 return context
481class EditorialToolsVolumeItemsView(EditorRequiredMixin, TemplateView):
482 template_name = "editorial_tools/volume-items.html"
484 def get_context_data(self, **kwargs):
485 vid = kwargs.get("vid")
486 issues_articles, collection = model_helpers.get_issues_in_volume(vid)
487 context = super().get_context_data(**kwargs)
488 context["issues_articles"] = issues_articles
489 context["collection"] = collection
490 return context
493class EditorialToolsArticleView(EditorRequiredMixin, TemplateView):
494 template_name = "editorial_tools/find-article.html"
496 def get_context_data(self, **kwargs):
497 colid = kwargs.get("colid")
498 doi = kwargs.get("doi")
499 article = get_object_or_404(Article, doi=doi, my_container__my_collection__pid=colid)
501 context = super().get_context_data(**kwargs)
502 context["article"] = article
503 context["citation_base"] = article.get_citation_base().strip(", .")
504 return context
507class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView):
508 template_name = "editorial_tools/graphical-abstract.html"
510 def get_context_data(self, **kwargs):
511 doi = kwargs.get("doi", "")
512 article = get_object_or_404(Article, doi=doi)
514 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
515 context = super().get_context_data(**kwargs)
516 context["author"] = "; ".join(get_names(article, "author"))
517 context["citation_base"] = article.get_citation_base().strip(", .")
518 context["article"] = article
519 context["date_modified"] = obj.date_modified
520 context["form"] = GraphicalAbstractForm(instance=obj)
521 context["graphical_abstract"] = obj.graphical_abstract
522 context["illustration"] = obj.illustration
523 return context
526class GraphicalAbstractDeployView(EditorRequiredMixin, View):
527 def __get_path_and_replace_tiff_file(self, obj_attribute_file):
528 """
529 Returns the path of the attribute.
530 If the attribute is a tiff file, converts it to jpg, delete the old tiff file, and return the new path of the attribute.
532 Checks if paths have already been processed, to prevent issues related to object mutation.
533 """
534 if obj_attribute_file.name.lower().endswith((".tiff", ".tif")):
535 jpeg_path = ImageManager(obj_attribute_file.path).to_jpeg(delete_original_tiff=True)
536 with open(jpeg_path, "rb") as fp:
537 obj_attribute_file.save(os.path.basename(jpeg_path), File(fp), save=True)
538 return jpeg_path
540 return obj_attribute_file.path
542 def post(self, request, *args, **kwargs):
543 doi = kwargs.get("doi", "")
544 site = kwargs.get("site", "both")
545 article = get_object_or_404(Article, doi=doi)
547 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
548 form = GraphicalAbstractForm(request.POST, request.FILES or None, instance=obj)
549 if form.is_valid(): 549 ↛ 578line 549 didn't jump to line 578 because the condition on line 549 was always true
550 obj.date_modified = timezone.now()
551 data = {"date_modified": obj.date_modified}
552 form.save()
553 files = {}
555 for attribute in ("graphical_abstract", "illustration"):
556 obj_attribute_file = getattr(obj, attribute, None)
557 if obj_attribute_file and os.path.exists(obj_attribute_file.path): 557 ↛ 558line 557 didn't jump to line 558 because the condition on line 557 was never true
558 file_path = self.__get_path_and_replace_tiff_file(obj_attribute_file)
559 with open(file_path, "rb") as fp:
560 files.update({attribute: (obj_attribute_file.name, fp.read())})
562 collection = article.my_container.my_collection
563 urls = get_server_urls(collection, site=site)
564 response = requests.models.Response()
565 for url in urls: 565 ↛ 576line 565 didn't jump to line 576 because the loop on line 565 didn't complete
566 url = url + reverse("api-graphical-abstract", kwargs={"doi": doi})
567 try:
568 if not obj.graphical_abstract and not obj.illustration: 568 ↛ 571line 568 didn't jump to line 571 because the condition on line 568 was always true
569 response = requests.delete(url, data=data, files=files, timeout=15)
570 else:
571 response = requests.post(url, data=data, files=files, timeout=15)
572 except requests.exceptions.RequestException as e:
573 response.status_code = 503
574 response.reason = e.args[0]
575 break
576 return HttpResponse(status=response.status_code, reason=response.reason)
577 else:
578 return HttpResponseBadRequest()
581def parse_content(content):
582 table = re.search(r'(.*?)(<table id="summary".+?</table>)(.*)', content, re.DOTALL)
583 if not table:
584 return {"head": content, "tail": "", "articles": []}
586 articles = []
587 rows = re.findall(r"<tr>.+?</tr>", table.group(2), re.DOTALL)
588 for row in rows:
589 citation = re.search(r'<div href=".*?">(.*?)</div>', row, re.DOTALL)
590 href = re.search(r'href="(.+?)\/?">', row)
591 doi = re.search(r"(10[.].+)", href.group(1)) if href else ""
592 src = re.search(r'<img.+?src="(.+?)"', row)
593 item = {}
594 item["citation"] = citation.group(1) if citation else ""
595 item["doi"] = doi.group(1) if doi else href.group(1) if href else ""
596 item["src"] = src.group(1) if src else ""
597 item["imageName"] = item["src"].split("/")[-1] if item["src"] else ""
598 if item["doi"] or item["src"]:
599 articles.append(item)
600 return {"head": table.group(1), "tail": table.group(3), "articles": articles}
603class VirtualIssueParseView(EditorRequiredMixin, View):
604 def get(self, request, *args, **kwargs):
605 pid = kwargs.get("pid", "")
606 page = get_object_or_404(Page, id=pid)
608 data = {"pid": pid}
609 data["colid"] = kwargs.get("colid", "")
610 journal = model_helpers.get_collection(data["colid"])
611 data["journal_title"] = journal.title_tex.replace(".", "")
612 site_id = model_helpers.get_site_id(data["colid"])
613 data["page"] = model_to_dict(page)
614 pages = Page.objects.filter(site_id=site_id).exclude(id=pid)
615 data["parents"] = [model_to_dict(p, fields=["id", "menu_title"]) for p in pages]
617 content_fr = parse_content(page.content_fr)
618 data["head_fr"] = content_fr["head"]
619 data["tail_fr"] = content_fr["tail"]
621 content_en = parse_content(page.content_en)
622 data["articles"] = content_en["articles"]
623 data["head_en"] = content_en["head"]
624 data["tail_en"] = content_en["tail"]
625 return JsonResponse(data)
628class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView):
629 template_name = "editorial_tools/virtual-issue.html"
631 def get(self, request, *args, **kwargs):
632 pid = kwargs.get("pid", "")
633 get_object_or_404(Page, id=pid)
634 return super().get(request, *args, **kwargs)
637class VirtualIssueCreateView(EditorRequiredMixin, View):
638 def get(self, request, *args, **kwargs):
639 colid = kwargs.get("colid", "")
640 site_id = model_helpers.get_site_id(colid)
641 parent, _ = Page.objects.get_or_create(
642 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES,
643 parent_page=None,
644 site_id=site_id,
645 )
646 page = Page.objects.create(
647 menu_title_en="New virtual issue",
648 menu_title_fr="Nouvelle collection transverse",
649 parent_page=parent,
650 site_id=site_id,
651 state="draft",
652 )
653 kwargs = {"colid": colid, "pid": page.id}
654 return HttpResponseRedirect(reverse("virtual_issue_update", kwargs=kwargs))
657class SpecialIssuesIndex(EditorRequiredMixin, TemplateView):
658 template_name = "editorial_tools/special-issues-index.html"
660 def get_context_data(self, **kwargs):
661 colid = kwargs.get("colid", "")
663 context = super().get_context_data(**kwargs)
664 context["colid"] = colid
665 collection = Collection.objects.get(pid=colid)
666 context["special_issues"] = Container.objects.filter(
667 Q(ctype="issue_special") | Q(ctype="issue_special_img")
668 ).filter(my_collection=collection)
670 context["journal"] = model_helpers.get_collection(colid, sites=False)
671 return context
674class SpecialIssueEditView(EditorRequiredMixin, TemplateView):
675 template_name = "editorial_tools/special-issue-edit.html"
677 def get_context_data(self, **kwargs):
678 context = super().get_context_data(**kwargs)
679 return context
682class VirtualIssuesIndex(EditorRequiredMixin, TemplateView):
683 template_name = "editorial_tools/virtual-issues-index.html"
685 def get_context_data(self, **kwargs):
686 colid = kwargs.get("colid", "")
687 site_id = model_helpers.get_site_id(colid)
688 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES)
689 pages = Page.objects.filter(site_id=site_id, parent_page=vi)
690 context = super().get_context_data(**kwargs)
691 context["journal"] = model_helpers.get_collection(colid)
692 context["pages"] = pages
693 return context
696def get_citation_fr(doi, citation_en):
697 citation_fr = citation_en
698 article = Article.objects.filter(doi=doi).first()
699 if article and article.trans_title_html:
700 trans_title = article.trans_title_html
701 try:
702 citation_fr = re.sub(
703 r'(<a href="https:\/\/doi\.org.*">)([^<]+)',
704 rf"\1{trans_title}",
705 citation_en,
706 )
707 except re.error:
708 pass
709 return citation_fr
712def summary_build(articles, colid):
713 summary_fr = ""
714 summary_en = ""
715 head = '<table id="summary"><tbody>'
716 tail = "</tbody></table>"
717 style = "max-width:180px;max-height:200px"
718 colid_lo = colid.lower()
719 site_domain = SITE_REGISTER[colid_lo]["site_domain"].split("/")
720 site_domain = "/" + site_domain[-1] if len(site_domain) == 2 else ""
722 for article in articles:
723 image_src = article.get("src", "")
724 image_name = article.get("imageName", "")
725 doi = article.get("doi", "")
726 citation_en = article.get("citation", "")
727 if doi or citation_en:
728 row_fr = f'<div href="{doi}">{get_citation_fr(doi, citation_en)}</div>'
729 row_en = f'<div href="{doi}">{citation_en}</div>'
730 if image_src:
731 date = datetime.now().strftime("%Y/%m/%d/")
732 base_url = get_media_base_url(colid)
733 suffix = os.path.join(base_url, "uploads", date)
734 image_url = os.path.join(site_domain, suffix, image_name)
735 image_header = "^data:image/.+;base64,"
736 if re.match(image_header, image_src):
737 image_src = re.sub(image_header, "", image_src)
738 base64_data = base64.b64decode(image_src)
739 base_root = get_media_base_root(colid)
740 path = os.path.join(base_root, "uploads", date)
741 os.makedirs(path, exist_ok=True)
742 with open(path + image_name, "wb") as fp:
743 fp.write(base64_data)
744 im = f'<img src="{image_url}" style="{style}" />'
745 # TODO mettre la vrai valeur pour le SITE_DOMAIN
746 elif settings.SITE_DOMAIN == "http://127.0.0.1:8002":
747 im = f'<img src="{image_src}" style="{style}" />'
748 else:
749 im = f'<img src="{site_domain}{image_src}" style="{style}" />'
750 summary_fr += f"<tr><td>{im}</td><td>{row_fr}</td></tr>"
751 summary_en += f"<tr><td>{im}</td><td>{row_en}</td></tr>"
752 summary_fr = head + summary_fr + tail
753 summary_en = head + summary_en + tail
754 return {"summary_fr": summary_fr, "summary_en": summary_en}
757# @method_decorator([csrf_exempt], name="dispatch")
758class VirtualIssueDeployView(HandleCMSMixin, View):
759 """
760 called by the Virtual.vue VueJS component, when the virtual issue is saved
761 We get data in JSON and we need to update the corresponding Page.
762 The Page is then immediately posted to the test_website.
763 The "Apply the changes to the production website" button is then used to update the (prod) website
764 => See DeployCMSAPIView
765 """
767 def post(self, request, *args, **kwargs):
768 self.init_data(self.kwargs)
769 if check_lock():
770 msg = "Trammel is under maintenance. Please try again later."
771 messages.error(self.request, msg)
772 return JsonResponse({"messages": msg, "status": 503})
774 pid = kwargs.get("pid")
775 colid = self.colid
776 data = json.loads(request.body)
777 summary = summary_build(data["articles"], colid)
778 page = get_object_or_404(Page, id=pid)
779 page.slug = page.slug_fr = page.slug_en = None
780 page.menu_title_fr = data["title_fr"]
781 page.menu_title_en = data["title_en"]
782 page.content_fr = data["head_fr"] + summary["summary_fr"] + data["tail_fr"]
783 page.content_en = data["head_en"] + summary["summary_en"] + data["tail_en"]
784 page.state = data["page"]["state"]
785 page.menu_order = data["page"]["menu_order"]
786 page.parent_page = Page.objects.filter(id=data["page"]["parent_page"]).first()
787 page.save()
789 response = deploy_cms("test_website", self.collection)
790 if response.status_code == 503:
791 messages.error(
792 self.request, "The journal website is under maintenance. Please try again later."
793 )
795 return response # HttpResponse(status=response.status_code, reason=response.reason)
798class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView):
799 template_name = "editorial_tools/special-issue-edit.html"
801 def get_context_data(self, **kwargs):
802 context = super().get_context_data(**kwargs)
803 return context
805 def set_contrib_addresses(self, contrib, contribution):
806 for address in contrib:
807 contrib_address = ContribAddress(contribution=contribution, address=address)
808 contrib_address.save()
810 def delete(self, pid):
811 special_issue = Container.objects.get(pid=pid)
812 cmd = base_ptf_cmds.addContainerPtfCmd()
813 cmd.set_object_to_be_deleted(special_issue)
814 cmd.undo()
816 def get(self, request, *args, **kwargs):
817 pid = kwargs.get("pid", "")
819 data = {"pid": pid}
820 colid = kwargs.get("colid", "")
821 data["colid"] = colid
822 journal = model_helpers.get_collection(colid, sites=False)
823 name = resolve(request.path_info).url_name
824 if name == "special_issue_delete": 824 ↛ 825line 824 didn't jump to line 825 because the condition on line 824 was never true
825 self.delete(pid)
826 return redirect("special_issues_index", data["colid"])
828 data["journal_title"] = journal.title_tex.replace(".", "")
830 if pid != "create":
831 container = get_object_or_404(Container, pid=pid)
832 # TODO: pass the lang and trans_lang as well
833 # In VueJS (Special.vu)e, titleFr = title_html
834 # June 2025: Title objects are added for translated titles
835 # keep using trans_title_html for backward compatibility
836 translated_title = container.title_set.all().filter(lang="fr", type="main").first()
837 if translated_title: 837 ↛ 839line 837 didn't jump to line 839 because the condition on line 837 was always true
838 data["title"] = translated_title.title_html
839 data["doi"] = container.doi
840 data["trans_title"] = container.title_html
841 data["year"] = container.year
842 data["volume"] = container.volume
843 data["articles"] = [
844 {"doi": article.resource_doi, "citation": article.citation}
845 for article in container.resources_in_special_issue.all().order_by("seq")
846 ]
847 if container.ctype == "issue_special_img": 847 ↛ 848line 847 didn't jump to line 848 because the condition on line 847 was never true
848 data["use_resources_icon"] = True
849 else:
850 data["use_resources_icon"] = False
852 contribs = model_data_converter.db_to_contributors(container.contributions)
853 data["contribs"] = contribs
854 abstract_set = container.abstract_set.all()
855 data["head_fr"] = (
856 abstract_set.filter(tag="intro", lang="fr").first().value_html
857 if abstract_set.filter(tag="intro", lang="fr").exists()
858 else ""
859 )
860 data["head_en"] = (
861 abstract_set.filter(tag="intro", lang="en").first().value_html
862 if abstract_set.filter(tag="intro", lang="en").exists()
863 else ""
864 )
865 data["tail_fr"] = (
866 abstract_set.filter(tag="tail", lang="fr").first().value_html
867 if abstract_set.filter(tag="tail", lang="fr").exists()
868 else ""
869 )
870 data["tail_en"] = (
871 abstract_set.filter(tag="tail", lang="en").first().value_html
872 if abstract_set.filter(tag="tail", lang="en").exists()
873 else ""
874 )
875 data["editor_bio_en"] = (
876 abstract_set.filter(tag="bio_en").first().value_html
877 if abstract_set.filter(tag="bio_en").exists()
878 else ""
879 )
880 data["editor_bio_fr"] = (
881 abstract_set.filter(tag="bio_fr").first().value_html
882 if abstract_set.filter(tag="bio_fr").exists()
883 else ""
884 )
886 streams = container.datastream_set.all()
887 data["pdf_file_name"] = ""
888 data["edito_file_name"] = ""
889 data["edito_display_name"] = ""
890 for stream in streams: # don't work 890 ↛ 891line 890 didn't jump to line 891 because the loop on line 890 never started
891 if os.path.basename(stream.location).split(".")[0] == data["pid"]:
892 data["pdf_file_name"] = stream.text
893 try:
894 # edito related objects metadata contains both file real name and displayed name in issue summary
895 edito_name_infos = container.relatedobject_set.get(rel="edito").metadata.split(
896 "$$$"
897 )
898 data["edito_file_name"] = edito_name_infos[0]
899 data["edito_display_name"] = edito_name_infos[1]
901 except RelatedObject.DoesNotExist:
902 pass
903 try:
904 container_icon = container.extlink_set.get(rel="icon")
906 data["icon_location"] = container_icon.location
907 except ExtLink.DoesNotExist:
908 data["icon_location"] = ""
909 # try:
910 # special_issue_icon = container.extlink_set.get(rel="icon")
911 # data["special_issue_icon"] = special_issue_icon.location
912 # except ExtLink.DoesNotExist:
913 # data["special_issue_icon"] = None
915 else:
916 data["title"] = ""
917 data["doi"] = None
918 data["trans_title"] = ""
919 data["year"] = ""
920 data["volume"] = ""
921 data["articles"] = []
922 data["contribs"] = []
924 data["head_fr"] = ""
925 data["head_en"] = ""
926 data["tail_fr"] = ""
927 data["tail_en"] = ""
928 data["editor_bio_en"] = ""
929 data["editor_bio_fr"] = ""
930 data["pdf_file_name"] = ""
931 data["edito_file_name"] = ""
932 data["use_resources_icon"] = False
934 return JsonResponse(data)
936 def post(self, request, *args, **kwargs):
937 # le but est de faire un IssueDAta
938 pid = kwargs.get("pid", "")
939 colid = kwargs.get("colid", "")
940 journal = collection = model_helpers.get_collection(colid, sites=False)
941 special_issue = create_issuedata()
942 year = request.POST["year"]
943 # TODO 1: the values should be the tex values, not the html ones
944 # TODO 2: In VueJS, titleFr = title
945 trans_title_html = request.POST["title"]
946 title_html = request.POST["trans_title"]
947 issues = collection.content.all().order_by("-year")
948 same_year_issues = issues.filter(year=int(year))
949 if same_year_issues.exists(): 949 ↛ 951line 949 didn't jump to line 951 because the condition on line 949 was always true
950 volume = same_year_issues.first().volume
951 elif issues.exists() and colid != "HOUCHES": # because we don't want a volume for houches
952 ref_volume = issues.filter(year=2024).first().volume
953 volume = int(ref_volume) + (
954 int(year) - 2024
955 ) # 2024 is the ref year for wich we know the volume is 347
956 else:
957 volume = ""
958 if pid != "create":
959 # TODO: do not use the pk, but the pid in the URLs
960 container = get_object_or_404(Container, pid=pid)
961 lang = container.lang
962 trans_title = container.title_set.all().filter(type="main").first()
963 if not trans_title: 963 ↛ 964line 963 didn't jump to line 964 because the condition on line 963 was never true
964 raise ValueError(
965 "Cannot find trans_lang: Container does not have a main title translation"
966 )
967 trans_lang = trans_title.lang
968 xpub = create_publisherdata()
969 xpub.name = container.my_publisher.pub_name
970 special_issue.provider = container.provider
971 special_issue.number = container.number
972 special_issue_pid = pid
973 special_issue.date_pre_published = container.date_pre_published
974 special_issue.date_published = container.date_published
975 # used for first special issues created withou a proper doi
976 # can be remove when no doi's less special issue existe
977 if not container.doi: 977 ↛ 978line 977 didn't jump to line 978 because the condition on line 977 was never true
978 special_issue.doi = model_helpers.assign_container_doi(colid)
979 else:
980 special_issue.doi = container.doi
981 else:
982 lang = "en"
983 container = None
984 trans_lang = "fr"
985 xpub = create_publisherdata()
986 special_issue.doi = model_helpers.assign_container_doi(colid)
988 if colid == "HOUCHES": 988 ↛ 989line 988 didn't jump to line 989 because the condition on line 988 was never true
989 xpub.name = "UGA Éditions"
990 else:
991 xpub.name = issues.first().my_publisher.pub_name
992 special_issue.provider = collection.provider
994 special_issues = issues.filter(year=year).filter(
995 Q(ctype="issue_special") | Q(ctype="issue") | Q(ctype="issue_special_img")
996 )
997 if special_issues: 997 ↛ 1007line 997 didn't jump to line 1007 because the condition on line 997 was always true
998 all_special_issues_numbers = [
999 int(si.number[1:]) for si in special_issues if si.number[1:].isnumeric()
1000 ]
1001 if len(all_special_issues_numbers) > 0:
1002 max_number = max(all_special_issues_numbers)
1003 else:
1004 max_number = 0
1006 else:
1007 max_number = 0
1008 special_issue.number = f"S{max_number + 1}"
1009 special_issue_pid = f"{colid}_{year}__{volume}_{special_issue.number}"
1011 if request.POST["use_resources_icon"] == "true": 1011 ↛ 1012line 1011 didn't jump to line 1012 because the condition on line 1011 was never true
1012 special_issue.ctype = "issue_special_img"
1013 else:
1014 special_issue.ctype = "issue_special"
1016 existing_issue = model_helpers.get_resource(special_issue_pid)
1017 if pid == "create" and existing_issue is not None: 1017 ↛ 1018line 1017 didn't jump to line 1018 because the condition on line 1017 was never true
1018 raise ValueError(f"The special issue with the pid {special_issue_pid} already exists")
1020 special_issue.lang = lang
1021 special_issue.title_html = title_html
1022 special_issue.title_xml = build_title_xml(
1023 title=title_html, lang=lang, title_type="issue-title"
1024 )
1026 title_xml = build_title_xml(
1027 title=trans_title_html, lang=trans_lang, title_type="issue-title"
1028 )
1029 title = create_titledata(
1030 lang=trans_lang, type="main", title_html=trans_title_html, title_xml=title_xml
1031 )
1032 special_issue.titles = [title]
1034 special_issue.year = year
1035 special_issue.volume = volume
1036 special_issue.journal = journal
1037 special_issue.publisher = xpub
1038 special_issue.pid = special_issue_pid
1039 special_issue.last_modified_iso_8601_date_str = datetime.now().strftime(
1040 "%Y-%m-%d %H:%M:%S"
1041 )
1043 articles = []
1044 contribs = []
1045 index = 0
1047 if "nb_articles" in request.POST.keys():
1048 while index < int(request.POST["nb_articles"]):
1049 article = json.loads(request.POST[f"article[{index}]"])
1050 article["citation"] = xml_utils.replace_html_entities(article["citation"])
1051 # if not article["citation"]:
1052 # index += 1
1053 # continue
1054 articles.append(article)
1056 index += 1
1058 special_issue.articles = [Munch(article) for article in articles]
1059 index = 0
1060 # TODO make a function to call to add a contributor
1061 if "nb_contrib" in request.POST.keys():
1062 while index < int(request.POST["nb_contrib"]):
1063 contrib = json.loads(request.POST[f"contrib[{index}]"])
1064 contributor = create_contributor()
1065 contributor["first_name"] = contrib["first_name"]
1066 contributor["last_name"] = contrib["last_name"]
1067 contributor["orcid"] = contrib["orcid"]
1068 contributor["role"] = "editor"
1070 contrib_xml = xml_utils.get_contrib_xml(contrib)
1071 contributor["contrib_xml"] = contrib_xml
1072 contribs.append(Munch(contributor))
1073 index += 1
1074 special_issue.contributors = contribs
1076 # Part of the code that handle forwords and lastwords
1078 request_datas = [
1079 {"request_key": "head_fr", "abstract_type": "intro"},
1080 {"request_key": "head_en", "abstract_type": "intro"},
1081 {"request_key": "tail_fr", "abstract_type": "tail"},
1082 {"request_key": "tail_en", "abstract_type": "tail"},
1083 {"request_key": "editor_bio_fr", "abstract_type": "bio_fr"},
1084 {"request_key": "editor_bio_en", "abstract_type": "bio_en"},
1085 ]
1087 special_issue.abstracts = []
1088 abstracts_xml = []
1089 for request_data in request_datas:
1090 lang = request_data["request_key"][-2:]
1091 value_html = request.POST[request_data["request_key"]]
1093 ckeditor_data = build_jats_data_from_html_field(
1094 value_html,
1095 tag="abstract",
1096 text_lang=lang,
1097 resource_lang="en",
1098 field_type=request_data["abstract_type"],
1099 mml_formulas=[],
1100 issue_pid=colid,
1101 pid=special_issue.pid,
1102 )
1104 abstract_data = create_abstract(
1105 tag=request_data["abstract_type"],
1106 lang=lang,
1107 value_html=value_html,
1108 value_tex=ckeditor_data["value_tex"],
1109 value_xml=ckeditor_data["value_xml"],
1110 )
1112 special_issue.abstracts.append(abstract_data)
1113 abstracts_xml.append(ckeditor_data["value_xml"])
1115 figures = self.create_related_objects_from_abstract(
1116 abstracts_xml, colid, special_issue.pid
1117 )
1118 special_issue.related_objects = figures
1120 # This part handle pdf files included in special issue. Can be editor of full pdf version
1121 # Both are stored in same directory
1123 pdf_file_path = resolver.get_disk_location(
1124 f"{settings.RESOURCES_ROOT}",
1125 f"{collection.pid}",
1126 "pdf",
1127 special_issue_pid,
1128 article_id=None,
1129 do_create_folder=False,
1130 )
1131 pdf_path = os.path.dirname(pdf_file_path)
1132 if "pdf" in self.request.FILES: 1132 ↛ 1133line 1132 didn't jump to line 1133 because the condition on line 1132 was never true
1133 if os.path.isfile(f"{pdf_path}/{pid}.pdf"):
1134 os.remove(f"{pdf_path}/{pid}.pdf")
1135 if "edito" in self.request.FILES: 1135 ↛ 1136line 1135 didn't jump to line 1136 because the condition on line 1135 was never true
1136 if os.path.isfile(f"{pdf_path}/{pid}_edito.pdf"):
1137 os.remove(f"{pdf_path}/{pid}_edito.pdf")
1139 if request.POST["pdf_name"] != "No file uploaded": 1139 ↛ 1140line 1139 didn't jump to line 1140 because the condition on line 1139 was never true
1140 if "pdf" in self.request.FILES:
1141 pdf_file_name = self.request.FILES["pdf"].name
1142 location = pdf_path + "/" + special_issue_pid + ".pdf"
1143 with open(location, "wb+") as destination:
1144 for chunk in self.request.FILES["pdf"].chunks():
1145 destination.write(chunk)
1147 else:
1148 pdf_file_name = request.POST["pdf_name"]
1149 location = pdf_path + "/" + special_issue_pid + ".pdf"
1151 pdf_stream_data = create_datastream()
1152 pdf_stream_data["location"] = location.replace("/mersenne_test_data/", "")
1153 pdf_stream_data["mimetype"] = "application/pdf"
1154 pdf_stream_data["rel"] = "full-text"
1155 pdf_stream_data["text"] = pdf_file_name
1156 special_issue.streams.append(pdf_stream_data)
1158 if request.POST["edito_name"] != "No file uploaded": 1158 ↛ 1159line 1158 didn't jump to line 1159 because the condition on line 1158 was never true
1159 if "edito" in self.request.FILES:
1160 location = pdf_path + "/" + special_issue_pid + "_edito.pdf"
1161 edito_file_name = self.request.FILES["edito"].name
1162 edito_display_name = request.POST["edito_display_name"]
1163 with open(location, "wb+") as destination:
1164 for chunk in self.request.FILES["edito"].chunks():
1165 destination.write(chunk)
1166 else:
1167 location = pdf_path + "/" + special_issue_pid + "_edito.pdf"
1168 edito_file_name = request.POST["edito_name"]
1169 edito_display_name = request.POST["edito_display_name"]
1171 location = location.replace("/mersenne_test_data/", "")
1172 data = {
1173 "rel": "edito",
1174 "mimetype": "application/pdf",
1175 "location": location,
1176 "base": None,
1177 "metadata": edito_file_name + "$$$" + edito_display_name,
1178 }
1179 special_issue.related_objects.append(data)
1180 # Handle special issue icon. It is stored in same directory that pdf version or edito.
1181 # The icon is linked to special issue as an ExtLink
1182 if "icon" in request.FILES: 1182 ↛ 1183line 1182 didn't jump to line 1183 because the condition on line 1182 was never true
1183 icon_file = request.FILES["icon"]
1184 relative_file_name = resolver.copy_file_obj_to_article_folder(
1185 icon_file,
1186 collection.pid,
1187 special_issue.pid,
1188 special_issue.pid,
1189 )
1190 if ".tif" in relative_file_name:
1191 jpeg_path = ImageManager(
1192 os.path.join(settings.RESOURCES_ROOT, relative_file_name)
1193 ).to_jpeg()
1194 relative_file_name = str(jpeg_path).replace(settings.RESOURCES_ROOT + "/", "")
1195 data = {
1196 "rel": "icon",
1197 "location": relative_file_name,
1198 "base": None,
1199 "seq": 1,
1200 "metadata": "",
1201 }
1202 special_issue.ext_links.append(data)
1203 elif "icon" in request.POST.keys(): 1203 ↛ 1204line 1203 didn't jump to line 1204 because the condition on line 1203 was never true
1204 if request.POST["icon"] != "[object Object]":
1205 icon_file = request.POST["icon"].replace("/icon/", "")
1206 data = {
1207 "rel": "icon",
1208 "location": icon_file,
1209 "base": None,
1210 "seq": 1,
1211 "metadata": "",
1212 }
1213 special_issue.ext_links.append(data)
1215 special_issue = Munch(special_issue.__dict__)
1216 params = {"xissue": special_issue, "use_body": False}
1217 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params)
1218 cmd.do()
1219 # tail_fr_html = xml_utils.replace_html_entities(request.POST["tail_fr"])
1220 # tail_en_html = xml_utils.replace_html_entities(request.POST["tail_en"])
1221 return redirect("special_issue_edit_api", colid, special_issue.pid)
1223 def create_related_objects_from_abstract(self, abstracts, colid, pid):
1224 figures = []
1225 for abstract in abstracts:
1226 abstract_xml = abstract.encode("utf8")
1228 tree = etree.fromstring(abstract_xml)
1230 pics = tree.xpath("//graphic")
1231 for pic in pics: 1231 ↛ 1232line 1231 didn't jump to line 1232 because the loop on line 1231 never started
1232 base = None
1233 pic_location = pic.attrib["specific-use"]
1234 basename = os.path.basename(pic.attrib["href"])
1235 ext = basename.split(".")[-1]
1236 base = get_media_base_root(colid)
1237 data_location = os.path.join(
1238 settings.RESOURCES_ROOT, "media", base, "uploads", pic_location, basename
1239 )
1240 # we use related objects to send pics to journal site. Directory where pic is stored in trammel may differ
1241 # from the directory in journal site. So one need to save the pic in same directory that journal's one
1242 # so related objects can go for the correct one
1243 img = Image.open(data_location)
1244 final_data_location = os.path.join(
1245 settings.RESOURCES_ROOT, colid, pid, "src", "figures"
1246 )
1247 if not os.path.isdir(final_data_location):
1248 os.makedirs(final_data_location)
1249 relative_path = os.path.join(colid, pid, "src", "figures", basename)
1250 final_data_location = f"{final_data_location}/{basename}"
1251 img.save(final_data_location)
1252 if ext == "png":
1253 mimetype = "image/png"
1254 else:
1255 mimetype = "image/jpeg"
1256 data = {
1257 "rel": "html-image",
1258 "mimetype": mimetype,
1259 "location": relative_path,
1260 "base": base,
1261 "metadata": "",
1262 }
1263 if data not in figures:
1264 figures.append(data)
1265 return figures
1268class PageIndexView(EditorRequiredMixin, TemplateView):
1269 template_name = "mersenne_cms/page_index.html"
1271 def get_context_data(self, **kwargs):
1272 colid = kwargs.get("colid", "")
1273 site_id = model_helpers.get_site_id(colid)
1274 vi = Page.objects.filter(site_id=site_id, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES).first()
1275 if vi: 1275 ↛ 1276line 1275 didn't jump to line 1276 because the condition on line 1275 was never true
1276 pages = Page.objects.filter(site_id=site_id).exclude(parent_page=vi)
1277 else:
1278 pages = Page.objects.filter(site_id=site_id)
1279 context = super().get_context_data(**kwargs)
1280 context["colid"] = colid
1281 context["journal"] = model_helpers.get_collection(colid)
1282 context["pages"] = pages
1283 context["news"] = News.objects.filter(site_id=site_id)
1284 context["fields_lang"] = "fr" if model_helpers.is_site_fr_only(site_id) else "en"
1285 return context
1288class PageBaseView(HandleCMSMixin, View):
1289 template_name = "mersenne_cms/page_form.html"
1290 model = Page
1291 form_class = PageForm
1293 def dispatch(self, request, *args, **kwargs):
1294 self.colid = self.kwargs["colid"]
1295 self.collection = model_helpers.get_collection(self.colid, sites=False)
1296 self.site_id = model_helpers.get_site_id(self.colid)
1298 return super().dispatch(request, *args, **kwargs)
1300 def get_success_url(self):
1301 return reverse("page_index", kwargs={"colid": self.colid})
1303 def get_context_data(self, **kwargs):
1304 context = super().get_context_data(**kwargs)
1305 context["journal"] = self.collection
1306 return context
1308 def update_test_website(self):
1309 response = deploy_cms("test_website", self.collection)
1310 if response.status_code < 300: 1310 ↛ 1313line 1310 didn't jump to line 1313 because the condition on line 1310 was always true
1311 messages.success(self.request, "The test website has been updated")
1312 else:
1313 text = "ERROR: Unable to update the test website<br/>"
1315 if response.status_code == 503:
1316 text += "The test website is under maintenance. Please try again later.<br/>"
1317 else:
1318 text += f"Please contact the centre Mersenne<br/><br/>Status code: {response.status_code}<br/>"
1319 if hasattr(response, "content") and response.content:
1320 text += f"{response.content.decode()}<br/>"
1321 if hasattr(response, "reason") and response.reason:
1322 text += f"Reason: {response.reason}<br/>"
1323 if hasattr(response, "text") and response.text:
1324 text += f"Details: {response.text}<br/>"
1325 messages.error(self.request, mark_safe(text))
1327 def get_form_kwargs(self):
1328 kwargs = super().get_form_kwargs()
1329 kwargs["site_id"] = self.site_id
1330 kwargs["user"] = self.request.user
1331 return kwargs
1333 def form_valid(self, form):
1334 form.save()
1336 self.update_test_website()
1338 return HttpResponseRedirect(self.get_success_url())
1341# @method_decorator([csrf_exempt], name="dispatch")
1342class PageDeleteView(PageBaseView):
1343 def post(self, request, *args, **kwargs):
1344 colid = kwargs.get("colid", "")
1345 pk = kwargs.get("pk")
1346 page = get_object_or_404(Page, id=pk)
1347 if page.mersenne_id:
1348 raise PermissionDenied
1350 page.delete()
1352 self.update_test_website()
1354 if page.parent_page and page.parent_page.mersenne_id == MERSENNE_ID_VIRTUAL_ISSUES:
1355 return HttpResponseRedirect(reverse("virtual_issues_index", kwargs={"colid": colid}))
1356 else:
1357 return HttpResponseRedirect(reverse("page_index", kwargs={"colid": colid}))
1360class PageCreateView(PageBaseView, CreateView):
1361 def get_context_data(self, **kwargs):
1362 context = super().get_context_data(**kwargs)
1363 context["title"] = "Add a menu page"
1364 return context
1367class PageUpdateView(PageBaseView, UpdateView):
1368 def get_context_data(self, **kwargs):
1369 context = super().get_context_data(**kwargs)
1370 context["title"] = "Edit a menu page"
1371 return context
1374class NewsBaseView(PageBaseView):
1375 template_name = "mersenne_cms/news_form.html"
1376 model = News
1377 form_class = NewsForm
1380class NewsDeleteView(NewsBaseView):
1381 def post(self, request, *args, **kwargs):
1382 pk = kwargs.get("pk")
1383 news = get_object_or_404(News, id=pk)
1385 news.delete()
1387 self.update_test_website()
1389 return HttpResponseRedirect(self.get_success_url())
1392class NewsCreateView(NewsBaseView, CreateView):
1393 def get_context_data(self, **kwargs):
1394 context = super().get_context_data(**kwargs)
1395 context["title"] = "Add a News"
1396 return context
1399class NewsUpdateView(NewsBaseView, UpdateView):
1400 def get_context_data(self, **kwargs):
1401 context = super().get_context_data(**kwargs)
1402 context["title"] = "Edit a News"
1403 return context
1406# def page_create_view(request, colid):
1407# context = {}
1408# if not is_authorized_editor(request.user, colid):
1409# raise PermissionDenied
1410# collection = model_helpers.get_collection(colid)
1411# page = Page(site_id=model_helpers.get_site_id(colid))
1412# form = PageForm(request.POST or None, instance=page)
1413# if form.is_valid():
1414# form.save()
1415# response = deploy_cms("test_website", collection)
1416# if response.status_code < 300:
1417# messages.success(request, "Page created successfully.")
1418# else:
1419# text = f"ERROR: page creation failed<br/>Status code: {response.status_code}<br/>"
1420# if hasattr(response, "reason") and response.reason:
1421# text += f"Reason: {response.reason}<br/>"
1422# if hasattr(response, "text") and response.text:
1423# text += f"Details: {response.text}<br/>"
1424# messages.error(request, mark_safe(text))
1425# kwargs = {"colid": colid, "pid": form.instance.id}
1426# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1427#
1428# context["form"] = form
1429# context["title"] = "Add a menu page"
1430# context["journal"] = collection
1431# return render(request, "mersenne_cms/page_form.html", context)
1434# def page_update_view(request, colid, pid):
1435# context = {}
1436# if not is_authorized_editor(request.user, colid):
1437# raise PermissionDenied
1438#
1439# collection = model_helpers.get_collection(colid)
1440# page = get_object_or_404(Page, id=pid)
1441# form = PageForm(request.POST or None, instance=page)
1442# if form.is_valid():
1443# form.save()
1444# response = deploy_cms("test_website", collection)
1445# if response.status_code < 300:
1446# messages.success(request, "Page updated successfully.")
1447# else:
1448# text = f"ERROR: page update failed<br/>Status code: {response.status_code}<br/>"
1449# if hasattr(response, "reason") and response.reason:
1450# text += f"Reason: {response.reason}<br/>"
1451# if hasattr(response, "text") and response.text:
1452# text += f"Details: {response.text}<br/>"
1453# messages.error(request, mark_safe(text))
1454# kwargs = {"colid": colid, "pid": form.instance.id}
1455# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1456#
1457# context["form"] = form
1458# context["pid"] = pid
1459# context["title"] = "Edit a menu page"
1460# context["journal"] = collection
1461# return render(request, "mersenne_cms/page_form.html", context)