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

872 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-06-09 12:44 +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 = f"/{prefixes[colid]}{settings.MEDIA_URL}/CR" 

129 

130 return path 

131 

132 

133def change_ckeditor_storage(colid): 

134 """ 

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

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

137 To do that we have to 

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

139 - modify the storage location 

140 """ 

141 

142 from ckeditor_uploader import utils, views 

143 from django.core.files.storage import FileSystemStorage 

144 

145 storage = FileSystemStorage( 

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

147 ) 

148 

149 utils.storage = storage 

150 views.storage = storage 

151 

152 

153class EditorRequiredMixin(UserPassesTestMixin): 

154 def test_func(self): 

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

156 

157 

158class CollectionImageUploadView(EditorRequiredMixin, ImageUploadView): 

159 """ 

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

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

162 To do that we have to 

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

164 - modify the storage location 

165 """ 

166 

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

168 colid = kwargs["colid"] 

169 

170 change_ckeditor_storage(colid) 

171 

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

173 

174 

175class CollectionBrowseView(EditorRequiredMixin, View): 

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

177 colid = kwargs["colid"] 

178 

179 change_ckeditor_storage(colid) 

180 

181 return browse(request) 

182 

183 

184file_upload_in_collection = csrf_exempt(CollectionImageUploadView.as_view()) 

185file_browse_in_collection = csrf_exempt(CollectionBrowseView.as_view()) 

186 

187 

188def deploy_cms(site, collection): 

189 colid = collection.pid 

190 base_url = getattr(collection, site)() 

191 

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

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

194 

195 if site == "website": 

196 from_base_path = get_media_base_root_in_test(colid) 

197 to_base_path = get_media_base_root_in_prod(colid) 

198 

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

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

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

202 if os.path.exists(from_path): 

203 try: 

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

205 except OSError as exception: 

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

207 

208 site_id = model_helpers.get_site_id(colid) 

209 if model_helpers.get_site_default_language(site_id): 

210 from modeltranslation import fields, manager 

211 

212 old_ftor = manager.get_language 

213 manager.get_language = monkey_get_language_en 

214 fields.get_language = monkey_get_language_en 

215 

216 pages = get_pages_content(colid) 

217 news = get_news_content(colid) 

218 

219 manager.get_language = old_ftor 

220 fields.get_language = old_ftor 

221 else: 

222 pages = get_pages_content(colid) 

223 news = get_news_content(colid) 

224 

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

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

227 

228 try: 

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

230 

231 if response.status_code == 503: 

232 e = ServerUnderMaintenance( 

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

234 ) 

235 return HttpResponseServerError(e, status=503) 

236 

237 except Timeout as exception: 

238 return HttpResponse(exception, status=408) 

239 except Exception as exception: 

240 return HttpResponseServerError(exception) 

241 

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

243 

244 

245class HandleCMSMixin(EditorRequiredMixin): 

246 """ 

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

248 """ 

249 

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

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

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

253 

254 def init_data(self, kwargs): 

255 self.collection = None 

256 

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

258 if self.colid: 

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

260 if not self.collection: 

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

262 

263 test_server_url = self.collection.test_website() 

264 if not test_server_url: 

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

266 

267 prod_server_url = self.collection.website() 

268 if not prod_server_url: 

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

270 

271 

272class GetCMSFromSiteAPIView(HandleCMSMixin, View): 

273 """ 

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

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

276 """ 

277 

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

279 self.init_data(self.kwargs) 

280 

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

282 

283 try: 

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

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

286 

287 # Just to need to save the json on disk 

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

289 # /mersenne_test_data/@colid/media 

290 folder = get_media_base_root(self.colid) 

291 os.makedirs(folder, exist_ok=True) 

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

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

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

295 

296 except Timeout as exception: 

297 return HttpResponse(exception, status=408) 

298 except Exception as exception: 

299 return HttpResponseServerError(exception) 

300 

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

302 

303 

304def monkey_get_language_en(): 

305 return "en" 

306 

307 

308class RestoreCMSAPIView(HandleCMSMixin, View): 

309 """ 

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

311 """ 

312 

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

314 self.init_data(self.kwargs) 

315 

316 folder = get_media_base_root(self.colid) 

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

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

319 json_data = json.load(f) 

320 

321 pages = json_data.get("pages") 

322 

323 site_id = model_helpers.get_site_id(self.colid) 

324 if model_helpers.get_site_default_language(site_id): 

325 from modeltranslation import fields, manager 

326 

327 old_ftor = manager.get_language 

328 manager.get_language = monkey_get_language_en 

329 fields.get_language = monkey_get_language_en 

330 

331 import_pages(pages, self.colid) 

332 

333 manager.get_language = old_ftor 

334 fields.get_language = old_ftor 

335 else: 

336 import_pages(pages, self.colid) 

337 

338 if "news" in json_data: 

339 news = json_data.get("news") 

340 import_news(news, self.colid) 

341 

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

343 

344 

345class DeployCMSAPIView(HandleCMSMixin, View): 

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

347 self.init_data(self.kwargs) 

348 

349 if check_lock(): 

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

351 messages.error(self.request, msg) 

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

353 

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

355 

356 response = deploy_cms(site, self.collection) 

357 

358 if response.status_code == 503: 

359 messages.error( 

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

361 ) 

362 

363 return response 

364 

365 

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

367 urls = [""] 

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

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

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

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

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

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

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

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

376 return urls 

377 

378 

379class SuggestDeployView(EditorRequiredMixin, View): 

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

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

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

383 article = get_object_or_404(Article, doi=doi) 

384 

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

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

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

388 data = form.cleaned_data 

389 obj.date_modified = timezone.now() 

390 form.save() 

391 collection = article.my_container.my_collection 

392 urls = get_server_urls(collection, site=site) 

393 response = requests.models.Response() 

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

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

396 try: 

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

398 except requests.exceptions.RequestException as e: 

399 response.status_code = 503 

400 response.reason = e.args[0] 

401 break 

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

403 else: 

404 return HttpResponseBadRequest() 

405 

406 

407def suggest_debug(results, article, message): 

408 crop_results = 5 

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

410 dois = [] 

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

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

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

414 for item in results["docs"]: 

415 doi = item.get("doi") 

416 if doi: 

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

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

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

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

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

422 url = "" 

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

424 if suggest and suggest.my_container: 

425 collection = suggest.my_container.my_collection 

426 base_url = collection.website() or "" 

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

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

429 

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

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

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

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

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

435 terms = results["interestingTerms"] 

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

437 tail.extend(reversed(terms)) 

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

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

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

441 else: 

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

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

444 

445 

446class SuggestUpdateView(EditorRequiredMixin, TemplateView): 

447 template_name = "editorial_tools/suggested.html" 

448 

449 def get_context_data(self, **kwargs): 

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

451 article = get_object_or_404(Article, doi=doi) 

452 

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

454 collection = article.my_container.my_collection 

455 base_url = collection.website() or "" 

456 response = requests.models.Response() 

457 try: 

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

459 except requests.exceptions.RequestException as e: 

460 response.status_code = 503 

461 response.reason = e.args[0] 

462 msg = { 

463 "url": response.url, 

464 "status": response.status_code, 

465 "err": response.reason, 

466 } 

467 results = None 

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

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

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

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

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

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

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

475 context["article"] = article 

476 context["date_modified"] = obj.date_modified 

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

478 return context 

479 

480 

481class EditorialToolsVolumeItemsView(EditorRequiredMixin, TemplateView): 

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

483 

484 def get_context_data(self, **kwargs): 

485 vid = kwargs.get("vid") 

486 issues_articles, collection = model_helpers.get_issues_in_volume(vid) 

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

488 context["issues_articles"] = issues_articles 

489 context["collection"] = collection 

490 return context 

491 

492 

493class EditorialToolsArticleView(EditorRequiredMixin, TemplateView): 

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

495 

496 def get_context_data(self, **kwargs): 

497 colid = kwargs.get("colid") 

498 doi = kwargs.get("doi") 

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

500 

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

502 context["article"] = article 

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

504 return context 

505 

506 

507class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView): 

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

509 

510 def get_context_data(self, **kwargs): 

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

512 article = get_object_or_404(Article, doi=doi) 

513 

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

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

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

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

518 context["article"] = article 

519 context["date_modified"] = obj.date_modified 

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

521 context["graphical_abstract"] = obj.graphical_abstract 

522 context["illustration"] = obj.illustration 

523 return context 

524 

525 

526class GraphicalAbstractDeployView(EditorRequiredMixin, View): 

527 def __get_path_and_replace_tiff_file(self, obj_attribute_file): 

528 """ 

529 Returns the path of the attribute. 

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

531 

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

533 """ 

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

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

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

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

538 return jpeg_path 

539 

540 return obj_attribute_file.path 

541 

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

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

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

545 article = get_object_or_404(Article, doi=doi) 

546 

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

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

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

550 obj.date_modified = timezone.now() 

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

552 form.save() 

553 files = {} 

554 

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

556 obj_attribute_file = getattr(obj, attribute, None) 

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

558 file_path = self.__get_path_and_replace_tiff_file(obj_attribute_file) 

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

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

561 

562 collection = article.my_container.my_collection 

563 urls = get_server_urls(collection, site=site) 

564 response = requests.models.Response() 

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

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

567 try: 

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

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

570 else: 

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

572 except requests.exceptions.RequestException as e: 

573 response.status_code = 503 

574 response.reason = e.args[0] 

575 break 

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

577 else: 

578 return HttpResponseBadRequest() 

579 

580 

581def parse_content(content): 

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

583 if not table: 

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

585 

586 articles = [] 

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

588 for row in rows: 

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

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

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

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

593 item = {} 

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

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

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

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

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

599 articles.append(item) 

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

601 

602 

603class VirtualIssueParseView(EditorRequiredMixin, View): 

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

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

606 page = get_object_or_404(Page, id=pid) 

607 

608 data = {"pid": pid} 

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

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

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

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

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

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

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

616 

617 content_fr = parse_content(page.content_fr) 

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

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

620 

621 content_en = parse_content(page.content_en) 

622 data["articles"] = content_en["articles"] 

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

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

625 return JsonResponse(data) 

626 

627 

628class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView): 

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

630 

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

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

633 get_object_or_404(Page, id=pid) 

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

635 

636 

637class VirtualIssueCreateView(EditorRequiredMixin, View): 

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

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

640 site_id = model_helpers.get_site_id(colid) 

641 parent, _ = Page.objects.get_or_create( 

642 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES, 

643 parent_page=None, 

644 site_id=site_id, 

645 ) 

646 page = Page.objects.create( 

647 menu_title_en="New virtual issue", 

648 menu_title_fr="Nouvelle collection transverse", 

649 parent_page=parent, 

650 site_id=site_id, 

651 state="draft", 

652 ) 

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

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

655 

656 

657class SpecialIssuesIndex(EditorRequiredMixin, TemplateView): 

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

659 

660 def get_context_data(self, **kwargs): 

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

662 

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

664 context["colid"] = colid 

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

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

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

668 ).filter(my_collection=collection) 

669 

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

671 return context 

672 

673 

674class SpecialIssueEditView(EditorRequiredMixin, TemplateView): 

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

676 

677 def get_context_data(self, **kwargs): 

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

679 return context 

680 

681 

682class VirtualIssuesIndex(EditorRequiredMixin, TemplateView): 

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

684 

685 def get_context_data(self, **kwargs): 

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

687 site_id = model_helpers.get_site_id(colid) 

688 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES) 

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

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

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

692 context["pages"] = pages 

693 return context 

694 

695 

696def get_citation_fr(doi, citation_en): 

697 citation_fr = citation_en 

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

699 if article and article.trans_title_html: 

700 trans_title = article.trans_title_html 

701 try: 

702 citation_fr = re.sub( 

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

704 rf"\1{trans_title}", 

705 citation_en, 

706 ) 

707 except re.error: 

708 pass 

709 return citation_fr 

710 

711 

712def summary_build(articles, colid): 

713 summary_fr = "" 

714 summary_en = "" 

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

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

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

718 colid_lo = colid.lower() 

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

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

721 

722 for article in articles: 

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

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

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

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

727 if doi or citation_en: 

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

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

730 if image_src: 

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

732 base_url = get_media_base_url(colid) 

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

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

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

736 if re.match(image_header, image_src): 

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

738 base64_data = base64.b64decode(image_src) 

739 base_root = get_media_base_root(colid) 

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

741 os.makedirs(path, exist_ok=True) 

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

743 fp.write(base64_data) 

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

745 # TODO mettre la vrai valeur pour le SITE_DOMAIN 

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

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

748 else: 

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

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

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

752 summary_fr = head + summary_fr + tail 

753 summary_en = head + summary_en + tail 

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

755 

756 

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

758class VirtualIssueDeployView(HandleCMSMixin, View): 

759 """ 

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

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

762 The Page is then immediately posted to the test_website. 

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

764 => See DeployCMSAPIView 

765 """ 

766 

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

768 self.init_data(self.kwargs) 

769 if check_lock(): 

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

771 messages.error(self.request, msg) 

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

773 

774 pid = kwargs.get("pid") 

775 colid = self.colid 

776 data = json.loads(request.body) 

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

778 page = get_object_or_404(Page, id=pid) 

779 page.slug = page.slug_fr = page.slug_en = None 

780 page.menu_title_fr = data["title_fr"] 

781 page.menu_title_en = data["title_en"] 

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

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

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

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

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

787 page.save() 

788 

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

790 if response.status_code == 503: 

791 messages.error( 

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

793 ) 

794 

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

796 

797 

798class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView): 

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

800 

801 def get_context_data(self, **kwargs): 

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

803 return context 

804 

805 def set_contrib_addresses(self, contrib, contribution): 

806 for address in contrib: 

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

808 contrib_address.save() 

809 

810 def delete(self, pid): 

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

812 cmd = base_ptf_cmds.addContainerPtfCmd() 

813 cmd.set_object_to_be_deleted(special_issue) 

814 cmd.undo() 

815 

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

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

818 

819 data = {"pid": pid} 

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

821 data["colid"] = colid 

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

823 name = resolve(request.path_info).url_name 

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

825 self.delete(pid) 

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

827 

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

829 

830 if pid != "create": 

831 container = get_object_or_404(Container, pid=pid) 

832 # TODO: pass the lang and trans_lang as well 

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

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

835 # keep using trans_title_html for backward compatibility 

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

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

838 data["title"] = translated_title.title_html 

839 data["doi"] = container.doi 

840 data["trans_title"] = container.title_html 

841 data["year"] = container.year 

842 data["volume"] = container.volume 

843 data["articles"] = [ 

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

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

846 ] 

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

848 data["use_resources_icon"] = True 

849 else: 

850 data["use_resources_icon"] = False 

851 

852 contribs = model_data_converter.db_to_contributors(container.contributions) 

853 data["contribs"] = contribs 

854 abstract_set = container.abstract_set.all() 

855 data["head_fr"] = ( 

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

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

858 else "" 

859 ) 

860 data["head_en"] = ( 

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

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

863 else "" 

864 ) 

865 data["tail_fr"] = ( 

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

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

868 else "" 

869 ) 

870 data["tail_en"] = ( 

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

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

873 else "" 

874 ) 

875 data["editor_bio_en"] = ( 

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

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

878 else "" 

879 ) 

880 data["editor_bio_fr"] = ( 

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

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

883 else "" 

884 ) 

885 

886 streams = container.datastream_set.all() 

887 data["pdf_file_name"] = "" 

888 data["edito_file_name"] = "" 

889 data["edito_display_name"] = "" 

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

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

892 data["pdf_file_name"] = stream.text 

893 try: 

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

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

896 "$$$" 

897 ) 

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

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

900 

901 except RelatedObject.DoesNotExist: 

902 pass 

903 try: 

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

905 

906 data["icon_location"] = container_icon.location 

907 except ExtLink.DoesNotExist: 

908 data["icon_location"] = "" 

909 # try: 

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

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

912 # except ExtLink.DoesNotExist: 

913 # data["special_issue_icon"] = None 

914 

915 else: 

916 data["title"] = "" 

917 data["doi"] = None 

918 data["trans_title"] = "" 

919 data["year"] = "" 

920 data["volume"] = "" 

921 data["articles"] = [] 

922 data["contribs"] = [] 

923 

924 data["head_fr"] = "" 

925 data["head_en"] = "" 

926 data["tail_fr"] = "" 

927 data["tail_en"] = "" 

928 data["editor_bio_en"] = "" 

929 data["editor_bio_fr"] = "" 

930 data["pdf_file_name"] = "" 

931 data["edito_file_name"] = "" 

932 data["use_resources_icon"] = False 

933 

934 return JsonResponse(data) 

935 

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

937 # le but est de faire un IssueDAta 

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

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

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

941 special_issue = create_issuedata() 

942 year = request.POST["year"] 

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

944 # TODO 2: In VueJS, titleFr = title 

945 trans_title_html = request.POST["title"] 

946 title_html = request.POST["trans_title"] 

947 issues = collection.content.all().order_by("-year") 

948 same_year_issues = issues.filter(year=int(year)) 

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

950 volume = same_year_issues.first().volume 

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

952 ref_volume = issues.filter(year=2024).first().volume 

953 volume = int(ref_volume) + ( 

954 int(year) - 2024 

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

956 else: 

957 volume = "" 

958 if pid != "create": 

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

960 container = get_object_or_404(Container, pid=pid) 

961 lang = container.lang 

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

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

964 raise ValueError( 

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

966 ) 

967 trans_lang = trans_title.lang 

968 xpub = create_publisherdata() 

969 xpub.name = container.my_publisher.pub_name 

970 special_issue.provider = container.provider 

971 special_issue.number = container.number 

972 special_issue_pid = pid 

973 special_issue.date_pre_published = container.date_pre_published 

974 special_issue.date_published = container.date_published 

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

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

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

978 special_issue.doi = model_helpers.assign_container_doi(colid) 

979 else: 

980 special_issue.doi = container.doi 

981 else: 

982 lang = "en" 

983 container = None 

984 trans_lang = "fr" 

985 xpub = create_publisherdata() 

986 special_issue.doi = model_helpers.assign_container_doi(colid) 

987 

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

989 xpub.name = "UGA Éditions" 

990 else: 

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

992 special_issue.provider = collection.provider 

993 

994 special_issues = issues.filter(year=year).filter( 

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

996 ) 

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

998 all_special_issues_numbers = [ 

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

1000 ] 

1001 if len(all_special_issues_numbers) > 0: 

1002 max_number = max(all_special_issues_numbers) 

1003 else: 

1004 max_number = 0 

1005 

1006 else: 

1007 max_number = 0 

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

1009 special_issue_pid = f"{colid}_{year}__{volume}_{special_issue.number}" 

1010 

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

1012 special_issue.ctype = "issue_special_img" 

1013 else: 

1014 special_issue.ctype = "issue_special" 

1015 

1016 existing_issue = model_helpers.get_resource(special_issue_pid) 

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

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

1019 

1020 special_issue.lang = lang 

1021 special_issue.title_html = title_html 

1022 special_issue.title_xml = build_title_xml( 

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

1024 ) 

1025 

1026 title_xml = build_title_xml( 

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

1028 ) 

1029 title = create_titledata( 

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

1031 ) 

1032 special_issue.titles = [title] 

1033 

1034 special_issue.year = year 

1035 special_issue.volume = volume 

1036 special_issue.journal = journal 

1037 special_issue.publisher = xpub 

1038 special_issue.pid = special_issue_pid 

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

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

1041 ) 

1042 

1043 articles = [] 

1044 contribs = [] 

1045 index = 0 

1046 

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

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

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

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

1051 # if not article["citation"]: 

1052 # index += 1 

1053 # continue 

1054 articles.append(article) 

1055 

1056 index += 1 

1057 

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

1059 index = 0 

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

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

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

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

1064 contributor = create_contributor() 

1065 contributor["first_name"] = contrib["first_name"] 

1066 contributor["last_name"] = contrib["last_name"] 

1067 contributor["orcid"] = contrib["orcid"] 

1068 contributor["role"] = "editor" 

1069 

1070 contrib_xml = xml_utils.get_contrib_xml(contrib) 

1071 contributor["contrib_xml"] = contrib_xml 

1072 contribs.append(Munch(contributor)) 

1073 index += 1 

1074 special_issue.contributors = contribs 

1075 

1076 # Part of the code that handle forwords and lastwords 

1077 

1078 request_datas = [ 

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

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

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

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

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

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

1085 ] 

1086 

1087 special_issue.abstracts = [] 

1088 abstracts_xml = [] 

1089 for request_data in request_datas: 

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

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

1092 

1093 ckeditor_data = build_jats_data_from_html_field( 

1094 value_html, 

1095 tag="abstract", 

1096 text_lang=lang, 

1097 resource_lang="en", 

1098 field_type=request_data["abstract_type"], 

1099 mml_formulas=[], 

1100 issue_pid=colid, 

1101 pid=special_issue.pid, 

1102 ) 

1103 

1104 abstract_data = create_abstract( 

1105 tag=request_data["abstract_type"], 

1106 lang=lang, 

1107 value_html=value_html, 

1108 value_tex=ckeditor_data["value_tex"], 

1109 value_xml=ckeditor_data["value_xml"], 

1110 ) 

1111 

1112 special_issue.abstracts.append(abstract_data) 

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

1114 

1115 figures = self.create_related_objects_from_abstract( 

1116 abstracts_xml, colid, special_issue.pid 

1117 ) 

1118 special_issue.related_objects = figures 

1119 

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

1121 # Both are stored in same directory 

1122 

1123 pdf_file_path = resolver.get_disk_location( 

1124 f"{settings.RESOURCES_ROOT}", 

1125 f"{collection.pid}", 

1126 "pdf", 

1127 special_issue_pid, 

1128 article_id=None, 

1129 do_create_folder=False, 

1130 ) 

1131 pdf_path = os.path.dirname(pdf_file_path) 

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

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

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

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

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

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

1138 

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

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

1141 pdf_file = request.FILES["pdf"] 

1142 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1143 pdf_file, 

1144 collection.pid, 

1145 special_issue.pid, 

1146 special_issue.pid, 

1147 ) 

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

1149 

1150 else: 

1151 pdf_file_name = request.POST["pdf_name"] 

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

1153 

1154 pdf_stream_data = create_datastream() 

1155 pdf_stream_data["location"] = relative_file_name 

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

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

1158 pdf_stream_data["text"] = pdf_file_name 

1159 special_issue.streams.append(pdf_stream_data) 

1160 

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

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

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

1164 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1165 edito_file, 

1166 collection.pid, 

1167 special_issue.pid, 

1168 special_issue.pid, 

1169 ) 

1170 

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

1172 edito_display_name = request.POST["edito_display_name"] 

1173 else: 

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

1175 edito_file_name = request.POST["edito_name"] 

1176 edito_display_name = request.POST["edito_display_name"] 

1177 

1178 data = { 

1179 "rel": "edito", 

1180 "mimetype": "application/pdf", 

1181 "location": relative_file_name, 

1182 "base": None, 

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

1184 } 

1185 special_issue.related_objects.append(data) 

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

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

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

1189 icon_file = request.FILES["icon"] 

1190 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1191 icon_file, 

1192 collection.pid, 

1193 special_issue.pid, 

1194 special_issue.pid, 

1195 ) 

1196 if ".tif" in relative_file_name: 

1197 jpeg_path = ImageManager( 

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

1199 ).to_jpeg() 

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

1201 data = { 

1202 "rel": "icon", 

1203 "location": relative_file_name, 

1204 "base": None, 

1205 "seq": 1, 

1206 "metadata": "", 

1207 } 

1208 special_issue.ext_links.append(data) 

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

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

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

1212 data = { 

1213 "rel": "icon", 

1214 "location": icon_file, 

1215 "base": None, 

1216 "seq": 1, 

1217 "metadata": "", 

1218 } 

1219 special_issue.ext_links.append(data) 

1220 

1221 special_issue = Munch(special_issue.__dict__) 

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

1223 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params) 

1224 cmd.do() 

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

1226 

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

1228 figures = [] 

1229 for abstract in abstracts: 

1230 abstract_xml = abstract.encode("utf8") 

1231 

1232 tree = etree.fromstring(abstract_xml) 

1233 

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

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

1236 base = None 

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

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

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

1240 base = get_media_base_root(colid) 

1241 data_location = os.path.join( 

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

1243 ) 

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

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

1246 # so related objects can go for the correct one 

1247 img = Image.open(data_location) 

1248 final_data_location = os.path.join( 

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

1250 ) 

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

1252 os.makedirs(final_data_location) 

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

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

1255 img.save(final_data_location) 

1256 if ext == "png": 

1257 mimetype = "image/png" 

1258 else: 

1259 mimetype = "image/jpeg" 

1260 data = { 

1261 "rel": "html-image", 

1262 "mimetype": mimetype, 

1263 "location": relative_path, 

1264 "base": base, 

1265 "metadata": "", 

1266 } 

1267 if data not in figures: 

1268 figures.append(data) 

1269 return figures 

1270 

1271 

1272class PageIndexView(EditorRequiredMixin, TemplateView): 

1273 template_name = "mersenne_cms/page_index.html" 

1274 

1275 def get_context_data(self, **kwargs): 

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

1277 site_id = model_helpers.get_site_id(colid) 

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

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

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

1281 else: 

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

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

1284 context["colid"] = colid 

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

1286 context["pages"] = pages 

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

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

1289 return context 

1290 

1291 

1292class PageBaseView(HandleCMSMixin, View): 

1293 template_name = "mersenne_cms/page_form.html" 

1294 model = Page 

1295 form_class = PageForm 

1296 

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

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

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

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

1301 

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

1303 

1304 def get_success_url(self): 

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

1306 

1307 def get_context_data(self, **kwargs): 

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

1309 context["journal"] = self.collection 

1310 return context 

1311 

1312 def update_test_website(self): 

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

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

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

1316 else: 

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

1318 

1319 if response.status_code == 503: 

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

1321 else: 

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

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

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

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

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

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

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

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

1330 

1331 def get_form_kwargs(self): 

1332 kwargs = super().get_form_kwargs() 

1333 kwargs["site_id"] = self.site_id 

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

1335 return kwargs 

1336 

1337 def form_valid(self, form): 

1338 form.save() 

1339 

1340 self.update_test_website() 

1341 

1342 return HttpResponseRedirect(self.get_success_url()) 

1343 

1344 

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

1346class PageDeleteView(PageBaseView): 

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

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

1349 pk = kwargs.get("pk") 

1350 page = get_object_or_404(Page, id=pk) 

1351 if page.mersenne_id: 

1352 raise PermissionDenied 

1353 

1354 page.delete() 

1355 

1356 self.update_test_website() 

1357 

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

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

1360 else: 

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

1362 

1363 

1364class PageCreateView(PageBaseView, CreateView): 

1365 def get_context_data(self, **kwargs): 

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

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

1368 return context 

1369 

1370 

1371class PageUpdateView(PageBaseView, UpdateView): 

1372 def get_context_data(self, **kwargs): 

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

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

1375 return context 

1376 

1377 

1378class NewsBaseView(PageBaseView): 

1379 template_name = "mersenne_cms/news_form.html" 

1380 model = News 

1381 form_class = NewsForm 

1382 

1383 

1384class NewsDeleteView(NewsBaseView): 

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

1386 pk = kwargs.get("pk") 

1387 news = get_object_or_404(News, id=pk) 

1388 

1389 news.delete() 

1390 

1391 self.update_test_website() 

1392 

1393 return HttpResponseRedirect(self.get_success_url()) 

1394 

1395 

1396class NewsCreateView(NewsBaseView, CreateView): 

1397 def get_context_data(self, **kwargs): 

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

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

1400 return context 

1401 

1402 

1403class NewsUpdateView(NewsBaseView, UpdateView): 

1404 def get_context_data(self, **kwargs): 

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

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

1407 return context 

1408 

1409 

1410# def page_create_view(request, colid): 

1411# context = {} 

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

1413# raise PermissionDenied 

1414# collection = model_helpers.get_collection(colid) 

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

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

1417# if form.is_valid(): 

1418# form.save() 

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

1420# if response.status_code < 300: 

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

1422# else: 

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

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

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

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

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

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

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

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

1431# 

1432# context["form"] = form 

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

1434# context["journal"] = collection 

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

1436 

1437 

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

1439# context = {} 

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

1441# raise PermissionDenied 

1442# 

1443# collection = model_helpers.get_collection(colid) 

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

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

1446# if form.is_valid(): 

1447# form.save() 

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

1449# if response.status_code < 300: 

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

1451# else: 

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

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

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

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

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

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

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

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

1460# 

1461# context["form"] = form 

1462# context["pid"] = pid 

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

1464# context["journal"] = collection 

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