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

873 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-07-07 14:30 +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.db.models import Q 

15from django.forms.models import model_to_dict 

16from django.http import ( 

17 Http404, 

18 HttpResponse, 

19 HttpResponseBadRequest, 

20 HttpResponseRedirect, 

21 HttpResponseServerError, 

22 JsonResponse, 

23) 

24from django.shortcuts import get_object_or_404, redirect 

25from django.urls import resolve, reverse 

26from django.utils import timezone 

27from django.utils.safestring import mark_safe 

28from django.views.decorators.csrf import csrf_exempt 

29from django.views.generic import CreateView, TemplateView, UpdateView, View 

30from lxml import etree 

31from mersenne_cms.models import ( 

32 MERSENNE_ID_VIRTUAL_ISSUES, 

33 News, 

34 Page, 

35 get_news_content, 

36 get_pages_content, 

37 import_news, 

38 import_pages, 

39) 

40from munch import Munch 

41from PIL import Image 

42from ptf import model_data_converter, model_helpers 

43from ptf.cmds import solr_cmds, xml_cmds 

44from ptf.cmds.ptf_cmds import base_ptf_cmds 

45from ptf.cmds.xml import xml_utils 

46from ptf.cmds.xml.ckeditor.ckeditor_parser import CkeditorParser 

47from ptf.cmds.xml.jats.builder.issue import build_title_xml 

48from ptf.display import resolver 

49 

50# from ptf.display import resolver 

51from ptf.exceptions import ServerUnderMaintenance 

52 

53# from ptf.model_data import ArticleData 

54from ptf.model_data import ( 

55 create_contributor, 

56 create_datastream, 

57 create_issuedata, 

58 create_publisherdata, 

59 create_titledata, 

60) 

61from ptf.model_data_converter import jats_from_abstract 

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 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 site_name = settings.SITE_NAME if hasattr(settings, "SITE_NAME") else "" 

486 is_cr = len(site_name) == 6 and site_name[0:2] == "cr" 

487 issues_articles, collection = model_helpers.get_issues_in_volume(vid, is_cr) 

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

489 context["issues_articles"] = issues_articles 

490 context["collection"] = collection 

491 return context 

492 

493 

494class EditorialToolsArticleView(EditorRequiredMixin, TemplateView): 

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

496 

497 def get_context_data(self, **kwargs): 

498 colid = kwargs.get("colid") 

499 doi = kwargs.get("doi") 

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

501 

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

503 context["article"] = article 

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

505 return context 

506 

507 

508class GraphicalAbstractUpdateView(EditorRequiredMixin, TemplateView): 

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

510 

511 def get_context_data(self, **kwargs): 

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

513 article = get_object_or_404(Article, doi=doi) 

514 

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

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

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

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

519 context["article"] = article 

520 context["date_modified"] = obj.date_modified 

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

522 context["graphical_abstract"] = obj.graphical_abstract 

523 context["illustration"] = obj.illustration 

524 return context 

525 

526 

527class GraphicalAbstractDeployView(EditorRequiredMixin, View): 

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

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

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

531 article = get_object_or_404(Article, doi=doi) 

532 

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

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

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

536 obj.date_modified = timezone.now() 

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

538 form.save() 

539 files = {} 

540 if obj.graphical_abstract and os.path.exists(obj.graphical_abstract.path): 540 ↛ 541line 540 didn't jump to line 541 because the condition on line 540 was never true

541 with open(obj.graphical_abstract.path, "rb") as fp: 

542 files.update({"graphical_abstract": (obj.graphical_abstract.name, fp.read())}) 

543 if obj.illustration and os.path.exists(obj.illustration.path): 543 ↛ 544line 543 didn't jump to line 544 because the condition on line 543 was never true

544 with open(obj.illustration.path, "rb") as fp: 

545 files.update({"illustration": (obj.illustration.name, fp.read())}) 

546 collection = article.my_container.my_collection 

547 urls = get_server_urls(collection, site=site) 

548 response = requests.models.Response() 

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

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

551 try: 

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

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

554 else: 

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

556 except requests.exceptions.RequestException as e: 

557 response.status_code = 503 

558 response.reason = e.args[0] 

559 break 

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

561 else: 

562 return HttpResponseBadRequest() 

563 

564 

565def parse_content(content): 

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

567 if not table: 

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

569 

570 articles = [] 

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

572 for row in rows: 

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

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

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

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

577 item = {} 

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

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

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

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

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

583 articles.append(item) 

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

585 

586 

587class VirtualIssueParseView(EditorRequiredMixin, View): 

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

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

590 page = get_object_or_404(Page, id=pid) 

591 

592 data = {"pid": pid} 

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

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

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

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

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

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

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

600 

601 content_fr = parse_content(page.content_fr) 

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

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

604 

605 content_en = parse_content(page.content_en) 

606 data["articles"] = content_en["articles"] 

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

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

609 return JsonResponse(data) 

610 

611 

612class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView): 

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

614 

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

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

617 get_object_or_404(Page, id=pid) 

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

619 

620 

621class VirtualIssueCreateView(EditorRequiredMixin, View): 

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

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

624 site_id = model_helpers.get_site_id(colid) 

625 parent, _ = Page.objects.get_or_create( 

626 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES, 

627 parent_page=None, 

628 site_id=site_id, 

629 ) 

630 page = Page.objects.create( 

631 menu_title_en="New virtual issue", 

632 menu_title_fr="Nouvelle collection transverse", 

633 parent_page=parent, 

634 site_id=site_id, 

635 state="draft", 

636 ) 

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

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

639 

640 

641class SpecialIssuesIndex(EditorRequiredMixin, TemplateView): 

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

643 

644 def get_context_data(self, **kwargs): 

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

646 

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

648 context["colid"] = colid 

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

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

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

652 ).filter(my_collection=collection) 

653 

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

655 return context 

656 

657 

658class SpecialIssueEditView(EditorRequiredMixin, TemplateView): 

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

660 

661 def get_context_data(self, **kwargs): 

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

663 return context 

664 

665 

666class VirtualIssuesIndex(EditorRequiredMixin, TemplateView): 

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

668 

669 def get_context_data(self, **kwargs): 

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

671 site_id = model_helpers.get_site_id(colid) 

672 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES) 

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

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

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

676 context["pages"] = pages 

677 return context 

678 

679 

680def get_citation_fr(doi, citation_en): 

681 citation_fr = citation_en 

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

683 if article and article.trans_title_html: 

684 trans_title = article.trans_title_html 

685 try: 

686 citation_fr = re.sub( 

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

688 rf"\1{trans_title}", 

689 citation_en, 

690 ) 

691 except re.error: 

692 pass 

693 return citation_fr 

694 

695 

696def summary_build(articles, colid): 

697 summary_fr = "" 

698 summary_en = "" 

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

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

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

702 colid_lo = colid.lower() 

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

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

705 

706 for article in articles: 

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

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

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

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

711 if doi or citation_en: 

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

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

714 if image_src: 

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

716 base_url = get_media_base_url(colid) 

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

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

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

720 if re.match(image_header, image_src): 

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

722 base64_data = base64.b64decode(image_src) 

723 base_root = get_media_base_root(colid) 

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

725 os.makedirs(path, exist_ok=True) 

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

727 fp.write(base64_data) 

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

729 # TODO mettre la vrai valeur pour le SITE_DOMAIN 

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

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

732 else: 

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

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

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

736 summary_fr = head + summary_fr + tail 

737 summary_en = head + summary_en + tail 

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

739 

740 

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

742class VirtualIssueDeployView(HandleCMSMixin, View): 

743 """ 

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

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

746 The Page is then immediately posted to the test_website. 

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

748 => See DeployCMSAPIView 

749 """ 

750 

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

752 self.init_data(self.kwargs) 

753 if check_lock(): 

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

755 messages.error(self.request, msg) 

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

757 

758 pid = kwargs.get("pid") 

759 colid = self.colid 

760 data = json.loads(request.body) 

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

762 page = get_object_or_404(Page, id=pid) 

763 page.slug = page.slug_fr = page.slug_en = None 

764 page.menu_title_fr = data["title_fr"] 

765 page.menu_title_en = data["title_en"] 

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

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

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

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

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

771 page.save() 

772 

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

774 if response.status_code == 503: 

775 messages.error( 

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

777 ) 

778 

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

780 

781 

782class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView): 

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

784 

785 def get_context_data(self, **kwargs): 

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

787 return context 

788 

789 def set_contrib_addresses(self, contrib, contribution): 

790 for address in contrib: 

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

792 contrib_address.save() 

793 

794 def delete(self, pid): 

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

796 cmd = base_ptf_cmds.addContainerPtfCmd() 

797 cmd.set_object_to_be_deleted(special_issue) 

798 cmd.undo() 

799 

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

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

802 

803 data = {"pid": pid} 

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

805 data["colid"] = colid 

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

807 name = resolve(request.path_info).url_name 

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

809 self.delete(pid) 

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

811 

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

813 

814 if pid != "create": 

815 container = get_object_or_404(Container, pid=pid) 

816 # TODO: pass the lang and trans_lang as well 

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

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

819 # keep using trans_title_html for backward compatibility 

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

821 data["title"] = container.trans_title_html 

822 else: 

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

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

825 data["title"] = title["title_html"] 

826 data["doi"] = container.doi 

827 data["trans_title"] = container.title_html 

828 data["year"] = container.year 

829 data["volume"] = container.volume 

830 data["articles"] = [ 

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

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

833 ] 

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

835 data["use_resources_icon"] = True 

836 else: 

837 data["use_resources_icon"] = False 

838 

839 contribs = model_data_converter.db_to_contributors(container.contributions) 

840 data["contribs"] = contribs 

841 abstract_set = container.abstract_set.all() 

842 data["head_fr"] = ( 

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

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

845 else "" 

846 ) 

847 data["head_en"] = ( 

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

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

850 else "" 

851 ) 

852 data["tail_fr"] = ( 

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

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

855 else "" 

856 ) 

857 data["tail_en"] = ( 

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

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

860 else "" 

861 ) 

862 data["editor_bio_en"] = ( 

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

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

865 else "" 

866 ) 

867 data["editor_bio_fr"] = ( 

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

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

870 else "" 

871 ) 

872 

873 streams = container.datastream_set.all() 

874 data["pdf_file_name"] = "" 

875 data["edito_file_name"] = "" 

876 data["edito_display_name"] = "" 

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

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

879 data["pdf_file_name"] = stream.text 

880 try: 

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

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

883 "$$$" 

884 ) 

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

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

887 

888 except RelatedObject.DoesNotExist: 

889 pass 

890 try: 

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

892 

893 data["icon_location"] = container_icon.location 

894 except ExtLink.DoesNotExist: 

895 data["icon_location"] = "" 

896 # try: 

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

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

899 # except ExtLink.DoesNotExist: 

900 # data["special_issue_icon"] = None 

901 

902 else: 

903 data["title"] = "" 

904 data["doi"] = None 

905 data["trans_title"] = "" 

906 data["year"] = "" 

907 data["volume"] = "" 

908 data["articles"] = [] 

909 data["contribs"] = [] 

910 

911 data["head_fr"] = "" 

912 data["head_en"] = "" 

913 data["tail_fr"] = "" 

914 data["tail_en"] = "" 

915 data["editor_bio_en"] = "" 

916 data["editor_bio_fr"] = "" 

917 data["pdf_file_name"] = "" 

918 data["edito_file_name"] = "" 

919 data["use_resources_icon"] = False 

920 

921 return JsonResponse(data) 

922 

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

924 # le but est de faire un IssueDAta 

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

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

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

928 special_issue = create_issuedata() 

929 year = request.POST["year"] 

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

931 # TODO 2: In VueJS, titleFr = title 

932 trans_title_html = request.POST["title"] 

933 title_html = request.POST["trans_title"] 

934 if pid != "create": 

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

936 container = get_object_or_404(Container, pid=pid) 

937 lang = container.lang 

938 trans_lang = container.trans_lang 

939 xpub = create_publisherdata() 

940 xpub.name = container.my_publisher.pid 

941 special_issue.provider = container.provider 

942 special_issue.number = container.number 

943 volume = container.volume 

944 special_issue_pid = pid 

945 special_issue.date_pre_published = container.date_pre_published 

946 special_issue.date_published = container.date_published 

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

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

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

950 special_issue.doi = model_helpers.assign_container_doi(colid) 

951 else: 

952 special_issue.doi = container.doi 

953 else: 

954 lang = "en" 

955 container = None 

956 trans_lang = "fr" 

957 xpub = create_publisherdata() 

958 special_issue.doi = model_helpers.assign_container_doi(colid) 

959 volume = "" 

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

961 # if cras_issues.exists(): 

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

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

964 volume = same_year_issues.first().volume 

965 elif ( 

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

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

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

969 else: 

970 volume = "" 

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

972 # if issues["sorted_issues"]: 

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

974 # for v in volumes: 

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

976 # volume = v["volume"] 

977 # break 

978 

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

980 xpub.name = "UGA Éditions" 

981 else: 

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

983 # xpub.name = parent_container.my_publisher.pid 

984 special_issue.provider = collection.provider 

985 

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

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

988 ) 

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

990 all_special_issues_numbers = [ 

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

992 ] 

993 if len(all_special_issues_numbers) > 0: 

994 max_number = max(all_special_issues_numbers) 

995 else: 

996 max_number = 0 

997 

998 else: 

999 max_number = 0 

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

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

1002 

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

1004 special_issue.ctype = "issue_special_img" 

1005 else: 

1006 special_issue.ctype = "issue_special" 

1007 

1008 existing_issue = model_helpers.get_resource(special_issue_pid) 

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

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

1011 

1012 special_issue.lang = lang 

1013 special_issue.title_html = title_html 

1014 special_issue.title_xml = build_title_xml( 

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

1016 ) 

1017 

1018 special_issue.trans_lang = trans_lang 

1019 special_issue.trans_title_html = trans_title_html 

1020 title_xml = build_title_xml( 

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

1022 ) 

1023 title = create_titledata( 

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

1025 ) 

1026 special_issue.titles = [title] 

1027 

1028 special_issue.year = year 

1029 special_issue.volume = volume 

1030 special_issue.journal = journal 

1031 special_issue.publisher = xpub 

1032 special_issue.pid = special_issue_pid 

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

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

1035 ) 

1036 

1037 articles = [] 

1038 contribs = [] 

1039 index = 0 

1040 

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

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

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

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

1045 # if not article["citation"]: 

1046 # index += 1 

1047 # continue 

1048 articles.append(article) 

1049 

1050 index += 1 

1051 

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

1053 index = 0 

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

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

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

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

1058 contributor = create_contributor() 

1059 contributor["first_name"] = contrib["first_name"] 

1060 contributor["last_name"] = contrib["last_name"] 

1061 contributor["orcid"] = contrib["orcid"] 

1062 contributor["role"] = "editor" 

1063 

1064 contrib_xml = xml_utils.get_contrib_xml(contrib) 

1065 contributor["contrib_xml"] = contrib_xml 

1066 contribs.append(Munch(contributor)) 

1067 index += 1 

1068 special_issue.contributors = contribs 

1069 

1070 # Part of the code that handle forwords and lastwords 

1071 

1072 xhead_fr, head_fr_xml = self.create_abstract_from_vuejs( 

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

1074 ) 

1075 xtail_fr, tail_fr_xml = self.create_abstract_from_vuejs( 

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

1077 ) 

1078 xhead_en, head_en_xml = self.create_abstract_from_vuejs( 

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

1080 ) 

1081 

1082 xtail_en, tail_en_xml = self.create_abstract_from_vuejs( 

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

1084 ) 

1085 

1086 xeditor_bio_en, editor_bio_en_xml = self.create_abstract_from_vuejs( 

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

1088 ) 

1089 

1090 xeditor_bio_fr, editor_bio_fr_xml = self.create_abstract_from_vuejs( 

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

1092 ) 

1093 

1094 abstracts = [ 

1095 head_fr_xml, 

1096 head_en_xml, 

1097 tail_fr_xml, 

1098 tail_en_xml, 

1099 editor_bio_fr_xml, 

1100 editor_bio_en_xml, 

1101 ] 

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

1103 special_issue.related_objects = figures 

1104 # TODO can be factorized? 

1105 special_issue.abstracts = [ 

1106 { 

1107 "tag": "head_fr", 

1108 "lang": "fr", 

1109 "value_html": xhead_fr.value_html, 

1110 "value_tex": xhead_fr.value_tex, 

1111 "value_xml": head_fr_xml, 

1112 }, 

1113 { 

1114 "tag": "head_en", 

1115 "lang": "en", 

1116 "value_html": xhead_en.value_html, 

1117 "value_tex": xhead_en.value_tex, 

1118 "value_xml": head_en_xml, 

1119 }, 

1120 { 

1121 "tag": "tail_fr", 

1122 "lang": "fr", 

1123 "value_html": xtail_fr.value_html, 

1124 "value_tex": xtail_fr.value_tex, 

1125 "value_xml": tail_fr_xml, 

1126 }, 

1127 { 

1128 "tag": "tail_en", 

1129 "lang": "en", 

1130 "value_html": xtail_en.value_html, 

1131 "value_tex": xtail_en.value_tex, 

1132 "value_xml": tail_en_xml, 

1133 }, 

1134 { 

1135 "tag": "bio_en", 

1136 "lang": "en", 

1137 "value_html": xeditor_bio_en.value_html, 

1138 "value_tex": xeditor_bio_en.value_tex, 

1139 "value_xml": editor_bio_en_xml, 

1140 }, 

1141 { 

1142 "tag": "bio_fr", 

1143 "lang": "fr", 

1144 "value_html": xeditor_bio_fr.value_html, 

1145 "value_tex": xeditor_bio_fr.value_tex, 

1146 "value_xml": editor_bio_fr_xml, 

1147 }, 

1148 ] 

1149 

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

1151 # Both are stored in same directory 

1152 

1153 pdf_file_path = resolver.get_disk_location( 

1154 f"{settings.RESOURCES_ROOT}", 

1155 f"{collection.pid}", 

1156 "pdf", 

1157 special_issue_pid, 

1158 article_id=None, 

1159 do_create_folder=False, 

1160 ) 

1161 pdf_path = os.path.dirname(pdf_file_path) 

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

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

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

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

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

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

1168 

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

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

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

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

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

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

1175 destination.write(chunk) 

1176 

1177 else: 

1178 pdf_file_name = request.POST["pdf_name"] 

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

1180 

1181 pdf_stream_data = create_datastream() 

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

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

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

1185 pdf_stream_data["text"] = pdf_file_name 

1186 special_issue.streams.append(pdf_stream_data) 

1187 

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

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

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

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

1192 edito_display_name = request.POST["edito_display_name"] 

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

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

1195 destination.write(chunk) 

1196 else: 

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

1198 edito_file_name = request.POST["edito_name"] 

1199 edito_display_name = request.POST["edito_display_name"] 

1200 

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

1202 data = { 

1203 "rel": "edito", 

1204 "mimetype": "application/pdf", 

1205 "location": location, 

1206 "base": None, 

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

1208 } 

1209 special_issue.related_objects.append(data) 

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

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

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

1213 icon_file = request.FILES["icon"] 

1214 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1215 icon_file, 

1216 collection.pid, 

1217 special_issue.pid, 

1218 special_issue.pid, 

1219 ) 

1220 data = { 

1221 "rel": "icon", 

1222 "location": relative_file_name, 

1223 "base": None, 

1224 "seq": 1, 

1225 "metadata": "", 

1226 } 

1227 special_issue.ext_links.append(data) 

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

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

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

1231 data = { 

1232 "rel": "icon", 

1233 "location": icon_file, 

1234 "base": None, 

1235 "seq": 1, 

1236 "metadata": "", 

1237 } 

1238 special_issue.ext_links.append(data) 

1239 

1240 special_issue = Munch(special_issue.__dict__) 

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

1242 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params) 

1243 cmd.do() 

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

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

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

1247 

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

1249 abstract_html = xml_utils.replace_html_entities(abstract) 

1250 xabstract = CkeditorParser( 

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

1252 ) 

1253 abstract_xml = jats_from_abstract(lang, lang, xabstract, position) 

1254 return xabstract, abstract_xml 

1255 

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

1257 figures = [] 

1258 for abstract in abstracts: 

1259 abstract_xml = abstract.encode("utf8") 

1260 

1261 tree = etree.fromstring(abstract_xml) 

1262 

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

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

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

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

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

1268 base = get_media_base_root(colid) 

1269 data_location = os.path.join( 

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

1271 ) 

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

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

1274 # so related objects can go for the correct one 

1275 img = Image.open(data_location) 

1276 final_data_location = os.path.join( 

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

1278 ) 

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

1280 os.makedirs(final_data_location) 

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

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

1283 img.save(final_data_location) 

1284 if ext == "png": 

1285 mimetype = "image/png" 

1286 else: 

1287 mimetype = "image/jpeg" 

1288 data = { 

1289 "rel": "html-image", 

1290 "mimetype": mimetype, 

1291 "location": relative_path, 

1292 "base": None, 

1293 "metadata": "", 

1294 } 

1295 if data not in figures: 

1296 figures.append(data) 

1297 return figures 

1298 

1299 

1300class PageIndexView(EditorRequiredMixin, TemplateView): 

1301 template_name = "mersenne_cms/page_index.html" 

1302 

1303 def get_context_data(self, **kwargs): 

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

1305 site_id = model_helpers.get_site_id(colid) 

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

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

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

1309 else: 

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

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

1312 context["colid"] = colid 

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

1314 context["pages"] = pages 

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

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

1317 return context 

1318 

1319 

1320class PageBaseView(HandleCMSMixin, View): 

1321 template_name = "mersenne_cms/page_form.html" 

1322 model = Page 

1323 form_class = PageForm 

1324 

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

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

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

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

1329 

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

1331 

1332 def get_success_url(self): 

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

1334 

1335 def get_context_data(self, **kwargs): 

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

1337 context["journal"] = self.collection 

1338 return context 

1339 

1340 def update_test_website(self): 

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

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

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

1344 else: 

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

1346 

1347 if response.status_code == 503: 

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

1349 else: 

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

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

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

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

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

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

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

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

1358 

1359 def get_form_kwargs(self): 

1360 kwargs = super().get_form_kwargs() 

1361 kwargs["site_id"] = self.site_id 

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

1363 return kwargs 

1364 

1365 def form_valid(self, form): 

1366 form.save() 

1367 

1368 self.update_test_website() 

1369 

1370 return HttpResponseRedirect(self.get_success_url()) 

1371 

1372 

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

1374class PageDeleteView(PageBaseView): 

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

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

1377 pk = kwargs.get("pk") 

1378 page = get_object_or_404(Page, id=pk) 

1379 if page.mersenne_id: 

1380 raise PermissionDenied 

1381 

1382 page.delete() 

1383 

1384 self.update_test_website() 

1385 

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

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

1388 else: 

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

1390 

1391 

1392class PageCreateView(PageBaseView, CreateView): 

1393 def get_context_data(self, **kwargs): 

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

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

1396 return context 

1397 

1398 

1399class PageUpdateView(PageBaseView, UpdateView): 

1400 def get_context_data(self, **kwargs): 

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

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

1403 return context 

1404 

1405 

1406class NewsBaseView(PageBaseView): 

1407 template_name = "mersenne_cms/news_form.html" 

1408 model = News 

1409 form_class = NewsForm 

1410 

1411 

1412class NewsDeleteView(NewsBaseView): 

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

1414 pk = kwargs.get("pk") 

1415 news = get_object_or_404(News, id=pk) 

1416 

1417 news.delete() 

1418 

1419 self.update_test_website() 

1420 

1421 return HttpResponseRedirect(self.get_success_url()) 

1422 

1423 

1424class NewsCreateView(NewsBaseView, CreateView): 

1425 def get_context_data(self, **kwargs): 

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

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

1428 return context 

1429 

1430 

1431class NewsUpdateView(NewsBaseView, UpdateView): 

1432 def get_context_data(self, **kwargs): 

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

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

1435 return context 

1436 

1437 

1438# def page_create_view(request, colid): 

1439# context = {} 

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

1441# raise PermissionDenied 

1442# collection = model_helpers.get_collection(colid) 

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

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

1445# if form.is_valid(): 

1446# form.save() 

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

1448# if response.status_code < 300: 

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

1450# else: 

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

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

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

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

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

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

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

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

1459# 

1460# context["form"] = form 

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

1462# context["journal"] = collection 

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

1464 

1465 

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

1467# context = {} 

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

1469# raise PermissionDenied 

1470# 

1471# collection = model_helpers.get_collection(colid) 

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

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

1474# if form.is_valid(): 

1475# form.save() 

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

1477# if response.status_code < 300: 

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

1479# else: 

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

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

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

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

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

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

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

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

1488# 

1489# context["form"] = form 

1490# context["pid"] = pid 

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

1492# context["journal"] = collection 

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