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

879 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-09-18 08:07 +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 colid = colid.replace("/", "") 

171 

172 change_ckeditor_storage(colid) 

173 

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

175 

176 

177class CollectionBrowseView(EditorRequiredMixin, View): 

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

179 colid = kwargs["colid"] 

180 

181 change_ckeditor_storage(colid) 

182 

183 return browse(request) 

184 

185 

186file_upload_in_collection = csrf_exempt(CollectionImageUploadView.as_view()) 

187file_browse_in_collection = csrf_exempt(CollectionBrowseView.as_view()) 

188 

189 

190def deploy_cms(site, collection): 

191 colid = collection.pid 

192 base_url = getattr(collection, site)() 

193 

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

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

196 

197 if site == "website": 

198 from_base_path = get_media_base_root_in_test(colid) 

199 to_base_path = get_media_base_root_in_prod(colid) 

200 

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

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

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

204 if os.path.exists(from_path): 

205 try: 

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

207 except OSError as exception: 

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

209 

210 site_id = model_helpers.get_site_id(colid) 

211 if model_helpers.get_site_default_language(site_id): 

212 from modeltranslation import fields, manager 

213 

214 old_ftor = manager.get_language 

215 manager.get_language = monkey_get_language_en 

216 fields.get_language = monkey_get_language_en 

217 

218 pages = get_pages_content(colid) 

219 news = get_news_content(colid) 

220 

221 manager.get_language = old_ftor 

222 fields.get_language = old_ftor 

223 else: 

224 pages = get_pages_content(colid) 

225 news = get_news_content(colid) 

226 

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

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

229 

230 try: 

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

232 

233 if response.status_code == 503: 

234 e = ServerUnderMaintenance( 

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

236 ) 

237 return HttpResponseServerError(e, status=503) 

238 

239 except Timeout as exception: 

240 return HttpResponse(exception, status=408) 

241 except Exception as exception: 

242 return HttpResponseServerError(exception) 

243 

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

245 

246 

247class HandleCMSMixin(EditorRequiredMixin): 

248 """ 

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

250 """ 

251 

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

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

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

255 

256 def init_data(self, kwargs): 

257 self.collection = None 

258 

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

260 if self.colid: 

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

262 if not self.collection: 

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

264 

265 test_server_url = self.collection.test_website() 

266 if not test_server_url: 

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

268 

269 prod_server_url = self.collection.website() 

270 if not prod_server_url: 

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

272 

273 

274class GetCMSFromSiteAPIView(HandleCMSMixin, View): 

275 """ 

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

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

278 """ 

279 

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

281 self.init_data(self.kwargs) 

282 

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

284 

285 try: 

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

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

288 

289 # Just to need to save the json on disk 

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

291 # /mersenne_test_data/@colid/media 

292 folder = get_media_base_root(self.colid) 

293 os.makedirs(folder, exist_ok=True) 

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

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

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

297 

298 except Timeout as exception: 

299 return HttpResponse(exception, status=408) 

300 except Exception as exception: 

301 return HttpResponseServerError(exception) 

302 

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

304 

305 

306def monkey_get_language_en(): 

307 return "en" 

308 

309 

310class RestoreCMSAPIView(HandleCMSMixin, View): 

311 """ 

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

313 """ 

314 

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

316 self.init_data(self.kwargs) 

317 

318 folder = get_media_base_root(self.colid) 

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

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

321 json_data = json.load(f) 

322 

323 pages = json_data.get("pages") 

324 

325 site_id = model_helpers.get_site_id(self.colid) 

326 if model_helpers.get_site_default_language(site_id): 

327 from modeltranslation import fields, manager 

328 

329 old_ftor = manager.get_language 

330 manager.get_language = monkey_get_language_en 

331 fields.get_language = monkey_get_language_en 

332 

333 import_pages(pages, self.colid) 

334 

335 manager.get_language = old_ftor 

336 fields.get_language = old_ftor 

337 else: 

338 import_pages(pages, self.colid) 

339 

340 if "news" in json_data: 

341 news = json_data.get("news") 

342 import_news(news, self.colid) 

343 

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

345 

346 

347class DeployCMSAPIView(HandleCMSMixin, View): 

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

349 self.init_data(self.kwargs) 

350 

351 if check_lock(): 

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

353 messages.error(self.request, msg) 

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

355 

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

357 

358 response = deploy_cms(site, self.collection) 

359 

360 if response.status_code == 503: 

361 messages.error( 

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

363 ) 

364 

365 return response 

366 

367 

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

369 urls = [""] 

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

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

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

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

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

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

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

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

378 return urls 

379 

380 

381class SuggestDeployView(EditorRequiredMixin, View): 

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

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

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

385 article = get_object_or_404(Article, doi=doi) 

386 

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

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

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

390 data = form.cleaned_data 

391 obj.date_modified = timezone.now() 

392 form.save() 

393 collection = article.my_container.my_collection 

394 urls = get_server_urls(collection, site=site) 

395 response = requests.models.Response() 

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

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

398 try: 

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

400 except requests.exceptions.RequestException as e: 

401 response.status_code = 503 

402 response.reason = e.args[0] 

403 break 

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

405 else: 

406 return HttpResponseBadRequest() 

407 

408 

409def suggest_debug(results, article, message): 

410 crop_results = 5 

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

412 dois = [] 

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

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

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

416 for item in results["docs"]: 

417 doi = item.get("doi") 

418 if doi: 

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

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

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

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

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

424 url = "" 

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

426 if suggest and suggest.my_container: 

427 collection = suggest.my_container.my_collection 

428 base_url = collection.website() or "" 

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

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

431 

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

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

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

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

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

437 terms = results["interestingTerms"] 

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

439 tail.extend(reversed(terms)) 

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

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

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

443 else: 

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

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

446 

447 

448class SuggestUpdateView(EditorRequiredMixin, TemplateView): 

449 template_name = "editorial_tools/suggested.html" 

450 

451 def get_context_data(self, **kwargs): 

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

453 article = get_object_or_404(Article, doi=doi) 

454 

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

456 collection = article.my_container.my_collection 

457 base_url = collection.website() or "" 

458 response = requests.models.Response() 

459 try: 

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

461 except requests.exceptions.RequestException as e: 

462 response.status_code = 503 

463 response.reason = e.args[0] 

464 msg = { 

465 "url": response.url, 

466 "status": response.status_code, 

467 "err": response.reason, 

468 } 

469 results = None 

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

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

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

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

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

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

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

477 context["article"] = article 

478 context["date_modified"] = obj.date_modified 

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

480 return context 

481 

482 

483class EditorialToolsVolumeItemsView(EditorRequiredMixin, TemplateView): 

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

485 

486 def get_context_data(self, **kwargs): 

487 vid = kwargs.get("vid") 

488 issues_articles, collection = model_helpers.get_issues_in_volume(vid) 

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

490 context["issues_articles"] = issues_articles 

491 context["collection"] = collection 

492 return context 

493 

494 

495class EditorialToolsArticleView(EditorRequiredMixin, TemplateView): 

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

497 

498 def get_context_data(self, **kwargs): 

499 colid = kwargs.get("colid") 

500 doi = kwargs.get("doi") 

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

502 

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

504 context["article"] = article 

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

506 return context 

507 

508 

509class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView): 

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

511 

512 def get_context_data(self, **kwargs): 

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

514 article = get_object_or_404(Article, doi=doi) 

515 

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

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

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

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

520 context["article"] = article 

521 context["date_modified"] = obj.date_modified 

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

523 context["graphical_abstract"] = obj.graphical_abstract 

524 context["illustration"] = obj.illustration 

525 return context 

526 

527 

528class GraphicalAbstractDeployView(EditorRequiredMixin, View): 

529 def __get_path_and_replace_tiff_file(self, obj_attribute_file): 

530 """ 

531 Returns the path of the attribute. 

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

533 

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

535 """ 

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

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

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

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

540 return jpeg_path 

541 

542 return obj_attribute_file.path 

543 

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

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

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

547 article = get_object_or_404(Article, doi=doi) 

548 

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

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

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

552 obj.date_modified = timezone.now() 

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

554 form.save() 

555 files = {} 

556 

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

558 obj_attribute_file = getattr(obj, attribute, None) 

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

560 file_path = self.__get_path_and_replace_tiff_file(obj_attribute_file) 

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

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

563 

564 collection = article.my_container.my_collection 

565 urls = get_server_urls(collection, site=site) 

566 response = requests.models.Response() 

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

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

569 try: 

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

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

572 else: 

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

574 except requests.exceptions.RequestException as e: 

575 response.status_code = 503 

576 response.reason = e.args[0] 

577 break 

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

579 else: 

580 return HttpResponseBadRequest() 

581 

582 

583def parse_content(content): 

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

585 if not table: 

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

587 

588 articles = [] 

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

590 for row in rows: 

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

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

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

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

595 item = {} 

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

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

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

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

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

601 articles.append(item) 

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

603 

604 

605class VirtualIssueParseView(EditorRequiredMixin, View): 

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

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

608 page = get_object_or_404(Page, id=pid) 

609 

610 data = {"pid": pid} 

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

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

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

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

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

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

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

618 

619 content_fr = parse_content(page.content_fr) 

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

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

622 

623 content_en = parse_content(page.content_en) 

624 data["articles"] = content_en["articles"] 

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

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

627 return JsonResponse(data) 

628 

629 

630class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView): 

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

632 

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

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

635 get_object_or_404(Page, id=pid) 

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

637 

638 

639class VirtualIssueCreateView(EditorRequiredMixin, View): 

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

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

642 site_id = model_helpers.get_site_id(colid) 

643 parent, _ = Page.objects.get_or_create( 

644 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES, 

645 parent_page=None, 

646 site_id=site_id, 

647 ) 

648 page = Page.objects.create( 

649 menu_title_en="New virtual issue", 

650 menu_title_fr="Nouvelle collection transverse", 

651 parent_page=parent, 

652 site_id=site_id, 

653 state="draft", 

654 ) 

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

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

657 

658 

659class SpecialIssuesIndex(EditorRequiredMixin, TemplateView): 

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

661 

662 def get_context_data(self, **kwargs): 

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

664 

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

666 context["colid"] = colid 

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

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

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

670 ).filter(my_collection=collection) 

671 

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

673 return context 

674 

675 

676class SpecialIssueEditView(EditorRequiredMixin, TemplateView): 

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

678 

679 def get_context_data(self, **kwargs): 

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

681 return context 

682 

683 

684class VirtualIssuesIndex(EditorRequiredMixin, TemplateView): 

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

686 

687 def get_context_data(self, **kwargs): 

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

689 site_id = model_helpers.get_site_id(colid) 

690 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES) 

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

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

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

694 context["pages"] = pages 

695 return context 

696 

697 

698def get_citation_fr(doi, citation_en): 

699 citation_fr = citation_en 

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

701 if article and article.trans_title_html: 

702 trans_title = article.trans_title_html 

703 try: 

704 citation_fr = re.sub( 

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

706 rf"\1{trans_title}", 

707 citation_en, 

708 ) 

709 except re.error: 

710 pass 

711 return citation_fr 

712 

713 

714def summary_build(articles, colid): 

715 summary_fr = "" 

716 summary_en = "" 

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

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

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

720 colid_lo = colid.lower() 

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

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

723 

724 for article in articles: 

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

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

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

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

729 if doi or citation_en: 

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

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

732 if image_src: 

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

734 base_url = get_media_base_url(colid) 

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

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

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

738 if re.match(image_header, image_src): 

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

740 base64_data = base64.b64decode(image_src) 

741 base_root = get_media_base_root(colid) 

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

743 os.makedirs(path, exist_ok=True) 

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

745 fp.write(base64_data) 

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

747 # TODO mettre la vrai valeur pour le SITE_DOMAIN 

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

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

750 else: 

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

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

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

754 summary_fr = head + summary_fr + tail 

755 summary_en = head + summary_en + tail 

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

757 

758 

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

760class VirtualIssueDeployView(HandleCMSMixin, View): 

761 """ 

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

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

764 The Page is then immediately posted to the test_website. 

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

766 => See DeployCMSAPIView 

767 """ 

768 

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

770 self.init_data(self.kwargs) 

771 if check_lock(): 

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

773 messages.error(self.request, msg) 

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

775 

776 pid = kwargs.get("pid") 

777 colid = self.colid 

778 data = json.loads(request.body) 

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

780 page = get_object_or_404(Page, id=pid) 

781 page.slug = page.slug_fr = page.slug_en = None 

782 page.menu_title_fr = data["title_fr"] 

783 page.menu_title_en = data["title_en"] 

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

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

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

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

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

789 page.save() 

790 

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

792 if response.status_code == 503: 

793 messages.error( 

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

795 ) 

796 

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

798 

799 

800class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView): 

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

802 

803 def get_context_data(self, **kwargs): 

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

805 return context 

806 

807 def set_contrib_addresses(self, contrib, contribution): 

808 for address in contrib: 

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

810 contrib_address.save() 

811 

812 def delete(self, pid): 

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

814 cmd = base_ptf_cmds.addContainerPtfCmd() 

815 cmd.set_object_to_be_deleted(special_issue) 

816 cmd.undo() 

817 

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

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

820 

821 data = {"pid": pid} 

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

823 data["colid"] = colid 

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

825 name = resolve(request.path_info).url_name 

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

827 self.delete(pid) 

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

829 

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

831 

832 if pid != "create": 

833 container = get_object_or_404(Container, pid=pid) 

834 # TODO: pass the lang and trans_lang as well 

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

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

837 # keep using trans_title_html for backward compatibility 

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

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

840 data["title"] = translated_title.title_html 

841 data["doi"] = container.doi 

842 data["trans_title"] = container.title_html 

843 data["year"] = container.year_str 

844 data["volume"] = container.volume 

845 data["articles"] = [ 

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

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

848 ] 

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

850 data["use_resources_icon"] = True 

851 else: 

852 data["use_resources_icon"] = False 

853 

854 contribs = model_data_converter.db_to_contributors(container.contributions) 

855 data["contribs"] = contribs 

856 abstract_set = container.abstract_set.all() 

857 data["head_fr"] = ( 

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

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

860 else "" 

861 ) 

862 data["head_en"] = ( 

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

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

865 else "" 

866 ) 

867 data["tail_fr"] = ( 

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

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

870 else "" 

871 ) 

872 data["tail_en"] = ( 

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

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

875 else "" 

876 ) 

877 data["editor_bio_en"] = ( 

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

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

880 else "" 

881 ) 

882 data["editor_bio_fr"] = ( 

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

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

885 else "" 

886 ) 

887 

888 streams = container.datastream_set.all() 

889 data["pdf_file_name"] = "" 

890 data["edito_file_name"] = "" 

891 data["edito_display_name"] = "" 

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

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

894 data["pdf_file_name"] = stream.text 

895 try: 

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

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

898 "$$$" 

899 ) 

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

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

902 

903 except RelatedObject.DoesNotExist: 

904 pass 

905 try: 

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

907 

908 data["icon_location"] = container_icon.location 

909 except ExtLink.DoesNotExist: 

910 data["icon_location"] = "" 

911 # try: 

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

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

914 # except ExtLink.DoesNotExist: 

915 # data["special_issue_icon"] = None 

916 

917 else: 

918 data["title"] = "" 

919 data["doi"] = None 

920 data["trans_title"] = "" 

921 data["year"] = "" 

922 data["volume"] = "" 

923 data["articles"] = [] 

924 data["contribs"] = [] 

925 

926 data["head_fr"] = "" 

927 data["head_en"] = "" 

928 data["tail_fr"] = "" 

929 data["tail_en"] = "" 

930 data["editor_bio_en"] = "" 

931 data["editor_bio_fr"] = "" 

932 data["pdf_file_name"] = "" 

933 data["edito_file_name"] = "" 

934 data["use_resources_icon"] = False 

935 

936 return JsonResponse(data) 

937 

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

939 # le but est de faire un IssueDAta 

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

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

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

943 special_issue = create_issuedata() 

944 year_str = request.POST["year"] 

945 years = year_str.split("-") 

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

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

948 else: 

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

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

951 # TODO 2: In VueJS, titleFr = title 

952 trans_title_html = request.POST["title"] 

953 title_html = request.POST["trans_title"] 

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

955 same_year_issues = issues.filter(fyear=fyear) 

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

957 volume = same_year_issues.first().volume 

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

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

960 volume = int(ref_volume) + ( 

961 fyear - 2024 

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

963 else: 

964 volume = "" 

965 if pid != "create": 

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

967 container = get_object_or_404(Container, pid=pid) 

968 lang = container.lang 

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

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

971 raise ValueError( 

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

973 ) 

974 trans_lang = trans_title.lang 

975 xpub = create_publisherdata() 

976 xpub.name = container.my_publisher.pub_name 

977 special_issue.provider = container.provider 

978 special_issue.number = container.number 

979 special_issue_pid = pid 

980 special_issue.date_pre_published = container.date_pre_published 

981 special_issue.date_published = container.date_published 

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

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

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

985 special_issue.doi = model_helpers.assign_container_doi(colid) 

986 else: 

987 special_issue.doi = container.doi 

988 else: 

989 lang = "en" 

990 container = None 

991 trans_lang = "fr" 

992 xpub = create_publisherdata() 

993 special_issue.doi = model_helpers.assign_container_doi(colid) 

994 

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

996 xpub.name = "UGA Éditions" 

997 else: 

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

999 special_issue.provider = collection.provider 

1000 

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

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

1003 ) 

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

1005 all_special_issues_numbers = [ 

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

1007 ] 

1008 if len(all_special_issues_numbers) > 0: 

1009 max_number = max(all_special_issues_numbers) 

1010 else: 

1011 max_number = 0 

1012 

1013 else: 

1014 max_number = 0 

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

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

1017 

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

1019 special_issue.ctype = "issue_special_img" 

1020 else: 

1021 special_issue.ctype = "issue_special" 

1022 

1023 existing_issue = model_helpers.get_resource(special_issue_pid) 

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

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

1026 

1027 special_issue.lang = lang 

1028 special_issue.title_html = title_html 

1029 special_issue.title_xml = build_title_xml( 

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

1031 ) 

1032 

1033 title_xml = build_title_xml( 

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

1035 ) 

1036 title = create_titledata( 

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

1038 ) 

1039 special_issue.titles = [title] 

1040 

1041 special_issue.fyear = fyear 

1042 special_issue.lyear = lyear 

1043 special_issue.volume = volume 

1044 special_issue.journal = journal 

1045 special_issue.publisher = xpub 

1046 special_issue.pid = special_issue_pid 

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

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

1049 ) 

1050 

1051 articles = [] 

1052 contribs = [] 

1053 index = 0 

1054 

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

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

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

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

1059 # if not article["citation"]: 

1060 # index += 1 

1061 # continue 

1062 articles.append(article) 

1063 

1064 index += 1 

1065 

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

1067 index = 0 

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

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

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

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

1072 contributor = create_contributor() 

1073 contributor["first_name"] = contrib["first_name"] 

1074 contributor["last_name"] = contrib["last_name"] 

1075 contributor["orcid"] = contrib["orcid"] 

1076 contributor["role"] = "editor" 

1077 

1078 contrib_xml = xml_utils.get_contrib_xml(contrib) 

1079 contributor["contrib_xml"] = contrib_xml 

1080 contribs.append(Munch(contributor)) 

1081 index += 1 

1082 special_issue.contributors = contribs 

1083 

1084 # Part of the code that handle forwords and lastwords 

1085 

1086 request_datas = [ 

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

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

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

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

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

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

1093 ] 

1094 

1095 special_issue.abstracts = [] 

1096 abstracts_xml = [] 

1097 for request_data in request_datas: 

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

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

1100 

1101 ckeditor_data = build_jats_data_from_html_field( 

1102 value_html, 

1103 tag="abstract", 

1104 text_lang=lang, 

1105 resource_lang="en", 

1106 field_type=request_data["abstract_type"], 

1107 mml_formulas=[], 

1108 issue_pid=colid, 

1109 pid=special_issue.pid, 

1110 ) 

1111 

1112 abstract_data = create_abstract( 

1113 tag=request_data["abstract_type"], 

1114 lang=lang, 

1115 value_html=value_html, 

1116 value_tex=ckeditor_data["value_tex"], 

1117 value_xml=ckeditor_data["value_xml"], 

1118 ) 

1119 

1120 special_issue.abstracts.append(abstract_data) 

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

1122 

1123 figures = self.create_related_objects_from_abstract( 

1124 abstracts_xml, colid, special_issue.pid 

1125 ) 

1126 special_issue.related_objects = figures 

1127 

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

1129 # Both are stored in same directory 

1130 

1131 pdf_file_path = resolver.get_disk_location( 

1132 f"{settings.RESOURCES_ROOT}", 

1133 f"{collection.pid}", 

1134 "pdf", 

1135 special_issue_pid, 

1136 article_id=None, 

1137 do_create_folder=False, 

1138 ) 

1139 pdf_path = os.path.dirname(pdf_file_path) 

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

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

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

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

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

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

1146 

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

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

1149 pdf_file = request.FILES["pdf"] 

1150 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1151 pdf_file, 

1152 collection.pid, 

1153 special_issue.pid, 

1154 special_issue.pid, 

1155 ) 

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

1157 

1158 else: 

1159 pdf_file_name = request.POST["pdf_name"] 

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

1161 

1162 pdf_stream_data = create_datastream() 

1163 pdf_stream_data["location"] = relative_file_name 

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

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

1166 pdf_stream_data["text"] = pdf_file_name 

1167 special_issue.streams.append(pdf_stream_data) 

1168 

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

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

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

1172 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1173 edito_file, 

1174 collection.pid, 

1175 special_issue.pid, 

1176 special_issue.pid, 

1177 ) 

1178 

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

1180 edito_display_name = request.POST["edito_display_name"] 

1181 else: 

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

1183 edito_file_name = request.POST["edito_name"] 

1184 edito_display_name = request.POST["edito_display_name"] 

1185 

1186 data = { 

1187 "rel": "edito", 

1188 "mimetype": "application/pdf", 

1189 "location": relative_file_name, 

1190 "base": None, 

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

1192 } 

1193 special_issue.related_objects.append(data) 

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

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

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

1197 icon_file = request.FILES["icon"] 

1198 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1199 icon_file, 

1200 collection.pid, 

1201 special_issue.pid, 

1202 special_issue.pid, 

1203 ) 

1204 if ".tif" in relative_file_name: 

1205 jpeg_path = ImageManager( 

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

1207 ).to_jpeg() 

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

1209 data = { 

1210 "rel": "icon", 

1211 "location": relative_file_name, 

1212 "base": None, 

1213 "seq": 1, 

1214 "metadata": "", 

1215 } 

1216 special_issue.ext_links.append(data) 

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

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

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

1220 data = { 

1221 "rel": "icon", 

1222 "location": icon_file, 

1223 "base": None, 

1224 "seq": 1, 

1225 "metadata": "", 

1226 } 

1227 special_issue.ext_links.append(data) 

1228 

1229 special_issue = Munch(special_issue.__dict__) 

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

1231 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params) 

1232 cmd.do() 

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

1234 

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

1236 figures = [] 

1237 for abstract in abstracts: 

1238 abstract_xml = abstract.encode("utf8") 

1239 

1240 tree = etree.fromstring(abstract_xml) 

1241 

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

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

1244 base = None 

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

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

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

1248 base = get_media_base_root(colid) 

1249 data_location = os.path.join( 

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

1251 ) 

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

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

1254 # so related objects can go for the correct one 

1255 img = Image.open(data_location) 

1256 final_data_location = os.path.join( 

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

1258 ) 

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

1260 os.makedirs(final_data_location) 

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

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

1263 img.save(final_data_location) 

1264 if ext == "png": 

1265 mimetype = "image/png" 

1266 else: 

1267 mimetype = "image/jpeg" 

1268 data = { 

1269 "rel": "html-image", 

1270 "mimetype": mimetype, 

1271 "location": relative_path, 

1272 "base": base, 

1273 "metadata": "", 

1274 } 

1275 if data not in figures: 

1276 figures.append(data) 

1277 return figures 

1278 

1279 

1280class PageIndexView(EditorRequiredMixin, TemplateView): 

1281 template_name = "mersenne_cms/page_index.html" 

1282 

1283 def get_context_data(self, **kwargs): 

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

1285 site_id = model_helpers.get_site_id(colid) 

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

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

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

1289 else: 

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

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

1292 context["colid"] = colid 

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

1294 context["pages"] = pages 

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

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

1297 return context 

1298 

1299 

1300class PageBaseView(HandleCMSMixin, View): 

1301 template_name = "mersenne_cms/page_form.html" 

1302 model = Page 

1303 form_class = PageForm 

1304 

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

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

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

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

1309 

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

1311 

1312 def get_success_url(self): 

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

1314 

1315 def get_context_data(self, **kwargs): 

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

1317 context["journal"] = self.collection 

1318 return context 

1319 

1320 def update_test_website(self): 

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

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

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

1324 else: 

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

1326 

1327 if response.status_code == 503: 

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

1329 else: 

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

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

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

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

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

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

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

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

1338 

1339 def get_form_kwargs(self): 

1340 kwargs = super().get_form_kwargs() 

1341 kwargs["site_id"] = self.site_id 

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

1343 return kwargs 

1344 

1345 def form_valid(self, form): 

1346 form.save() 

1347 

1348 self.update_test_website() 

1349 

1350 return HttpResponseRedirect(self.get_success_url()) 

1351 

1352 

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

1354class PageDeleteView(PageBaseView): 

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

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

1357 pk = kwargs.get("pk") 

1358 page = get_object_or_404(Page, id=pk) 

1359 if page.mersenne_id: 

1360 raise PermissionDenied 

1361 

1362 page.delete() 

1363 

1364 self.update_test_website() 

1365 

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

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

1368 else: 

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

1370 

1371 

1372class PageCreateView(PageBaseView, CreateView): 

1373 def get_context_data(self, **kwargs): 

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

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

1376 return context 

1377 

1378 

1379class PageUpdateView(PageBaseView, UpdateView): 

1380 def get_context_data(self, **kwargs): 

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

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

1383 return context 

1384 

1385 

1386class NewsBaseView(PageBaseView): 

1387 template_name = "mersenne_cms/news_form.html" 

1388 model = News 

1389 form_class = NewsForm 

1390 

1391 

1392class NewsDeleteView(NewsBaseView): 

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

1394 pk = kwargs.get("pk") 

1395 news = get_object_or_404(News, id=pk) 

1396 

1397 news.delete() 

1398 

1399 self.update_test_website() 

1400 

1401 return HttpResponseRedirect(self.get_success_url()) 

1402 

1403 

1404class NewsCreateView(NewsBaseView, CreateView): 

1405 def get_context_data(self, **kwargs): 

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

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

1408 return context 

1409 

1410 

1411class NewsUpdateView(NewsBaseView, UpdateView): 

1412 def get_context_data(self, **kwargs): 

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

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

1415 return context 

1416 

1417 

1418# def page_create_view(request, colid): 

1419# context = {} 

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

1421# raise PermissionDenied 

1422# collection = model_helpers.get_collection(colid) 

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

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

1425# if form.is_valid(): 

1426# form.save() 

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

1428# if response.status_code < 300: 

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

1430# else: 

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

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

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

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

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

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

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

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

1439# 

1440# context["form"] = form 

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

1442# context["journal"] = collection 

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

1444 

1445 

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

1447# context = {} 

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

1449# raise PermissionDenied 

1450# 

1451# collection = model_helpers.get_collection(colid) 

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

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

1454# if form.is_valid(): 

1455# form.save() 

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

1457# if response.status_code < 300: 

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

1459# else: 

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

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

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

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

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

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

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

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

1468# 

1469# context["form"] = form 

1470# context["pid"] = pid 

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

1472# context["journal"] = collection 

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