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