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

884 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-07-30 15:02 +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 convert_tiff_to_jpg_from_path, 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, processed_attributes, 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 jpg_path = obj_attribute_file.path.rsplit(".", 1)[0] + ".jpg" 

535 if obj_attribute_file.name not in processed_attributes: 

536 processed_attributes[obj_attribute_file.path] = obj_attribute_file 

537 original_path = obj_attribute_file.path 

538 convert_tiff_to_jpg_from_path(original_path) 

539 with open(jpg_path, "rb") as fp: 

540 obj_attribute_file.save(os.path.basename(jpg_path), File(fp), save=True) 

541 os.remove(original_path) 

542 return jpg_path 

543 return obj_attribute_file.path 

544 

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

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

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

548 article = get_object_or_404(Article, doi=doi) 

549 

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

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

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

553 obj.date_modified = timezone.now() 

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

555 form.save() 

556 files = {} 

557 

558 processed_attributes = {} 

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

560 obj_attribute_file = getattr(obj, attribute, None) 

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

562 file_path = self.__get_path_and_replace_tiff_file( 

563 processed_attributes, obj_attribute_file 

564 ) 

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

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

567 

568 collection = article.my_container.my_collection 

569 urls = get_server_urls(collection, site=site) 

570 response = requests.models.Response() 

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

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

573 try: 

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

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

576 else: 

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

578 except requests.exceptions.RequestException as e: 

579 response.status_code = 503 

580 response.reason = e.args[0] 

581 break 

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

583 else: 

584 return HttpResponseBadRequest() 

585 

586 

587def parse_content(content): 

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

589 if not table: 

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

591 

592 articles = [] 

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

594 for row in rows: 

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

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

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

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

599 item = {} 

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

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

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

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

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

605 articles.append(item) 

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

607 

608 

609class VirtualIssueParseView(EditorRequiredMixin, View): 

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

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

612 page = get_object_or_404(Page, id=pid) 

613 

614 data = {"pid": pid} 

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

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

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

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

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

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

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

622 

623 content_fr = parse_content(page.content_fr) 

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

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

626 

627 content_en = parse_content(page.content_en) 

628 data["articles"] = content_en["articles"] 

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

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

631 return JsonResponse(data) 

632 

633 

634class VirtualIssueUpdateView(EditorRequiredMixin, TemplateView): 

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

636 

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

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

639 get_object_or_404(Page, id=pid) 

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

641 

642 

643class VirtualIssueCreateView(EditorRequiredMixin, View): 

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

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

646 site_id = model_helpers.get_site_id(colid) 

647 parent, _ = Page.objects.get_or_create( 

648 mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES, 

649 parent_page=None, 

650 site_id=site_id, 

651 ) 

652 page = Page.objects.create( 

653 menu_title_en="New virtual issue", 

654 menu_title_fr="Nouvelle collection transverse", 

655 parent_page=parent, 

656 site_id=site_id, 

657 state="draft", 

658 ) 

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

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

661 

662 

663class SpecialIssuesIndex(EditorRequiredMixin, TemplateView): 

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

665 

666 def get_context_data(self, **kwargs): 

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

668 

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

670 context["colid"] = colid 

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

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

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

674 ).filter(my_collection=collection) 

675 

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

677 return context 

678 

679 

680class SpecialIssueEditView(EditorRequiredMixin, TemplateView): 

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

682 

683 def get_context_data(self, **kwargs): 

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

685 return context 

686 

687 

688class VirtualIssuesIndex(EditorRequiredMixin, TemplateView): 

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

690 

691 def get_context_data(self, **kwargs): 

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

693 site_id = model_helpers.get_site_id(colid) 

694 vi = get_object_or_404(Page, mersenne_id=MERSENNE_ID_VIRTUAL_ISSUES) 

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

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

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

698 context["pages"] = pages 

699 return context 

700 

701 

702def get_citation_fr(doi, citation_en): 

703 citation_fr = citation_en 

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

705 if article and article.trans_title_html: 

706 trans_title = article.trans_title_html 

707 try: 

708 citation_fr = re.sub( 

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

710 rf"\1{trans_title}", 

711 citation_en, 

712 ) 

713 except re.error: 

714 pass 

715 return citation_fr 

716 

717 

718def summary_build(articles, colid): 

719 summary_fr = "" 

720 summary_en = "" 

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

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

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

724 colid_lo = colid.lower() 

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

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

727 

728 for article in articles: 

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

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

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

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

733 if doi or citation_en: 

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

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

736 if image_src: 

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

738 base_url = get_media_base_url(colid) 

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

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

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

742 if re.match(image_header, image_src): 

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

744 base64_data = base64.b64decode(image_src) 

745 base_root = get_media_base_root(colid) 

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

747 os.makedirs(path, exist_ok=True) 

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

749 fp.write(base64_data) 

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

751 # TODO mettre la vrai valeur pour le SITE_DOMAIN 

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

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

754 else: 

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

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

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

758 summary_fr = head + summary_fr + tail 

759 summary_en = head + summary_en + tail 

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

761 

762 

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

764class VirtualIssueDeployView(HandleCMSMixin, View): 

765 """ 

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

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

768 The Page is then immediately posted to the test_website. 

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

770 => See DeployCMSAPIView 

771 """ 

772 

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

774 self.init_data(self.kwargs) 

775 if check_lock(): 

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

777 messages.error(self.request, msg) 

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

779 

780 pid = kwargs.get("pid") 

781 colid = self.colid 

782 data = json.loads(request.body) 

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

784 page = get_object_or_404(Page, id=pid) 

785 page.slug = page.slug_fr = page.slug_en = None 

786 page.menu_title_fr = data["title_fr"] 

787 page.menu_title_en = data["title_en"] 

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

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

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

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

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

793 page.save() 

794 

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

796 if response.status_code == 503: 

797 messages.error( 

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

799 ) 

800 

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

802 

803 

804class SpecialIssueEditAPIView(HandleCMSMixin, TemplateView): 

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

806 

807 def get_context_data(self, **kwargs): 

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

809 return context 

810 

811 def set_contrib_addresses(self, contrib, contribution): 

812 for address in contrib: 

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

814 contrib_address.save() 

815 

816 def delete(self, pid): 

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

818 cmd = base_ptf_cmds.addContainerPtfCmd() 

819 cmd.set_object_to_be_deleted(special_issue) 

820 cmd.undo() 

821 

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

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

824 

825 data = {"pid": pid} 

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

827 data["colid"] = colid 

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

829 name = resolve(request.path_info).url_name 

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

831 self.delete(pid) 

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

833 

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

835 

836 if pid != "create": 

837 container = get_object_or_404(Container, pid=pid) 

838 # TODO: pass the lang and trans_lang as well 

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

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

841 # keep using trans_title_html for backward compatibility 

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

843 data["title"] = container.trans_title_html 

844 else: 

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

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

847 data["title"] = title["title_html"] 

848 data["doi"] = container.doi 

849 data["trans_title"] = container.title_html 

850 data["year"] = container.year 

851 data["volume"] = container.volume 

852 data["articles"] = [ 

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

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

855 ] 

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

857 data["use_resources_icon"] = True 

858 else: 

859 data["use_resources_icon"] = False 

860 

861 contribs = model_data_converter.db_to_contributors(container.contributions) 

862 data["contribs"] = contribs 

863 abstract_set = container.abstract_set.all() 

864 data["head_fr"] = ( 

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

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

867 else "" 

868 ) 

869 data["head_en"] = ( 

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

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

872 else "" 

873 ) 

874 data["tail_fr"] = ( 

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

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

877 else "" 

878 ) 

879 data["tail_en"] = ( 

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

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

882 else "" 

883 ) 

884 data["editor_bio_en"] = ( 

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

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

887 else "" 

888 ) 

889 data["editor_bio_fr"] = ( 

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

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

892 else "" 

893 ) 

894 

895 streams = container.datastream_set.all() 

896 data["pdf_file_name"] = "" 

897 data["edito_file_name"] = "" 

898 data["edito_display_name"] = "" 

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

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

901 data["pdf_file_name"] = stream.text 

902 try: 

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

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

905 "$$$" 

906 ) 

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

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

909 

910 except RelatedObject.DoesNotExist: 

911 pass 

912 try: 

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

914 

915 data["icon_location"] = container_icon.location 

916 except ExtLink.DoesNotExist: 

917 data["icon_location"] = "" 

918 # try: 

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

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

921 # except ExtLink.DoesNotExist: 

922 # data["special_issue_icon"] = None 

923 

924 else: 

925 data["title"] = "" 

926 data["doi"] = None 

927 data["trans_title"] = "" 

928 data["year"] = "" 

929 data["volume"] = "" 

930 data["articles"] = [] 

931 data["contribs"] = [] 

932 

933 data["head_fr"] = "" 

934 data["head_en"] = "" 

935 data["tail_fr"] = "" 

936 data["tail_en"] = "" 

937 data["editor_bio_en"] = "" 

938 data["editor_bio_fr"] = "" 

939 data["pdf_file_name"] = "" 

940 data["edito_file_name"] = "" 

941 data["use_resources_icon"] = False 

942 

943 return JsonResponse(data) 

944 

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

946 # le but est de faire un IssueDAta 

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

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

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

950 special_issue = create_issuedata() 

951 year = request.POST["year"] 

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

953 # TODO 2: In VueJS, titleFr = title 

954 trans_title_html = request.POST["title"] 

955 title_html = request.POST["trans_title"] 

956 if pid != "create": 

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

958 container = get_object_or_404(Container, pid=pid) 

959 lang = container.lang 

960 trans_lang = container.trans_lang 

961 xpub = create_publisherdata() 

962 xpub.name = container.my_publisher.pid 

963 special_issue.provider = container.provider 

964 special_issue.number = container.number 

965 volume = container.volume 

966 special_issue_pid = pid 

967 special_issue.date_pre_published = container.date_pre_published 

968 special_issue.date_published = container.date_published 

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

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

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

972 special_issue.doi = model_helpers.assign_container_doi(colid) 

973 else: 

974 special_issue.doi = container.doi 

975 else: 

976 lang = "en" 

977 container = None 

978 trans_lang = "fr" 

979 xpub = create_publisherdata() 

980 special_issue.doi = model_helpers.assign_container_doi(colid) 

981 volume = "" 

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

983 # if cras_issues.exists(): 

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

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

986 volume = same_year_issues.first().volume 

987 elif ( 

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

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

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

991 else: 

992 volume = "" 

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

994 # if issues["sorted_issues"]: 

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

996 # for v in volumes: 

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

998 # volume = v["volume"] 

999 # break 

1000 

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

1002 xpub.name = "UGA Éditions" 

1003 else: 

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

1005 # xpub.name = parent_container.my_publisher.pid 

1006 special_issue.provider = collection.provider 

1007 

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

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

1010 ) 

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

1012 all_special_issues_numbers = [ 

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

1014 ] 

1015 if len(all_special_issues_numbers) > 0: 

1016 max_number = max(all_special_issues_numbers) 

1017 else: 

1018 max_number = 0 

1019 

1020 else: 

1021 max_number = 0 

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

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

1024 

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

1026 special_issue.ctype = "issue_special_img" 

1027 else: 

1028 special_issue.ctype = "issue_special" 

1029 

1030 existing_issue = model_helpers.get_resource(special_issue_pid) 

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

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

1033 

1034 special_issue.lang = lang 

1035 special_issue.title_html = title_html 

1036 special_issue.title_xml = build_title_xml( 

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

1038 ) 

1039 

1040 special_issue.trans_lang = trans_lang 

1041 special_issue.trans_title_html = trans_title_html 

1042 title_xml = build_title_xml( 

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

1044 ) 

1045 title = create_titledata( 

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

1047 ) 

1048 special_issue.titles = [title] 

1049 

1050 special_issue.year = year 

1051 special_issue.volume = volume 

1052 special_issue.journal = journal 

1053 special_issue.publisher = xpub 

1054 special_issue.pid = special_issue_pid 

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

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

1057 ) 

1058 

1059 articles = [] 

1060 contribs = [] 

1061 index = 0 

1062 

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

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

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

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

1067 # if not article["citation"]: 

1068 # index += 1 

1069 # continue 

1070 articles.append(article) 

1071 

1072 index += 1 

1073 

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

1075 index = 0 

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

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

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

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

1080 contributor = create_contributor() 

1081 contributor["first_name"] = contrib["first_name"] 

1082 contributor["last_name"] = contrib["last_name"] 

1083 contributor["orcid"] = contrib["orcid"] 

1084 contributor["role"] = "editor" 

1085 

1086 contrib_xml = xml_utils.get_contrib_xml(contrib) 

1087 contributor["contrib_xml"] = contrib_xml 

1088 contribs.append(Munch(contributor)) 

1089 index += 1 

1090 special_issue.contributors = contribs 

1091 

1092 # Part of the code that handle forwords and lastwords 

1093 

1094 xhead_fr, head_fr_xml = self.create_abstract_from_vuejs( 

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

1096 ) 

1097 xtail_fr, tail_fr_xml = self.create_abstract_from_vuejs( 

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

1099 ) 

1100 xhead_en, head_en_xml = self.create_abstract_from_vuejs( 

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

1102 ) 

1103 

1104 xtail_en, tail_en_xml = self.create_abstract_from_vuejs( 

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

1106 ) 

1107 

1108 xeditor_bio_en, editor_bio_en_xml = self.create_abstract_from_vuejs( 

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

1110 ) 

1111 

1112 xeditor_bio_fr, editor_bio_fr_xml = self.create_abstract_from_vuejs( 

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

1114 ) 

1115 

1116 abstracts = [ 

1117 head_fr_xml, 

1118 head_en_xml, 

1119 tail_fr_xml, 

1120 tail_en_xml, 

1121 editor_bio_fr_xml, 

1122 editor_bio_en_xml, 

1123 ] 

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

1125 special_issue.related_objects = figures 

1126 # TODO can be factorized? 

1127 special_issue.abstracts = [ 

1128 { 

1129 "tag": "head_fr", 

1130 "lang": "fr", 

1131 "value_html": xhead_fr.value_html, 

1132 "value_tex": xhead_fr.value_tex, 

1133 "value_xml": head_fr_xml, 

1134 }, 

1135 { 

1136 "tag": "head_en", 

1137 "lang": "en", 

1138 "value_html": xhead_en.value_html, 

1139 "value_tex": xhead_en.value_tex, 

1140 "value_xml": head_en_xml, 

1141 }, 

1142 { 

1143 "tag": "tail_fr", 

1144 "lang": "fr", 

1145 "value_html": xtail_fr.value_html, 

1146 "value_tex": xtail_fr.value_tex, 

1147 "value_xml": tail_fr_xml, 

1148 }, 

1149 { 

1150 "tag": "tail_en", 

1151 "lang": "en", 

1152 "value_html": xtail_en.value_html, 

1153 "value_tex": xtail_en.value_tex, 

1154 "value_xml": tail_en_xml, 

1155 }, 

1156 { 

1157 "tag": "bio_en", 

1158 "lang": "en", 

1159 "value_html": xeditor_bio_en.value_html, 

1160 "value_tex": xeditor_bio_en.value_tex, 

1161 "value_xml": editor_bio_en_xml, 

1162 }, 

1163 { 

1164 "tag": "bio_fr", 

1165 "lang": "fr", 

1166 "value_html": xeditor_bio_fr.value_html, 

1167 "value_tex": xeditor_bio_fr.value_tex, 

1168 "value_xml": editor_bio_fr_xml, 

1169 }, 

1170 ] 

1171 

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

1173 # Both are stored in same directory 

1174 

1175 pdf_file_path = resolver.get_disk_location( 

1176 f"{settings.RESOURCES_ROOT}", 

1177 f"{collection.pid}", 

1178 "pdf", 

1179 special_issue_pid, 

1180 article_id=None, 

1181 do_create_folder=False, 

1182 ) 

1183 pdf_path = os.path.dirname(pdf_file_path) 

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

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

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

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

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

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

1190 

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

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

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

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

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

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

1197 destination.write(chunk) 

1198 

1199 else: 

1200 pdf_file_name = request.POST["pdf_name"] 

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

1202 

1203 pdf_stream_data = create_datastream() 

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

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

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

1207 pdf_stream_data["text"] = pdf_file_name 

1208 special_issue.streams.append(pdf_stream_data) 

1209 

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

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

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

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

1214 edito_display_name = request.POST["edito_display_name"] 

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

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

1217 destination.write(chunk) 

1218 else: 

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

1220 edito_file_name = request.POST["edito_name"] 

1221 edito_display_name = request.POST["edito_display_name"] 

1222 

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

1224 data = { 

1225 "rel": "edito", 

1226 "mimetype": "application/pdf", 

1227 "location": location, 

1228 "base": None, 

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

1230 } 

1231 special_issue.related_objects.append(data) 

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

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

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

1235 icon_file = request.FILES["icon"] 

1236 relative_file_name = resolver.copy_file_obj_to_article_folder( 

1237 icon_file, 

1238 collection.pid, 

1239 special_issue.pid, 

1240 special_issue.pid, 

1241 ) 

1242 data = { 

1243 "rel": "icon", 

1244 "location": relative_file_name, 

1245 "base": None, 

1246 "seq": 1, 

1247 "metadata": "", 

1248 } 

1249 special_issue.ext_links.append(data) 

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

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

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

1253 data = { 

1254 "rel": "icon", 

1255 "location": icon_file, 

1256 "base": None, 

1257 "seq": 1, 

1258 "metadata": "", 

1259 } 

1260 special_issue.ext_links.append(data) 

1261 

1262 special_issue = Munch(special_issue.__dict__) 

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

1264 cmd = xml_cmds.addOrUpdateIssueXmlCmd(params) 

1265 cmd.do() 

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

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

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

1269 

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

1271 abstract_html = xml_utils.replace_html_entities(abstract) 

1272 xabstract = CkeditorParser( 

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

1274 ) 

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

1276 return xabstract, abstract_xml 

1277 

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

1279 figures = [] 

1280 for abstract in abstracts: 

1281 abstract_xml = abstract.encode("utf8") 

1282 

1283 tree = etree.fromstring(abstract_xml) 

1284 

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

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

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

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

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

1290 base = get_media_base_root(colid) 

1291 data_location = os.path.join( 

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

1293 ) 

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

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

1296 # so related objects can go for the correct one 

1297 img = Image.open(data_location) 

1298 final_data_location = os.path.join( 

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

1300 ) 

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

1302 os.makedirs(final_data_location) 

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

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

1305 img.save(final_data_location) 

1306 if ext == "png": 

1307 mimetype = "image/png" 

1308 else: 

1309 mimetype = "image/jpeg" 

1310 data = { 

1311 "rel": "html-image", 

1312 "mimetype": mimetype, 

1313 "location": relative_path, 

1314 "base": None, 

1315 "metadata": "", 

1316 } 

1317 if data not in figures: 

1318 figures.append(data) 

1319 return figures 

1320 

1321 

1322class PageIndexView(EditorRequiredMixin, TemplateView): 

1323 template_name = "mersenne_cms/page_index.html" 

1324 

1325 def get_context_data(self, **kwargs): 

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

1327 site_id = model_helpers.get_site_id(colid) 

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

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

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

1331 else: 

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

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

1334 context["colid"] = colid 

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

1336 context["pages"] = pages 

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

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

1339 return context 

1340 

1341 

1342class PageBaseView(HandleCMSMixin, View): 

1343 template_name = "mersenne_cms/page_form.html" 

1344 model = Page 

1345 form_class = PageForm 

1346 

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

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

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

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

1351 

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

1353 

1354 def get_success_url(self): 

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

1356 

1357 def get_context_data(self, **kwargs): 

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

1359 context["journal"] = self.collection 

1360 return context 

1361 

1362 def update_test_website(self): 

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

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

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

1366 else: 

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

1368 

1369 if response.status_code == 503: 

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

1371 else: 

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

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

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

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

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

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

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

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

1380 

1381 def get_form_kwargs(self): 

1382 kwargs = super().get_form_kwargs() 

1383 kwargs["site_id"] = self.site_id 

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

1385 return kwargs 

1386 

1387 def form_valid(self, form): 

1388 form.save() 

1389 

1390 self.update_test_website() 

1391 

1392 return HttpResponseRedirect(self.get_success_url()) 

1393 

1394 

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

1396class PageDeleteView(PageBaseView): 

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

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

1399 pk = kwargs.get("pk") 

1400 page = get_object_or_404(Page, id=pk) 

1401 if page.mersenne_id: 

1402 raise PermissionDenied 

1403 

1404 page.delete() 

1405 

1406 self.update_test_website() 

1407 

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

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

1410 else: 

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

1412 

1413 

1414class PageCreateView(PageBaseView, CreateView): 

1415 def get_context_data(self, **kwargs): 

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

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

1418 return context 

1419 

1420 

1421class PageUpdateView(PageBaseView, UpdateView): 

1422 def get_context_data(self, **kwargs): 

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

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

1425 return context 

1426 

1427 

1428class NewsBaseView(PageBaseView): 

1429 template_name = "mersenne_cms/news_form.html" 

1430 model = News 

1431 form_class = NewsForm 

1432 

1433 

1434class NewsDeleteView(NewsBaseView): 

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

1436 pk = kwargs.get("pk") 

1437 news = get_object_or_404(News, id=pk) 

1438 

1439 news.delete() 

1440 

1441 self.update_test_website() 

1442 

1443 return HttpResponseRedirect(self.get_success_url()) 

1444 

1445 

1446class NewsCreateView(NewsBaseView, CreateView): 

1447 def get_context_data(self, **kwargs): 

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

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

1450 return context 

1451 

1452 

1453class NewsUpdateView(NewsBaseView, UpdateView): 

1454 def get_context_data(self, **kwargs): 

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

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

1457 return context 

1458 

1459 

1460# def page_create_view(request, colid): 

1461# context = {} 

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

1463# raise PermissionDenied 

1464# collection = model_helpers.get_collection(colid) 

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

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

1467# if form.is_valid(): 

1468# form.save() 

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

1470# if response.status_code < 300: 

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

1472# else: 

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

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

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

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

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

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

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

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

1481# 

1482# context["form"] = form 

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

1484# context["journal"] = collection 

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

1486 

1487 

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

1489# context = {} 

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

1491# raise PermissionDenied 

1492# 

1493# collection = model_helpers.get_collection(colid) 

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

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

1496# if form.is_valid(): 

1497# form.save() 

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

1499# if response.status_code < 300: 

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

1501# else: 

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

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

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

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

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

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

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

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

1510# 

1511# context["form"] = form 

1512# context["pid"] = pid 

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

1514# context["journal"] = collection 

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