Coverage for sites/ptf_tools/ptf_tools/views/cms_views.py: 43%
571 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-05-19 19:20 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-05-19 19:20 +0000
1import base64
2import json
3import os
4import re
5import shutil
6from datetime import datetime
8import requests
9from ckeditor_uploader.views import ImageUploadView
10from ckeditor_uploader.views import browse
11from requests import Timeout
13from django.conf import settings
14from django.contrib import messages
15from django.contrib.auth.mixins import UserPassesTestMixin
16from django.core.exceptions import PermissionDenied
17from django.forms.models import model_to_dict
18from django.http import Http404
19from django.http import HttpResponse
20from django.http import HttpResponseBadRequest
21from django.http import HttpResponseRedirect
22from django.http import HttpResponseServerError
23from django.http import JsonResponse
24from django.shortcuts import get_object_or_404
25from django.urls import reverse
26from django.utils import timezone
27from django.utils.safestring import mark_safe
28from django.views.decorators.csrf import csrf_exempt
29from django.views.generic import CreateView
30from django.views.generic import TemplateView
31from django.views.generic import UpdateView
32from django.views.generic import View
34from mersenne_cms.models import MERSENNE_ID_VIRTUAL_ISSUES
35from mersenne_cms.models import News
36from mersenne_cms.models import Page
37from mersenne_cms.models import get_news_content
38from mersenne_cms.models import get_pages_content
39from mersenne_cms.models import import_news
40from mersenne_cms.models import import_pages
41from ptf import model_helpers
42from ptf.cmds import solr_cmds
43from ptf.exceptions import ServerUnderMaintenance
44from ptf.models import Article
45from ptf.models import GraphicalAbstract
46from ptf.models import RelatedArticles
47from ptf.models import get_names
48from ptf.site_register import SITE_REGISTER
49from ptf_tools.forms import GraphicalAbstractForm
50from ptf_tools.forms import NewsForm
51from ptf_tools.forms import PageForm
52from ptf_tools.forms import RelatedForm
53from ptf_tools.utils import is_authorized_editor
55from .base_views import check_lock
58def get_media_base_root(colid):
59 """
60 Base folder where media files are stored in Trammel
61 """
62 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]:
63 colid = "CR"
65 return os.path.join(settings.RESOURCES_ROOT, "media", colid)
68def get_media_base_root_in_test(colid):
69 """
70 Base folder where media files are stored in the test website
71 Use the same folder as the Trammel media folder so that no copy is necessary when deploy in test
72 """
73 return get_media_base_root(colid)
76def get_media_base_root_in_prod(colid):
77 """
78 Base folder where media files are stored in the prod website
79 """
80 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]:
81 colid = "CR"
83 return os.path.join(settings.MERSENNE_PROD_DATA_FOLDER, "media", colid)
86def get_media_base_url(colid):
87 path = os.path.join(settings.MEDIA_URL, colid)
89 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]:
90 prefixes = {
91 "CRMECA": "mecanique",
92 "CRBIOL": "biologies",
93 "CRGEOS": "geoscience",
94 "CRCHIM": "chimie",
95 "CRMATH": "mathematique",
96 "CRPHYS": "physique",
97 }
98 path = f"/{prefixes[colid]}{settings.MEDIA_URL}/CR"
100 return path
103def change_ckeditor_storage(colid):
104 """
105 By default, CKEditor stores all the files under 1 folder (MEDIA_ROOT)
106 We want to store the files under a subfolder of @colid
107 To do that we have to
108 - change the URL calling this view to pass the site_id (info used by the Pages to filter the objects)
109 - modify the storage location
110 """
112 from ckeditor_uploader import utils
113 from ckeditor_uploader import views
115 from django.core.files.storage import FileSystemStorage
117 storage = FileSystemStorage(
118 location=get_media_base_root(colid), base_url=get_media_base_url(colid)
119 )
121 utils.storage = storage
122 views.storage = storage
125class EditorRequiredMixin(UserPassesTestMixin):
126 def test_func(self):
127 return is_authorized_editor(self.request.user, self.kwargs.get("colid"))
130class CollectionImageUploadView(EditorRequiredMixin, ImageUploadView):
131 """
132 By default, CKEditor stores all the files under 1 folder (MEDIA_ROOT)
133 We want to store the files under a subfolder of @colid
134 To do that we have to
135 - change the URL calling this view to pass the site_id (info used by the Pages to filter the objects)
136 - modify the storage location
137 """
139 def dispatch(self, request, *args, **kwargs):
140 colid = kwargs["colid"]
142 change_ckeditor_storage(colid)
144 return super().dispatch(request, **kwargs)
147class CollectionBrowseView(EditorRequiredMixin, View):
148 def dispatch(self, request, **kwargs):
149 colid = kwargs["colid"]
151 change_ckeditor_storage(colid)
153 return browse(request)
156file_upload_in_collection = csrf_exempt(CollectionImageUploadView.as_view())
157file_browse_in_collection = csrf_exempt(CollectionBrowseView.as_view())
160def deploy_cms(site, collection):
161 colid = collection.pid
162 base_url = getattr(collection, site)()
164 if base_url is None: 164 ↛ 167line 164 didn't jump to line 167, because the condition on line 164 was never false
165 return JsonResponse({"message": "OK"})
167 if site == "website":
168 from_base_path = get_media_base_root_in_test(colid)
169 to_base_path = get_media_base_root_in_prod(colid)
171 for sub_path in ["uploads", "images"]:
172 from_path = os.path.join(from_base_path, sub_path)
173 to_path = os.path.join(to_base_path, sub_path)
174 if os.path.exists(from_path):
175 try:
176 shutil.copytree(from_path, to_path, dirs_exist_ok=True)
177 except OSError as exception:
178 return HttpResponseServerError(f"Error during copy: {exception}")
180 pages = get_pages_content(colid)
181 news = get_news_content(colid)
183 data = json.dumps({"pages": json.loads(pages), "news": json.loads(news)})
184 url = getattr(collection, site)() + "/import_cms/"
186 try:
187 response = requests.put(url, data=data, verify=False)
189 if response.status_code == 503:
190 e = ServerUnderMaintenance(
191 "The journal test website is under maintenance. Please try again later."
192 )
193 return HttpResponseServerError(e, status=503)
195 except Timeout as exception:
196 return HttpResponse(exception, status=408)
197 except Exception as exception:
198 return HttpResponseServerError(exception)
200 return JsonResponse({"message": "OK"})
203class HandleCMSMixin(EditorRequiredMixin):
204 """
205 Mixin for classes that need to send request to (test) website to import/export CMS content (pages, news)
206 """
208 # def dispatch(self, request, *args, **kwargs):
209 # self.colid = self.kwargs["colid"]
210 # return super().dispatch(request, *args, **kwargs)
212 def init_data(self, kwargs):
213 self.collection = None
215 self.colid = kwargs.get("colid", None)
216 if self.colid:
217 self.collection = model_helpers.get_collection(self.colid)
218 if not self.collection:
219 raise Http404(f"{self.colid} does not exist")
221 test_server_url = self.collection.test_website()
222 if not test_server_url:
223 raise Http404("The collection has no test site")
225 prod_server_url = self.collection.website()
226 if not prod_server_url:
227 raise Http404("The collection has no prod site")
230class GetCMSFromSiteAPIView(HandleCMSMixin, View):
231 """
232 Get the CMS content from the (test) website and save it on disk.
233 It can be used if needed to restore the Trammel content with RestoreCMSAPIView below
234 """
236 def get(self, request, *args, **kwargs):
237 self.init_data(self.kwargs)
239 site = kwargs.get("site", "test_website")
241 try:
242 url = getattr(self.collection, site)() + "/export_cms/"
243 response = requests.get(url, verify=False)
245 # Just to need to save the json on disk
246 # Media files are already saved in MEDIA_ROOT which is equal to
247 # /mersenne_test_data/@colid/media
248 folder = get_media_base_root(self.colid)
249 os.makedirs(folder, exist_ok=True)
250 filename = os.path.join(folder, f"pages_{self.colid}.json")
251 with open(filename, mode="w", encoding="utf-8") as file:
252 file.write(response.content.decode(encoding="utf-8"))
254 except Timeout as exception:
255 return HttpResponse(exception, status=408)
256 except Exception as exception:
257 return HttpResponseServerError(exception)
259 return JsonResponse({"message": "OK", "status": 200})
262class RestoreCMSAPIView(HandleCMSMixin, View):
263 """
264 Restore the Trammel CMS content (of a colid) from disk
265 """
267 def get(self, request, *args, **kwargs):
268 self.init_data(self.kwargs)
270 folder = get_media_base_root(self.colid)
271 filename = os.path.join(folder, f"pages_{self.colid}.json")
272 with open(filename, encoding="utf-8") as f:
273 json_data = json.load(f)
275 pages = json_data.get("pages")
276 import_pages(pages, self.colid)
278 if "news" in json_data:
279 news = json_data.get("news")
280 import_news(news, self.colid)
282 return JsonResponse({"message": "OK", "status": 200})
285class DeployCMSAPIView(HandleCMSMixin, View):
286 def get(self, request, *args, **kwargs):
287 self.init_data(self.kwargs)
289 if check_lock():
290 msg = "Trammel is under maintenance. Please try again later."
291 messages.error(self.request, msg)
292 return JsonResponse({"messages": msg, "status": 503})
294 site = kwargs.get("site", "test_website")
296 response = deploy_cms(site, self.collection)
298 if response.status_code == 503:
299 messages.error(
300 self.request, "The journal website is under maintenance. Please try again later."
301 )
303 return response
306def get_server_urls(collection, site="test_website"):
307 urls = [""]
308 if hasattr(settings, "MERSENNE_DEV_URL"): 308 ↛ 310line 308 didn't jump to line 310, because the condition on line 308 was never true
309 # set RESOURCES_ROOT and apache config accordingly (for instance with "/mersenne_dev_data")
310 url = getattr(collection, "test_website")().split(".fr")
311 urls = [settings.MERSENNE_DEV_URL + url[1] if len(url) == 2 else ""]
312 elif site == "both": 312 ↛ 313line 312 didn't jump to line 313, because the condition on line 312 was never true
313 urls = [getattr(collection, "test_website")(), getattr(collection, "website")()]
314 elif hasattr(collection, site) and getattr(collection, site)(): 314 ↛ 315line 314 didn't jump to line 315, because the condition on line 314 was never true
315 urls = [getattr(collection, site)()]
316 return urls
319class SuggestDeployView(EditorRequiredMixin, View):
320 def post(self, request, *args, **kwargs):
321 doi = kwargs.get("doi", "")
322 site = kwargs.get("site", "test_website")
323 article = get_object_or_404(Article, doi=doi)
325 obj, created = RelatedArticles.objects.get_or_create(resource=article)
326 form = RelatedForm(request.POST or None, instance=obj)
327 if form.is_valid(): 327 ↛ 344line 327 didn't jump to line 344, because the condition on line 327 was never false
328 data = form.cleaned_data
329 obj.date_modified = timezone.now()
330 form.save()
331 collection = article.my_container.my_collection
332 urls = get_server_urls(collection, site=site)
333 response = requests.models.Response()
334 for url in urls: 334 ↛ 342line 334 didn't jump to line 342, because the loop on line 334 didn't complete
335 url = url + reverse("api-update-suggest", kwargs={"doi": doi})
336 try:
337 response = requests.post(url, data=data, timeout=15)
338 except requests.exceptions.RequestException as e:
339 response.status_code = 503
340 response.reason = e.args[0]
341 break
342 return HttpResponse(status=response.status_code, reason=response.reason)
343 else:
344 return HttpResponseBadRequest()
347def suggest_debug(results, article, message):
348 crop_results = 5
349 if results: 349 ↛ 350line 349 didn't jump to line 350, because the condition on line 349 was never true
350 dois = []
351 results["docs"] = results["docs"][:crop_results]
352 numFound = f'({len(results["docs"])} sur {results["numFound"]} documents)'
353 head = f"Résultats de la recherche automatique {numFound} :\n\n"
354 for item in results["docs"]:
355 doi = item.get("doi")
356 if doi:
357 explain = results["explain"][item["id"]]
358 terms = re.findall(r"([0-9.]+?) = weight\((.+?:.+?) in", explain)
359 terms.sort(key=lambda t: t[0], reverse=True)
360 details = (" + ").join(f"{round(float(s), 1)}:{t}" for s, t in terms)
361 score = f'Score : {round(float(item["score"]), 1)} (= {details})\n'
362 url = ""
363 suggest = Article.objects.filter(doi=doi).first()
364 if suggest and suggest.my_container:
365 collection = suggest.my_container.my_collection
366 base_url = collection.website() or ""
367 url = base_url + "/articles/" + doi
368 dois.append((doi, url, score))
370 tail = f'\n\nScore minimum retenu : {results["params"]["min_score"]}\n\n\n'
371 tail += "Termes principaux utilisés pour la requête "
372 tail = [tail + "(champ:terme recherché | pertinence du terme) :\n"]
373 if results["params"]["mlt.fl"] == "all":
374 tail.append(" * all = body + abstract + title + authors + keywords\n")
375 terms = results["interestingTerms"]
376 terms = [" | ".join((x[0], str(x[1]))) for x in zip(terms[::2], terms[1::2])]
377 tail.extend(reversed(terms))
378 tail.append("\n\nParamètres de la requête :\n")
379 tail.extend([f"{k}: {v} " for k, v in results["params"].items()])
380 return [(head, dois, "\n".join(tail))]
381 else:
382 msg = f'Erreur {message["status"]} {message["err"]} at {message["url"]}'
383 return [(msg, [], "")]
386class SuggestUpdateView(EditorRequiredMixin, TemplateView):
387 template_name = "editorial_tools/suggested.html"
389 def get_context_data(self, **kwargs):
390 doi = kwargs.get("doi", "")
391 article = get_object_or_404(Article, doi=doi)
393 obj, created = RelatedArticles.objects.get_or_create(resource=article)
394 collection = article.my_container.my_collection
395 base_url = collection.website() or ""
396 response = requests.models.Response()
397 try:
398 response = requests.get(base_url + "/mlt/" + doi, timeout=10.0)
399 except requests.exceptions.RequestException as e:
400 response.status_code = 503
401 response.reason = e.args[0]
402 msg = {
403 "url": response.url,
404 "status": response.status_code,
405 "err": response.reason,
406 }
407 results = None
408 if response.status_code == 200: 408 ↛ 409line 408 didn't jump to line 409, because the condition on line 408 was never true
409 results = solr_cmds.auto_suggest_doi(obj, article, response.json())
410 context = super().get_context_data(**kwargs)
411 context["debug"] = suggest_debug(results, article, msg)
412 context["form"] = RelatedForm(instance=obj)
413 context["author"] = "; ".join(get_names(article, "author"))
414 context["citation_base"] = article.get_citation_base().strip(", .")
415 context["article"] = article
416 context["date_modified"] = obj.date_modified
417 context["url"] = base_url + "/articles/" + doi
418 return context
421class EditorialToolsVolumeItemsView(EditorRequiredMixin, TemplateView):
422 template_name = "editorial_tools/volume-items.html"
424 def get_context_data(self, **kwargs):
425 vid = kwargs.get("vid")
426 site_name = settings.SITE_NAME if hasattr(settings, "SITE_NAME") else ""
427 is_cr = len(site_name) == 6 and site_name[0:2] == "cr"
428 issues_articles, collection = model_helpers.get_issues_in_volume(vid, is_cr)
429 context = super().get_context_data(**kwargs)
430 context["issues_articles"] = issues_articles
431 context["collection"] = collection
432 return context
435class EditorialToolsArticleView(EditorRequiredMixin, TemplateView):
436 template_name = "editorial_tools/find-article.html"
438 def get_context_data(self, **kwargs):
439 colid = kwargs.get("colid")
440 doi = kwargs.get("doi")
441 article = get_object_or_404(Article, doi=doi, my_container__my_collection__pid=colid)
443 context = super().get_context_data(**kwargs)
444 context["article"] = article
445 context["citation_base"] = article.get_citation_base().strip(", .")
446 return context
449class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView):
450 template_name = "editorial_tools/graphical-abstract.html"
452 def get_context_data(self, **kwargs):
453 doi = kwargs.get("doi", "")
454 article = get_object_or_404(Article, doi=doi)
456 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
457 context = super().get_context_data(**kwargs)
458 context["author"] = "; ".join(get_names(article, "author"))
459 context["citation_base"] = article.get_citation_base().strip(", .")
460 context["article"] = article
461 context["date_modified"] = obj.date_modified
462 context["form"] = GraphicalAbstractForm(instance=obj)
463 context["graphical_abstract"] = obj.graphical_abstract
464 context["illustration"] = obj.illustration
465 return context
468class GraphicalAbstractDeployView(EditorRequiredMixin, View):
469 def post(self, request, *args, **kwargs):
470 doi = kwargs.get("doi", "")
471 site = kwargs.get("site", "both")
472 article = get_object_or_404(Article, doi=doi)
474 obj, created = GraphicalAbstract.objects.get_or_create(resource=article)
475 form = GraphicalAbstractForm(request.POST, request.FILES or None, instance=obj)
476 if form.is_valid(): 476 ↛ 503line 476 didn't jump to line 503, because the condition on line 476 was never false
477 obj.date_modified = timezone.now()
478 data = {"date_modified": obj.date_modified}
479 form.save()
480 files = {}
481 if obj.graphical_abstract and os.path.exists(obj.graphical_abstract.path): 481 ↛ 482line 481 didn't jump to line 482, because the condition on line 481 was never true
482 with open(obj.graphical_abstract.path, "rb") as fp:
483 files.update({"graphical_abstract": (obj.graphical_abstract.name, fp.read())})
484 if obj.illustration and os.path.exists(obj.illustration.path): 484 ↛ 485line 484 didn't jump to line 485, because the condition on line 484 was never true
485 with open(obj.illustration.path, "rb") as fp:
486 files.update({"illustration": (obj.illustration.name, fp.read())})
487 collection = article.my_container.my_collection
488 urls = get_server_urls(collection, site=site)
489 response = requests.models.Response()
490 for url in urls: 490 ↛ 501line 490 didn't jump to line 501, because the loop on line 490 didn't complete
491 url = url + reverse("api-graphical-abstract", kwargs={"doi": doi})
492 try:
493 if not obj.graphical_abstract and not obj.illustration: 493 ↛ 496line 493 didn't jump to line 496, because the condition on line 493 was never false
494 response = requests.delete(url, data=data, files=files, timeout=15)
495 else:
496 response = requests.post(url, data=data, files=files, timeout=15)
497 except requests.exceptions.RequestException as e:
498 response.status_code = 503
499 response.reason = e.args[0]
500 break
501 return HttpResponse(status=response.status_code, reason=response.reason)
502 else:
503 return HttpResponseBadRequest()
506def parse_content(content):
507 table = re.search(r'(.*?)(<table id="summary".+?</table>)(.*)', content, re.DOTALL)
508 if not table:
509 return {"head": content, "tail": "", "articles": []}
511 articles = []
512 rows = re.findall(r"<tr>.+?</tr>", table.group(2), re.DOTALL)
513 for row in rows:
514 citation = re.search(r'<div href=".*?">(.*?)</div>', row, re.DOTALL)
515 href = re.search(r'href="(.+?)\/?">', row)
516 doi = re.search(r"(10[.].+)", href.group(1)) if href else ""
517 src = re.search(r'<img.+?src="(.+?)"', row)
518 item = {}
519 item["citation"] = citation.group(1) if citation else ""
520 item["doi"] = doi.group(1) if doi else href.group(1) if href else ""
521 item["src"] = src.group(1) if src else ""
522 item["imageName"] = item["src"].split("/")[-1] if item["src"] else ""
523 if item["doi"] or item["src"]:
524 articles.append(item)
525 return {"head": table.group(1), "tail": table.group(3), "articles": articles}
528class VirtualIssueParseView(EditorRequiredMixin, View):
529 def get(self, request, *args, **kwargs):
530 pid = kwargs.get("pid", "")
531 page = get_object_or_404(Page, id=pid)
533 data = {"pid": pid}
534 data["colid"] = kwargs.get("colid", "")
535 journal = model_helpers.get_collection(data["colid"])
536 data["journal_title"] = journal.title_tex.replace(".", "")
537 site_id = model_helpers.get_site_id(data["colid"])
538 data["page"] = model_to_dict(page)
539 pages = Page.objects.filter(site_id=site_id).exclude(id=pid)
540 data["parents"] = [model_to_dict(p, fields=["id", "menu_title"]) for p in pages]
542 content_fr = parse_content(page.content_fr)
543 data["head_fr"] = content_fr["head"]
544 data["tail_fr"] = content_fr["tail"]
546 content_en = parse_content(page.content_en)
547 data["articles"] = content_en["articles"]
548 data["head_en"] = content_en["head"]
549 data["tail_en"] = content_en["tail"]
550 return JsonResponse(data)
553class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView):
554 template_name = "editorial_tools/virtual-issue.html"
556 def get(self, request, *args, **kwargs):
557 pid = kwargs.get("pid", "")
558 get_object_or_404(Page, id=pid)
560 return super().get(request, *args, **kwargs)
563class VirtualIssueCreateView(EditorRequiredMixin, View):
564 def get(self, request, *args, **kwargs):
565 colid = kwargs.get("colid", "")
566 site_id = model_helpers.get_site_id(colid)
567 parent, _ = Page.objects.get_or_create(
568 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES,
569 parent_page=None,
570 site_id=site_id,
571 )
572 page = Page.objects.create(
573 menu_title_en="New virtual issue",
574 menu_title_fr="Nouvelle collection transverse",
575 parent_page=parent,
576 site_id=site_id,
577 state="draft",
578 )
579 kwargs = {"colid": colid, "pid": page.id}
580 return HttpResponseRedirect(reverse("virtual_issue_update", kwargs=kwargs))
583class VirtualIssuesIndex(EditorRequiredMixin, TemplateView):
584 template_name = "editorial_tools/virtual-issues-index.html"
586 def get_context_data(self, **kwargs):
587 colid = kwargs.get("colid", "")
588 site_id = model_helpers.get_site_id(colid)
589 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES)
590 pages = Page.objects.filter(site_id=site_id, parent_page=vi)
591 context = super().get_context_data(**kwargs)
592 context["journal"] = model_helpers.get_collection(colid)
593 context["pages"] = pages
594 return context
597def get_citation_fr(doi, citation_en):
598 citation_fr = citation_en
599 article = Article.objects.filter(doi=doi).first()
600 if article and article.trans_title_html:
601 trans_title = article.trans_title_html
602 try:
603 citation_fr = re.sub(
604 r'(<a href="https:\/\/doi\.org.*">)([^<]+)',
605 rf"\1{trans_title}",
606 citation_en,
607 )
608 except re.error:
609 pass
610 return citation_fr
613def summary_build(articles, colid):
614 summary_fr = ""
615 summary_en = ""
616 head = '<table id="summary"><tbody>'
617 tail = "</tbody></table>"
618 style = "max-width:180px;max-height:200px"
619 colid_lo = colid.lower()
620 site_domain = SITE_REGISTER[colid_lo]["site_domain"].split("/")
621 site_domain = "/" + site_domain[-1] if len(site_domain) == 2 else ""
623 for article in articles:
624 image_src = article.get("src", "")
625 image_name = article.get("imageName", "")
626 doi = article.get("doi", "")
627 citation_en = article.get("citation", "")
628 if doi or citation_en:
629 row_fr = f'<div href="{doi}">{get_citation_fr(doi, citation_en)}</div>'
630 row_en = f'<div href="{doi}">{citation_en}</div>'
631 if image_src:
632 date = datetime.now().strftime("%Y/%m/%d/")
633 base_url = get_media_base_url(colid)
634 suffix = os.path.join(base_url, "uploads", date)
635 image_url = os.path.join(site_domain, suffix, image_name)
636 image_header = "^data:image/.+;base64,"
637 if re.match(image_header, image_src):
638 image_src = re.sub(image_header, "", image_src)
639 base64_data = base64.b64decode(image_src)
640 base_root = get_media_base_root(colid)
641 path = os.path.join(base_root, "uploads", date)
642 os.makedirs(path, exist_ok=True)
643 with open(path + image_name, "wb") as fp:
644 fp.write(base64_data)
645 im = f'<img src="{image_url}" style="{style}" />'
646 else:
647 im = f'<img src="{site_domain}{image_src}" style="{style}" />'
648 summary_fr += f"<tr><td>{im}</td><td>{row_fr}</td></tr>"
649 summary_en += f"<tr><td>{im}</td><td>{row_en}</td></tr>"
650 summary_fr = head + summary_fr + tail
651 summary_en = head + summary_en + tail
652 return {"summary_fr": summary_fr, "summary_en": summary_en}
655# @method_decorator([csrf_exempt], name="dispatch")
656class VirtualIssueDeployView(HandleCMSMixin, View):
657 """
658 called by the Virtual.vue VueJS component, when the virtual issue is saved
659 We get data in JSON and we need to update the corresponding Page.
660 The Page is then immediately posted to the test_website.
661 The "Apply the changes to the production website" button is then used to update the (prod) website
662 => See DeployCMSAPIView
663 """
665 def post(self, request, *args, **kwargs):
666 self.init_data(self.kwargs)
668 pid = kwargs.get("pid")
669 colid = self.colid
670 data = json.loads(request.body)
671 summary = summary_build(data["articles"], colid)
672 page = get_object_or_404(Page, id=pid)
673 page.slug = page.slug_fr = page.slug_en = None
674 page.menu_title_fr = data["title_fr"]
675 page.menu_title_en = data["title_en"]
676 page.content_fr = data["head_fr"] + summary["summary_fr"] + data["tail_fr"]
677 page.content_en = data["head_en"] + summary["summary_en"] + data["tail_en"]
678 page.state = data["page"]["state"]
679 page.menu_order = data["page"]["menu_order"]
680 page.parent_page = Page.objects.filter(id=data["page"]["parent_page"]).first()
681 page.save()
683 response = deploy_cms("test_website", self.collection)
684 if response.status_code == 503:
685 messages.error(
686 self.request, "The journal website is under maintenance. Please try again later."
687 )
689 return response # HttpResponse(status=response.status_code, reason=response.reason)
692class PageIndexView(EditorRequiredMixin, TemplateView):
693 template_name = "mersenne_cms/page_index.html"
695 def get_context_data(self, **kwargs):
696 colid = kwargs.get("colid", "")
697 site_id = model_helpers.get_site_id(colid)
698 vi = Page.objects.filter(site_id=site_id, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES).first()
699 if vi: 699 ↛ 700line 699 didn't jump to line 700, because the condition on line 699 was never true
700 pages = Page.objects.filter(site_id=site_id).exclude(parent_page=vi)
701 else:
702 pages = Page.objects.filter(site_id=site_id)
703 context = super().get_context_data(**kwargs)
704 context["colid"] = colid
705 context["journal"] = model_helpers.get_collection(colid)
706 context["pages"] = pages
707 context["news"] = News.objects.filter(site_id=site_id)
708 context["fields_lang"] = "fr" if model_helpers.is_site_fr_only(site_id) else "en"
709 return context
712class PageBaseView(HandleCMSMixin, View):
713 template_name = "mersenne_cms/page_form.html"
714 model = Page
715 form_class = PageForm
717 def dispatch(self, request, *args, **kwargs):
718 self.colid = self.kwargs["colid"]
719 self.collection = model_helpers.get_collection(self.colid, sites=False)
720 self.site_id = model_helpers.get_site_id(self.colid)
722 return super().dispatch(request, *args, **kwargs)
724 def get_success_url(self):
725 return reverse("page_index", kwargs={"colid": self.colid})
727 def get_context_data(self, **kwargs):
728 context = super().get_context_data(**kwargs)
729 context["journal"] = self.collection
730 return context
732 def update_test_website(self):
733 response = deploy_cms("test_website", self.collection)
734 if response.status_code < 300: 734 ↛ 737line 734 didn't jump to line 737, because the condition on line 734 was never false
735 messages.success(self.request, "The test website has been updated")
736 else:
737 text = "ERROR: Unable to update the test website<br/>"
739 if response.status_code == 503:
740 text += "The test website is under maintenance. Please try again later.<br/>"
741 else:
742 text += f"Please contact the centre Mersenne<br/><br/>Status code: {response.status_code}<br/>"
743 if hasattr(response, "content") and response.content:
744 text += f"{response.content.decode()}<br/>"
745 if hasattr(response, "reason") and response.reason:
746 text += f"Reason: {response.reason}<br/>"
747 if hasattr(response, "text") and response.text:
748 text += f"Details: {response.text}<br/>"
749 messages.error(self.request, mark_safe(text))
751 def get_form_kwargs(self):
752 kwargs = super().get_form_kwargs()
753 kwargs["site_id"] = self.site_id
754 kwargs["user"] = self.request.user
755 return kwargs
757 def form_valid(self, form):
758 form.save()
760 self.update_test_website()
762 return HttpResponseRedirect(self.get_success_url())
765# @method_decorator([csrf_exempt], name="dispatch")
766class PageDeleteView(PageBaseView):
767 def post(self, request, *args, **kwargs):
768 colid = kwargs.get("colid", "")
769 pk = kwargs.get("pk")
770 page = get_object_or_404(Page, id=pk)
771 if page.mersenne_id:
772 raise PermissionDenied
774 page.delete()
776 self.update_test_website()
778 if page.parent_page and page.parent_page.mersenne_id == MERSENNE_ID_VIRTUAL_ISSUES:
779 return HttpResponseRedirect(reverse("virtual_issues_index", kwargs={"colid": colid}))
780 else:
781 return HttpResponseRedirect(reverse("page_index", kwargs={"colid": colid}))
784class PageCreateView(PageBaseView, CreateView):
785 def get_context_data(self, **kwargs):
786 context = super().get_context_data(**kwargs)
787 context["title"] = "Add a menu page"
788 return context
791class PageUpdateView(PageBaseView, UpdateView):
792 def get_context_data(self, **kwargs):
793 context = super().get_context_data(**kwargs)
794 context["title"] = "Edit a menu page"
795 return context
798class NewsBaseView(PageBaseView):
799 template_name = "mersenne_cms/news_form.html"
800 model = News
801 form_class = NewsForm
804class NewsDeleteView(NewsBaseView):
805 def post(self, request, *args, **kwargs):
806 pk = kwargs.get("pk")
807 news = get_object_or_404(News, id=pk)
809 news.delete()
811 self.update_test_website()
813 return HttpResponseRedirect(self.get_success_url())
816class NewsCreateView(NewsBaseView, CreateView):
817 def get_context_data(self, **kwargs):
818 context = super().get_context_data(**kwargs)
819 context["title"] = "Add a News"
820 return context
823class NewsUpdateView(NewsBaseView, UpdateView):
824 def get_context_data(self, **kwargs):
825 context = super().get_context_data(**kwargs)
826 context["title"] = "Edit a News"
827 return context
830# def page_create_view(request, colid):
831# context = {}
832# if not is_authorized_editor(request.user, colid):
833# raise PermissionDenied
834# collection = model_helpers.get_collection(colid)
835# page = Page(site_id=model_helpers.get_site_id(colid))
836# form = PageForm(request.POST or None, instance=page)
837# if form.is_valid():
838# form.save()
839# response = deploy_cms("test_website", collection)
840# if response.status_code < 300:
841# messages.success(request, "Page created successfully.")
842# else:
843# text = f"ERROR: page creation failed<br/>Status code: {response.status_code}<br/>"
844# if hasattr(response, "reason") and response.reason:
845# text += f"Reason: {response.reason}<br/>"
846# if hasattr(response, "text") and response.text:
847# text += f"Details: {response.text}<br/>"
848# messages.error(request, mark_safe(text))
849# kwargs = {"colid": colid, "pid": form.instance.id}
850# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
851#
852# context["form"] = form
853# context["title"] = "Add a menu page"
854# context["journal"] = collection
855# return render(request, "mersenne_cms/page_form.html", context)
858# def page_update_view(request, colid, pid):
859# context = {}
860# if not is_authorized_editor(request.user, colid):
861# raise PermissionDenied
862#
863# collection = model_helpers.get_collection(colid)
864# page = get_object_or_404(Page, id=pid)
865# form = PageForm(request.POST or None, instance=page)
866# if form.is_valid():
867# form.save()
868# response = deploy_cms("test_website", collection)
869# if response.status_code < 300:
870# messages.success(request, "Page updated successfully.")
871# else:
872# text = f"ERROR: page update failed<br/>Status code: {response.status_code}<br/>"
873# if hasattr(response, "reason") and response.reason:
874# text += f"Reason: {response.reason}<br/>"
875# if hasattr(response, "text") and response.text:
876# text += f"Details: {response.text}<br/>"
877# messages.error(request, mark_safe(text))
878# kwargs = {"colid": colid, "pid": form.instance.id}
879# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs))
880#
881# context["form"] = form
882# context["pid"] = pid
883# context["title"] = "Edit a menu page"
884# context["journal"] = collection
885# return render(request, "mersenne_cms/page_form.html", context)