Coverage for src/ptf_tools/views/cms_views.py: 48%
878 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-31 09:10 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-31 09:10 +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 if container.trans_title_html: 836 ↛ 839line 836 didn't jump to line 839 because the condition on line 836 was always true
837 data["title"] = container.trans_title_html
838 else:
839 for title in container.title_set.all():
840 if title["lang"] == "fr" and title["type"] == "main":
841 data["title"] = title["title_html"]
842 data["doi"] = container.doi
843 data["trans_title"] = container.title_html
844 data["year"] = container.year
845 data["volume"] = container.volume
846 data["articles"] = [
847 {"doi": article.resource_doi, "citation": article.citation}
848 for article in container.resources_in_special_issue.all().order_by("seq")
849 ]
850 if container.ctype == "issue_special_img": 850 ↛ 851line 850 didn't jump to line 851 because the condition on line 850 was never true
851 data["use_resources_icon"] = True
852 else:
853 data["use_resources_icon"] = False
855 contribs = model_data_converter.db_to_contributors(container.contributions)
856 data["contribs"] = contribs
857 abstract_set = container.abstract_set.all()
858 data["head_fr"] = (
859 abstract_set.filter(tag="intro", lang="fr").first().value_html
860 if abstract_set.filter(tag="intro", lang="fr").exists()
861 else ""
862 )
863 data["head_en"] = (
864 abstract_set.filter(tag="intro", lang="en").first().value_html
865 if abstract_set.filter(tag="intro", lang="en").exists()
866 else ""
867 )
868 data["tail_fr"] = (
869 abstract_set.filter(tag="tail", lang="fr").first().value_html
870 if abstract_set.filter(tag="tail", lang="fr").exists()
871 else ""
872 )
873 data["tail_en"] = (
874 abstract_set.filter(tag="tail", lang="en").first().value_html
875 if abstract_set.filter(tag="tail", lang="en").exists()
876 else ""
877 )
878 data["editor_bio_en"] = (
879 abstract_set.filter(tag="bio_en").first().value_html
880 if abstract_set.filter(tag="bio_en").exists()
881 else ""
882 )
883 data["editor_bio_fr"] = (
884 abstract_set.filter(tag="bio_fr").first().value_html
885 if abstract_set.filter(tag="bio_fr").exists()
886 else ""
887 )
889 streams = container.datastream_set.all()
890 data["pdf_file_name"] = ""
891 data["edito_file_name"] = ""
892 data["edito_display_name"] = ""
893 for stream in streams: # don't work 893 ↛ 894line 893 didn't jump to line 894 because the loop on line 893 never started
894 if os.path.basename(stream.location).split(".")[0] == data["pid"]:
895 data["pdf_file_name"] = stream.text
896 try:
897 # edito related objects metadata contains both file real name and displayed name in issue summary
898 edito_name_infos = container.relatedobject_set.get(rel="edito").metadata.split(
899 "$$$"
900 )
901 data["edito_file_name"] = edito_name_infos[0]
902 data["edito_display_name"] = edito_name_infos[1]
904 except RelatedObject.DoesNotExist:
905 pass
906 try:
907 container_icon = container.extlink_set.get(rel="icon")
909 data["icon_location"] = container_icon.location
910 except ExtLink.DoesNotExist:
911 data["icon_location"] = ""
912 # try:
913 # special_issue_icon = container.extlink_set.get(rel="icon")
914 # data["special_issue_icon"] = special_issue_icon.location
915 # except ExtLink.DoesNotExist:
916 # data["special_issue_icon"] = None
918 else:
919 data["title"] = ""
920 data["doi"] = None
921 data["trans_title"] = ""
922 data["year"] = ""
923 data["volume"] = ""
924 data["articles"] = []
925 data["contribs"] = []
927 data["head_fr"] = ""
928 data["head_en"] = ""
929 data["tail_fr"] = ""
930 data["tail_en"] = ""
931 data["editor_bio_en"] = ""
932 data["editor_bio_fr"] = ""
933 data["pdf_file_name"] = ""
934 data["edito_file_name"] = ""
935 data["use_resources_icon"] = False
937 return JsonResponse(data)
939 def post(self, request, *args, **kwargs):
940 # le but est de faire un IssueDAta
941 pid = kwargs.get("pid", "")
942 colid = kwargs.get("colid", "")
943 journal = collection = model_helpers.get_collection(colid, sites=False)
944 special_issue = create_issuedata()
945 year = request.POST["year"]
946 # TODO 1: the values should be the tex values, not the html ones
947 # TODO 2: In VueJS, titleFr = title
948 trans_title_html = request.POST["title"]
949 title_html = request.POST["trans_title"]
950 issues = collection.content.all().order_by("-year")
951 same_year_issues = issues.filter(year=int(year))
952 if same_year_issues.exists(): 952 ↛ 954line 952 didn't jump to line 954 because the condition on line 952 was always true
953 volume = same_year_issues.first().volume
954 elif issues.exists() and colid != "HOUCHES": # because we don't want a volume for houches
955 ref_volume = issues.filter(year=2024).first().volume
956 volume = int(ref_volume) + (
957 int(year) - 2024
958 ) # 2024 is the ref year for wich we know the volume is 347
959 else:
960 volume = ""
961 if pid != "create":
962 # TODO: do not use the pk, but the pid in the URLs
963 container: Container = get_object_or_404(Container, pid=pid)
964 lang = container.lang
965 trans_lang = container.trans_lang
966 xpub = create_publisherdata()
967 xpub.name = container.my_publisher.pub_name
968 special_issue.provider = container.provider
969 special_issue.number = container.number
970 special_issue_pid = pid
971 special_issue.date_pre_published = container.date_pre_published
972 special_issue.date_published = container.date_published
973 # used for first special issues created withou a proper doi
974 # can be remove when no doi's less special issue existe
975 if not container.doi: 975 ↛ 976line 975 didn't jump to line 976 because the condition on line 975 was never true
976 special_issue.doi = model_helpers.assign_container_doi(colid)
977 else:
978 special_issue.doi = container.doi
979 else:
980 lang = "en"
981 container = None
982 trans_lang = "fr"
983 xpub = create_publisherdata()
984 special_issue.doi = model_helpers.assign_container_doi(colid)
986 if colid == "HOUCHES": 986 ↛ 987line 986 didn't jump to line 987 because the condition on line 986 was never true
987 xpub.name = "UGA Éditions"
988 else:
989 xpub.name = issues.first().my_publisher.pub_name
990 special_issue.provider = collection.provider
992 special_issues = issues.filter(year=year).filter(
993 Q(ctype="issue_special") | Q(ctype="issue") | Q(ctype="issue_special_img")
994 )
995 if special_issues: 995 ↛ 1005line 995 didn't jump to line 1005 because the condition on line 995 was always true
996 all_special_issues_numbers = [
997 int(si.number[1:]) for si in special_issues if si.number[1:].isnumeric()
998 ]
999 if len(all_special_issues_numbers) > 0:
1000 max_number = max(all_special_issues_numbers)
1001 else:
1002 max_number = 0
1004 else:
1005 max_number = 0
1006 special_issue.number = f"S{max_number + 1}"
1007 special_issue_pid = f"{colid}_{year}__{volume}_{special_issue.number}"
1009 if request.POST["use_resources_icon"] == "true": 1009 ↛ 1010line 1009 didn't jump to line 1010 because the condition on line 1009 was never true
1010 special_issue.ctype = "issue_special_img"
1011 else:
1012 special_issue.ctype = "issue_special"
1014 existing_issue = model_helpers.get_resource(special_issue_pid)
1015 if pid == "create" and existing_issue is not None: 1015 ↛ 1016line 1015 didn't jump to line 1016 because the condition on line 1015 was never true
1016 raise ValueError(f"The special issue with the pid {special_issue_pid} already exists")
1018 special_issue.lang = lang
1019 special_issue.title_html = title_html
1020 special_issue.title_xml = build_title_xml(
1021 title=title_html, lang=lang, title_type="issue-title"
1022 )
1024 special_issue.trans_lang = trans_lang
1025 special_issue.trans_title_html = trans_title_html
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)