Coverage for src/ptf_tools/views/cms_views.py: 48%
881 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-10 13:49 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-10 13:49 +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 issues = collection.content.all().order_by("-year")
950 same_year_issues = issues.filter(year=int(year))
951 if same_year_issues.exists(): 951 ↛ 953line 951 didn't jump to line 953 because the condition on line 951 was always true
952 volume = same_year_issues.first().volume
953 elif issues.exists() and colid != "HOUCHES": # because we don't want a volume for houches
954 ref_volume = issues.filter(year=2024).first().volume
955 volume = int(ref_volume) + (
956 int(year) - 2024
957 ) # 2024 is the ref year for wich we know the volume is 347
958 else:
959 volume = ""
960 if pid != "create":
961 # TODO: do not use the pk, but the pid in the URLs
962 container = get_object_or_404(Container, pid=pid)
963 lang = container.lang
964 trans_lang = container.trans_lang
965 xpub = create_publisherdata()
966 xpub.name = container.my_publisher.pid
967 special_issue.provider = container.provider
968 special_issue.number = container.number
969 special_issue_pid = pid
970 special_issue.date_pre_published = container.date_pre_published
971 special_issue.date_published = container.date_published
972 # used for first special issues created withou a proper doi
973 # can be remove when no doi's less special issue existe
974 if not container.doi: 974 ↛ 975line 974 didn't jump to line 975 because the condition on line 974 was never true
975 special_issue.doi = model_helpers.assign_container_doi(colid)
976 else:
977 special_issue.doi = container.doi
978 else:
979 lang = "en"
980 container = None
981 trans_lang = "fr"
982 xpub = create_publisherdata()
983 special_issue.doi = model_helpers.assign_container_doi(colid)
985 if colid == "HOUCHES": 985 ↛ 986line 985 didn't jump to line 986 because the condition on line 985 was never true
986 xpub.name = "UGA Éditions"
987 else:
988 xpub.name = issues.first().my_publisher.pid
989 special_issue.provider = collection.provider
991 special_issues = issues.filter(year=year).filter(
992 Q(ctype="issue_special") | Q(ctype="issue") | Q(ctype="issue_special_img")
993 )
994 if special_issues: 994 ↛ 1004line 994 didn't jump to line 1004 because the condition on line 994 was always true
995 all_special_issues_numbers = [
996 int(si.number[1:]) for si in special_issues if si.number[1:].isnumeric()
997 ]
998 if len(all_special_issues_numbers) > 0:
999 max_number = max(all_special_issues_numbers)
1000 else:
1001 max_number = 0
1003 else:
1004 max_number = 0
1005 special_issue.number = f"S{max_number + 1}"
1006 special_issue_pid = f"{colid}_{year}__{volume}_{special_issue.number}"
1008 if request.POST["use_resources_icon"] == "true": 1008 ↛ 1009line 1008 didn't jump to line 1009 because the condition on line 1008 was never true
1009 special_issue.ctype = "issue_special_img"
1010 else:
1011 special_issue.ctype = "issue_special"
1013 existing_issue = model_helpers.get_resource(special_issue_pid)
1014 if pid == "create" and existing_issue is not None: 1014 ↛ 1015line 1014 didn't jump to line 1015 because the condition on line 1014 was never true
1015 raise ValueError(f"The special issue with the pid {special_issue_pid} already exists")
1017 special_issue.lang = lang
1018 special_issue.title_html = title_html
1019 special_issue.title_xml = build_title_xml(
1020 title=title_html, lang=lang, title_type="issue-title"
1021 )
1023 special_issue.trans_lang = trans_lang
1024 special_issue.trans_title_html = trans_title_html
1025 title_xml = build_title_xml(
1026 title=trans_title_html, lang=trans_lang, title_type="issue-title"
1027 )
1028 title = create_titledata(
1029 lang=trans_lang, type="main", title_html=trans_title_html, title_xml=title_xml
1030 )
1031 special_issue.titles = [title]
1033 special_issue.year = year
1034 special_issue.volume = volume
1035 special_issue.journal = journal
1036 special_issue.publisher = xpub
1037 special_issue.pid = special_issue_pid
1038 special_issue.last_modified_iso_8601_date_str = datetime.now().strftime(
1039 "%Y-%m-%d %H:%M:%S"
1040 )
1042 articles = []
1043 contribs = []
1044 index = 0
1046 if "nb_articles" in request.POST.keys():
1047 while index < int(request.POST["nb_articles"]):
1048 article = json.loads(request.POST[f"article[{index}]"])
1049 article["citation"] = xml_utils.replace_html_entities(article["citation"])
1050 # if not article["citation"]:
1051 # index += 1
1052 # continue
1053 articles.append(article)
1055 index += 1
1057 special_issue.articles = [Munch(article) for article in articles]
1058 index = 0
1059 # TODO make a function to call to add a contributor
1060 if "nb_contrib" in request.POST.keys():
1061 while index < int(request.POST["nb_contrib"]):
1062 contrib = json.loads(request.POST[f"contrib[{index}]"])
1063 contributor = create_contributor()
1064 contributor["first_name"] = contrib["first_name"]
1065 contributor["last_name"] = contrib["last_name"]
1066 contributor["orcid"] = contrib["orcid"]
1067 contributor["role"] = "editor"
1069 contrib_xml = xml_utils.get_contrib_xml(contrib)
1070 contributor["contrib_xml"] = contrib_xml
1071 contribs.append(Munch(contributor))
1072 index += 1
1073 special_issue.contributors = contribs
1075 # Part of the code that handle forwords and lastwords
1077 xhead_fr, head_fr_xml = self.create_abstract_from_vuejs(
1078 request.POST["head_fr"], "fr", "intro", colid, special_issue_pid
1079 )
1080 xtail_fr, tail_fr_xml = self.create_abstract_from_vuejs(
1081 request.POST["tail_fr"], "fr", "tail", colid, special_issue_pid
1082 )
1083 xhead_en, head_en_xml = self.create_abstract_from_vuejs(
1084 request.POST["head_en"], "en", "intro", colid, special_issue_pid
1085 )
1087 xtail_en, tail_en_xml = self.create_abstract_from_vuejs(
1088 request.POST["tail_en"], "en", "tail", colid, special_issue_pid
1089 )
1091 xeditor_bio_en, editor_bio_en_xml = self.create_abstract_from_vuejs(
1092 request.POST["editor_bio_en"], "en", "bio_en", colid, special_issue_pid
1093 )
1095 xeditor_bio_fr, editor_bio_fr_xml = self.create_abstract_from_vuejs(
1096 request.POST["editor_bio_fr"], "fr", "bio_fr", colid, special_issue_pid
1097 )
1099 abstracts = [
1100 head_fr_xml,
1101 head_en_xml,
1102 tail_fr_xml,
1103 tail_en_xml,
1104 editor_bio_fr_xml,
1105 editor_bio_en_xml,
1106 ]
1107 figures = self.create_related_objects_from_abstract(abstracts, colid, special_issue.pid)
1108 special_issue.related_objects = figures
1109 # TODO can be factorized?
1110 special_issue.abstracts = [
1111 {
1112 "tag": "head_fr",
1113 "lang": "fr",
1114 "value_html": xhead_fr.value_html,
1115 "value_tex": xhead_fr.value_tex,
1116 "value_xml": head_fr_xml,
1117 },
1118 {
1119 "tag": "head_en",
1120 "lang": "en",
1121 "value_html": xhead_en.value_html,
1122 "value_tex": xhead_en.value_tex,
1123 "value_xml": head_en_xml,
1124 },
1125 {
1126 "tag": "tail_fr",
1127 "lang": "fr",
1128 "value_html": xtail_fr.value_html,
1129 "value_tex": xtail_fr.value_tex,
1130 "value_xml": tail_fr_xml,
1131 },
1132 {
1133 "tag": "tail_en",
1134 "lang": "en",
1135 "value_html": xtail_en.value_html,
1136 "value_tex": xtail_en.value_tex,
1137 "value_xml": tail_en_xml,
1138 },
1139 {
1140 "tag": "bio_en",
1141 "lang": "en",
1142 "value_html": xeditor_bio_en.value_html,
1143 "value_tex": xeditor_bio_en.value_tex,
1144 "value_xml": editor_bio_en_xml,
1145 },
1146 {
1147 "tag": "bio_fr",
1148 "lang": "fr",
1149 "value_html": xeditor_bio_fr.value_html,
1150 "value_tex": xeditor_bio_fr.value_tex,
1151 "value_xml": editor_bio_fr_xml,
1152 },
1153 ]
1155 # This part handle pdf files included in special issue. Can be editor of full pdf version
1156 # Both are stored in same directory
1158 pdf_file_path = resolver.get_disk_location(
1159 f"{settings.RESOURCES_ROOT}",
1160 f"{collection.pid}",
1161 "pdf",
1162 special_issue_pid,
1163 article_id=None,
1164 do_create_folder=False,
1165 )
1166 pdf_path = os.path.dirname(pdf_file_path)
1167 if "pdf" in self.request.FILES: 1167 ↛ 1168line 1167 didn't jump to line 1168 because the condition on line 1167 was never true
1168 if os.path.isfile(f"{pdf_path}/{pid}.pdf"):
1169 os.remove(f"{pdf_path}/{pid}.pdf")
1170 if "edito" in self.request.FILES: 1170 ↛ 1171line 1170 didn't jump to line 1171 because the condition on line 1170 was never true
1171 if os.path.isfile(f"{pdf_path}/{pid}_edito.pdf"):
1172 os.remove(f"{pdf_path}/{pid}_edito.pdf")
1174 if request.POST["pdf_name"] != "No file uploaded": 1174 ↛ 1175line 1174 didn't jump to line 1175 because the condition on line 1174 was never true
1175 if "pdf" in self.request.FILES:
1176 pdf_file_name = self.request.FILES["pdf"].name
1177 location = pdf_path + "/" + special_issue_pid + ".pdf"
1178 with open(location, "wb+") as destination:
1179 for chunk in self.request.FILES["pdf"].chunks():
1180 destination.write(chunk)
1182 else:
1183 pdf_file_name = request.POST["pdf_name"]
1184 location = pdf_path + "/" + special_issue_pid + ".pdf"
1186 pdf_stream_data = create_datastream()
1187 pdf_stream_data["location"] = location.replace("/mersenne_test_data/", "")
1188 pdf_stream_data["mimetype"] = "application/pdf"
1189 pdf_stream_data["rel"] = "full-text"
1190 pdf_stream_data["text"] = pdf_file_name
1191 special_issue.streams.append(pdf_stream_data)
1193 if request.POST["edito_name"] != "No file uploaded": 1193 ↛ 1194line 1193 didn't jump to line 1194 because the condition on line 1193 was never true
1194 if "edito" in self.request.FILES:
1195 location = pdf_path + "/" + special_issue_pid + "_edito.pdf"
1196 edito_file_name = self.request.FILES["edito"].name
1197 edito_display_name = request.POST["edito_display_name"]
1198 with open(location, "wb+") as destination:
1199 for chunk in self.request.FILES["edito"].chunks():
1200 destination.write(chunk)
1201 else:
1202 location = pdf_path + "/" + special_issue_pid + "_edito.pdf"
1203 edito_file_name = request.POST["edito_name"]
1204 edito_display_name = request.POST["edito_display_name"]
1206 location = location.replace("/mersenne_test_data/", "")
1207 data = {
1208 "rel": "edito",
1209 "mimetype": "application/pdf",
1210 "location": location,
1211 "base": None,
1212 "metadata": edito_file_name + "$$$" + edito_display_name,
1213 }
1214 special_issue.related_objects.append(data)
1215 # Handle special issue icon. It is stored in same directory that pdf version or edito.
1216 # The icon is linked to special issue as an ExtLink
1217 if "icon" in request.FILES: 1217 ↛ 1218line 1217 didn't jump to line 1218 because the condition on line 1217 was never true
1218 icon_file = request.FILES["icon"]
1219 relative_file_name = resolver.copy_file_obj_to_article_folder(
1220 icon_file,
1221 collection.pid,
1222 special_issue.pid,
1223 special_issue.pid,
1224 )
1225 if ".tif" in relative_file_name:
1226 jpeg_path = ImageManager(
1227 os.path.join(settings.RESOURCES_ROOT, relative_file_name)
1228 ).to_jpeg()
1229 relative_file_name = str(jpeg_path).replace(settings.RESOURCES_ROOT + "/", "")
1230 data = {
1231 "rel": "icon",
1232 "location": relative_file_name,
1233 "base": None,
1234 "seq": 1,
1235 "metadata": "",
1236 }
1237 special_issue.ext_links.append(data)
1238 elif "icon" in request.POST.keys(): 1238 ↛ 1239line 1238 didn't jump to line 1239 because the condition on line 1238 was never true
1239 if request.POST["icon"] != "[object Object]":
1240 icon_file = request.POST["icon"].replace("/icon/", "")
1241 data = {
1242 "rel": "icon",
1243 "location": icon_file,
1244 "base": None,
1245 "seq": 1,
1246 "metadata": "",
1247 }
1248 special_issue.ext_links.append(data)
1250 special_issue = Munch(special_issue.__dict__)
1251 params = {"xissue": special_issue, "use_body": False}
1252 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params)
1253 cmd.do()
1254 # tail_fr_html = xml_utils.replace_html_entities(request.POST["tail_fr"])
1255 # tail_en_html = xml_utils.replace_html_entities(request.POST["tail_en"])
1256 return redirect("special_issue_edit_api", colid, special_issue.pid)
1258 def create_abstract_from_vuejs(self, abstract, lang, position, colid, pid):
1259 abstract_html = xml_utils.replace_html_entities(abstract)
1260 xabstract = CkeditorParser(
1261 html_value=abstract_html, issue_pid=colid, pid=pid, mml_formulas=[]
1262 )
1263 abstract_xml = get_abstract_xml(xabstract.value_xml, lang, position)
1264 return xabstract, abstract_xml
1266 def create_related_objects_from_abstract(self, abstracts, colid, pid):
1267 figures = []
1268 for abstract in abstracts:
1269 abstract_xml = abstract.encode("utf8")
1271 tree = etree.fromstring(abstract_xml)
1273 pics = tree.xpath("//graphic")
1274 for pic in pics: 1274 ↛ 1275line 1274 didn't jump to line 1275 because the loop on line 1274 never started
1275 base = None
1276 pic_location = pic.attrib["specific-use"]
1277 basename = os.path.basename(pic.attrib["href"])
1278 ext = basename.split(".")[-1]
1279 base = get_media_base_root(colid)
1280 data_location = os.path.join(
1281 settings.RESOURCES_ROOT, "media", base, "uploads", pic_location, basename
1282 )
1283 # we use related objects to send pics to journal site. Directory where pic is stored in trammel may differ
1284 # from the directory in journal site. So one need to save the pic in same directory that journal's one
1285 # so related objects can go for the correct one
1286 img = Image.open(data_location)
1287 final_data_location = os.path.join(
1288 settings.RESOURCES_ROOT, colid, pid, "src", "figures"
1289 )
1290 if not os.path.isdir(final_data_location):
1291 os.makedirs(final_data_location)
1292 relative_path = os.path.join(colid, pid, "src", "figures", basename)
1293 final_data_location = f"{final_data_location}/{basename}"
1294 img.save(final_data_location)
1295 if ext == "png":
1296 mimetype = "image/png"
1297 else:
1298 mimetype = "image/jpeg"
1299 data = {
1300 "rel": "html-image",
1301 "mimetype": mimetype,
1302 "location": relative_path,
1303 "base": base,
1304 "metadata": "",
1305 }
1306 if data not in figures:
1307 figures.append(data)
1308 return figures
1311class PageIndexView(EditorRequiredMixin, TemplateView):
1312 template_name = "mersenne_cms/page_index.html"
1314 def get_context_data(self, **kwargs):
1315 colid = kwargs.get("colid", "")
1316 site_id = model_helpers.get_site_id(colid)
1317 vi = Page.objects.filter(site_id=site_id, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES).first()
1318 if vi: 1318 ↛ 1319line 1318 didn't jump to line 1319 because the condition on line 1318 was never true
1319 pages = Page.objects.filter(site_id=site_id).exclude(parent_page=vi)
1320 else:
1321 pages = Page.objects.filter(site_id=site_id)
1322 context = super().get_context_data(**kwargs)
1323 context["colid"] = colid
1324 context["journal"] = model_helpers.get_collection(colid)
1325 context["pages"] = pages
1326 context["news"] = News.objects.filter(site_id=site_id)
1327 context["fields_lang"] = "fr" if model_helpers.is_site_fr_only(site_id) else "en"
1328 return context
1331class PageBaseView(HandleCMSMixin, View):
1332 template_name = "mersenne_cms/page_form.html"
1333 model = Page
1334 form_class = PageForm
1336 def dispatch(self, request, *args, **kwargs):
1337 self.colid = self.kwargs["colid"]
1338 self.collection = model_helpers.get_collection(self.colid, sites=False)
1339 self.site_id = model_helpers.get_site_id(self.colid)
1341 return super().dispatch(request, *args, **kwargs)
1343 def get_success_url(self):
1344 return reverse("page_index", kwargs={"colid": self.colid})
1346 def get_context_data(self, **kwargs):
1347 context = super().get_context_data(**kwargs)
1348 context["journal"] = self.collection
1349 return context
1351 def update_test_website(self):
1352 response = deploy_cms("test_website", self.collection)
1353 if response.status_code < 300: 1353 ↛ 1356line 1353 didn't jump to line 1356 because the condition on line 1353 was always true
1354 messages.success(self.request, "The test website has been updated")
1355 else:
1356 text = "ERROR: Unable to update the test website<br/>"
1358 if response.status_code == 503:
1359 text += "The test website is under maintenance. Please try again later.<br/>"
1360 else:
1361 text += f"Please contact the centre Mersenne<br/><br/>Status code: {response.status_code}<br/>"
1362 if hasattr(response, "content") and response.content:
1363 text += f"{response.content.decode()}<br/>"
1364 if hasattr(response, "reason") and response.reason:
1365 text += f"Reason: {response.reason}<br/>"
1366 if hasattr(response, "text") and response.text:
1367 text += f"Details: {response.text}<br/>"
1368 messages.error(self.request, mark_safe(text))
1370 def get_form_kwargs(self):
1371 kwargs = super().get_form_kwargs()
1372 kwargs["site_id"] = self.site_id
1373 kwargs["user"] = self.request.user
1374 return kwargs
1376 def form_valid(self, form):
1377 form.save()
1379 self.update_test_website()
1381 return HttpResponseRedirect(self.get_success_url())
1384# @method_decorator([csrf_exempt], name="dispatch")
1385class PageDeleteView(PageBaseView):
1386 def post(self, request, *args, **kwargs):
1387 colid = kwargs.get("colid", "")
1388 pk = kwargs.get("pk")
1389 page = get_object_or_404(Page, id=pk)
1390 if page.mersenne_id:
1391 raise PermissionDenied
1393 page.delete()
1395 self.update_test_website()
1397 if page.parent_page and page.parent_page.mersenne_id == MERSENNE_ID_VIRTUAL_ISSUES:
1398 return HttpResponseRedirect(reverse("virtual_issues_index", kwargs={"colid": colid}))
1399 else:
1400 return HttpResponseRedirect(reverse("page_index", kwargs={"colid": colid}))
1403class PageCreateView(PageBaseView, CreateView):
1404 def get_context_data(self, **kwargs):
1405 context = super().get_context_data(**kwargs)
1406 context["title"] = "Add a menu page"
1407 return context
1410class PageUpdateView(PageBaseView, UpdateView):
1411 def get_context_data(self, **kwargs):
1412 context = super().get_context_data(**kwargs)
1413 context["title"] = "Edit a menu page"
1414 return context
1417class NewsBaseView(PageBaseView):
1418 template_name = "mersenne_cms/news_form.html"
1419 model = News
1420 form_class = NewsForm
1423class NewsDeleteView(NewsBaseView):
1424 def post(self, request, *args, **kwargs):
1425 pk = kwargs.get("pk")
1426 news = get_object_or_404(News, id=pk)
1428 news.delete()
1430 self.update_test_website()
1432 return HttpResponseRedirect(self.get_success_url())
1435class NewsCreateView(NewsBaseView, CreateView):
1436 def get_context_data(self, **kwargs):
1437 context = super().get_context_data(**kwargs)
1438 context["title"] = "Add a News"
1439 return context
1442class NewsUpdateView(NewsBaseView, UpdateView):
1443 def get_context_data(self, **kwargs):
1444 context = super().get_context_data(**kwargs)
1445 context["title"] = "Edit a News"
1446 return context
1449# def page_create_view(request, colid):
1450# context = {}
1451# if not is_authorized_editor(request.user, colid):
1452# raise PermissionDenied
1453# collection = model_helpers.get_collection(colid)
1454# page = Page(site_id=model_helpers.get_site_id(colid))
1455# form = PageForm(request.POST or None, instance=page)
1456# if form.is_valid():
1457# form.save()
1458# response = deploy_cms("test_website", collection)
1459# if response.status_code < 300:
1460# messages.success(request, "Page created successfully.")
1461# else:
1462# text = f"ERROR: page creation failed<br/>Status code: {response.status_code}<br/>"
1463# if hasattr(response, "reason") and response.reason:
1464# text += f"Reason: {response.reason}<br/>"
1465# if hasattr(response, "text") and response.text:
1466# text += f"Details: {response.text}<br/>"
1467# messages.error(request, mark_safe(text))
1468# kwargs = {"colid": colid, "pid": form.instance.id}
1469# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1470#
1471# context["form"] = form
1472# context["title"] = "Add a menu page"
1473# context["journal"] = collection
1474# return render(request, "mersenne_cms/page_form.html", context)
1477# def page_update_view(request, colid, pid):
1478# context = {}
1479# if not is_authorized_editor(request.user, colid):
1480# raise PermissionDenied
1481#
1482# collection = model_helpers.get_collection(colid)
1483# page = get_object_or_404(Page, id=pid)
1484# form = PageForm(request.POST or None, instance=page)
1485# if form.is_valid():
1486# form.save()
1487# response = deploy_cms("test_website", collection)
1488# if response.status_code < 300:
1489# messages.success(request, "Page updated successfully.")
1490# else:
1491# text = f"ERROR: page update failed<br/>Status code: {response.status_code}<br/>"
1492# if hasattr(response, "reason") and response.reason:
1493# text += f"Reason: {response.reason}<br/>"
1494# if hasattr(response, "text") and response.text:
1495# text += f"Details: {response.text}<br/>"
1496# messages.error(request, mark_safe(text))
1497# kwargs = {"colid": colid, "pid": form.instance.id}
1498# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
1499#
1500# context["form"] = form
1501# context["pid"] = pid
1502# context["title"] = "Edit a menu page"
1503# context["journal"] = collection
1504# return render(request, "mersenne_cms/page_form.html", context)