Coverage for src/ptf_tools/views/cms_views.py: 48%
879 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-09-08 12:26 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-09-08 12:26 +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.ckeditor_parser import CkeditorParser
48from ptf.cmds.xml.jats.builder.issue import build_title_xml, get_abstract_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_contributor,
57 create_datastream,
58 create_issuedata,
59 create_publisherdata,
60 create_titledata,
61)
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 ImageManager, 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 issues_articles, collection = model_helpers.get_issues_in_volume(vid)
486 context = super().get_context_data(**kwargs)
487 context["issues_articles"] = issues_articles
488 context["collection"] = collection
489 return context
492class EditorialToolsArticleView(EditorRequiredMixin, TemplateView):
493 template_name = "editorial_tools/find-article.html"
495 def get_context_data(self, **kwargs):
496 colid = kwargs.get("colid")
497 doi = kwargs.get("doi")
498 article = get_object_or_404(Article, doi=doi, my_container__my_collection__pid=colid)
500 context = super().get_context_data(**kwargs)
501 context["article"] = article
502 context["citation_base"] = article.get_citation_base().strip(", .")
503 return context
506class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView):
507 template_name = "editorial_tools/graphical-abstract.html"
509 def get_context_data(self, **kwargs):
510 doi = kwargs.get("doi", "")
511 article = get_object_or_404(Article, doi=doi)
513 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
514 context = super().get_context_data(**kwargs)
515 context["author"] = "; ".join(get_names(article, "author"))
516 context["citation_base"] = article.get_citation_base().strip(", .")
517 context["article"] = article
518 context["date_modified"] = obj.date_modified
519 context["form"] = GraphicalAbstractForm(instance=obj)
520 context["graphical_abstract"] = obj.graphical_abstract
521 context["illustration"] = obj.illustration
522 return context
525class GraphicalAbstractDeployView(EditorRequiredMixin, View):
526 def __get_path_and_replace_tiff_file(self, obj_attribute_file):
527 """
528 Returns the path of the attribute.
529 If the attribute is a tiff file, converts it to jpg, delete the old tiff file, and return the new path of the attribute.
531 Checks if paths have already been processed, to prevent issues related to object mutation.
532 """
533 if obj_attribute_file.name.lower().endswith((".tiff", ".tif")):
534 jpeg_path = ImageManager(obj_attribute_file.path).to_jpeg(delete_original_tiff=True)
535 with open(jpeg_path, "rb") as fp:
536 obj_attribute_file.save(os.path.basename(jpeg_path), File(fp), save=True)
537 return jpeg_path
539 return obj_attribute_file.path
541 def post(self, request, *args, **kwargs):
542 doi = kwargs.get("doi", "")
543 site = kwargs.get("site", "both")
544 article = get_object_or_404(Article, doi=doi)
546 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
547 form = GraphicalAbstractForm(request.POST, request.FILES or None, instance=obj)
548 if form.is_valid(): 548 ↛ 577line 548 didn't jump to line 577 because the condition on line 548 was always true
549 obj.date_modified = timezone.now()
550 data = {"date_modified": obj.date_modified}
551 form.save()
552 files = {}
554 for attribute in ("graphical_abstract", "illustration"):
555 obj_attribute_file = getattr(obj, attribute, None)
556 if obj_attribute_file and os.path.exists(obj_attribute_file.path): 556 ↛ 557line 556 didn't jump to line 557 because the condition on line 556 was never true
557 file_path = self.__get_path_and_replace_tiff_file(obj_attribute_file)
558 with open(file_path, "rb") as fp:
559 files.update({attribute: (obj_attribute_file.name, fp.read())})
561 collection = article.my_container.my_collection
562 urls = get_server_urls(collection, site=site)
563 response = requests.models.Response()
564 for url in urls: 564 ↛ 575line 564 didn't jump to line 575 because the loop on line 564 didn't complete
565 url = url + reverse("api-graphical-abstract", kwargs={"doi": doi})
566 try:
567 if not obj.graphical_abstract and not obj.illustration: 567 ↛ 570line 567 didn't jump to line 570 because the condition on line 567 was always true
568 response = requests.delete(url, data=data, files=files, timeout=15)
569 else:
570 response = requests.post(url, data=data, files=files, timeout=15)
571 except requests.exceptions.RequestException as e:
572 response.status_code = 503
573 response.reason = e.args[0]
574 break
575 return HttpResponse(status=response.status_code, reason=response.reason)
576 else:
577 return HttpResponseBadRequest()
580def parse_content(content):
581 table = re.search(r'(.*?)(<table id="summary".+?</table>)(.*)', content, re.DOTALL)
582 if not table:
583 return {"head": content, "tail": "", "articles": []}
585 articles = []
586 rows = re.findall(r"<tr>.+?</tr>", table.group(2), re.DOTALL)
587 for row in rows:
588 citation = re.search(r'<div href=".*?">(.*?)</div>', row, re.DOTALL)
589 href = re.search(r'href="(.+?)\/?">', row)
590 doi = re.search(r"(10[.].+)", href.group(1)) if href else ""
591 src = re.search(r'<img.+?src="(.+?)"', row)
592 item = {}
593 item["citation"] = citation.group(1) if citation else ""
594 item["doi"] = doi.group(1) if doi else href.group(1) if href else ""
595 item["src"] = src.group(1) if src else ""
596 item["imageName"] = item["src"].split("/")[-1] if item["src"] else ""
597 if item["doi"] or item["src"]:
598 articles.append(item)
599 return {"head": table.group(1), "tail": table.group(3), "articles": articles}
602class VirtualIssueParseView(EditorRequiredMixin, View):
603 def get(self, request, *args, **kwargs):
604 pid = kwargs.get("pid", "")
605 page = get_object_or_404(Page, id=pid)
607 data = {"pid": pid}
608 data["colid"] = kwargs.get("colid", "")
609 journal = model_helpers.get_collection(data["colid"])
610 data["journal_title"] = journal.title_tex.replace(".", "")
611 site_id = model_helpers.get_site_id(data["colid"])
612 data["page"] = model_to_dict(page)
613 pages = Page.objects.filter(site_id=site_id).exclude(id=pid)
614 data["parents"] = [model_to_dict(p, fields=["id", "menu_title"]) for p in pages]
616 content_fr = parse_content(page.content_fr)
617 data["head_fr"] = content_fr["head"]
618 data["tail_fr"] = content_fr["tail"]
620 content_en = parse_content(page.content_en)
621 data["articles"] = content_en["articles"]
622 data["head_en"] = content_en["head"]
623 data["tail_en"] = content_en["tail"]
624 return JsonResponse(data)
627class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView):
628 template_name = "editorial_tools/virtual-issue.html"
630 def get(self, request, *args, **kwargs):
631 pid = kwargs.get("pid", "")
632 get_object_or_404(Page, id=pid)
633 return super().get(request, *args, **kwargs)
636class VirtualIssueCreateView(EditorRequiredMixin, View):
637 def get(self, request, *args, **kwargs):
638 colid = kwargs.get("colid", "")
639 site_id = model_helpers.get_site_id(colid)
640 parent, _ = Page.objects.get_or_create(
641 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES,
642 parent_page=None,
643 site_id=site_id,
644 )
645 page = Page.objects.create(
646 menu_title_en="New virtual issue",
647 menu_title_fr="Nouvelle collection transverse",
648 parent_page=parent,
649 site_id=site_id,
650 state="draft",
651 )
652 kwargs = {"colid": colid, "pid": page.id}
653 return HttpResponseRedirect(reverse("virtual_issue_update", kwargs=kwargs))
656class SpecialIssuesIndex(EditorRequiredMixin, TemplateView):
657 template_name = "editorial_tools/special-issues-index.html"
659 def get_context_data(self, **kwargs):
660 colid = kwargs.get("colid", "")
662 context = super().get_context_data(**kwargs)
663 context["colid"] = colid
664 collection = Collection.objects.get(pid=colid)
665 context["special_issues"] = Container.objects.filter(
666 Q(ctype="issue_special") | Q(ctype="issue_special_img")
667 ).filter(my_collection=collection)
669 context["journal"] = model_helpers.get_collection(colid, sites=False)
670 return context
673class SpecialIssueEditView(EditorRequiredMixin, TemplateView):
674 template_name = "editorial_tools/special-issue-edit.html"
676 def get_context_data(self, **kwargs):
677 context = super().get_context_data(**kwargs)
678 return context
681class VirtualIssuesIndex(EditorRequiredMixin, TemplateView):
682 template_name = "editorial_tools/virtual-issues-index.html"
684 def get_context_data(self, **kwargs):
685 colid = kwargs.get("colid", "")
686 site_id = model_helpers.get_site_id(colid)
687 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES)
688 pages = Page.objects.filter(site_id=site_id, parent_page=vi)
689 context = super().get_context_data(**kwargs)
690 context["journal"] = model_helpers.get_collection(colid)
691 context["pages"] = pages
692 return context
695def get_citation_fr(doi, citation_en):
696 citation_fr = citation_en
697 article = Article.objects.filter(doi=doi).first()
698 if article and article.trans_title_html:
699 trans_title = article.trans_title_html
700 try:
701 citation_fr = re.sub(
702 r'(<a href="https:\/\/doi\.org.*">)([^<]+)',
703 rf"\1{trans_title}",
704 citation_en,
705 )
706 except re.error:
707 pass
708 return citation_fr
711def summary_build(articles, colid):
712 summary_fr = ""
713 summary_en = ""
714 head = '<table id="summary"><tbody>'
715 tail = "</tbody></table>"
716 style = "max-width:180px;max-height:200px"
717 colid_lo = colid.lower()
718 site_domain = SITE_REGISTER[colid_lo]["site_domain"].split("/")
719 site_domain = "/" + site_domain[-1] if len(site_domain) == 2 else ""
721 for article in articles:
722 image_src = article.get("src", "")
723 image_name = article.get("imageName", "")
724 doi = article.get("doi", "")
725 citation_en = article.get("citation", "")
726 if doi or citation_en:
727 row_fr = f'<div href="{doi}">{get_citation_fr(doi, citation_en)}</div>'
728 row_en = f'<div href="{doi}">{citation_en}</div>'
729 if image_src:
730 date = datetime.now().strftime("%Y/%m/%d/")
731 base_url = get_media_base_url(colid)
732 suffix = os.path.join(base_url, "uploads", date)
733 image_url = os.path.join(site_domain, suffix, image_name)
734 image_header = "^data:image/.+;base64,"
735 if re.match(image_header, image_src):
736 image_src = re.sub(image_header, "", image_src)
737 base64_data = base64.b64decode(image_src)
738 base_root = get_media_base_root(colid)
739 path = os.path.join(base_root, "uploads", date)
740 os.makedirs(path, exist_ok=True)
741 with open(path + image_name, "wb") as fp:
742 fp.write(base64_data)
743 im = f'<img src="{image_url}" style="{style}" />'
744 # TODO mettre la vrai valeur pour le SITE_DOMAIN
745 elif settings.SITE_DOMAIN == "http://127.0.0.1:8002":
746 im = f'<img src="{image_src}" style="{style}" />'
747 else:
748 im = f'<img src="{site_domain}{image_src}" style="{style}" />'
749 summary_fr += f"<tr><td>{im}</td><td>{row_fr}</td></tr>"
750 summary_en += f"<tr><td>{im}</td><td>{row_en}</td></tr>"
751 summary_fr = head + summary_fr + tail
752 summary_en = head + summary_en + tail
753 return {"summary_fr": summary_fr, "summary_en": summary_en}
756# @method_decorator([csrf_exempt], name="dispatch")
757class VirtualIssueDeployView(HandleCMSMixin, View):
758 """
759 called by the Virtual.vue VueJS component, when the virtual issue is saved
760 We get data in JSON and we need to update the corresponding Page.
761 The Page is then immediately posted to the test_website.
762 The "Apply the changes to the production website" button is then used to update the (prod) website
763 => See DeployCMSAPIView
764 """
766 def post(self, request, *args, **kwargs):
767 self.init_data(self.kwargs)
768 if check_lock():
769 msg = "Trammel is under maintenance. Please try again later."
770 messages.error(self.request, msg)
771 return JsonResponse({"messages": msg, "status": 503})
773 pid = kwargs.get("pid")
774 colid = self.colid
775 data = json.loads(request.body)
776 summary = summary_build(data["articles"], colid)
777 page = get_object_or_404(Page, id=pid)
778 page.slug = page.slug_fr = page.slug_en = None
779 page.menu_title_fr = data["title_fr"]
780 page.menu_title_en = data["title_en"]
781 page.content_fr = data["head_fr"] + summary["summary_fr"] + data["tail_fr"]
782 page.content_en = data["head_en"] + summary["summary_en"] + data["tail_en"]
783 page.state = data["page"]["state"]
784 page.menu_order = data["page"]["menu_order"]
785 page.parent_page = Page.objects.filter(id=data["page"]["parent_page"]).first()
786 page.save()
788 response = deploy_cms("test_website", self.collection)
789 if response.status_code == 503:
790 messages.error(
791 self.request, "The journal website is under maintenance. Please try again later."
792 )
794 return response # HttpResponse(status=response.status_code, reason=response.reason)
797class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView):
798 template_name = "editorial_tools/special-issue-edit.html"
800 def get_context_data(self, **kwargs):
801 context = super().get_context_data(**kwargs)
802 return context
804 def set_contrib_addresses(self, contrib, contribution):
805 for address in contrib:
806 contrib_address = ContribAddress(contribution=contribution, address=address)
807 contrib_address.save()
809 def delete(self, pid):
810 special_issue = Container.objects.get(pid=pid)
811 cmd = base_ptf_cmds.addContainerPtfCmd()
812 cmd.set_object_to_be_deleted(special_issue)
813 cmd.undo()
815 def get(self, request, *args, **kwargs):
816 pid = kwargs.get("pid", "")
818 data = {"pid": pid}
819 colid = kwargs.get("colid", "")
820 data["colid"] = colid
821 journal = model_helpers.get_collection(colid, sites=False)
822 name = resolve(request.path_info).url_name
823 if name == "special_issue_delete": 823 ↛ 824line 823 didn't jump to line 824 because the condition on line 823 was never true
824 self.delete(pid)
825 return redirect("special_issues_index", data["colid"])
827 data["journal_title"] = journal.title_tex.replace(".", "")
829 if pid != "create":
830 container = get_object_or_404(Container, pid=pid)
831 # TODO: pass the lang and trans_lang as well
832 # In VueJS (Special.vu)e, titleFr = title_html
833 # June 2025: Title objects are added for translated titles
834 # keep using trans_title_html for backward compatibility
835 if container.trans_title_html: 835 ↛ 838line 835 didn't jump to line 838 because the condition on line 835 was always true
836 data["title"] = container.trans_title_html
837 else:
838 for title in container.title_set.all():
839 if title["lang"] == "fr" and title["type"] == "main":
840 data["title"] = title["title_html"]
841 data["doi"] = container.doi
842 data["trans_title"] = container.title_html
843 data["year"] = container.year
844 data["volume"] = container.volume
845 data["articles"] = [
846 {"doi": article.resource_doi, "citation": article.citation}
847 for article in container.resources_in_special_issue.all().order_by("seq")
848 ]
849 if container.ctype == "issue_special_img": 849 ↛ 850line 849 didn't jump to line 850 because the condition on line 849 was never true
850 data["use_resources_icon"] = True
851 else:
852 data["use_resources_icon"] = False
854 contribs = model_data_converter.db_to_contributors(container.contributions)
855 data["contribs"] = contribs
856 abstract_set = container.abstract_set.all()
857 data["head_fr"] = (
858 abstract_set.filter(tag="head_fr").first().value_html
859 if abstract_set.filter(tag="head_fr").exists()
860 else ""
861 )
862 data["head_en"] = (
863 abstract_set.filter(tag="head_en").first().value_html
864 if abstract_set.filter(tag="head_en").exists()
865 else ""
866 )
867 data["tail_fr"] = (
868 abstract_set.filter(tag="tail_fr").first().value_html
869 if abstract_set.filter(tag="tail_fr").exists()
870 else ""
871 )
872 data["tail_en"] = (
873 abstract_set.filter(tag="tail_en").first().value_html
874 if abstract_set.filter(tag="tail_en").exists()
875 else ""
876 )
877 data["editor_bio_en"] = (
878 abstract_set.filter(tag="bio_en").first().value_html
879 if abstract_set.filter(tag="bio_en").exists()
880 else ""
881 )
882 data["editor_bio_fr"] = (
883 abstract_set.filter(tag="bio_fr").first().value_html
884 if abstract_set.filter(tag="bio_fr").exists()
885 else ""
886 )
888 streams = container.datastream_set.all()
889 data["pdf_file_name"] = ""
890 data["edito_file_name"] = ""
891 data["edito_display_name"] = ""
892 for stream in streams: # don't work 892 ↛ 893line 892 didn't jump to line 893 because the loop on line 892 never started
893 if os.path.basename(stream.location).split(".")[0] == data["pid"]:
894 data["pdf_file_name"] = stream.text
895 try:
896 # edito related objects metadata contains both file real name and displayed name in issue summary
897 edito_name_infos = container.relatedobject_set.get(rel="edito").metadata.split(
898 "$$$"
899 )
900 data["edito_file_name"] = edito_name_infos[0]
901 data["edito_display_name"] = edito_name_infos[1]
903 except RelatedObject.DoesNotExist:
904 pass
905 try:
906 container_icon = container.extlink_set.get(rel="icon")
908 data["icon_location"] = container_icon.location
909 except ExtLink.DoesNotExist:
910 data["icon_location"] = ""
911 # try:
912 # special_issue_icon = container.extlink_set.get(rel="icon")
913 # data["special_issue_icon"] = special_issue_icon.location
914 # except ExtLink.DoesNotExist:
915 # data["special_issue_icon"] = None
917 else:
918 data["title"] = ""
919 data["doi"] = None
920 data["trans_title"] = ""
921 data["year"] = ""
922 data["volume"] = ""
923 data["articles"] = []
924 data["contribs"] = []
926 data["head_fr"] = ""
927 data["head_en"] = ""
928 data["tail_fr"] = ""
929 data["tail_en"] = ""
930 data["editor_bio_en"] = ""
931 data["editor_bio_fr"] = ""
932 data["pdf_file_name"] = ""
933 data["edito_file_name"] = ""
934 data["use_resources_icon"] = False
936 return JsonResponse(data)
938 def post(self, request, *args, **kwargs):
939 # le but est de faire un IssueDAta
940 pid = kwargs.get("pid", "")
941 colid = kwargs.get("colid", "")
942 journal = collection = model_helpers.get_collection(colid, sites=False)
943 special_issue = create_issuedata()
944 year = request.POST["year"]
945 # TODO 1: the values should be the tex values, not the html ones
946 # TODO 2: In VueJS, titleFr = title
947 trans_title_html = request.POST["title"]
948 title_html = request.POST["trans_title"]
949 if pid != "create":
950 # TODO: do not use the pk, but the pid in the URLs
951 container = get_object_or_404(Container, pid=pid)
952 lang = container.lang
953 trans_lang = container.trans_lang
954 xpub = create_publisherdata()
955 xpub.name = container.my_publisher.pid
956 special_issue.provider = container.provider
957 special_issue.number = container.number
958 volume = container.volume
959 special_issue_pid = pid
960 special_issue.date_pre_published = container.date_pre_published
961 special_issue.date_published = container.date_published
962 # used for first special issues created withou a proper doi
963 # can be remove when no doi's less special issue existe
964 if not container.doi: 964 ↛ 965line 964 didn't jump to line 965 because the condition on line 964 was never true
965 special_issue.doi = model_helpers.assign_container_doi(colid)
966 else:
967 special_issue.doi = container.doi
968 else:
969 lang = "en"
970 container = None
971 trans_lang = "fr"
972 xpub = create_publisherdata()
973 special_issue.doi = model_helpers.assign_container_doi(colid)
974 volume = ""
975 issues = collection.content.all().order_by("-year")
976 # if cras_issues.exists():
977 same_year_issues = issues.filter(year=int(year))
978 if same_year_issues.exists(): 978 ↛ 980line 978 didn't jump to line 980 because the condition on line 978 was always true
979 volume = same_year_issues.first().volume
980 elif (
981 issues.exists() and colid != "HOUCHES"
982 ): # because we don't want a volume for houches
983 volume = str(int(issues.first().volume) + 1)
984 else:
985 volume = ""
986 # issues = model_helpers.get_volumes_in_collection(collection, get_special_issues=True)
987 # if issues["sorted_issues"]:
988 # volumes = issues["sorted_issues"][0]["volumes"]
989 # for v in volumes:
990 # if v["fyear"] == int(year):
991 # volume = v["volume"]
992 # break
994 if colid == "HOUCHES": 994 ↛ 995line 994 didn't jump to line 995 because the condition on line 994 was never true
995 xpub.name = "UGA Éditions"
996 else:
997 xpub.name = issues.first().my_publisher.pid
998 # xpub.name = parent_container.my_publisher.pid
999 special_issue.provider = collection.provider
1001 special_issues = issues.filter(year=year).filter(
1002 Q(ctype="issue_special") | Q(ctype="issue") | Q(ctype="issue_special_img")
1003 )
1004 if special_issues: 1004 ↛ 1014line 1004 didn't jump to line 1014 because the condition on line 1004 was always true
1005 all_special_issues_numbers = [
1006 int(si.number[1:]) for si in special_issues if si.number[1:].isnumeric()
1007 ]
1008 if len(all_special_issues_numbers) > 0:
1009 max_number = max(all_special_issues_numbers)
1010 else:
1011 max_number = 0
1013 else:
1014 max_number = 0
1015 special_issue.number = f"S{max_number + 1}"
1016 special_issue_pid = f"{colid}_{year}__{volume}_{special_issue.number}"
1018 if request.POST["use_resources_icon"] == "true": 1018 ↛ 1019line 1018 didn't jump to line 1019 because the condition on line 1018 was never true
1019 special_issue.ctype = "issue_special_img"
1020 else:
1021 special_issue.ctype = "issue_special"
1023 existing_issue = model_helpers.get_resource(special_issue_pid)
1024 if pid == "create" and existing_issue is not None: 1024 ↛ 1025line 1024 didn't jump to line 1025 because the condition on line 1024 was never true
1025 raise ValueError(f"The special issue with the pid {special_issue_pid} already exists")
1027 special_issue.lang = lang
1028 special_issue.title_html = title_html
1029 special_issue.title_xml = build_title_xml(
1030 title=title_html, lang=lang, title_type="issue-title"
1031 )
1033 special_issue.trans_lang = trans_lang
1034 special_issue.trans_title_html = trans_title_html
1035 title_xml = build_title_xml(
1036 title=trans_title_html, lang=trans_lang, title_type="issue-title"
1037 )
1038 title = create_titledata(
1039 lang=trans_lang, type="main", title_html=trans_title_html, title_xml=title_xml
1040 )
1041 special_issue.titles = [title]
1043 special_issue.year = year
1044 special_issue.volume = volume
1045 special_issue.journal = journal
1046 special_issue.publisher = xpub
1047 special_issue.pid = special_issue_pid
1048 special_issue.last_modified_iso_8601_date_str = datetime.now().strftime(
1049 "%Y-%m-%d %H:%M:%S"
1050 )
1052 articles = []
1053 contribs = []
1054 index = 0
1056 if "nb_articles" in request.POST.keys():
1057 while index < int(request.POST["nb_articles"]):
1058 article = json.loads(request.POST[f"article[{index}]"])
1059 article["citation"] = xml_utils.replace_html_entities(article["citation"])
1060 # if not article["citation"]:
1061 # index += 1
1062 # continue
1063 articles.append(article)
1065 index += 1
1067 special_issue.articles = [Munch(article) for article in articles]
1068 index = 0
1069 # TODO make a function to call to add a contributor
1070 if "nb_contrib" in request.POST.keys():
1071 while index < int(request.POST["nb_contrib"]):
1072 contrib = json.loads(request.POST[f"contrib[{index}]"])
1073 contributor = create_contributor()
1074 contributor["first_name"] = contrib["first_name"]
1075 contributor["last_name"] = contrib["last_name"]
1076 contributor["orcid"] = contrib["orcid"]
1077 contributor["role"] = "editor"
1079 contrib_xml = xml_utils.get_contrib_xml(contrib)
1080 contributor["contrib_xml"] = contrib_xml
1081 contribs.append(Munch(contributor))
1082 index += 1
1083 special_issue.contributors = contribs
1085 # Part of the code that handle forwords and lastwords
1087 xhead_fr, head_fr_xml = self.create_abstract_from_vuejs(
1088 request.POST["head_fr"], "fr", "intro", colid, special_issue_pid
1089 )
1090 xtail_fr, tail_fr_xml = self.create_abstract_from_vuejs(
1091 request.POST["tail_fr"], "fr", "tail", colid, special_issue_pid
1092 )
1093 xhead_en, head_en_xml = self.create_abstract_from_vuejs(
1094 request.POST["head_en"], "en", "intro", colid, special_issue_pid
1095 )
1097 xtail_en, tail_en_xml = self.create_abstract_from_vuejs(
1098 request.POST["tail_en"], "en", "tail", colid, special_issue_pid
1099 )
1101 xeditor_bio_en, editor_bio_en_xml = self.create_abstract_from_vuejs(
1102 request.POST["editor_bio_en"], "en", "bio_en", colid, special_issue_pid
1103 )
1105 xeditor_bio_fr, editor_bio_fr_xml = self.create_abstract_from_vuejs(
1106 request.POST["editor_bio_fr"], "fr", "bio_fr", colid, special_issue_pid
1107 )
1109 abstracts = [
1110 head_fr_xml,
1111 head_en_xml,
1112 tail_fr_xml,
1113 tail_en_xml,
1114 editor_bio_fr_xml,
1115 editor_bio_en_xml,
1116 ]
1117 figures = self.create_related_objects_from_abstract(abstracts, colid, special_issue.pid)
1118 special_issue.related_objects = figures
1119 # TODO can be factorized?
1120 special_issue.abstracts = [
1121 {
1122 "tag": "head_fr",
1123 "lang": "fr",
1124 "value_html": xhead_fr.value_html,
1125 "value_tex": xhead_fr.value_tex,
1126 "value_xml": head_fr_xml,
1127 },
1128 {
1129 "tag": "head_en",
1130 "lang": "en",
1131 "value_html": xhead_en.value_html,
1132 "value_tex": xhead_en.value_tex,
1133 "value_xml": head_en_xml,
1134 },
1135 {
1136 "tag": "tail_fr",
1137 "lang": "fr",
1138 "value_html": xtail_fr.value_html,
1139 "value_tex": xtail_fr.value_tex,
1140 "value_xml": tail_fr_xml,
1141 },
1142 {
1143 "tag": "tail_en",
1144 "lang": "en",
1145 "value_html": xtail_en.value_html,
1146 "value_tex": xtail_en.value_tex,
1147 "value_xml": tail_en_xml,
1148 },
1149 {
1150 "tag": "bio_en",
1151 "lang": "en",
1152 "value_html": xeditor_bio_en.value_html,
1153 "value_tex": xeditor_bio_en.value_tex,
1154 "value_xml": editor_bio_en_xml,
1155 },
1156 {
1157 "tag": "bio_fr",
1158 "lang": "fr",
1159 "value_html": xeditor_bio_fr.value_html,
1160 "value_tex": xeditor_bio_fr.value_tex,
1161 "value_xml": editor_bio_fr_xml,
1162 },
1163 ]
1165 # This part handle pdf files included in special issue. Can be editor of full pdf version
1166 # Both are stored in same directory
1168 pdf_file_path = resolver.get_disk_location(
1169 f"{settings.RESOURCES_ROOT}",
1170 f"{collection.pid}",
1171 "pdf",
1172 special_issue_pid,
1173 article_id=None,
1174 do_create_folder=False,
1175 )
1176 pdf_path = os.path.dirname(pdf_file_path)
1177 if "pdf" in self.request.FILES: 1177 ↛ 1178line 1177 didn't jump to line 1178 because the condition on line 1177 was never true
1178 if os.path.isfile(f"{pdf_path}/{pid}.pdf"):
1179 os.remove(f"{pdf_path}/{pid}.pdf")
1180 if "edito" in self.request.FILES: 1180 ↛ 1181line 1180 didn't jump to line 1181 because the condition on line 1180 was never true
1181 if os.path.isfile(f"{pdf_path}/{pid}_edito.pdf"):
1182 os.remove(f"{pdf_path}/{pid}_edito.pdf")
1184 if request.POST["pdf_name"] != "No file uploaded": 1184 ↛ 1185line 1184 didn't jump to line 1185 because the condition on line 1184 was never true
1185 if "pdf" in self.request.FILES:
1186 pdf_file_name = self.request.FILES["pdf"].name
1187 location = pdf_path + "/" + special_issue_pid + ".pdf"
1188 with open(location, "wb+") as destination:
1189 for chunk in self.request.FILES["pdf"].chunks():
1190 destination.write(chunk)
1192 else:
1193 pdf_file_name = request.POST["pdf_name"]
1194 location = pdf_path + "/" + special_issue_pid + ".pdf"
1196 pdf_stream_data = create_datastream()
1197 pdf_stream_data["location"] = location.replace("/mersenne_test_data/", "")
1198 pdf_stream_data["mimetype"] = "application/pdf"
1199 pdf_stream_data["rel"] = "full-text"
1200 pdf_stream_data["text"] = pdf_file_name
1201 special_issue.streams.append(pdf_stream_data)
1203 if request.POST["edito_name"] != "No file uploaded": 1203 ↛ 1204line 1203 didn't jump to line 1204 because the condition on line 1203 was never true
1204 if "edito" in self.request.FILES:
1205 location = pdf_path + "/" + special_issue_pid + "_edito.pdf"
1206 edito_file_name = self.request.FILES["edito"].name
1207 edito_display_name = request.POST["edito_display_name"]
1208 with open(location, "wb+") as destination:
1209 for chunk in self.request.FILES["edito"].chunks():
1210 destination.write(chunk)
1211 else:
1212 location = pdf_path + "/" + special_issue_pid + "_edito.pdf"
1213 edito_file_name = request.POST["edito_name"]
1214 edito_display_name = request.POST["edito_display_name"]
1216 location = location.replace("/mersenne_test_data/", "")
1217 data = {
1218 "rel": "edito",
1219 "mimetype": "application/pdf",
1220 "location": location,
1221 "base": None,
1222 "metadata": edito_file_name + "$$$" + edito_display_name,
1223 }
1224 special_issue.related_objects.append(data)
1225 # Handle special issue icon. It is stored in same directory that pdf version or edito.
1226 # The icon is linked to special issue as an ExtLink
1227 if "icon" in request.FILES: 1227 ↛ 1228line 1227 didn't jump to line 1228 because the condition on line 1227 was never true
1228 icon_file = request.FILES["icon"]
1229 relative_file_name = resolver.copy_file_obj_to_article_folder(
1230 icon_file,
1231 collection.pid,
1232 special_issue.pid,
1233 special_issue.pid,
1234 )
1235 data = {
1236 "rel": "icon",
1237 "location": relative_file_name,
1238 "base": None,
1239 "seq": 1,
1240 "metadata": "",
1241 }
1242 special_issue.ext_links.append(data)
1243 elif "icon" in request.POST.keys(): 1243 ↛ 1244line 1243 didn't jump to line 1244 because the condition on line 1243 was never true
1244 if request.POST["icon"] != "[object Object]":
1245 icon_file = request.POST["icon"].replace("/icon/", "")
1246 data = {
1247 "rel": "icon",
1248 "location": icon_file,
1249 "base": None,
1250 "seq": 1,
1251 "metadata": "",
1252 }
1253 special_issue.ext_links.append(data)
1255 special_issue = Munch(special_issue.__dict__)
1256 params = {"xissue": special_issue, "use_body": False}
1257 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params)
1258 cmd.do()
1259 # tail_fr_html = xml_utils.replace_html_entities(request.POST["tail_fr"])
1260 # tail_en_html = xml_utils.replace_html_entities(request.POST["tail_en"])
1261 return redirect("special_issue_edit_api", colid, special_issue.pid)
1263 def create_abstract_from_vuejs(self, abstract, lang, position, colid, pid):
1264 abstract_html = xml_utils.replace_html_entities(abstract)
1265 xabstract = CkeditorParser(
1266 html_value=abstract_html, issue_pid=colid, pid=pid, mml_formulas=[]
1267 )
1268 abstract_xml = get_abstract_xml(xabstract.value_xml, lang, position)
1269 return xabstract, abstract_xml
1271 def create_related_objects_from_abstract(self, abstracts, colid, pid):
1272 figures = []
1273 for abstract in abstracts:
1274 abstract_xml = abstract.encode("utf8")
1276 tree = etree.fromstring(abstract_xml)
1278 pics = tree.xpath("//graphic")
1279 for pic in pics: 1279 ↛ 1280line 1279 didn't jump to line 1280 because the loop on line 1279 never started
1280 base = None
1281 pic_location = pic.attrib["specific-use"]
1282 basename = os.path.basename(pic.attrib["href"])
1283 ext = basename.split(".")[-1]
1284 base = get_media_base_root(colid)
1285 data_location = os.path.join(
1286 settings.RESOURCES_ROOT, "media", base, "uploads", pic_location, basename
1287 )
1288 # we use related objects to send pics to journal site. Directory where pic is stored in trammel may differ
1289 # from the directory in journal site. So one need to save the pic in same directory that journal's one
1290 # so related objects can go for the correct one
1291 img = Image.open(data_location)
1292 final_data_location = os.path.join(
1293 settings.RESOURCES_ROOT, colid, pid, "src", "figures"
1294 )
1295 if not os.path.isdir(final_data_location):
1296 os.makedirs(final_data_location)
1297 relative_path = os.path.join(colid, pid, "src", "figures", basename)
1298 final_data_location = f"{final_data_location}/{basename}"
1299 img.save(final_data_location)
1300 if ext == "png":
1301 mimetype = "image/png"
1302 else:
1303 mimetype = "image/jpeg"
1304 data = {
1305 "rel": "html-image",
1306 "mimetype": mimetype,
1307 "location": relative_path,
1308 "base": base,
1309 "metadata": "",
1310 }
1311 if data not in figures:
1312 figures.append(data)
1313 return figures
1316class PageIndexView(EditorRequiredMixin, TemplateView):
1317 template_name = "mersenne_cms/page_index.html"
1319 def get_context_data(self, **kwargs):
1320 colid = kwargs.get("colid", "")
1321 site_id = model_helpers.get_site_id(colid)
1322 vi = Page.objects.filter(site_id=site_id, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES).first()
1323 if vi: 1323 ↛ 1324line 1323 didn't jump to line 1324 because the condition on line 1323 was never true
1324 pages = Page.objects.filter(site_id=site_id).exclude(parent_page=vi)
1325 else:
1326 pages = Page.objects.filter(site_id=site_id)
1327 context = super().get_context_data(**kwargs)
1328 context["colid"] = colid
1329 context["journal"] = model_helpers.get_collection(colid)
1330 context["pages"] = pages
1331 context["news"] = News.objects.filter(site_id=site_id)
1332 context["fields_lang"] = "fr" if model_helpers.is_site_fr_only(site_id) else "en"
1333 return context
1336class PageBaseView(HandleCMSMixin, View):
1337 template_name = "mersenne_cms/page_form.html"
1338 model = Page
1339 form_class = PageForm
1341 def dispatch(self, request, *args, **kwargs):
1342 self.colid = self.kwargs["colid"]
1343 self.collection = model_helpers.get_collection(self.colid, sites=False)
1344 self.site_id = model_helpers.get_site_id(self.colid)
1346 return super().dispatch(request, *args, **kwargs)
1348 def get_success_url(self):
1349 return reverse("page_index", kwargs={"colid": self.colid})
1351 def get_context_data(self, **kwargs):
1352 context = super().get_context_data(**kwargs)
1353 context["journal"] = self.collection
1354 return context
1356 def update_test_website(self):
1357 response = deploy_cms("test_website", self.collection)
1358 if response.status_code < 300: 1358 ↛ 1361line 1358 didn't jump to line 1361 because the condition on line 1358 was always true
1359 messages.success(self.request, "The test website has been updated")
1360 else:
1361 text = "ERROR: Unable to update the test website<br/>"
1363 if response.status_code == 503:
1364 text += "The test website is under maintenance. Please try again later.<br/>"
1365 else:
1366 text += f"Please contact the centre Mersenne<br/><br/>Status code: {response.status_code}<br/>"
1367 if hasattr(response, "content") and response.content:
1368 text += f"{response.content.decode()}<br/>"
1369 if hasattr(response, "reason") and response.reason:
1370 text += f"Reason: {response.reason}<br/>"
1371 if hasattr(response, "text") and response.text:
1372 text += f"Details: {response.text}<br/>"
1373 messages.error(self.request, mark_safe(text))
1375 def get_form_kwargs(self):
1376 kwargs = super().get_form_kwargs()
1377 kwargs["site_id"] = self.site_id
1378 kwargs["user"] = self.request.user
1379 return kwargs
1381 def form_valid(self, form):
1382 form.save()
1384 self.update_test_website()
1386 return HttpResponseRedirect(self.get_success_url())
1389# @method_decorator([csrf_exempt], name="dispatch")
1390class PageDeleteView(PageBaseView):
1391 def post(self, request, *args, **kwargs):
1392 colid = kwargs.get("colid", "")
1393 pk = kwargs.get("pk")
1394 page = get_object_or_404(Page, id=pk)
1395 if page.mersenne_id:
1396 raise PermissionDenied
1398 page.delete()
1400 self.update_test_website()
1402 if page.parent_page and page.parent_page.mersenne_id == MERSENNE_ID_VIRTUAL_ISSUES:
1403 return HttpResponseRedirect(reverse("virtual_issues_index", kwargs={"colid": colid}))
1404 else:
1405 return HttpResponseRedirect(reverse("page_index", kwargs={"colid": colid}))
1408class PageCreateView(PageBaseView, CreateView):
1409 def get_context_data(self, **kwargs):
1410 context = super().get_context_data(**kwargs)
1411 context["title"] = "Add a menu page"
1412 return context
1415class PageUpdateView(PageBaseView, UpdateView):
1416 def get_context_data(self, **kwargs):
1417 context = super().get_context_data(**kwargs)
1418 context["title"] = "Edit a menu page"
1419 return context
1422class NewsBaseView(PageBaseView):
1423 template_name = "mersenne_cms/news_form.html"
1424 model = News
1425 form_class = NewsForm
1428class NewsDeleteView(NewsBaseView):
1429 def post(self, request, *args, **kwargs):
1430 pk = kwargs.get("pk")
1431 news = get_object_or_404(News, id=pk)
1433 news.delete()
1435 self.update_test_website()
1437 return HttpResponseRedirect(self.get_success_url())
1440class NewsCreateView(NewsBaseView, CreateView):
1441 def get_context_data(self, **kwargs):
1442 context = super().get_context_data(**kwargs)
1443 context["title"] = "Add a News"
1444 return context
1447class NewsUpdateView(NewsBaseView, UpdateView):
1448 def get_context_data(self, **kwargs):
1449 context = super().get_context_data(**kwargs)
1450 context["title"] = "Edit a News"
1451 return context
1454# def page_create_view(request, colid):
1455# context = {}
1456# if not is_authorized_editor(request.user, colid):
1457# raise PermissionDenied
1458# collection = model_helpers.get_collection(colid)
1459# page = Page(site_id=model_helpers.get_site_id(colid))
1460# form = PageForm(request.POST or None, instance=page)
1461# if form.is_valid():
1462# form.save()
1463# response = deploy_cms("test_website", collection)
1464# if response.status_code < 300:
1465# messages.success(request, "Page created successfully.")
1466# else:
1467# text = f"ERROR: page creation failed<br/>Status code: {response.status_code}<br/>"
1468# if hasattr(response, "reason") and response.reason:
1469# text += f"Reason: {response.reason}<br/>"
1470# if hasattr(response, "text") and response.text:
1471# text += f"Details: {response.text}<br/>"
1472# messages.error(request, mark_safe(text))
1473# kwargs = {"colid": colid, "pid": form.instance.id}
1474# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1475#
1476# context["form"] = form
1477# context["title"] = "Add a menu page"
1478# context["journal"] = collection
1479# return render(request, "mersenne_cms/page_form.html", context)
1482# def page_update_view(request, colid, pid):
1483# context = {}
1484# if not is_authorized_editor(request.user, colid):
1485# raise PermissionDenied
1486#
1487# collection = model_helpers.get_collection(colid)
1488# page = get_object_or_404(Page, id=pid)
1489# form = PageForm(request.POST or None, instance=page)
1490# if form.is_valid():
1491# form.save()
1492# response = deploy_cms("test_website", collection)
1493# if response.status_code < 300:
1494# messages.success(request, "Page updated successfully.")
1495# else:
1496# text = f"ERROR: page update failed<br/>Status code: {response.status_code}<br/>"
1497# if hasattr(response, "reason") and response.reason:
1498# text += f"Reason: {response.reason}<br/>"
1499# if hasattr(response, "text") and response.text:
1500# text += f"Details: {response.text}<br/>"
1501# messages.error(request, mark_safe(text))
1502# kwargs = {"colid": colid, "pid": form.instance.id}
1503# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1504#
1505# context["form"] = form
1506# context["pid"] = pid
1507# context["title"] = "Edit a menu page"
1508# context["journal"] = collection
1509# return render(request, "mersenne_cms/page_form.html", context)