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

881 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-10 13:49 +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 issues = collection.content.all().order_by("-year") 

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

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

952 volume = same_year_issues.first().volume 

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

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

955 volume = int(ref_volume) + ( 

956 int(year) - 2024 

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

958 else: 

959 volume = "" 

960 if pid != "create": 

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

962 container = get_object_or_404(Container, pid=pid) 

963 lang = container.lang 

964 trans_lang = container.trans_lang 

965 xpub = create_publisherdata() 

966 xpub.name = container.my_publisher.pid 

967 special_issue.provider = container.provider 

968 special_issue.number = container.number 

969 special_issue_pid = pid 

970 special_issue.date_pre_published = container.date_pre_published 

971 special_issue.date_published = container.date_published 

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

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

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

975 special_issue.doi = model_helpers.assign_container_doi(colid) 

976 else: 

977 special_issue.doi = container.doi 

978 else: 

979 lang = "en" 

980 container = None 

981 trans_lang = "fr" 

982 xpub = create_publisherdata() 

983 special_issue.doi = model_helpers.assign_container_doi(colid) 

984 

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

986 xpub.name = "UGA Éditions" 

987 else: 

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

989 special_issue.provider = collection.provider 

990 

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

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

993 ) 

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

995 all_special_issues_numbers = [ 

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

997 ] 

998 if len(all_special_issues_numbers) > 0: 

999 max_number = max(all_special_issues_numbers) 

1000 else: 

1001 max_number = 0 

1002 

1003 else: 

1004 max_number = 0 

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

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

1007 

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

1009 special_issue.ctype = "issue_special_img" 

1010 else: 

1011 special_issue.ctype = "issue_special" 

1012 

1013 existing_issue = model_helpers.get_resource(special_issue_pid) 

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

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

1016 

1017 special_issue.lang = lang 

1018 special_issue.title_html = title_html 

1019 special_issue.title_xml = build_title_xml( 

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

1021 ) 

1022 

1023 special_issue.trans_lang = trans_lang 

1024 special_issue.trans_title_html = trans_title_html 

1025 title_xml = build_title_xml( 

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

1027 ) 

1028 title = create_titledata( 

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

1030 ) 

1031 special_issue.titles = [title] 

1032 

1033 special_issue.year = year 

1034 special_issue.volume = volume 

1035 special_issue.journal = journal 

1036 special_issue.publisher = xpub 

1037 special_issue.pid = special_issue_pid 

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

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

1040 ) 

1041 

1042 articles = [] 

1043 contribs = [] 

1044 index = 0 

1045 

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

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

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

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

1050 # if not article["citation"]: 

1051 # index += 1 

1052 # continue 

1053 articles.append(article) 

1054 

1055 index += 1 

1056 

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

1058 index = 0 

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

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

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

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

1063 contributor = create_contributor() 

1064 contributor["first_name"] = contrib["first_name"] 

1065 contributor["last_name"] = contrib["last_name"] 

1066 contributor["orcid"] = contrib["orcid"] 

1067 contributor["role"] = "editor" 

1068 

1069 contrib_xml = xml_utils.get_contrib_xml(contrib) 

1070 contributor["contrib_xml"] = contrib_xml 

1071 contribs.append(Munch(contributor)) 

1072 index += 1 

1073 special_issue.contributors = contribs 

1074 

1075 # Part of the code that handle forwords and lastwords 

1076 

1077 xhead_fr, head_fr_xml = self.create_abstract_from_vuejs( 

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

1079 ) 

1080 xtail_fr, tail_fr_xml = self.create_abstract_from_vuejs( 

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

1082 ) 

1083 xhead_en, head_en_xml = self.create_abstract_from_vuejs( 

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

1085 ) 

1086 

1087 xtail_en, tail_en_xml = self.create_abstract_from_vuejs( 

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

1089 ) 

1090 

1091 xeditor_bio_en, editor_bio_en_xml = self.create_abstract_from_vuejs( 

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

1093 ) 

1094 

1095 xeditor_bio_fr, editor_bio_fr_xml = self.create_abstract_from_vuejs( 

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

1097 ) 

1098 

1099 abstracts = [ 

1100 head_fr_xml, 

1101 head_en_xml, 

1102 tail_fr_xml, 

1103 tail_en_xml, 

1104 editor_bio_fr_xml, 

1105 editor_bio_en_xml, 

1106 ] 

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

1108 special_issue.related_objects = figures 

1109 # TODO can be factorized? 

1110 special_issue.abstracts = [ 

1111 { 

1112 "tag": "head_fr", 

1113 "lang": "fr", 

1114 "value_html": xhead_fr.value_html, 

1115 "value_tex": xhead_fr.value_tex, 

1116 "value_xml": head_fr_xml, 

1117 }, 

1118 { 

1119 "tag": "head_en", 

1120 "lang": "en", 

1121 "value_html": xhead_en.value_html, 

1122 "value_tex": xhead_en.value_tex, 

1123 "value_xml": head_en_xml, 

1124 }, 

1125 { 

1126 "tag": "tail_fr", 

1127 "lang": "fr", 

1128 "value_html": xtail_fr.value_html, 

1129 "value_tex": xtail_fr.value_tex, 

1130 "value_xml": tail_fr_xml, 

1131 }, 

1132 { 

1133 "tag": "tail_en", 

1134 "lang": "en", 

1135 "value_html": xtail_en.value_html, 

1136 "value_tex": xtail_en.value_tex, 

1137 "value_xml": tail_en_xml, 

1138 }, 

1139 { 

1140 "tag": "bio_en", 

1141 "lang": "en", 

1142 "value_html": xeditor_bio_en.value_html, 

1143 "value_tex": xeditor_bio_en.value_tex, 

1144 "value_xml": editor_bio_en_xml, 

1145 }, 

1146 { 

1147 "tag": "bio_fr", 

1148 "lang": "fr", 

1149 "value_html": xeditor_bio_fr.value_html, 

1150 "value_tex": xeditor_bio_fr.value_tex, 

1151 "value_xml": editor_bio_fr_xml, 

1152 }, 

1153 ] 

1154 

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

1156 # Both are stored in same directory 

1157 

1158 pdf_file_path = resolver.get_disk_location( 

1159 f"{settings.RESOURCES_ROOT}", 

1160 f"{collection.pid}", 

1161 "pdf", 

1162 special_issue_pid, 

1163 article_id=None, 

1164 do_create_folder=False, 

1165 ) 

1166 pdf_path = os.path.dirname(pdf_file_path) 

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

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

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

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

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

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

1173 

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

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

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

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

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

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

1180 destination.write(chunk) 

1181 

1182 else: 

1183 pdf_file_name = request.POST["pdf_name"] 

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

1185 

1186 pdf_stream_data = create_datastream() 

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

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

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

1190 pdf_stream_data["text"] = pdf_file_name 

1191 special_issue.streams.append(pdf_stream_data) 

1192 

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

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

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

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

1197 edito_display_name = request.POST["edito_display_name"] 

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

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

1200 destination.write(chunk) 

1201 else: 

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

1203 edito_file_name = request.POST["edito_name"] 

1204 edito_display_name = request.POST["edito_display_name"] 

1205 

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

1207 data = { 

1208 "rel": "edito", 

1209 "mimetype": "application/pdf", 

1210 "location": location, 

1211 "base": None, 

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

1213 } 

1214 special_issue.related_objects.append(data) 

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

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

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

1218 icon_file = request.FILES["icon"] 

1219 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1220 icon_file, 

1221 collection.pid, 

1222 special_issue.pid, 

1223 special_issue.pid, 

1224 ) 

1225 if ".tif" in relative_file_name: 

1226 jpeg_path = ImageManager( 

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

1228 ).to_jpeg() 

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

1230 data = { 

1231 "rel": "icon", 

1232 "location": relative_file_name, 

1233 "base": None, 

1234 "seq": 1, 

1235 "metadata": "", 

1236 } 

1237 special_issue.ext_links.append(data) 

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

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

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

1241 data = { 

1242 "rel": "icon", 

1243 "location": icon_file, 

1244 "base": None, 

1245 "seq": 1, 

1246 "metadata": "", 

1247 } 

1248 special_issue.ext_links.append(data) 

1249 

1250 special_issue = Munch(special_issue.__dict__) 

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

1252 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params) 

1253 cmd.do() 

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

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

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

1257 

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

1259 abstract_html = xml_utils.replace_html_entities(abstract) 

1260 xabstract = CkeditorParser( 

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

1262 ) 

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

1264 return xabstract, abstract_xml 

1265 

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

1267 figures = [] 

1268 for abstract in abstracts: 

1269 abstract_xml = abstract.encode("utf8") 

1270 

1271 tree = etree.fromstring(abstract_xml) 

1272 

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

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

1275 base = None 

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

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

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

1279 base = get_media_base_root(colid) 

1280 data_location = os.path.join( 

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

1282 ) 

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

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

1285 # so related objects can go for the correct one 

1286 img = Image.open(data_location) 

1287 final_data_location = os.path.join( 

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

1289 ) 

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

1291 os.makedirs(final_data_location) 

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

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

1294 img.save(final_data_location) 

1295 if ext == "png": 

1296 mimetype = "image/png" 

1297 else: 

1298 mimetype = "image/jpeg" 

1299 data = { 

1300 "rel": "html-image", 

1301 "mimetype": mimetype, 

1302 "location": relative_path, 

1303 "base": base, 

1304 "metadata": "", 

1305 } 

1306 if data not in figures: 

1307 figures.append(data) 

1308 return figures 

1309 

1310 

1311class PageIndexView(EditorRequiredMixin, TemplateView): 

1312 template_name = "mersenne_cms/page_index.html" 

1313 

1314 def get_context_data(self, **kwargs): 

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

1316 site_id = model_helpers.get_site_id(colid) 

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

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

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

1320 else: 

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

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

1323 context["colid"] = colid 

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

1325 context["pages"] = pages 

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

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

1328 return context 

1329 

1330 

1331class PageBaseView(HandleCMSMixin, View): 

1332 template_name = "mersenne_cms/page_form.html" 

1333 model = Page 

1334 form_class = PageForm 

1335 

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

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

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

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

1340 

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

1342 

1343 def get_success_url(self): 

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

1345 

1346 def get_context_data(self, **kwargs): 

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

1348 context["journal"] = self.collection 

1349 return context 

1350 

1351 def update_test_website(self): 

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

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

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

1355 else: 

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

1357 

1358 if response.status_code == 503: 

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

1360 else: 

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

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

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

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

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

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

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

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

1369 

1370 def get_form_kwargs(self): 

1371 kwargs = super().get_form_kwargs() 

1372 kwargs["site_id"] = self.site_id 

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

1374 return kwargs 

1375 

1376 def form_valid(self, form): 

1377 form.save() 

1378 

1379 self.update_test_website() 

1380 

1381 return HttpResponseRedirect(self.get_success_url()) 

1382 

1383 

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

1385class PageDeleteView(PageBaseView): 

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

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

1388 pk = kwargs.get("pk") 

1389 page = get_object_or_404(Page, id=pk) 

1390 if page.mersenne_id: 

1391 raise PermissionDenied 

1392 

1393 page.delete() 

1394 

1395 self.update_test_website() 

1396 

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

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

1399 else: 

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

1401 

1402 

1403class PageCreateView(PageBaseView, CreateView): 

1404 def get_context_data(self, **kwargs): 

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

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

1407 return context 

1408 

1409 

1410class PageUpdateView(PageBaseView, UpdateView): 

1411 def get_context_data(self, **kwargs): 

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

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

1414 return context 

1415 

1416 

1417class NewsBaseView(PageBaseView): 

1418 template_name = "mersenne_cms/news_form.html" 

1419 model = News 

1420 form_class = NewsForm 

1421 

1422 

1423class NewsDeleteView(NewsBaseView): 

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

1425 pk = kwargs.get("pk") 

1426 news = get_object_or_404(News, id=pk) 

1427 

1428 news.delete() 

1429 

1430 self.update_test_website() 

1431 

1432 return HttpResponseRedirect(self.get_success_url()) 

1433 

1434 

1435class NewsCreateView(NewsBaseView, CreateView): 

1436 def get_context_data(self, **kwargs): 

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

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

1439 return context 

1440 

1441 

1442class NewsUpdateView(NewsBaseView, UpdateView): 

1443 def get_context_data(self, **kwargs): 

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

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

1446 return context 

1447 

1448 

1449# def page_create_view(request, colid): 

1450# context = {} 

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

1452# raise PermissionDenied 

1453# collection = model_helpers.get_collection(colid) 

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

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

1456# if form.is_valid(): 

1457# form.save() 

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

1459# if response.status_code < 300: 

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

1461# else: 

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

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

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

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

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

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

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

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

1470# 

1471# context["form"] = form 

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

1473# context["journal"] = collection 

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

1475 

1476 

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

1478# context = {} 

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

1480# raise PermissionDenied 

1481# 

1482# collection = model_helpers.get_collection(colid) 

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

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

1485# if form.is_valid(): 

1486# form.save() 

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

1488# if response.status_code < 300: 

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

1490# else: 

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

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

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

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

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

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

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

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

1499# 

1500# context["form"] = form 

1501# context["pid"] = pid 

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

1503# context["journal"] = collection 

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