Coverage for src / ptf_tools / views / cms_views.py: 49%

878 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-08-03 13:45 +0000

1import base64 

2import json 

3import os 

4import re 

5import shutil 

6from datetime import datetime 

7 

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.utils import build_jats_data_from_html_field 

48from ptf.cmds.xml.jats.builder.issue import build_title_xml 

49from ptf.display import resolver 

50 

51# from ptf.display import resolver 

52from ptf.exceptions import ServerUnderMaintenance 

53 

54# from ptf.model_data import ArticleData 

55from ptf.model_data import ( 

56 create_abstract, 

57 create_contributor, 

58 create_datastream, 

59 create_issuedata, 

60 create_publisherdata, 

61 create_titledata, 

62) 

63 

64# from ptf.models import ExtLink 

65# from ptf.models import ResourceInSpecialIssue 

66# from ptf.models import Contribution 

67# from ptf.models import Collection 

68from ptf.models import ( 

69 Article, 

70 Collection, 

71 Container, 

72 ContribAddress, 

73 ExtLink, 

74 GraphicalAbstract, 

75 RelatedArticles, 

76 RelatedObject, 

77) 

78from ptf.site_register import SITE_REGISTER 

79from ptf.utils import ImageManager, get_names 

80from requests import Timeout 

81 

82from ptf_tools.forms import GraphicalAbstractForm, NewsForm, PageForm, RelatedForm 

83from ptf_tools.utils import is_authorized_editor 

84 

85from .base_views import check_lock 

86 

87 

88def get_media_base_root(colid): 

89 """ 

90 Base folder where media files are stored in Trammel 

91 """ 

92 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]: 

93 colid = "CR" 

94 

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

96 

97 

98def get_media_base_root_in_test(colid): 

99 """ 

100 Base folder where media files are stored in the test website 

101 Use the same folder as the Trammel media folder so that no copy is necessary when deploy in test 

102 """ 

103 return get_media_base_root(colid) 

104 

105 

106def get_media_base_root_in_prod(colid): 

107 """ 

108 Base folder where media files are stored in the prod website 

109 """ 

110 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]: 

111 colid = "CR" 

112 

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

114 

115 

116def get_media_base_url(colid): 

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

118 

119 if colid in ["CRMECA", "CRBIOL", "CRGEOS", "CRCHIM", "CRMATH", "CRPHYS"]: 

120 prefixes = { 

121 "CRMECA": "mecanique", 

122 "CRBIOL": "biologies", 

123 "CRGEOS": "geoscience", 

124 "CRCHIM": "chimie", 

125 "CRMATH": "mathematique", 

126 "CRPHYS": "physique", 

127 } 

128 path = os.path.join(settings.MEDIA_URL, "CR") 

129 path = f"/{prefixes[colid]}{path}" 

130 

131 return path 

132 

133 

134def change_ckeditor_storage(colid): 

135 """ 

136 By default, CKEditor stores all the files under 1 folder (MEDIA_ROOT) 

137 We want to store the files under a subfolder of @colid 

138 To do that we have to 

139 - change the URL calling this view to pass the site_id (info used by the Pages to filter the objects) 

140 - modify the storage location 

141 """ 

142 

143 from ckeditor_uploader import utils, views 

144 from django.core.files.storage import FileSystemStorage 

145 

146 storage = FileSystemStorage( 

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

148 ) 

149 

150 utils.storage = storage 

151 views.storage = storage 

152 

153 

154class EditorRequiredMixin(UserPassesTestMixin): 

155 def test_func(self): 

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

157 

158 

159class CollectionImageUploadView(EditorRequiredMixin, ImageUploadView): 

160 """ 

161 By default, CKEditor stores all the files under 1 folder (MEDIA_ROOT) 

162 We want to store the files under a subfolder of @colid 

163 To do that we have to 

164 - change the URL calling this view to pass the site_id (info used by the Pages to filter the objects) 

165 - modify the storage location 

166 """ 

167 

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

169 colid = kwargs["colid"] 

170 

171 change_ckeditor_storage(colid) 

172 

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

174 

175 

176class CollectionBrowseView(EditorRequiredMixin, View): 

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

178 colid = kwargs["colid"] 

179 

180 change_ckeditor_storage(colid) 

181 

182 return browse(request) 

183 

184 

185file_upload_in_collection = csrf_exempt(CollectionImageUploadView.as_view()) 

186file_browse_in_collection = csrf_exempt(CollectionBrowseView.as_view()) 

187 

188 

189def deploy_cms(site, collection): 

190 colid = collection.pid 

191 base_url = getattr(collection, site)() 

192 

193 if base_url is None: 193 ↛ 196line 193 didn't jump to line 196 because the condition on line 193 was always true

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

195 

196 if site == "website": 

197 from_base_path = get_media_base_root_in_test(colid) 

198 to_base_path = get_media_base_root_in_prod(colid) 

199 

200 for sub_path in ["uploads", "images"]: 

201 from_path = os.path.join(from_base_path, sub_path) 

202 to_path = os.path.join(to_base_path, sub_path) 

203 if os.path.exists(from_path): 

204 try: 

205 shutil.copytree(from_path, to_path, dirs_exist_ok=True) 

206 except OSError as exception: 

207 return HttpResponseServerError(f"Error during copy: {exception}") 

208 

209 site_id = model_helpers.get_site_id(colid) 

210 if model_helpers.get_site_default_language(site_id): 

211 from modeltranslation import fields, manager 

212 

213 old_ftor = manager.get_language 

214 manager.get_language = monkey_get_language_en 

215 fields.get_language = monkey_get_language_en 

216 

217 pages = get_pages_content(colid) 

218 news = get_news_content(colid) 

219 

220 manager.get_language = old_ftor 

221 fields.get_language = old_ftor 

222 else: 

223 pages = get_pages_content(colid) 

224 news = get_news_content(colid) 

225 

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

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

228 

229 try: 

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

231 

232 if response.status_code == 503: 

233 e = ServerUnderMaintenance( 

234 "The journal test website is under maintenance. Please try again later." 

235 ) 

236 return HttpResponseServerError(e, status=503) 

237 

238 except Timeout as exception: 

239 return HttpResponse(exception, status=408) 

240 except Exception as exception: 

241 return HttpResponseServerError(exception) 

242 

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

244 

245 

246class HandleCMSMixin(EditorRequiredMixin): 

247 """ 

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

249 """ 

250 

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

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

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

254 

255 def init_data(self, kwargs): 

256 self.collection = None 

257 

258 self.colid = kwargs.get("colid", None) 

259 if self.colid: 

260 self.collection = model_helpers.get_collection(self.colid) 

261 if not self.collection: 

262 raise Http404(f"{self.colid} does not exist") 

263 

264 test_server_url = self.collection.test_website() 

265 if not test_server_url: 

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

267 

268 prod_server_url = self.collection.website() 

269 if not prod_server_url: 

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

271 

272 

273class GetCMSFromSiteAPIView(HandleCMSMixin, View): 

274 """ 

275 Get the CMS content from the (test) website and save it on disk. 

276 It can be used if needed to restore the Trammel content with RestoreCMSAPIView below 

277 """ 

278 

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

280 self.init_data(self.kwargs) 

281 

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

283 

284 try: 

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

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

287 

288 # Just to need to save the json on disk 

289 # Media files are already saved in MEDIA_ROOT which is equal to 

290 # /mersenne_test_data/@colid/media 

291 folder = get_media_base_root(self.colid) 

292 os.makedirs(folder, exist_ok=True) 

293 filename = os.path.join(folder, f"pages_{self.colid}.json") 

294 with open(filename, mode="w", encoding="utf-8") as file: 

295 file.write(response.content.decode(encoding="utf-8")) 

296 

297 except Timeout as exception: 

298 return HttpResponse(exception, status=408) 

299 except Exception as exception: 

300 return HttpResponseServerError(exception) 

301 

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

303 

304 

305def monkey_get_language_en(): 

306 return "en" 

307 

308 

309class RestoreCMSAPIView(HandleCMSMixin, View): 

310 """ 

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

312 """ 

313 

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

315 self.init_data(self.kwargs) 

316 

317 folder = get_media_base_root(self.colid) 

318 filename = os.path.join(folder, f"pages_{self.colid}.json") 

319 with open(filename, encoding="utf-8") as f: 

320 json_data = json.load(f) 

321 

322 pages = json_data.get("pages") 

323 

324 site_id = model_helpers.get_site_id(self.colid) 

325 if model_helpers.get_site_default_language(site_id): 

326 from modeltranslation import fields, manager 

327 

328 old_ftor = manager.get_language 

329 manager.get_language = monkey_get_language_en 

330 fields.get_language = monkey_get_language_en 

331 

332 import_pages(pages, self.colid) 

333 

334 manager.get_language = old_ftor 

335 fields.get_language = old_ftor 

336 else: 

337 import_pages(pages, self.colid) 

338 

339 if "news" in json_data: 

340 news = json_data.get("news") 

341 import_news(news, self.colid) 

342 

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

344 

345 

346class DeployCMSAPIView(HandleCMSMixin, View): 

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

348 self.init_data(self.kwargs) 

349 

350 if check_lock(): 

351 msg = "Trammel is under maintenance. Please try again later." 

352 messages.error(self.request, msg) 

353 return JsonResponse({"messages": msg, "status": 503}) 

354 

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

356 

357 response = deploy_cms(site, self.collection) 

358 

359 if response.status_code == 503: 

360 messages.error( 

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

362 ) 

363 

364 return response 

365 

366 

367def get_server_urls(collection, site="test_website"): 

368 urls = [""] 

369 if hasattr(settings, "MERSENNE_DEV_URL"): 369 ↛ 371line 369 didn't jump to line 371 because the condition on line 369 was never true

370 # set RESOURCES_ROOT and apache config accordingly (for instance with "/mersenne_dev_data") 

371 url = getattr(collection, "test_website")().split(".fr") 

372 urls = [settings.MERSENNE_DEV_URL + url[1] if len(url) == 2 else ""] 

373 elif site == "both": 373 ↛ 374line 373 didn't jump to line 374 because the condition on line 373 was never true

374 urls = [getattr(collection, "test_website")(), getattr(collection, "website")()] 

375 elif hasattr(collection, site) and getattr(collection, site)(): 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true

376 urls = [getattr(collection, site)()] 

377 return urls 

378 

379 

380class SuggestDeployView(EditorRequiredMixin, View): 

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

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

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

384 article = get_object_or_404(Article, doi=doi) 

385 

386 obj, created = RelatedArticles.objects.get_or_create(resource=article) 

387 form = RelatedForm(request.POST or None, instance=obj) 

388 if form.is_valid(): 388 ↛ 405line 388 didn't jump to line 405 because the condition on line 388 was always true

389 data = form.cleaned_data 

390 obj.date_modified = timezone.now() 

391 form.save() 

392 collection = article.my_container.my_collection 

393 urls = get_server_urls(collection, site=site) 

394 response = requests.models.Response() 

395 for url in urls: 395 ↛ 403line 395 didn't jump to line 403 because the loop on line 395 didn't complete

396 url = url + reverse("api-update-suggest", kwargs={"doi": doi}) 

397 try: 

398 response = requests.post(url, data=data, timeout=15) 

399 except requests.exceptions.RequestException as e: 

400 response.status_code = 503 

401 response.reason = e.args[0] 

402 break 

403 return HttpResponse(status=response.status_code, reason=response.reason) 

404 else: 

405 return HttpResponseBadRequest() 

406 

407 

408def suggest_debug(results, article, message): 

409 crop_results = 5 

410 if results: 410 ↛ 411line 410 didn't jump to line 411 because the condition on line 410 was never true

411 dois = [] 

412 results["docs"] = results["docs"][:crop_results] 

413 numFound = f"({len(results['docs'])} sur {results['numFound']} documents)" 

414 head = f"Résultats de la recherche automatique {numFound} :\n\n" 

415 for item in results["docs"]: 

416 doi = item.get("doi") 

417 if doi: 

418 explain = results["explain"][item["id"]] 

419 terms = re.findall(r"([0-9.]+?) = weight\((.+?:.+?) in", explain) 

420 terms.sort(key=lambda t: t[0], reverse=True) 

421 details = (" + ").join(f"{round(float(s), 1)}:{t}" for s, t in terms) 

422 score = f"Score : {round(float(item['score']), 1)} (= {details})\n" 

423 url = "" 

424 suggest = Article.objects.filter(doi=doi).first() 

425 if suggest and suggest.my_container: 

426 collection = suggest.my_container.my_collection 

427 base_url = collection.website() or "" 

428 url = base_url + "/articles/" + doi 

429 dois.append((doi, url, score)) 

430 

431 tail = f"\n\nScore minimum retenu : {results['params']['min_score']}\n\n\n" 

432 tail += "Termes principaux utilisés pour la requête " 

433 tail = [tail + "(champ:terme recherché | pertinence du terme) :\n"] 

434 if results["params"]["mlt.fl"] == "all": 

435 tail.append(" * all = body + abstract + title + authors + keywords\n") 

436 terms = results["interestingTerms"] 

437 terms = [" | ".join((x[0], str(x[1]))) for x in zip(terms[::2], terms[1::2])] 

438 tail.extend(reversed(terms)) 

439 tail.append("\n\nParamètres de la requête :\n") 

440 tail.extend([f"{k}: {v} " for k, v in results["params"].items()]) 

441 return [(head, dois, "\n".join(tail))] 

442 else: 

443 msg = f"Erreur {message['status']} {message['err']} at {message['url']}" 

444 return [(msg, [], "")] 

445 

446 

447class SuggestUpdateView(EditorRequiredMixin, TemplateView): 

448 template_name = "editorial_tools/suggested.html" 

449 

450 def get_context_data(self, **kwargs): 

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

452 article = get_object_or_404(Article, doi=doi) 

453 

454 obj, created = RelatedArticles.objects.get_or_create(resource=article) 

455 collection = article.my_container.my_collection 

456 base_url = collection.website() or "" 

457 response = requests.models.Response() 

458 try: 

459 response = requests.get(base_url + "/mlt/" + doi, timeout=10.0) 

460 except requests.exceptions.RequestException as e: 

461 response.status_code = 503 

462 response.reason = e.args[0] 

463 msg = { 

464 "url": response.url, 

465 "status": response.status_code, 

466 "err": response.reason, 

467 } 

468 results = None 

469 if response.status_code == 200: 469 ↛ 470line 469 didn't jump to line 470 because the condition on line 469 was never true

470 results = solr_cmds.auto_suggest_doi(obj, article, response.json()) 

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

472 context["debug"] = suggest_debug(results, article, msg) 

473 context["form"] = RelatedForm(instance=obj) 

474 context["author"] = "; ".join(get_names(article, "author")) 

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

476 context["article"] = article 

477 context["date_modified"] = obj.date_modified 

478 context["url"] = base_url + "/articles/" + doi 

479 return context 

480 

481 

482class EditorialToolsVolumeItemsView(EditorRequiredMixin, TemplateView): 

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

484 

485 def get_context_data(self, **kwargs): 

486 vid = kwargs.get("vid") 

487 issues_articles, collection = model_helpers.get_issues_in_volume(vid) 

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

489 context["issues_articles"] = issues_articles 

490 context["collection"] = collection 

491 return context 

492 

493 

494class EditorialToolsArticleView(EditorRequiredMixin, TemplateView): 

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

496 

497 def get_context_data(self, **kwargs): 

498 colid = kwargs.get("colid") 

499 doi = kwargs.get("doi") 

500 article = get_object_or_404(Article, doi=doi, my_container__my_collection__pid=colid) 

501 

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

503 context["article"] = article 

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

505 return context 

506 

507 

508class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView): 

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

510 

511 def get_context_data(self, **kwargs): 

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

513 article = get_object_or_404(Article, doi=doi) 

514 

515 obj, created = GraphicalAbstract.objects.get_or_create(resource=article) 

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

517 context["author"] = "; ".join(get_names(article, "author")) 

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

519 context["article"] = article 

520 context["date_modified"] = obj.date_modified 

521 context["form"] = GraphicalAbstractForm(instance=obj) 

522 context["graphical_abstract"] = obj.graphical_abstract 

523 context["illustration"] = obj.illustration 

524 return context 

525 

526 

527class GraphicalAbstractDeployView(EditorRequiredMixin, View): 

528 def __get_path_and_replace_tiff_file(self, obj_attribute_file): 

529 """ 

530 Returns the path of the attribute. 

531 If the attribute is a tiff file, converts it to jpg, delete the old tiff file, and return the new path of the attribute. 

532 

533 Checks if paths have already been processed, to prevent issues related to object mutation. 

534 """ 

535 if obj_attribute_file.name.lower().endswith((".tiff", ".tif")): 

536 jpeg_path = ImageManager(obj_attribute_file.path).to_jpeg(delete_original_tiff=True) 

537 with open(jpeg_path, "rb") as fp: 

538 obj_attribute_file.save(os.path.basename(jpeg_path), File(fp), save=True) 

539 return jpeg_path 

540 

541 return obj_attribute_file.path 

542 

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

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

545 site = kwargs.get("site", "both") 

546 article = get_object_or_404(Article, doi=doi) 

547 

548 obj, created = GraphicalAbstract.objects.get_or_create(resource=article) 

549 form = GraphicalAbstractForm(request.POST, request.FILES or None, instance=obj) 

550 if form.is_valid(): 550 ↛ 579line 550 didn't jump to line 579 because the condition on line 550 was always true

551 obj.date_modified = timezone.now() 

552 data = {"date_modified": obj.date_modified} 

553 form.save() 

554 files = {} 

555 

556 for attribute in ("graphical_abstract", "illustration"): 

557 obj_attribute_file = getattr(obj, attribute, None) 

558 if obj_attribute_file and os.path.exists(obj_attribute_file.path): 558 ↛ 559line 558 didn't jump to line 559 because the condition on line 558 was never true

559 file_path = self.__get_path_and_replace_tiff_file(obj_attribute_file) 

560 with open(file_path, "rb") as fp: 

561 files.update({attribute: (obj_attribute_file.name, fp.read())}) 

562 

563 collection = article.my_container.my_collection 

564 urls = get_server_urls(collection, site=site) 

565 response = requests.models.Response() 

566 for url in urls: 566 ↛ 577line 566 didn't jump to line 577 because the loop on line 566 didn't complete

567 url = url + reverse("api-graphical-abstract", kwargs={"doi": doi}) 

568 try: 

569 if not obj.graphical_abstract and not obj.illustration: 569 ↛ 572line 569 didn't jump to line 572 because the condition on line 569 was always true

570 response = requests.delete(url, data=data, files=files, timeout=15) 

571 else: 

572 response = requests.post(url, data=data, files=files, timeout=15) 

573 except requests.exceptions.RequestException as e: 

574 response.status_code = 503 

575 response.reason = e.args[0] 

576 break 

577 return HttpResponse(status=response.status_code, reason=response.reason) 

578 else: 

579 return HttpResponseBadRequest() 

580 

581 

582def parse_content(content): 

583 table = re.search(r'(.*?)(<table id="summary".+?</table>)(.*)', content, re.DOTALL) 

584 if not table: 

585 return {"head": content, "tail": "", "articles": []} 

586 

587 articles = [] 

588 rows = re.findall(r"<tr>.+?</tr>", table.group(2), re.DOTALL) 

589 for row in rows: 

590 citation = re.search(r'<div href=".*?">(.*?)</div>', row, re.DOTALL) 

591 href = re.search(r'href="(.+?)\/?">', row) 

592 doi = re.search(r"(10[.].+)", href.group(1)) if href else "" 

593 src = re.search(r'<img.+?src="(.+?)"', row) 

594 item = {} 

595 item["citation"] = citation.group(1) if citation else "" 

596 item["doi"] = doi.group(1) if doi else href.group(1) if href else "" 

597 item["src"] = src.group(1) if src else "" 

598 item["imageName"] = item["src"].split("/")[-1] if item["src"] else "" 

599 if item["doi"] or item["src"]: 

600 articles.append(item) 

601 return {"head": table.group(1), "tail": table.group(3), "articles": articles} 

602 

603 

604class VirtualIssueParseView(EditorRequiredMixin, View): 

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

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

607 page = get_object_or_404(Page, id=pid) 

608 

609 data = {"pid": pid} 

610 data["colid"] = kwargs.get("colid", "") 

611 journal = model_helpers.get_collection(data["colid"]) 

612 data["journal_title"] = journal.title_tex.replace(".", "") 

613 site_id = model_helpers.get_site_id(data["colid"]) 

614 data["page"] = model_to_dict(page) 

615 pages = Page.objects.filter(site_id=site_id).exclude(id=pid) 

616 data["parents"] = [model_to_dict(p, fields=["id", "menu_title"]) for p in pages] 

617 

618 content_fr = parse_content(page.content_fr) 

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

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

621 

622 content_en = parse_content(page.content_en) 

623 data["articles"] = content_en["articles"] 

624 data["head_en"] = content_en["head"] 

625 data["tail_en"] = content_en["tail"] 

626 return JsonResponse(data) 

627 

628 

629class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView): 

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

631 

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

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

634 get_object_or_404(Page, id=pid) 

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

636 

637 

638class VirtualIssueCreateView(EditorRequiredMixin, View): 

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

640 colid = kwargs.get("colid", "") 

641 site_id = model_helpers.get_site_id(colid) 

642 parent, _ = Page.objects.get_or_create( 

643 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES, 

644 parent_page=None, 

645 site_id=site_id, 

646 ) 

647 page = Page.objects.create( 

648 menu_title_en="New virtual issue", 

649 menu_title_fr="Nouvelle collection transverse", 

650 parent_page=parent, 

651 site_id=site_id, 

652 state="draft", 

653 ) 

654 kwargs = {"colid": colid, "pid": page.id} 

655 return HttpResponseRedirect(reverse("virtual_issue_update", kwargs=kwargs)) 

656 

657 

658class SpecialIssuesIndex(EditorRequiredMixin, TemplateView): 

659 template_name = "editorial_tools/special-issues-index.html" 

660 

661 def get_context_data(self, **kwargs): 

662 colid = kwargs.get("colid", "") 

663 

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

665 context["colid"] = colid 

666 collection = Collection.objects.get(pid=colid) 

667 context["special_issues"] = Container.objects.filter( 

668 Q(ctype="issue_special") | Q(ctype="issue_special_img") 

669 ).filter(my_collection=collection) 

670 

671 context["journal"] = model_helpers.get_collection(colid, sites=False) 

672 return context 

673 

674 

675class SpecialIssueEditView(EditorRequiredMixin, TemplateView): 

676 template_name = "editorial_tools/special-issue-edit.html" 

677 

678 def get_context_data(self, **kwargs): 

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

680 return context 

681 

682 

683class VirtualIssuesIndex(EditorRequiredMixin, TemplateView): 

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

685 

686 def get_context_data(self, **kwargs): 

687 colid = kwargs.get("colid", "") 

688 site_id = model_helpers.get_site_id(colid) 

689 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES) 

690 pages = Page.objects.filter(site_id=site_id, parent_page=vi) 

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

692 context["journal"] = model_helpers.get_collection(colid) 

693 context["pages"] = pages 

694 return context 

695 

696 

697def get_citation_fr(doi, citation_en): 

698 citation_fr = citation_en 

699 article = Article.objects.filter(doi=doi).first() 

700 if article and article.trans_title_html: 

701 trans_title = article.trans_title_html 

702 try: 

703 citation_fr = re.sub( 

704 r'(<a href="https:\/\/doi\.org.*">)([^<]+)', 

705 rf"\1{trans_title}", 

706 citation_en, 

707 ) 

708 except re.error: 

709 pass 

710 return citation_fr 

711 

712 

713def summary_build(articles, colid): 

714 summary_fr = "" 

715 summary_en = "" 

716 head = '<table id="summary"><tbody>' 

717 tail = "</tbody></table>" 

718 style = "max-width:180px;max-height:200px" 

719 colid_lo = colid.lower() 

720 site_domain = SITE_REGISTER[colid_lo]["site_domain"].split("/") 

721 site_domain = "/" + site_domain[-1] if len(site_domain) == 2 else "" 

722 

723 for article in articles: 

724 image_src = article.get("src", "") 

725 image_name = article.get("imageName", "") 

726 doi = article.get("doi", "") 

727 citation_en = article.get("citation", "") 

728 if doi or citation_en: 

729 row_fr = f'<div href="{doi}">{get_citation_fr(doi, citation_en)}</div>' 

730 row_en = f'<div href="{doi}">{citation_en}</div>' 

731 if image_src: 

732 date = datetime.now().strftime("%Y/%m/%d/") 

733 base_url = get_media_base_url(colid) 

734 suffix = os.path.join(base_url, "uploads", date) 

735 image_url = os.path.join(site_domain, suffix, image_name) 

736 image_header = "^data:image/.+;base64," 

737 if re.match(image_header, image_src): 

738 image_src = re.sub(image_header, "", image_src) 

739 base64_data = base64.b64decode(image_src) 

740 base_root = get_media_base_root(colid) 

741 path = os.path.join(base_root, "uploads", date) 

742 os.makedirs(path, exist_ok=True) 

743 with open(path + image_name, "wb") as fp: 

744 fp.write(base64_data) 

745 im = f'<img src="{image_url}" style="{style}" />' 

746 # TODO mettre la vrai valeur pour le SITE_DOMAIN 

747 elif settings.SITE_DOMAIN == "http://127.0.0.1:8002": 

748 im = f'<img src="{image_src}" style="{style}" />' 

749 else: 

750 im = f'<img src="{site_domain}{image_src}" style="{style}" />' 

751 summary_fr += f"<tr><td>{im}</td><td>{row_fr}</td></tr>" 

752 summary_en += f"<tr><td>{im}</td><td>{row_en}</td></tr>" 

753 summary_fr = head + summary_fr + tail 

754 summary_en = head + summary_en + tail 

755 return {"summary_fr": summary_fr, "summary_en": summary_en} 

756 

757 

758# @method_decorator([csrf_exempt], name="dispatch") 

759class VirtualIssueDeployView(HandleCMSMixin, View): 

760 """ 

761 called by the Virtual.vue VueJS component, when the virtual issue is saved 

762 We get data in JSON and we need to update the corresponding Page. 

763 The Page is then immediately posted to the test_website. 

764 The "Apply changes to production website" button is then used to update the (prod) website 

765 => See DeployCMSAPIView 

766 """ 

767 

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

769 self.init_data(self.kwargs) 

770 if check_lock(): 

771 msg = "Trammel is under maintenance. Please try again later." 

772 messages.error(self.request, msg) 

773 return JsonResponse({"messages": msg, "status": 503}) 

774 

775 pid = kwargs.get("pid") 

776 colid = self.colid 

777 data = json.loads(request.body) 

778 summary = summary_build(data["articles"], colid) 

779 page = get_object_or_404(Page, id=pid) 

780 page.slug = page.slug_fr = page.slug_en = None 

781 page.menu_title_fr = data["title_fr"] 

782 page.menu_title_en = data["title_en"] 

783 page.content_fr = data["head_fr"] + summary["summary_fr"] + data["tail_fr"] 

784 page.content_en = data["head_en"] + summary["summary_en"] + data["tail_en"] 

785 page.state = data["page"]["state"] 

786 page.menu_order = data["page"]["menu_order"] 

787 page.parent_page = Page.objects.filter(id=data["page"]["parent_page"]).first() 

788 page.save() 

789 

790 response = deploy_cms("test_website", self.collection) 

791 if response.status_code == 503: 

792 messages.error( 

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

794 ) 

795 

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

797 

798 

799class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView): 

800 template_name = "editorial_tools/special-issue-edit.html" 

801 

802 def get_context_data(self, **kwargs): 

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

804 return context 

805 

806 def set_contrib_addresses(self, contrib, contribution): 

807 for address in contrib: 

808 contrib_address = ContribAddress(contribution=contribution, address=address) 

809 contrib_address.save() 

810 

811 def delete(self, pid): 

812 special_issue = Container.objects.get(pid=pid) 

813 cmd = base_ptf_cmds.addContainerPtfCmd() 

814 cmd.set_object_to_be_deleted(special_issue) 

815 cmd.undo() 

816 

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

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

819 

820 data = {"pid": pid} 

821 colid = kwargs.get("colid", "") 

822 data["colid"] = colid 

823 journal = model_helpers.get_collection(colid, sites=False) 

824 name = resolve(request.path_info).url_name 

825 if name == "special_issue_delete": 825 ↛ 826line 825 didn't jump to line 826 because the condition on line 825 was never true

826 self.delete(pid) 

827 return redirect("special_issues_index", data["colid"]) 

828 

829 data["journal_title"] = journal.title_tex.replace(".", "") 

830 

831 if pid != "create": 

832 container = get_object_or_404(Container, pid=pid) 

833 # TODO: pass the lang and trans_lang as well 

834 # In VueJS (Special.vu)e, titleFr = title_html 

835 # June 2025: Title objects are added for translated titles 

836 # keep using trans_title_html for backward compatibility 

837 translated_title = container.title_set.all().filter(lang="fr", type="main").first() 

838 if translated_title: 838 ↛ 840line 838 didn't jump to line 840 because the condition on line 838 was always true

839 data["title"] = translated_title.title_html 

840 data["doi"] = container.doi 

841 data["trans_title"] = container.title_html 

842 data["year"] = container.year_str 

843 data["volume"] = container.volume 

844 data["articles"] = [ 

845 {"doi": article.resource_doi, "citation": article.citation} 

846 for article in container.resources_in_special_issue.all().order_by("seq") 

847 ] 

848 if container.ctype == "issue_special_img": 848 ↛ 849line 848 didn't jump to line 849 because the condition on line 848 was never true

849 data["use_resources_icon"] = True 

850 else: 

851 data["use_resources_icon"] = False 

852 

853 contribs = model_data_converter.db_to_contributors(container.contributions) 

854 data["contribs"] = contribs 

855 abstract_set = container.abstract_set.all() 

856 data["head_fr"] = ( 

857 abstract_set.filter(tag="intro", lang="fr").first().value_html 

858 if abstract_set.filter(tag="intro", lang="fr").exists() 

859 else "" 

860 ) 

861 data["head_en"] = ( 

862 abstract_set.filter(tag="intro", lang="en").first().value_html 

863 if abstract_set.filter(tag="intro", lang="en").exists() 

864 else "" 

865 ) 

866 data["tail_fr"] = ( 

867 abstract_set.filter(tag="tail", lang="fr").first().value_html 

868 if abstract_set.filter(tag="tail", lang="fr").exists() 

869 else "" 

870 ) 

871 data["tail_en"] = ( 

872 abstract_set.filter(tag="tail", lang="en").first().value_html 

873 if abstract_set.filter(tag="tail", lang="en").exists() 

874 else "" 

875 ) 

876 data["editor_bio_en"] = ( 

877 abstract_set.filter(tag="bio_en").first().value_html 

878 if abstract_set.filter(tag="bio_en").exists() 

879 else "" 

880 ) 

881 data["editor_bio_fr"] = ( 

882 abstract_set.filter(tag="bio_fr").first().value_html 

883 if abstract_set.filter(tag="bio_fr").exists() 

884 else "" 

885 ) 

886 

887 streams = container.datastream_set.all() 

888 data["pdf_file_name"] = "" 

889 data["edito_file_name"] = "" 

890 data["edito_display_name"] = "" 

891 for stream in streams: # don't work 891 ↛ 892line 891 didn't jump to line 892 because the loop on line 891 never started

892 if os.path.basename(stream.location).split(".")[0] == data["pid"]: 

893 data["pdf_file_name"] = stream.text 

894 try: 

895 # edito related objects metadata contains both file real name and displayed name in issue summary 

896 edito_name_infos = container.relatedobject_set.get(rel="edito").metadata.split( 

897 "$$$" 

898 ) 

899 data["edito_file_name"] = edito_name_infos[0] 

900 data["edito_display_name"] = edito_name_infos[1] 

901 

902 except RelatedObject.DoesNotExist: 

903 pass 

904 try: 

905 container_icon = container.extlink_set.get(rel="icon") 

906 

907 data["icon_location"] = container_icon.location 

908 except ExtLink.DoesNotExist: 

909 data["icon_location"] = "" 

910 # try: 

911 # special_issue_icon = container.extlink_set.get(rel="icon") 

912 # data["special_issue_icon"] = special_issue_icon.location 

913 # except ExtLink.DoesNotExist: 

914 # data["special_issue_icon"] = None 

915 

916 else: 

917 data["title"] = "" 

918 data["doi"] = None 

919 data["trans_title"] = "" 

920 data["year"] = "" 

921 data["volume"] = "" 

922 data["articles"] = [] 

923 data["contribs"] = [] 

924 

925 data["head_fr"] = "" 

926 data["head_en"] = "" 

927 data["tail_fr"] = "" 

928 data["tail_en"] = "" 

929 data["editor_bio_en"] = "" 

930 data["editor_bio_fr"] = "" 

931 data["pdf_file_name"] = "" 

932 data["edito_file_name"] = "" 

933 data["use_resources_icon"] = False 

934 

935 return JsonResponse(data) 

936 

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

938 # le but est de faire un IssueDAta 

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

940 colid = kwargs.get("colid", "") 

941 journal = collection = model_helpers.get_collection(colid, sites=False) 

942 special_issue = create_issuedata() 

943 year_str = request.POST["year"] 

944 years = year_str.split("-") 

945 if len(years) > 1: 945 ↛ 946line 945 didn't jump to line 946 because the condition on line 945 was never true

946 fyear, lyear = int(years[0]), int(years[1]) 

947 else: 

948 fyear, lyear = int(years[0]), None 

949 # TODO 1: the values should be the tex values, not the html ones 

950 # TODO 2: In VueJS, titleFr = title 

951 trans_title_html = request.POST["title"] 

952 title_html = request.POST["trans_title"] 

953 issues = collection.content.all().order_by("-fyear") 

954 same_year_issues = issues.filter(fyear=fyear) 

955 if same_year_issues.exists(): 955 ↛ 957line 955 didn't jump to line 957 because the condition on line 955 was always true

956 volume = same_year_issues.first().volume 

957 elif issues.exists() and colid != "HOUCHES": # because we don't want a volume for houches 

958 ref_volume = issues.filter(fyear=2024).first().volume 

959 volume = int(ref_volume) + ( 

960 fyear - 2024 

961 ) # 2024 is the ref year for wich we know the volume is 347 

962 else: 

963 volume = "" 

964 if pid != "create": 

965 # TODO: do not use the pk, but the pid in the URLs 

966 container = get_object_or_404(Container, pid=pid) 

967 lang = container.lang 

968 trans_title = container.title_set.all().filter(type="main").first() 

969 if not trans_title: 969 ↛ 970line 969 didn't jump to line 970 because the condition on line 969 was never true

970 raise ValueError( 

971 "Cannot find trans_lang: Container does not have a main title translation" 

972 ) 

973 trans_lang = trans_title.lang 

974 xpub = create_publisherdata() 

975 xpub.name = container.my_publisher.pub_name 

976 special_issue.provider = container.provider 

977 special_issue.number = container.number 

978 special_issue_pid = pid 

979 special_issue.date_pre_published = container.date_pre_published 

980 special_issue.date_published = container.date_published 

981 # used for first special issues created withou a proper doi 

982 # can be remove when no doi's less special issue existe 

983 if not container.doi: 983 ↛ 984line 983 didn't jump to line 984 because the condition on line 983 was never true

984 special_issue.doi = model_helpers.assign_container_doi(colid) 

985 else: 

986 special_issue.doi = container.doi 

987 else: 

988 lang = "en" 

989 container = None 

990 trans_lang = "fr" 

991 xpub = create_publisherdata() 

992 special_issue.doi = model_helpers.assign_container_doi(colid) 

993 

994 if colid == "HOUCHES": 994 ↛ 995line 994 didn't jump to line 995 because the condition on line 994 was never true

995 xpub.name = "UGA Éditions" 

996 else: 

997 xpub.name = issues.first().my_publisher.pub_name 

998 special_issue.provider = collection.provider 

999 

1000 special_issues = issues.filter(fyear=fyear).filter( 

1001 Q(ctype="issue_special") | Q(ctype="issue") | Q(ctype="issue_special_img") 

1002 ) 

1003 if special_issues: 1003 ↛ 1013line 1003 didn't jump to line 1013 because the condition on line 1003 was always true

1004 all_special_issues_numbers = [ 

1005 int(si.number[1:]) for si in special_issues if si.number[1:].isnumeric() 

1006 ] 

1007 if len(all_special_issues_numbers) > 0: 

1008 max_number = max(all_special_issues_numbers) 

1009 else: 

1010 max_number = 0 

1011 

1012 else: 

1013 max_number = 0 

1014 special_issue.number = f"S{max_number + 1}" 

1015 special_issue_pid = f"{colid}_{year_str}__{volume}_{special_issue.number}" 

1016 

1017 if request.POST["use_resources_icon"] == "true": 1017 ↛ 1018line 1017 didn't jump to line 1018 because the condition on line 1017 was never true

1018 special_issue.ctype = "issue_special_img" 

1019 else: 

1020 special_issue.ctype = "issue_special" 

1021 

1022 existing_issue = model_helpers.get_resource(special_issue_pid) 

1023 if pid == "create" and existing_issue is not None: 1023 ↛ 1024line 1023 didn't jump to line 1024 because the condition on line 1023 was never true

1024 raise ValueError(f"The special issue with the pid {special_issue_pid} already exists") 

1025 

1026 special_issue.lang = lang 

1027 special_issue.title_html = title_html 

1028 special_issue.title_xml = build_title_xml( 

1029 title=title_html, lang=lang, title_type="issue-title" 

1030 ) 

1031 

1032 title_xml = build_title_xml( 

1033 title=trans_title_html, lang=trans_lang, title_type="issue-title" 

1034 ) 

1035 title = create_titledata( 

1036 lang=trans_lang, type="main", title_html=trans_title_html, title_xml=title_xml 

1037 ) 

1038 special_issue.titles = [title] 

1039 

1040 special_issue.fyear = fyear 

1041 special_issue.lyear = lyear 

1042 special_issue.volume = volume 

1043 special_issue.journal = journal 

1044 special_issue.publisher = xpub 

1045 special_issue.pid = special_issue_pid 

1046 special_issue.last_modified_iso_8601_date_str = datetime.now().strftime( 

1047 "%Y-%m-%d %H:%M:%S" 

1048 ) 

1049 

1050 articles = [] 

1051 contribs = [] 

1052 index = 0 

1053 

1054 if "nb_articles" in request.POST.keys(): 

1055 while index < int(request.POST["nb_articles"]): 

1056 article = json.loads(request.POST[f"article[{index}]"]) 

1057 article["citation"] = xml_utils.replace_html_entities(article["citation"]) 

1058 # if not article["citation"]: 

1059 # index += 1 

1060 # continue 

1061 articles.append(article) 

1062 

1063 index += 1 

1064 

1065 special_issue.articles = [Munch(article) for article in articles] 

1066 index = 0 

1067 # TODO make a function to call to add a contributor 

1068 if "nb_contrib" in request.POST.keys(): 

1069 while index < int(request.POST["nb_contrib"]): 

1070 contrib = json.loads(request.POST[f"contrib[{index}]"]) 

1071 contributor = create_contributor() 

1072 contributor["first_name"] = contrib["first_name"] 

1073 contributor["last_name"] = contrib["last_name"] 

1074 contributor["orcid"] = contrib["orcid"] 

1075 contributor["role"] = "editor" 

1076 

1077 contrib_xml = xml_utils.get_contrib_xml(contrib) 

1078 contributor["contrib_xml"] = contrib_xml 

1079 contribs.append(Munch(contributor)) 

1080 index += 1 

1081 special_issue.contributors = contribs 

1082 

1083 # Part of the code that handle forwords and lastwords 

1084 

1085 request_datas = [ 

1086 {"request_key": "head_fr", "abstract_type": "intro"}, 

1087 {"request_key": "head_en", "abstract_type": "intro"}, 

1088 {"request_key": "tail_fr", "abstract_type": "tail"}, 

1089 {"request_key": "tail_en", "abstract_type": "tail"}, 

1090 {"request_key": "editor_bio_fr", "abstract_type": "bio_fr"}, 

1091 {"request_key": "editor_bio_en", "abstract_type": "bio_en"}, 

1092 ] 

1093 

1094 special_issue.abstracts = [] 

1095 abstracts_xml = [] 

1096 for request_data in request_datas: 

1097 lang = request_data["request_key"][-2:] 

1098 value_html = request.POST[request_data["request_key"]] 

1099 

1100 ckeditor_data = build_jats_data_from_html_field( 

1101 value_html, 

1102 tag="abstract", 

1103 text_lang=lang, 

1104 resource_lang="en", 

1105 field_type=request_data["abstract_type"], 

1106 mml_formulas=[], 

1107 issue_pid=colid, 

1108 pid=special_issue.pid, 

1109 ) 

1110 

1111 abstract_data = create_abstract( 

1112 tag=request_data["abstract_type"], 

1113 lang=lang, 

1114 value_html=value_html, 

1115 value_tex=ckeditor_data["value_tex"], 

1116 value_xml=ckeditor_data["value_xml"], 

1117 ) 

1118 

1119 special_issue.abstracts.append(abstract_data) 

1120 abstracts_xml.append(ckeditor_data["value_xml"]) 

1121 

1122 figures = self.create_related_objects_from_abstract( 

1123 abstracts_xml, colid, special_issue.pid 

1124 ) 

1125 special_issue.related_objects = figures 

1126 

1127 # This part handle pdf files included in special issue. Can be editor of full pdf version 

1128 # Both are stored in same directory 

1129 

1130 pdf_file_path = resolver.get_disk_location( 

1131 f"{settings.RESOURCES_ROOT}", 

1132 f"{collection.pid}", 

1133 "pdf", 

1134 special_issue_pid, 

1135 article_id=None, 

1136 do_create_folder=False, 

1137 ) 

1138 pdf_path = os.path.dirname(pdf_file_path) 

1139 if "pdf" in self.request.FILES: 1139 ↛ 1140line 1139 didn't jump to line 1140 because the condition on line 1139 was never true

1140 if os.path.isfile(f"{pdf_path}/{pid}.pdf"): 

1141 os.remove(f"{pdf_path}/{pid}.pdf") 

1142 if "edito" in self.request.FILES: 1142 ↛ 1143line 1142 didn't jump to line 1143 because the condition on line 1142 was never true

1143 if os.path.isfile(f"{pdf_path}/{pid}_edito.pdf"): 

1144 os.remove(f"{pdf_path}/{pid}_edito.pdf") 

1145 

1146 if request.POST["pdf_name"] != "No file uploaded": 1146 ↛ 1147line 1146 didn't jump to line 1147 because the condition on line 1146 was never true

1147 if "pdf" in self.request.FILES: 

1148 pdf_file = request.FILES["pdf"] 

1149 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1150 pdf_file, 

1151 collection.pid, 

1152 special_issue.pid, 

1153 special_issue.pid, 

1154 ) 

1155 pdf_file_name = self.request.FILES["pdf"].name 

1156 

1157 else: 

1158 pdf_file_name = request.POST["pdf_name"] 

1159 relative_file_name = pdf_path + "/" + special_issue_pid + ".pdf" 

1160 

1161 pdf_stream_data = create_datastream() 

1162 pdf_stream_data["location"] = relative_file_name 

1163 pdf_stream_data["mimetype"] = "application/pdf" 

1164 pdf_stream_data["rel"] = "full-text" 

1165 pdf_stream_data["text"] = pdf_file_name 

1166 special_issue.streams.append(pdf_stream_data) 

1167 

1168 if request.POST["edito_name"] != "No file uploaded": 1168 ↛ 1169line 1168 didn't jump to line 1169 because the condition on line 1168 was never true

1169 if "edito" in self.request.FILES: 

1170 edito_file = self.request.FILES["edito"] 

1171 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1172 edito_file, 

1173 collection.pid, 

1174 special_issue.pid, 

1175 special_issue.pid, 

1176 ) 

1177 

1178 edito_file_name = self.request.FILES["edito"].name 

1179 edito_display_name = request.POST["edito_display_name"] 

1180 else: 

1181 relative_file_name = pdf_path + "/" + special_issue_pid + "_edito.pdf" 

1182 edito_file_name = request.POST["edito_name"] 

1183 edito_display_name = request.POST["edito_display_name"] 

1184 

1185 data = { 

1186 "rel": "edito", 

1187 "mimetype": "application/pdf", 

1188 "location": relative_file_name, 

1189 "base": None, 

1190 "metadata": edito_file_name + "$$$" + edito_display_name, 

1191 } 

1192 special_issue.related_objects.append(data) 

1193 # Handle special issue icon. It is stored in same directory that pdf version or edito. 

1194 # The icon is linked to special issue as an ExtLink 

1195 if "icon" in request.FILES: 1195 ↛ 1196line 1195 didn't jump to line 1196 because the condition on line 1195 was never true

1196 icon_file = request.FILES["icon"] 

1197 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1198 icon_file, 

1199 collection.pid, 

1200 special_issue.pid, 

1201 special_issue.pid, 

1202 ) 

1203 if ".tif" in relative_file_name: 

1204 jpeg_path = ImageManager( 

1205 os.path.join(settings.RESOURCES_ROOT, relative_file_name) 

1206 ).to_jpeg() 

1207 relative_file_name = str(jpeg_path).replace(settings.RESOURCES_ROOT + "/", "") 

1208 data = { 

1209 "rel": "icon", 

1210 "location": relative_file_name, 

1211 "base": None, 

1212 "seq": 1, 

1213 "metadata": "", 

1214 } 

1215 special_issue.ext_links.append(data) 

1216 elif "icon" in request.POST.keys(): 1216 ↛ 1217line 1216 didn't jump to line 1217 because the condition on line 1216 was never true

1217 if request.POST["icon"] != "[object Object]": 

1218 icon_file = request.POST["icon"].replace("/icon/", "") 

1219 data = { 

1220 "rel": "icon", 

1221 "location": icon_file, 

1222 "base": None, 

1223 "seq": 1, 

1224 "metadata": "", 

1225 } 

1226 special_issue.ext_links.append(data) 

1227 

1228 special_issue = Munch(special_issue.__dict__) 

1229 params = {"xissue": special_issue, "use_body": False} 

1230 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params) 

1231 cmd.do() 

1232 return redirect("special_issue_edit_api", colid, special_issue.pid) 

1233 

1234 def create_related_objects_from_abstract(self, abstracts, colid, pid): 

1235 figures = [] 

1236 for abstract in abstracts: 

1237 abstract_xml = abstract.encode("utf8") 

1238 

1239 tree = etree.fromstring(abstract_xml) 

1240 

1241 pics = tree.xpath("//graphic") 

1242 for pic in pics: 1242 ↛ 1243line 1242 didn't jump to line 1243 because the loop on line 1242 never started

1243 base = None 

1244 pic_location = pic.attrib["specific-use"] 

1245 basename = os.path.basename(pic.attrib["href"]) 

1246 ext = basename.split(".")[-1] 

1247 base = get_media_base_root(colid) 

1248 data_location = os.path.join( 

1249 settings.RESOURCES_ROOT, "media", base, "uploads", pic_location, basename 

1250 ) 

1251 # we use related objects to send pics to journal site. Directory where pic is stored in trammel may differ 

1252 # from the directory in journal site. So one need to save the pic in same directory that journal's one 

1253 # so related objects can go for the correct one 

1254 img = Image.open(data_location) 

1255 final_data_location = os.path.join( 

1256 settings.RESOURCES_ROOT, colid, pid, "src", "figures" 

1257 ) 

1258 if not os.path.isdir(final_data_location): 

1259 os.makedirs(final_data_location) 

1260 relative_path = os.path.join(colid, pid, "src", "figures", basename) 

1261 final_data_location = f"{final_data_location}/{basename}" 

1262 img.save(final_data_location) 

1263 if ext == "png": 

1264 mimetype = "image/png" 

1265 else: 

1266 mimetype = "image/jpeg" 

1267 data = { 

1268 "rel": "html-image", 

1269 "mimetype": mimetype, 

1270 "location": relative_path, 

1271 "base": base, 

1272 "metadata": "", 

1273 } 

1274 if data not in figures: 

1275 figures.append(data) 

1276 return figures 

1277 

1278 

1279class PageIndexView(EditorRequiredMixin, TemplateView): 

1280 template_name = "mersenne_cms/page_index.html" 

1281 

1282 def get_context_data(self, **kwargs): 

1283 colid = kwargs.get("colid", "") 

1284 site_id = model_helpers.get_site_id(colid) 

1285 vi = Page.objects.filter(site_id=site_id, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES).first() 

1286 if vi: 1286 ↛ 1287line 1286 didn't jump to line 1287 because the condition on line 1286 was never true

1287 pages = Page.objects.filter(site_id=site_id).exclude(parent_page=vi) 

1288 else: 

1289 pages = Page.objects.filter(site_id=site_id) 

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

1291 context["colid"] = colid 

1292 context["journal"] = model_helpers.get_collection(colid) 

1293 context["pages"] = pages 

1294 context["news"] = News.objects.filter(site_id=site_id) 

1295 context["fields_lang"] = "fr" if model_helpers.is_site_fr_only(site_id) else "en" 

1296 return context 

1297 

1298 

1299class PageBaseView(HandleCMSMixin, View): 

1300 template_name = "mersenne_cms/page_form.html" 

1301 model = Page 

1302 form_class = PageForm 

1303 

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

1305 self.colid = self.kwargs["colid"] 

1306 self.collection = model_helpers.get_collection(self.colid, sites=False) 

1307 self.site_id = model_helpers.get_site_id(self.colid) 

1308 

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

1310 

1311 def get_success_url(self): 

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

1313 

1314 def get_context_data(self, **kwargs): 

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

1316 context["journal"] = self.collection 

1317 return context 

1318 

1319 def update_test_website(self): 

1320 response = deploy_cms("test_website", self.collection) 

1321 if response.status_code < 300: 1321 ↛ 1324line 1321 didn't jump to line 1324 because the condition on line 1321 was always true

1322 messages.success(self.request, "The test website has been updated") 

1323 else: 

1324 text = "ERROR: Unable to update the test website<br/>" 

1325 

1326 if response.status_code == 503: 

1327 text += "The test website is under maintenance. Please try again later.<br/>" 

1328 else: 

1329 text += f"Please contact the centre Mersenne<br/><br/>Status code: {response.status_code}<br/>" 

1330 if hasattr(response, "content") and response.content: 

1331 text += f"{response.content.decode()}<br/>" 

1332 if hasattr(response, "reason") and response.reason: 

1333 text += f"Reason: {response.reason}<br/>" 

1334 if hasattr(response, "text") and response.text: 

1335 text += f"Details: {response.text}<br/>" 

1336 messages.error(self.request, mark_safe(text)) 

1337 

1338 def get_form_kwargs(self): 

1339 kwargs = super().get_form_kwargs() 

1340 kwargs["site_id"] = self.site_id 

1341 kwargs["user"] = self.request.user 

1342 return kwargs 

1343 

1344 def form_valid(self, form): 

1345 form.save() 

1346 

1347 self.update_test_website() 

1348 

1349 return HttpResponseRedirect(self.get_success_url()) 

1350 

1351 

1352# @method_decorator([csrf_exempt], name="dispatch") 

1353class PageDeleteView(PageBaseView): 

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

1355 colid = kwargs.get("colid", "") 

1356 pk = kwargs.get("pk") 

1357 page = get_object_or_404(Page, id=pk) 

1358 if page.mersenne_id: 

1359 raise PermissionDenied 

1360 

1361 page.delete() 

1362 

1363 self.update_test_website() 

1364 

1365 if page.parent_page and page.parent_page.mersenne_id == MERSENNE_ID_VIRTUAL_ISSUES: 

1366 return HttpResponseRedirect(reverse("virtual_issues_index", kwargs={"colid": colid})) 

1367 else: 

1368 return HttpResponseRedirect(reverse("page_index", kwargs={"colid": colid})) 

1369 

1370 

1371class PageCreateView(PageBaseView, CreateView): 

1372 def get_context_data(self, **kwargs): 

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

1374 context["title"] = "Add a menu page" 

1375 return context 

1376 

1377 

1378class PageUpdateView(PageBaseView, UpdateView): 

1379 def get_context_data(self, **kwargs): 

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

1381 context["title"] = "Edit a menu page" 

1382 return context 

1383 

1384 

1385class NewsBaseView(PageBaseView): 

1386 template_name = "mersenne_cms/news_form.html" 

1387 model = News 

1388 form_class = NewsForm 

1389 

1390 

1391class NewsDeleteView(NewsBaseView): 

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

1393 pk = kwargs.get("pk") 

1394 news = get_object_or_404(News, id=pk) 

1395 

1396 news.delete() 

1397 

1398 self.update_test_website() 

1399 

1400 return HttpResponseRedirect(self.get_success_url()) 

1401 

1402 

1403class NewsCreateView(NewsBaseView, CreateView): 

1404 def get_context_data(self, **kwargs): 

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

1406 context["title"] = "Add a News" 

1407 return context 

1408 

1409 

1410class NewsUpdateView(NewsBaseView, UpdateView): 

1411 def get_context_data(self, **kwargs): 

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

1413 context["title"] = "Edit a News" 

1414 return context 

1415 

1416 

1417# def page_create_view(request, colid): 

1418# context = {} 

1419# if not is_authorized_editor(request.user, colid): 

1420# raise PermissionDenied 

1421# collection = model_helpers.get_collection(colid) 

1422# page = Page(site_id=model_helpers.get_site_id(colid)) 

1423# form = PageForm(request.POST or None, instance=page) 

1424# if form.is_valid(): 

1425# form.save() 

1426# response = deploy_cms("test_website", collection) 

1427# if response.status_code < 300: 

1428# messages.success(request, "Page created successfully.") 

1429# else: 

1430# text = f"ERROR: page creation failed<br/>Status code: {response.status_code}<br/>" 

1431# if hasattr(response, "reason") and response.reason: 

1432# text += f"Reason: {response.reason}<br/>" 

1433# if hasattr(response, "text") and response.text: 

1434# text += f"Details: {response.text}<br/>" 

1435# messages.error(request, mark_safe(text)) 

1436# kwargs = {"colid": colid, "pid": form.instance.id} 

1437# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs)) 

1438# 

1439# context["form"] = form 

1440# context["title"] = "Add a menu page" 

1441# context["journal"] = collection 

1442# return render(request, "mersenne_cms/page_form.html", context) 

1443 

1444 

1445# def page_update_view(request, colid, pid): 

1446# context = {} 

1447# if not is_authorized_editor(request.user, colid): 

1448# raise PermissionDenied 

1449# 

1450# collection = model_helpers.get_collection(colid) 

1451# page = get_object_or_404(Page, id=pid) 

1452# form = PageForm(request.POST or None, instance=page) 

1453# if form.is_valid(): 

1454# form.save() 

1455# response = deploy_cms("test_website", collection) 

1456# if response.status_code < 300: 

1457# messages.success(request, "Page updated successfully.") 

1458# else: 

1459# text = f"ERROR: page update failed<br/>Status code: {response.status_code}<br/>" 

1460# if hasattr(response, "reason") and response.reason: 

1461# text += f"Reason: {response.reason}<br/>" 

1462# if hasattr(response, "text") and response.text: 

1463# text += f"Details: {response.text}<br/>" 

1464# messages.error(request, mark_safe(text)) 

1465# kwargs = {"colid": colid, "pid": form.instance.id} 

1466# return HttpResponseRedirect(reverse("page_update", kwargs=kwargs)) 

1467# 

1468# context["form"] = form 

1469# context["pid"] = pid 

1470# context["title"] = "Edit a menu page" 

1471# context["journal"] = collection 

1472# return render(request, "mersenne_cms/page_form.html", context)