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

878 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-31 09:10 +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 if container.trans_title_html: 836 ↛ 839line 836 didn't jump to line 839 because the condition on line 836 was always true

837 data["title"] = container.trans_title_html 

838 else: 

839 for title in container.title_set.all(): 

840 if title["lang"] == "fr" and title["type"] == "main": 

841 data["title"] = title["title_html"] 

842 data["doi"] = container.doi 

843 data["trans_title"] = container.title_html 

844 data["year"] = container.year 

845 data["volume"] = container.volume 

846 data["articles"] = [ 

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

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

849 ] 

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

851 data["use_resources_icon"] = True 

852 else: 

853 data["use_resources_icon"] = False 

854 

855 contribs = model_data_converter.db_to_contributors(container.contributions) 

856 data["contribs"] = contribs 

857 abstract_set = container.abstract_set.all() 

858 data["head_fr"] = ( 

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

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

861 else "" 

862 ) 

863 data["head_en"] = ( 

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

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

866 else "" 

867 ) 

868 data["tail_fr"] = ( 

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

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

871 else "" 

872 ) 

873 data["tail_en"] = ( 

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

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

876 else "" 

877 ) 

878 data["editor_bio_en"] = ( 

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

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

881 else "" 

882 ) 

883 data["editor_bio_fr"] = ( 

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

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

886 else "" 

887 ) 

888 

889 streams = container.datastream_set.all() 

890 data["pdf_file_name"] = "" 

891 data["edito_file_name"] = "" 

892 data["edito_display_name"] = "" 

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

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

895 data["pdf_file_name"] = stream.text 

896 try: 

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

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

899 "$$$" 

900 ) 

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

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

903 

904 except RelatedObject.DoesNotExist: 

905 pass 

906 try: 

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

908 

909 data["icon_location"] = container_icon.location 

910 except ExtLink.DoesNotExist: 

911 data["icon_location"] = "" 

912 # try: 

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

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

915 # except ExtLink.DoesNotExist: 

916 # data["special_issue_icon"] = None 

917 

918 else: 

919 data["title"] = "" 

920 data["doi"] = None 

921 data["trans_title"] = "" 

922 data["year"] = "" 

923 data["volume"] = "" 

924 data["articles"] = [] 

925 data["contribs"] = [] 

926 

927 data["head_fr"] = "" 

928 data["head_en"] = "" 

929 data["tail_fr"] = "" 

930 data["tail_en"] = "" 

931 data["editor_bio_en"] = "" 

932 data["editor_bio_fr"] = "" 

933 data["pdf_file_name"] = "" 

934 data["edito_file_name"] = "" 

935 data["use_resources_icon"] = False 

936 

937 return JsonResponse(data) 

938 

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

940 # le but est de faire un IssueDAta 

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

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

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

944 special_issue = create_issuedata() 

945 year = request.POST["year"] 

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

947 # TODO 2: In VueJS, titleFr = title 

948 trans_title_html = request.POST["title"] 

949 title_html = request.POST["trans_title"] 

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

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

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

953 volume = same_year_issues.first().volume 

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

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

956 volume = int(ref_volume) + ( 

957 int(year) - 2024 

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

959 else: 

960 volume = "" 

961 if pid != "create": 

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

963 container: Container = get_object_or_404(Container, pid=pid) 

964 lang = container.lang 

965 trans_lang = container.trans_lang 

966 xpub = create_publisherdata() 

967 xpub.name = container.my_publisher.pub_name 

968 special_issue.provider = container.provider 

969 special_issue.number = container.number 

970 special_issue_pid = pid 

971 special_issue.date_pre_published = container.date_pre_published 

972 special_issue.date_published = container.date_published 

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

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

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

976 special_issue.doi = model_helpers.assign_container_doi(colid) 

977 else: 

978 special_issue.doi = container.doi 

979 else: 

980 lang = "en" 

981 container = None 

982 trans_lang = "fr" 

983 xpub = create_publisherdata() 

984 special_issue.doi = model_helpers.assign_container_doi(colid) 

985 

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

987 xpub.name = "UGA Éditions" 

988 else: 

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

990 special_issue.provider = collection.provider 

991 

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

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

994 ) 

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

996 all_special_issues_numbers = [ 

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

998 ] 

999 if len(all_special_issues_numbers) > 0: 

1000 max_number = max(all_special_issues_numbers) 

1001 else: 

1002 max_number = 0 

1003 

1004 else: 

1005 max_number = 0 

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

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

1008 

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

1010 special_issue.ctype = "issue_special_img" 

1011 else: 

1012 special_issue.ctype = "issue_special" 

1013 

1014 existing_issue = model_helpers.get_resource(special_issue_pid) 

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

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

1017 

1018 special_issue.lang = lang 

1019 special_issue.title_html = title_html 

1020 special_issue.title_xml = build_title_xml( 

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

1022 ) 

1023 

1024 special_issue.trans_lang = trans_lang 

1025 special_issue.trans_title_html = trans_title_html 

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_name = self.request.FILES["pdf"].name 

1142 location = pdf_path + "/" + special_issue_pid + ".pdf" 

1143 with open(location, "wb+") as destination: 

1144 for chunk in self.request.FILES["pdf"].chunks(): 

1145 destination.write(chunk) 

1146 

1147 else: 

1148 pdf_file_name = request.POST["pdf_name"] 

1149 location = pdf_path + "/" + special_issue_pid + ".pdf" 

1150 

1151 pdf_stream_data = create_datastream() 

1152 pdf_stream_data["location"] = location.replace("/mersenne_test_data/", "") 

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

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

1155 pdf_stream_data["text"] = pdf_file_name 

1156 special_issue.streams.append(pdf_stream_data) 

1157 

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

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

1160 location = pdf_path + "/" + special_issue_pid + "_edito.pdf" 

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

1162 edito_display_name = request.POST["edito_display_name"] 

1163 with open(location, "wb+") as destination: 

1164 for chunk in self.request.FILES["edito"].chunks(): 

1165 destination.write(chunk) 

1166 else: 

1167 location = pdf_path + "/" + special_issue_pid + "_edito.pdf" 

1168 edito_file_name = request.POST["edito_name"] 

1169 edito_display_name = request.POST["edito_display_name"] 

1170 

1171 location = location.replace("/mersenne_test_data/", "") 

1172 data = { 

1173 "rel": "edito", 

1174 "mimetype": "application/pdf", 

1175 "location": location, 

1176 "base": None, 

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

1178 } 

1179 special_issue.related_objects.append(data) 

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

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

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

1183 icon_file = request.FILES["icon"] 

1184 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1185 icon_file, 

1186 collection.pid, 

1187 special_issue.pid, 

1188 special_issue.pid, 

1189 ) 

1190 if ".tif" in relative_file_name: 

1191 jpeg_path = ImageManager( 

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

1193 ).to_jpeg() 

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

1195 data = { 

1196 "rel": "icon", 

1197 "location": relative_file_name, 

1198 "base": None, 

1199 "seq": 1, 

1200 "metadata": "", 

1201 } 

1202 special_issue.ext_links.append(data) 

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

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

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

1206 data = { 

1207 "rel": "icon", 

1208 "location": icon_file, 

1209 "base": None, 

1210 "seq": 1, 

1211 "metadata": "", 

1212 } 

1213 special_issue.ext_links.append(data) 

1214 

1215 special_issue = Munch(special_issue.__dict__) 

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

1217 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params) 

1218 cmd.do() 

1219 # tail_fr_html = xml_utils.replace_html_entities(request.POST["tail_fr"]) 

1220 # tail_en_html = xml_utils.replace_html_entities(request.POST["tail_en"]) 

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

1222 

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

1224 figures = [] 

1225 for abstract in abstracts: 

1226 abstract_xml = abstract.encode("utf8") 

1227 

1228 tree = etree.fromstring(abstract_xml) 

1229 

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

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

1232 base = None 

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

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

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

1236 base = get_media_base_root(colid) 

1237 data_location = os.path.join( 

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

1239 ) 

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

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

1242 # so related objects can go for the correct one 

1243 img = Image.open(data_location) 

1244 final_data_location = os.path.join( 

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

1246 ) 

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

1248 os.makedirs(final_data_location) 

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

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

1251 img.save(final_data_location) 

1252 if ext == "png": 

1253 mimetype = "image/png" 

1254 else: 

1255 mimetype = "image/jpeg" 

1256 data = { 

1257 "rel": "html-image", 

1258 "mimetype": mimetype, 

1259 "location": relative_path, 

1260 "base": base, 

1261 "metadata": "", 

1262 } 

1263 if data not in figures: 

1264 figures.append(data) 

1265 return figures 

1266 

1267 

1268class PageIndexView(EditorRequiredMixin, TemplateView): 

1269 template_name = "mersenne_cms/page_index.html" 

1270 

1271 def get_context_data(self, **kwargs): 

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

1273 site_id = model_helpers.get_site_id(colid) 

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

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

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

1277 else: 

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

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

1280 context["colid"] = colid 

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

1282 context["pages"] = pages 

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

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

1285 return context 

1286 

1287 

1288class PageBaseView(HandleCMSMixin, View): 

1289 template_name = "mersenne_cms/page_form.html" 

1290 model = Page 

1291 form_class = PageForm 

1292 

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

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

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

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

1297 

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

1299 

1300 def get_success_url(self): 

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

1302 

1303 def get_context_data(self, **kwargs): 

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

1305 context["journal"] = self.collection 

1306 return context 

1307 

1308 def update_test_website(self): 

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

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

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

1312 else: 

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

1314 

1315 if response.status_code == 503: 

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

1317 else: 

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

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

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

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

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

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

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

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

1326 

1327 def get_form_kwargs(self): 

1328 kwargs = super().get_form_kwargs() 

1329 kwargs["site_id"] = self.site_id 

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

1331 return kwargs 

1332 

1333 def form_valid(self, form): 

1334 form.save() 

1335 

1336 self.update_test_website() 

1337 

1338 return HttpResponseRedirect(self.get_success_url()) 

1339 

1340 

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

1342class PageDeleteView(PageBaseView): 

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

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

1345 pk = kwargs.get("pk") 

1346 page = get_object_or_404(Page, id=pk) 

1347 if page.mersenne_id: 

1348 raise PermissionDenied 

1349 

1350 page.delete() 

1351 

1352 self.update_test_website() 

1353 

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

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

1356 else: 

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

1358 

1359 

1360class PageCreateView(PageBaseView, CreateView): 

1361 def get_context_data(self, **kwargs): 

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

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

1364 return context 

1365 

1366 

1367class PageUpdateView(PageBaseView, UpdateView): 

1368 def get_context_data(self, **kwargs): 

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

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

1371 return context 

1372 

1373 

1374class NewsBaseView(PageBaseView): 

1375 template_name = "mersenne_cms/news_form.html" 

1376 model = News 

1377 form_class = NewsForm 

1378 

1379 

1380class NewsDeleteView(NewsBaseView): 

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

1382 pk = kwargs.get("pk") 

1383 news = get_object_or_404(News, id=pk) 

1384 

1385 news.delete() 

1386 

1387 self.update_test_website() 

1388 

1389 return HttpResponseRedirect(self.get_success_url()) 

1390 

1391 

1392class NewsCreateView(NewsBaseView, CreateView): 

1393 def get_context_data(self, **kwargs): 

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

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

1396 return context 

1397 

1398 

1399class NewsUpdateView(NewsBaseView, UpdateView): 

1400 def get_context_data(self, **kwargs): 

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

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

1403 return context 

1404 

1405 

1406# def page_create_view(request, colid): 

1407# context = {} 

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

1409# raise PermissionDenied 

1410# collection = model_helpers.get_collection(colid) 

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

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

1413# if form.is_valid(): 

1414# form.save() 

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

1416# if response.status_code < 300: 

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

1418# else: 

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

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

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

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

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

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

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

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

1427# 

1428# context["form"] = form 

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

1430# context["journal"] = collection 

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

1432 

1433 

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

1435# context = {} 

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

1437# raise PermissionDenied 

1438# 

1439# collection = model_helpers.get_collection(colid) 

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

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

1442# if form.is_valid(): 

1443# form.save() 

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

1445# if response.status_code < 300: 

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

1447# else: 

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

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

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

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

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

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

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

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

1456# 

1457# context["form"] = form 

1458# context["pid"] = pid 

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

1460# context["journal"] = collection 

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