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

1import base64 

2import json 

3import os 

4import re 

5import shutil 

6from datetime import datetime 

7 

8import requests 

9from ckeditor_uploader.views import ImageUploadView 

10from ckeditor_uploader.views import browse 

11from requests import Timeout 

12 

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 

33 

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 

54 

55from .base_views import check_lock 

56 

57 

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" 

64 

65 return os.path.join(settings.RESOURCES_ROOT, "media", colid) 

66 

67 

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) 

74 

75 

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" 

82 

83 return os.path.join(settings.MERSENNE_PROD_DATA_FOLDER, "media", colid) 

84 

85 

86def get_media_base_url(colid): 

87 path = os.path.join(settings.MEDIA_URL, colid) 

88 

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" 

99 

100 return path 

101 

102 

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 """ 

111 

112 from ckeditor_uploader import utils 

113 from ckeditor_uploader import views 

114 

115 from django.core.files.storage import FileSystemStorage 

116 

117 storage = FileSystemStorage( 

118 location=get_media_base_root(colid), base_url=get_media_base_url(colid) 

119 ) 

120 

121 utils.storage = storage 

122 views.storage = storage 

123 

124 

125class EditorRequiredMixin(UserPassesTestMixin): 

126 def test_func(self): 

127 return is_authorized_editor(self.request.user, self.kwargs.get("colid")) 

128 

129 

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 """ 

138 

139 def dispatch(self, request, *args, **kwargs): 

140 colid = kwargs["colid"] 

141 

142 change_ckeditor_storage(colid) 

143 

144 return super().dispatch(request, **kwargs) 

145 

146 

147class CollectionBrowseView(EditorRequiredMixin, View): 

148 def dispatch(self, request, **kwargs): 

149 colid = kwargs["colid"] 

150 

151 change_ckeditor_storage(colid) 

152 

153 return browse(request) 

154 

155 

156file_upload_in_collection = csrf_exempt(CollectionImageUploadView.as_view()) 

157file_browse_in_collection = csrf_exempt(CollectionBrowseView.as_view()) 

158 

159 

160def deploy_cms(site, collection): 

161 colid = collection.pid 

162 base_url = getattr(collection, site)() 

163 

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"}) 

166 

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) 

170 

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}") 

179 

180 pages = get_pages_content(colid) 

181 news = get_news_content(colid) 

182 

183 data = json.dumps({"pages": json.loads(pages), "news": json.loads(news)}) 

184 url = getattr(collection, site)() + "/import_cms/" 

185 

186 try: 

187 response = requests.put(url, data=data, verify=False) 

188 

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) 

194 

195 except Timeout as exception: 

196 return HttpResponse(exception, status=408) 

197 except Exception as exception: 

198 return HttpResponseServerError(exception) 

199 

200 return JsonResponse({"message": "OK"}) 

201 

202 

203class HandleCMSMixin(EditorRequiredMixin): 

204 """ 

205 Mixin for classes that need to send request to (test) website to import/export CMS content (pages, news) 

206 """ 

207 

208 # def dispatch(self, request, *args, **kwargs): 

209 # self.colid = self.kwargs["colid"] 

210 # return super().dispatch(request, *args, **kwargs) 

211 

212 def init_data(self, kwargs): 

213 self.collection = None 

214 

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") 

220 

221 test_server_url = self.collection.test_website() 

222 if not test_server_url: 

223 raise Http404("The collection has no test site") 

224 

225 prod_server_url = self.collection.website() 

226 if not prod_server_url: 

227 raise Http404("The collection has no prod site") 

228 

229 

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 """ 

235 

236 def get(self, request, *args, **kwargs): 

237 self.init_data(self.kwargs) 

238 

239 site = kwargs.get("site", "test_website") 

240 

241 try: 

242 url = getattr(self.collection, site)() + "/export_cms/" 

243 response = requests.get(url, verify=False) 

244 

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")) 

253 

254 except Timeout as exception: 

255 return HttpResponse(exception, status=408) 

256 except Exception as exception: 

257 return HttpResponseServerError(exception) 

258 

259 return JsonResponse({"message": "OK", "status": 200}) 

260 

261 

262class RestoreCMSAPIView(HandleCMSMixin, View): 

263 """ 

264 Restore the Trammel CMS content (of a colid) from disk 

265 """ 

266 

267 def get(self, request, *args, **kwargs): 

268 self.init_data(self.kwargs) 

269 

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) 

274 

275 pages = json_data.get("pages") 

276 import_pages(pages, self.colid) 

277 

278 if "news" in json_data: 

279 news = json_data.get("news") 

280 import_news(news, self.colid) 

281 

282 return JsonResponse({"message": "OK", "status": 200}) 

283 

284 

285class DeployCMSAPIView(HandleCMSMixin, View): 

286 def get(self, request, *args, **kwargs): 

287 self.init_data(self.kwargs) 

288 

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}) 

293 

294 site = kwargs.get("site", "test_website") 

295 

296 response = deploy_cms(site, self.collection) 

297 

298 if response.status_code == 503: 

299 messages.error( 

300 self.request, "The journal website is under maintenance. Please try again later." 

301 ) 

302 

303 return response 

304 

305 

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 

317 

318 

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) 

324 

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() 

345 

346 

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)) 

369 

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, [], "")] 

384 

385 

386class SuggestUpdateView(EditorRequiredMixin, TemplateView): 

387 template_name = "editorial_tools/suggested.html" 

388 

389 def get_context_data(self, **kwargs): 

390 doi = kwargs.get("doi", "") 

391 article = get_object_or_404(Article, doi=doi) 

392 

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 

419 

420 

421class EditorialToolsVolumeItemsView(EditorRequiredMixin, TemplateView): 

422 template_name = "editorial_tools/volume-items.html" 

423 

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 

433 

434 

435class EditorialToolsArticleView(EditorRequiredMixin, TemplateView): 

436 template_name = "editorial_tools/find-article.html" 

437 

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) 

442 

443 context = super().get_context_data(**kwargs) 

444 context["article"] = article 

445 context["citation_base"] = article.get_citation_base().strip(", .") 

446 return context 

447 

448 

449class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView): 

450 template_name = "editorial_tools/graphical-abstract.html" 

451 

452 def get_context_data(self, **kwargs): 

453 doi = kwargs.get("doi", "") 

454 article = get_object_or_404(Article, doi=doi) 

455 

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 

466 

467 

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) 

473 

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() 

504 

505 

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": []} 

510 

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} 

526 

527 

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) 

532 

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] 

541 

542 content_fr = parse_content(page.content_fr) 

543 data["head_fr"] = content_fr["head"] 

544 data["tail_fr"] = content_fr["tail"] 

545 

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) 

551 

552 

553class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView): 

554 template_name = "editorial_tools/virtual-issue.html" 

555 

556 def get(self, request, *args, **kwargs): 

557 pid = kwargs.get("pid", "") 

558 get_object_or_404(Page, id=pid) 

559 

560 return super().get(request, *args, **kwargs) 

561 

562 

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)) 

581 

582 

583class VirtualIssuesIndex(EditorRequiredMixin, TemplateView): 

584 template_name = "editorial_tools/virtual-issues-index.html" 

585 

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 

595 

596 

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 

611 

612 

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 "" 

622 

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} 

653 

654 

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 """ 

664 

665 def post(self, request, *args, **kwargs): 

666 self.init_data(self.kwargs) 

667 

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() 

682 

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 ) 

688 

689 return response # HttpResponse(status=response.status_code, reason=response.reason) 

690 

691 

692class PageIndexView(EditorRequiredMixin, TemplateView): 

693 template_name = "mersenne_cms/page_index.html" 

694 

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 

710 

711 

712class PageBaseView(HandleCMSMixin, View): 

713 template_name = "mersenne_cms/page_form.html" 

714 model = Page 

715 form_class = PageForm 

716 

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) 

721 

722 return super().dispatch(request, *args, **kwargs) 

723 

724 def get_success_url(self): 

725 return reverse("page_index", kwargs={"colid": self.colid}) 

726 

727 def get_context_data(self, **kwargs): 

728 context = super().get_context_data(**kwargs) 

729 context["journal"] = self.collection 

730 return context 

731 

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/>" 

738 

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)) 

750 

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 

756 

757 def form_valid(self, form): 

758 form.save() 

759 

760 self.update_test_website() 

761 

762 return HttpResponseRedirect(self.get_success_url()) 

763 

764 

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 

773 

774 page.delete() 

775 

776 self.update_test_website() 

777 

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})) 

782 

783 

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 

789 

790 

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 

796 

797 

798class NewsBaseView(PageBaseView): 

799 template_name = "mersenne_cms/news_form.html" 

800 model = News 

801 form_class = NewsForm 

802 

803 

804class NewsDeleteView(NewsBaseView): 

805 def post(self, request, *args, **kwargs): 

806 pk = kwargs.get("pk") 

807 news = get_object_or_404(News, id=pk) 

808 

809 news.delete() 

810 

811 self.update_test_website() 

812 

813 return HttpResponseRedirect(self.get_success_url()) 

814 

815 

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 

821 

822 

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 

828 

829 

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) 

856 

857 

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)