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

879 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-09-08 12:26 +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.ckeditor_parser import CkeditorParser 

48from ptf.cmds.xml.jats.builder.issue import build_title_xml, get_abstract_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_contributor, 

57 create_datastream, 

58 create_issuedata, 

59 create_publisherdata, 

60 create_titledata, 

61) 

62 

63# from ptf.models import ExtLink 

64# from ptf.models import ResourceInSpecialIssue 

65# from ptf.models import Contribution 

66# from ptf.models import Collection 

67from ptf.models import ( 

68 Article, 

69 Collection, 

70 Container, 

71 ContribAddress, 

72 ExtLink, 

73 GraphicalAbstract, 

74 RelatedArticles, 

75 RelatedObject, 

76) 

77from ptf.site_register import SITE_REGISTER 

78from ptf.utils import ImageManager, get_names 

79from requests import Timeout 

80 

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

82from ptf_tools.utils import is_authorized_editor 

83 

84from .base_views import check_lock 

85 

86 

87def get_media_base_root(colid): 

88 """ 

89 Base folder where media files are stored in Trammel 

90 """ 

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

92 colid = "CR" 

93 

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

95 

96 

97def get_media_base_root_in_test(colid): 

98 """ 

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

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

101 """ 

102 return get_media_base_root(colid) 

103 

104 

105def get_media_base_root_in_prod(colid): 

106 """ 

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

108 """ 

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

110 colid = "CR" 

111 

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

113 

114 

115def get_media_base_url(colid): 

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

117 

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

119 prefixes = { 

120 "CRMECA": "mecanique", 

121 "CRBIOL": "biologies", 

122 "CRGEOS": "geoscience", 

123 "CRCHIM": "chimie", 

124 "CRMATH": "mathematique", 

125 "CRPHYS": "physique", 

126 } 

127 path = f"/{prefixes[colid]}{settings.MEDIA_URL}/CR" 

128 

129 return path 

130 

131 

132def change_ckeditor_storage(colid): 

133 """ 

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

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

136 To do that we have to 

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

138 - modify the storage location 

139 """ 

140 

141 from ckeditor_uploader import utils, views 

142 from django.core.files.storage import FileSystemStorage 

143 

144 storage = FileSystemStorage( 

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

146 ) 

147 

148 utils.storage = storage 

149 views.storage = storage 

150 

151 

152class EditorRequiredMixin(UserPassesTestMixin): 

153 def test_func(self): 

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

155 

156 

157class CollectionImageUploadView(EditorRequiredMixin, ImageUploadView): 

158 """ 

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

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

161 To do that we have to 

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

163 - modify the storage location 

164 """ 

165 

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

167 colid = kwargs["colid"] 

168 

169 change_ckeditor_storage(colid) 

170 

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

172 

173 

174class CollectionBrowseView(EditorRequiredMixin, View): 

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

176 colid = kwargs["colid"] 

177 

178 change_ckeditor_storage(colid) 

179 

180 return browse(request) 

181 

182 

183file_upload_in_collection = csrf_exempt(CollectionImageUploadView.as_view()) 

184file_browse_in_collection = csrf_exempt(CollectionBrowseView.as_view()) 

185 

186 

187def deploy_cms(site, collection): 

188 colid = collection.pid 

189 base_url = getattr(collection, site)() 

190 

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

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

193 

194 if site == "website": 

195 from_base_path = get_media_base_root_in_test(colid) 

196 to_base_path = get_media_base_root_in_prod(colid) 

197 

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

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

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

201 if os.path.exists(from_path): 

202 try: 

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

204 except OSError as exception: 

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

206 

207 site_id = model_helpers.get_site_id(colid) 

208 if model_helpers.get_site_default_language(site_id): 

209 from modeltranslation import fields, manager 

210 

211 old_ftor = manager.get_language 

212 manager.get_language = monkey_get_language_en 

213 fields.get_language = monkey_get_language_en 

214 

215 pages = get_pages_content(colid) 

216 news = get_news_content(colid) 

217 

218 manager.get_language = old_ftor 

219 fields.get_language = old_ftor 

220 else: 

221 pages = get_pages_content(colid) 

222 news = get_news_content(colid) 

223 

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

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

226 

227 try: 

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

229 

230 if response.status_code == 503: 

231 e = ServerUnderMaintenance( 

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

233 ) 

234 return HttpResponseServerError(e, status=503) 

235 

236 except Timeout as exception: 

237 return HttpResponse(exception, status=408) 

238 except Exception as exception: 

239 return HttpResponseServerError(exception) 

240 

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

242 

243 

244class HandleCMSMixin(EditorRequiredMixin): 

245 """ 

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

247 """ 

248 

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

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

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

252 

253 def init_data(self, kwargs): 

254 self.collection = None 

255 

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

257 if self.colid: 

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

259 if not self.collection: 

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

261 

262 test_server_url = self.collection.test_website() 

263 if not test_server_url: 

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

265 

266 prod_server_url = self.collection.website() 

267 if not prod_server_url: 

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

269 

270 

271class GetCMSFromSiteAPIView(HandleCMSMixin, View): 

272 """ 

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

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

275 """ 

276 

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

278 self.init_data(self.kwargs) 

279 

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

281 

282 try: 

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

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

285 

286 # Just to need to save the json on disk 

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

288 # /mersenne_test_data/@colid/media 

289 folder = get_media_base_root(self.colid) 

290 os.makedirs(folder, exist_ok=True) 

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

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

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

294 

295 except Timeout as exception: 

296 return HttpResponse(exception, status=408) 

297 except Exception as exception: 

298 return HttpResponseServerError(exception) 

299 

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

301 

302 

303def monkey_get_language_en(): 

304 return "en" 

305 

306 

307class RestoreCMSAPIView(HandleCMSMixin, View): 

308 """ 

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

310 """ 

311 

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

313 self.init_data(self.kwargs) 

314 

315 folder = get_media_base_root(self.colid) 

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

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

318 json_data = json.load(f) 

319 

320 pages = json_data.get("pages") 

321 

322 site_id = model_helpers.get_site_id(self.colid) 

323 if model_helpers.get_site_default_language(site_id): 

324 from modeltranslation import fields, manager 

325 

326 old_ftor = manager.get_language 

327 manager.get_language = monkey_get_language_en 

328 fields.get_language = monkey_get_language_en 

329 

330 import_pages(pages, self.colid) 

331 

332 manager.get_language = old_ftor 

333 fields.get_language = old_ftor 

334 else: 

335 import_pages(pages, self.colid) 

336 

337 if "news" in json_data: 

338 news = json_data.get("news") 

339 import_news(news, self.colid) 

340 

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

342 

343 

344class DeployCMSAPIView(HandleCMSMixin, View): 

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

346 self.init_data(self.kwargs) 

347 

348 if check_lock(): 

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

350 messages.error(self.request, msg) 

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

352 

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

354 

355 response = deploy_cms(site, self.collection) 

356 

357 if response.status_code == 503: 

358 messages.error( 

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

360 ) 

361 

362 return response 

363 

364 

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

366 urls = [""] 

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

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

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

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

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

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

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

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

375 return urls 

376 

377 

378class SuggestDeployView(EditorRequiredMixin, View): 

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

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

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

382 article = get_object_or_404(Article, doi=doi) 

383 

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

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

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

387 data = form.cleaned_data 

388 obj.date_modified = timezone.now() 

389 form.save() 

390 collection = article.my_container.my_collection 

391 urls = get_server_urls(collection, site=site) 

392 response = requests.models.Response() 

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

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

395 try: 

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

397 except requests.exceptions.RequestException as e: 

398 response.status_code = 503 

399 response.reason = e.args[0] 

400 break 

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

402 else: 

403 return HttpResponseBadRequest() 

404 

405 

406def suggest_debug(results, article, message): 

407 crop_results = 5 

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

409 dois = [] 

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

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

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

413 for item in results["docs"]: 

414 doi = item.get("doi") 

415 if doi: 

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

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

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

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

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

421 url = "" 

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

423 if suggest and suggest.my_container: 

424 collection = suggest.my_container.my_collection 

425 base_url = collection.website() or "" 

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

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

428 

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

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

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

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

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

434 terms = results["interestingTerms"] 

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

436 tail.extend(reversed(terms)) 

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

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

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

440 else: 

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

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

443 

444 

445class SuggestUpdateView(EditorRequiredMixin, TemplateView): 

446 template_name = "editorial_tools/suggested.html" 

447 

448 def get_context_data(self, **kwargs): 

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

450 article = get_object_or_404(Article, doi=doi) 

451 

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

453 collection = article.my_container.my_collection 

454 base_url = collection.website() or "" 

455 response = requests.models.Response() 

456 try: 

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

458 except requests.exceptions.RequestException as e: 

459 response.status_code = 503 

460 response.reason = e.args[0] 

461 msg = { 

462 "url": response.url, 

463 "status": response.status_code, 

464 "err": response.reason, 

465 } 

466 results = None 

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

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

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

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

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

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

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

474 context["article"] = article 

475 context["date_modified"] = obj.date_modified 

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

477 return context 

478 

479 

480class EditorialToolsVolumeItemsView(EditorRequiredMixin, TemplateView): 

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

482 

483 def get_context_data(self, **kwargs): 

484 vid = kwargs.get("vid") 

485 issues_articles, collection = model_helpers.get_issues_in_volume(vid) 

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

487 context["issues_articles"] = issues_articles 

488 context["collection"] = collection 

489 return context 

490 

491 

492class EditorialToolsArticleView(EditorRequiredMixin, TemplateView): 

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

494 

495 def get_context_data(self, **kwargs): 

496 colid = kwargs.get("colid") 

497 doi = kwargs.get("doi") 

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

499 

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

501 context["article"] = article 

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

503 return context 

504 

505 

506class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView): 

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

508 

509 def get_context_data(self, **kwargs): 

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

511 article = get_object_or_404(Article, doi=doi) 

512 

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

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

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

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

517 context["article"] = article 

518 context["date_modified"] = obj.date_modified 

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

520 context["graphical_abstract"] = obj.graphical_abstract 

521 context["illustration"] = obj.illustration 

522 return context 

523 

524 

525class GraphicalAbstractDeployView(EditorRequiredMixin, View): 

526 def __get_path_and_replace_tiff_file(self, obj_attribute_file): 

527 """ 

528 Returns the path of the attribute. 

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

530 

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

532 """ 

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

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

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

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

537 return jpeg_path 

538 

539 return obj_attribute_file.path 

540 

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

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

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

544 article = get_object_or_404(Article, doi=doi) 

545 

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

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

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

549 obj.date_modified = timezone.now() 

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

551 form.save() 

552 files = {} 

553 

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

555 obj_attribute_file = getattr(obj, attribute, None) 

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

557 file_path = self.__get_path_and_replace_tiff_file(obj_attribute_file) 

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

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

560 

561 collection = article.my_container.my_collection 

562 urls = get_server_urls(collection, site=site) 

563 response = requests.models.Response() 

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

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

566 try: 

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

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

569 else: 

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

571 except requests.exceptions.RequestException as e: 

572 response.status_code = 503 

573 response.reason = e.args[0] 

574 break 

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

576 else: 

577 return HttpResponseBadRequest() 

578 

579 

580def parse_content(content): 

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

582 if not table: 

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

584 

585 articles = [] 

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

587 for row in rows: 

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

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

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

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

592 item = {} 

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

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

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

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

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

598 articles.append(item) 

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

600 

601 

602class VirtualIssueParseView(EditorRequiredMixin, View): 

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

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

605 page = get_object_or_404(Page, id=pid) 

606 

607 data = {"pid": pid} 

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

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

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

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

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

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

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

615 

616 content_fr = parse_content(page.content_fr) 

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

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

619 

620 content_en = parse_content(page.content_en) 

621 data["articles"] = content_en["articles"] 

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

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

624 return JsonResponse(data) 

625 

626 

627class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView): 

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

629 

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

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

632 get_object_or_404(Page, id=pid) 

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

634 

635 

636class VirtualIssueCreateView(EditorRequiredMixin, View): 

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

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

639 site_id = model_helpers.get_site_id(colid) 

640 parent, _ = Page.objects.get_or_create( 

641 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES, 

642 parent_page=None, 

643 site_id=site_id, 

644 ) 

645 page = Page.objects.create( 

646 menu_title_en="New virtual issue", 

647 menu_title_fr="Nouvelle collection transverse", 

648 parent_page=parent, 

649 site_id=site_id, 

650 state="draft", 

651 ) 

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

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

654 

655 

656class SpecialIssuesIndex(EditorRequiredMixin, TemplateView): 

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

658 

659 def get_context_data(self, **kwargs): 

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

661 

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

663 context["colid"] = colid 

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

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

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

667 ).filter(my_collection=collection) 

668 

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

670 return context 

671 

672 

673class SpecialIssueEditView(EditorRequiredMixin, TemplateView): 

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

675 

676 def get_context_data(self, **kwargs): 

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

678 return context 

679 

680 

681class VirtualIssuesIndex(EditorRequiredMixin, TemplateView): 

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

683 

684 def get_context_data(self, **kwargs): 

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

686 site_id = model_helpers.get_site_id(colid) 

687 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES) 

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

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

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

691 context["pages"] = pages 

692 return context 

693 

694 

695def get_citation_fr(doi, citation_en): 

696 citation_fr = citation_en 

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

698 if article and article.trans_title_html: 

699 trans_title = article.trans_title_html 

700 try: 

701 citation_fr = re.sub( 

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

703 rf"\1{trans_title}", 

704 citation_en, 

705 ) 

706 except re.error: 

707 pass 

708 return citation_fr 

709 

710 

711def summary_build(articles, colid): 

712 summary_fr = "" 

713 summary_en = "" 

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

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

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

717 colid_lo = colid.lower() 

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

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

720 

721 for article in articles: 

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

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

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

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

726 if doi or citation_en: 

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

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

729 if image_src: 

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

731 base_url = get_media_base_url(colid) 

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

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

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

735 if re.match(image_header, image_src): 

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

737 base64_data = base64.b64decode(image_src) 

738 base_root = get_media_base_root(colid) 

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

740 os.makedirs(path, exist_ok=True) 

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

742 fp.write(base64_data) 

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

744 # TODO mettre la vrai valeur pour le SITE_DOMAIN 

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

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

747 else: 

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

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

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

751 summary_fr = head + summary_fr + tail 

752 summary_en = head + summary_en + tail 

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

754 

755 

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

757class VirtualIssueDeployView(HandleCMSMixin, View): 

758 """ 

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

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

761 The Page is then immediately posted to the test_website. 

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

763 => See DeployCMSAPIView 

764 """ 

765 

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

767 self.init_data(self.kwargs) 

768 if check_lock(): 

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

770 messages.error(self.request, msg) 

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

772 

773 pid = kwargs.get("pid") 

774 colid = self.colid 

775 data = json.loads(request.body) 

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

777 page = get_object_or_404(Page, id=pid) 

778 page.slug = page.slug_fr = page.slug_en = None 

779 page.menu_title_fr = data["title_fr"] 

780 page.menu_title_en = data["title_en"] 

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

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

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

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

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

786 page.save() 

787 

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

789 if response.status_code == 503: 

790 messages.error( 

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

792 ) 

793 

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

795 

796 

797class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView): 

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

799 

800 def get_context_data(self, **kwargs): 

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

802 return context 

803 

804 def set_contrib_addresses(self, contrib, contribution): 

805 for address in contrib: 

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

807 contrib_address.save() 

808 

809 def delete(self, pid): 

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

811 cmd = base_ptf_cmds.addContainerPtfCmd() 

812 cmd.set_object_to_be_deleted(special_issue) 

813 cmd.undo() 

814 

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

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

817 

818 data = {"pid": pid} 

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

820 data["colid"] = colid 

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

822 name = resolve(request.path_info).url_name 

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

824 self.delete(pid) 

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

826 

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

828 

829 if pid != "create": 

830 container = get_object_or_404(Container, pid=pid) 

831 # TODO: pass the lang and trans_lang as well 

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

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

834 # keep using trans_title_html for backward compatibility 

835 if container.trans_title_html: 835 ↛ 838line 835 didn't jump to line 838 because the condition on line 835 was always true

836 data["title"] = container.trans_title_html 

837 else: 

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

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

840 data["title"] = title["title_html"] 

841 data["doi"] = container.doi 

842 data["trans_title"] = container.title_html 

843 data["year"] = container.year 

844 data["volume"] = container.volume 

845 data["articles"] = [ 

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

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

848 ] 

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

850 data["use_resources_icon"] = True 

851 else: 

852 data["use_resources_icon"] = False 

853 

854 contribs = model_data_converter.db_to_contributors(container.contributions) 

855 data["contribs"] = contribs 

856 abstract_set = container.abstract_set.all() 

857 data["head_fr"] = ( 

858 abstract_set.filter(tag="head_fr").first().value_html 

859 if abstract_set.filter(tag="head_fr").exists() 

860 else "" 

861 ) 

862 data["head_en"] = ( 

863 abstract_set.filter(tag="head_en").first().value_html 

864 if abstract_set.filter(tag="head_en").exists() 

865 else "" 

866 ) 

867 data["tail_fr"] = ( 

868 abstract_set.filter(tag="tail_fr").first().value_html 

869 if abstract_set.filter(tag="tail_fr").exists() 

870 else "" 

871 ) 

872 data["tail_en"] = ( 

873 abstract_set.filter(tag="tail_en").first().value_html 

874 if abstract_set.filter(tag="tail_en").exists() 

875 else "" 

876 ) 

877 data["editor_bio_en"] = ( 

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

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

880 else "" 

881 ) 

882 data["editor_bio_fr"] = ( 

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

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

885 else "" 

886 ) 

887 

888 streams = container.datastream_set.all() 

889 data["pdf_file_name"] = "" 

890 data["edito_file_name"] = "" 

891 data["edito_display_name"] = "" 

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

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

894 data["pdf_file_name"] = stream.text 

895 try: 

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

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

898 "$$$" 

899 ) 

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

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

902 

903 except RelatedObject.DoesNotExist: 

904 pass 

905 try: 

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

907 

908 data["icon_location"] = container_icon.location 

909 except ExtLink.DoesNotExist: 

910 data["icon_location"] = "" 

911 # try: 

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

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

914 # except ExtLink.DoesNotExist: 

915 # data["special_issue_icon"] = None 

916 

917 else: 

918 data["title"] = "" 

919 data["doi"] = None 

920 data["trans_title"] = "" 

921 data["year"] = "" 

922 data["volume"] = "" 

923 data["articles"] = [] 

924 data["contribs"] = [] 

925 

926 data["head_fr"] = "" 

927 data["head_en"] = "" 

928 data["tail_fr"] = "" 

929 data["tail_en"] = "" 

930 data["editor_bio_en"] = "" 

931 data["editor_bio_fr"] = "" 

932 data["pdf_file_name"] = "" 

933 data["edito_file_name"] = "" 

934 data["use_resources_icon"] = False 

935 

936 return JsonResponse(data) 

937 

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

939 # le but est de faire un IssueDAta 

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

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

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

943 special_issue = create_issuedata() 

944 year = request.POST["year"] 

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

946 # TODO 2: In VueJS, titleFr = title 

947 trans_title_html = request.POST["title"] 

948 title_html = request.POST["trans_title"] 

949 if pid != "create": 

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

951 container = get_object_or_404(Container, pid=pid) 

952 lang = container.lang 

953 trans_lang = container.trans_lang 

954 xpub = create_publisherdata() 

955 xpub.name = container.my_publisher.pid 

956 special_issue.provider = container.provider 

957 special_issue.number = container.number 

958 volume = container.volume 

959 special_issue_pid = pid 

960 special_issue.date_pre_published = container.date_pre_published 

961 special_issue.date_published = container.date_published 

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

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

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

965 special_issue.doi = model_helpers.assign_container_doi(colid) 

966 else: 

967 special_issue.doi = container.doi 

968 else: 

969 lang = "en" 

970 container = None 

971 trans_lang = "fr" 

972 xpub = create_publisherdata() 

973 special_issue.doi = model_helpers.assign_container_doi(colid) 

974 volume = "" 

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

976 # if cras_issues.exists(): 

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

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

979 volume = same_year_issues.first().volume 

980 elif ( 

981 issues.exists() and colid != "HOUCHES" 

982 ): # because we don't want a volume for houches 

983 volume = str(int(issues.first().volume) + 1) 

984 else: 

985 volume = "" 

986 # issues = model_helpers.get_volumes_in_collection(collection, get_special_issues=True) 

987 # if issues["sorted_issues"]: 

988 # volumes = issues["sorted_issues"][0]["volumes"] 

989 # for v in volumes: 

990 # if v["fyear"] == int(year): 

991 # volume = v["volume"] 

992 # break 

993 

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

995 xpub.name = "UGA Éditions" 

996 else: 

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

998 # xpub.name = parent_container.my_publisher.pid 

999 special_issue.provider = collection.provider 

1000 

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

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

1003 ) 

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

1005 all_special_issues_numbers = [ 

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

1007 ] 

1008 if len(all_special_issues_numbers) > 0: 

1009 max_number = max(all_special_issues_numbers) 

1010 else: 

1011 max_number = 0 

1012 

1013 else: 

1014 max_number = 0 

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

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

1017 

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

1019 special_issue.ctype = "issue_special_img" 

1020 else: 

1021 special_issue.ctype = "issue_special" 

1022 

1023 existing_issue = model_helpers.get_resource(special_issue_pid) 

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

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

1026 

1027 special_issue.lang = lang 

1028 special_issue.title_html = title_html 

1029 special_issue.title_xml = build_title_xml( 

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

1031 ) 

1032 

1033 special_issue.trans_lang = trans_lang 

1034 special_issue.trans_title_html = trans_title_html 

1035 title_xml = build_title_xml( 

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

1037 ) 

1038 title = create_titledata( 

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

1040 ) 

1041 special_issue.titles = [title] 

1042 

1043 special_issue.year = year 

1044 special_issue.volume = volume 

1045 special_issue.journal = journal 

1046 special_issue.publisher = xpub 

1047 special_issue.pid = special_issue_pid 

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

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

1050 ) 

1051 

1052 articles = [] 

1053 contribs = [] 

1054 index = 0 

1055 

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

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

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

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

1060 # if not article["citation"]: 

1061 # index += 1 

1062 # continue 

1063 articles.append(article) 

1064 

1065 index += 1 

1066 

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

1068 index = 0 

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

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

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

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

1073 contributor = create_contributor() 

1074 contributor["first_name"] = contrib["first_name"] 

1075 contributor["last_name"] = contrib["last_name"] 

1076 contributor["orcid"] = contrib["orcid"] 

1077 contributor["role"] = "editor" 

1078 

1079 contrib_xml = xml_utils.get_contrib_xml(contrib) 

1080 contributor["contrib_xml"] = contrib_xml 

1081 contribs.append(Munch(contributor)) 

1082 index += 1 

1083 special_issue.contributors = contribs 

1084 

1085 # Part of the code that handle forwords and lastwords 

1086 

1087 xhead_fr, head_fr_xml = self.create_abstract_from_vuejs( 

1088 request.POST["head_fr"], "fr", "intro", colid, special_issue_pid 

1089 ) 

1090 xtail_fr, tail_fr_xml = self.create_abstract_from_vuejs( 

1091 request.POST["tail_fr"], "fr", "tail", colid, special_issue_pid 

1092 ) 

1093 xhead_en, head_en_xml = self.create_abstract_from_vuejs( 

1094 request.POST["head_en"], "en", "intro", colid, special_issue_pid 

1095 ) 

1096 

1097 xtail_en, tail_en_xml = self.create_abstract_from_vuejs( 

1098 request.POST["tail_en"], "en", "tail", colid, special_issue_pid 

1099 ) 

1100 

1101 xeditor_bio_en, editor_bio_en_xml = self.create_abstract_from_vuejs( 

1102 request.POST["editor_bio_en"], "en", "bio_en", colid, special_issue_pid 

1103 ) 

1104 

1105 xeditor_bio_fr, editor_bio_fr_xml = self.create_abstract_from_vuejs( 

1106 request.POST["editor_bio_fr"], "fr", "bio_fr", colid, special_issue_pid 

1107 ) 

1108 

1109 abstracts = [ 

1110 head_fr_xml, 

1111 head_en_xml, 

1112 tail_fr_xml, 

1113 tail_en_xml, 

1114 editor_bio_fr_xml, 

1115 editor_bio_en_xml, 

1116 ] 

1117 figures = self.create_related_objects_from_abstract(abstracts, colid, special_issue.pid) 

1118 special_issue.related_objects = figures 

1119 # TODO can be factorized? 

1120 special_issue.abstracts = [ 

1121 { 

1122 "tag": "head_fr", 

1123 "lang": "fr", 

1124 "value_html": xhead_fr.value_html, 

1125 "value_tex": xhead_fr.value_tex, 

1126 "value_xml": head_fr_xml, 

1127 }, 

1128 { 

1129 "tag": "head_en", 

1130 "lang": "en", 

1131 "value_html": xhead_en.value_html, 

1132 "value_tex": xhead_en.value_tex, 

1133 "value_xml": head_en_xml, 

1134 }, 

1135 { 

1136 "tag": "tail_fr", 

1137 "lang": "fr", 

1138 "value_html": xtail_fr.value_html, 

1139 "value_tex": xtail_fr.value_tex, 

1140 "value_xml": tail_fr_xml, 

1141 }, 

1142 { 

1143 "tag": "tail_en", 

1144 "lang": "en", 

1145 "value_html": xtail_en.value_html, 

1146 "value_tex": xtail_en.value_tex, 

1147 "value_xml": tail_en_xml, 

1148 }, 

1149 { 

1150 "tag": "bio_en", 

1151 "lang": "en", 

1152 "value_html": xeditor_bio_en.value_html, 

1153 "value_tex": xeditor_bio_en.value_tex, 

1154 "value_xml": editor_bio_en_xml, 

1155 }, 

1156 { 

1157 "tag": "bio_fr", 

1158 "lang": "fr", 

1159 "value_html": xeditor_bio_fr.value_html, 

1160 "value_tex": xeditor_bio_fr.value_tex, 

1161 "value_xml": editor_bio_fr_xml, 

1162 }, 

1163 ] 

1164 

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

1166 # Both are stored in same directory 

1167 

1168 pdf_file_path = resolver.get_disk_location( 

1169 f"{settings.RESOURCES_ROOT}", 

1170 f"{collection.pid}", 

1171 "pdf", 

1172 special_issue_pid, 

1173 article_id=None, 

1174 do_create_folder=False, 

1175 ) 

1176 pdf_path = os.path.dirname(pdf_file_path) 

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

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

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

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

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

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

1183 

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

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

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

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

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

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

1190 destination.write(chunk) 

1191 

1192 else: 

1193 pdf_file_name = request.POST["pdf_name"] 

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

1195 

1196 pdf_stream_data = create_datastream() 

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

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

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

1200 pdf_stream_data["text"] = pdf_file_name 

1201 special_issue.streams.append(pdf_stream_data) 

1202 

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

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

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

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

1207 edito_display_name = request.POST["edito_display_name"] 

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

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

1210 destination.write(chunk) 

1211 else: 

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

1213 edito_file_name = request.POST["edito_name"] 

1214 edito_display_name = request.POST["edito_display_name"] 

1215 

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

1217 data = { 

1218 "rel": "edito", 

1219 "mimetype": "application/pdf", 

1220 "location": location, 

1221 "base": None, 

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

1223 } 

1224 special_issue.related_objects.append(data) 

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

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

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

1228 icon_file = request.FILES["icon"] 

1229 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1230 icon_file, 

1231 collection.pid, 

1232 special_issue.pid, 

1233 special_issue.pid, 

1234 ) 

1235 data = { 

1236 "rel": "icon", 

1237 "location": relative_file_name, 

1238 "base": None, 

1239 "seq": 1, 

1240 "metadata": "", 

1241 } 

1242 special_issue.ext_links.append(data) 

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

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

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

1246 data = { 

1247 "rel": "icon", 

1248 "location": icon_file, 

1249 "base": None, 

1250 "seq": 1, 

1251 "metadata": "", 

1252 } 

1253 special_issue.ext_links.append(data) 

1254 

1255 special_issue = Munch(special_issue.__dict__) 

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

1257 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params) 

1258 cmd.do() 

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

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

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

1262 

1263 def create_abstract_from_vuejs(self, abstract, lang, position, colid, pid): 

1264 abstract_html = xml_utils.replace_html_entities(abstract) 

1265 xabstract = CkeditorParser( 

1266 html_value=abstract_html, issue_pid=colid, pid=pid, mml_formulas=[] 

1267 ) 

1268 abstract_xml = get_abstract_xml(xabstract.value_xml, lang, position) 

1269 return xabstract, abstract_xml 

1270 

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

1272 figures = [] 

1273 for abstract in abstracts: 

1274 abstract_xml = abstract.encode("utf8") 

1275 

1276 tree = etree.fromstring(abstract_xml) 

1277 

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

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

1280 base = None 

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

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

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

1284 base = get_media_base_root(colid) 

1285 data_location = os.path.join( 

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

1287 ) 

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

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

1290 # so related objects can go for the correct one 

1291 img = Image.open(data_location) 

1292 final_data_location = os.path.join( 

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

1294 ) 

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

1296 os.makedirs(final_data_location) 

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

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

1299 img.save(final_data_location) 

1300 if ext == "png": 

1301 mimetype = "image/png" 

1302 else: 

1303 mimetype = "image/jpeg" 

1304 data = { 

1305 "rel": "html-image", 

1306 "mimetype": mimetype, 

1307 "location": relative_path, 

1308 "base": base, 

1309 "metadata": "", 

1310 } 

1311 if data not in figures: 

1312 figures.append(data) 

1313 return figures 

1314 

1315 

1316class PageIndexView(EditorRequiredMixin, TemplateView): 

1317 template_name = "mersenne_cms/page_index.html" 

1318 

1319 def get_context_data(self, **kwargs): 

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

1321 site_id = model_helpers.get_site_id(colid) 

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

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

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

1325 else: 

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

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

1328 context["colid"] = colid 

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

1330 context["pages"] = pages 

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

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

1333 return context 

1334 

1335 

1336class PageBaseView(HandleCMSMixin, View): 

1337 template_name = "mersenne_cms/page_form.html" 

1338 model = Page 

1339 form_class = PageForm 

1340 

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

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

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

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

1345 

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

1347 

1348 def get_success_url(self): 

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

1350 

1351 def get_context_data(self, **kwargs): 

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

1353 context["journal"] = self.collection 

1354 return context 

1355 

1356 def update_test_website(self): 

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

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

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

1360 else: 

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

1362 

1363 if response.status_code == 503: 

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

1365 else: 

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

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

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

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

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

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

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

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

1374 

1375 def get_form_kwargs(self): 

1376 kwargs = super().get_form_kwargs() 

1377 kwargs["site_id"] = self.site_id 

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

1379 return kwargs 

1380 

1381 def form_valid(self, form): 

1382 form.save() 

1383 

1384 self.update_test_website() 

1385 

1386 return HttpResponseRedirect(self.get_success_url()) 

1387 

1388 

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

1390class PageDeleteView(PageBaseView): 

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

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

1393 pk = kwargs.get("pk") 

1394 page = get_object_or_404(Page, id=pk) 

1395 if page.mersenne_id: 

1396 raise PermissionDenied 

1397 

1398 page.delete() 

1399 

1400 self.update_test_website() 

1401 

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

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

1404 else: 

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

1406 

1407 

1408class PageCreateView(PageBaseView, CreateView): 

1409 def get_context_data(self, **kwargs): 

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

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

1412 return context 

1413 

1414 

1415class PageUpdateView(PageBaseView, UpdateView): 

1416 def get_context_data(self, **kwargs): 

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

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

1419 return context 

1420 

1421 

1422class NewsBaseView(PageBaseView): 

1423 template_name = "mersenne_cms/news_form.html" 

1424 model = News 

1425 form_class = NewsForm 

1426 

1427 

1428class NewsDeleteView(NewsBaseView): 

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

1430 pk = kwargs.get("pk") 

1431 news = get_object_or_404(News, id=pk) 

1432 

1433 news.delete() 

1434 

1435 self.update_test_website() 

1436 

1437 return HttpResponseRedirect(self.get_success_url()) 

1438 

1439 

1440class NewsCreateView(NewsBaseView, CreateView): 

1441 def get_context_data(self, **kwargs): 

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

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

1444 return context 

1445 

1446 

1447class NewsUpdateView(NewsBaseView, UpdateView): 

1448 def get_context_data(self, **kwargs): 

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

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

1451 return context 

1452 

1453 

1454# def page_create_view(request, colid): 

1455# context = {} 

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

1457# raise PermissionDenied 

1458# collection = model_helpers.get_collection(colid) 

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

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

1461# if form.is_valid(): 

1462# form.save() 

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

1464# if response.status_code < 300: 

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

1466# else: 

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

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

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

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

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

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

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

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

1475# 

1476# context["form"] = form 

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

1478# context["journal"] = collection 

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

1480 

1481 

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

1483# context = {} 

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

1485# raise PermissionDenied 

1486# 

1487# collection = model_helpers.get_collection(colid) 

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

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

1490# if form.is_valid(): 

1491# form.save() 

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

1493# if response.status_code < 300: 

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

1495# else: 

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

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

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

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

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

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

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

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

1504# 

1505# context["form"] = form 

1506# context["pid"] = pid 

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

1508# context["journal"] = collection 

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