Coverage for src / ptf_tools / views / base_views.py: 17%

1672 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-04-02 14:28 +0000

1import io 

2import json 

3import logging 

4import os 

5import re 

6from datetime import datetime 

7from itertools import groupby 

8 

9import jsonpickle 

10import requests 

11from allauth.account.signals import user_signed_up 

12from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin 

13from celery import Celery, current_app 

14from django.conf import settings 

15from django.contrib import messages 

16from django.contrib.auth.mixins import UserPassesTestMixin 

17from django.db.models import Q 

18from django.http import ( 

19 Http404, 

20 HttpRequest, 

21 HttpResponse, 

22 HttpResponseRedirect, 

23 HttpResponseServerError, 

24 JsonResponse, 

25) 

26from django.shortcuts import get_object_or_404, redirect, render 

27from django.urls import resolve, reverse 

28from django.utils import timezone 

29from django.views.decorators.http import require_http_methods 

30from django.views.generic import ListView, TemplateView, View 

31from django.views.generic.base import RedirectView 

32from django.views.generic.detail import SingleObjectMixin 

33from django.views.generic.edit import CreateView, FormView, UpdateView 

34from django_celery_results.models import TaskResult 

35from external.back.crossref.doi import checkDOI, recordDOI, recordPendingPublication 

36from extra_views import ( 

37 CreateWithInlinesView, 

38 InlineFormSetFactory, 

39 NamedFormsetsMixin, 

40 UpdateWithInlinesView, 

41) 

42 

43# from ptf.views import ArticleEditFormWithVueAPIView 

44from matching_back.views import ArticleEditFormWithVueAPIView 

45from ptf import model_data_converter, model_helpers, tex, utils 

46from ptf.cmds import ptf_cmds, xml_cmds 

47from ptf.cmds.base_cmds import make_int 

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

49from ptf.cmds.xml.xml_utils import replace_html_entities 

50from ptf.display import resolver 

51from ptf.exceptions import DOIException, PDFException, ServerUnderMaintenance 

52from ptf.model_data import create_issuedata, create_publisherdata, create_titledata 

53from ptf.models import ( 

54 Abstract, 

55 Article, 

56 BibItem, 

57 BibItemId, 

58 Collection, 

59 Container, 

60 ExtId, 

61 ExtLink, 

62 Resource, 

63 ResourceId, 

64 Title, 

65) 

66from ptf_back.cmds.xml_cmds import updateBibitemCitationXmlCmd 

67from ptf_back.locks import ( 

68 is_tex_conversion_locked, 

69 release_tex_conversion_lock, 

70) 

71from ptf_back.tex.tex_tasks import convert_article_tex 

72from pubmed.views import recordPubmed 

73from requests import Timeout 

74from task.tasks.archiving_tasks import archive_resource 

75 

76from comments_moderation.utils import get_comments_for_home, is_comment_moderator 

77from history import models as history_models 

78from history import views as history_views 

79from history.utils import ( 

80 get_gap, 

81 get_history_last_event_by, 

82 get_last_unsolved_error, 

83) 

84from ptf_tools.doaj import doaj_pid_register 

85from ptf_tools.forms import ( 

86 BibItemIdForm, 

87 CollectionForm, 

88 ContainerForm, 

89 DiffContainerForm, 

90 ExtIdForm, 

91 ExtLinkForm, 

92 FormSetHelper, 

93 ImportArticleForm, 

94 ImportContainerForm, 

95 ImportEditflowArticleForm, 

96 PtfFormHelper, 

97 PtfLargeModalFormHelper, 

98 PtfModalFormHelper, 

99 RegisterPubmedForm, 

100 ResourceIdForm, 

101 get_article_choices, 

102) 

103from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos 

104from ptf_tools.models import ResourceInNumdam 

105from ptf_tools.signals import update_user_from_invite 

106from ptf_tools.tasks import ( 

107 archive_numdam_collection, 

108 archive_numdam_collections, 

109) 

110from ptf_tools.templatetags.tools_helpers import get_authorized_collections 

111from ptf_tools.utils import is_authorized_editor 

112 

113logger = logging.getLogger(__name__) 

114 

115 

116def view_404(request: HttpRequest): 

117 """ 

118 Dummy view raising HTTP 404 exception. 

119 """ 

120 raise Http404 

121 

122 

123def check_collection(collection, server_url, server_type): 

124 """ 

125 Check if a collection exists on a serveur (test/prod) 

126 and upload the collection (XML, image) if necessary 

127 """ 

128 

129 url = server_url + reverse("collection_status", kwargs={"colid": collection.pid}) 

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

131 # First, upload the collection XML 

132 xml = ptf_cmds.exportPtfCmd({"pid": collection.pid}).do() 

133 body = xml.encode("utf8") 

134 

135 url = server_url + reverse("upload-serials") 

136 if response.status_code == 200: 

137 # PUT http verb is used for update 

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

139 else: 

140 # POST http verb is used for creation 

141 response = requests.post(url, data=body, verify=False) 

142 

143 # Second, copy the collection images 

144 # There is no need to copy files for the test server 

145 # Files were already copied in /mersenne_test_data during the ptf_tools import 

146 # We only need to copy files from /mersenne_test_data to 

147 # /mersenne_prod_data during an upload to prod 

148 if server_type == "website": 

149 resolver.copy_binary_files( 

150 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

151 ) 

152 elif server_type == "numdam": 

153 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

154 if collection.pid in settings.NUMDAM_COLLECTIONS: 

155 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

156 

157 resolver.copy_binary_files(collection, from_folder, settings.NUMDAM_DATA_ROOT) 

158 

159 

160def check_lock(): 

161 return hasattr(settings, "LOCK_FILE") and os.path.isfile(settings.LOCK_FILE) 

162 

163 

164def load_cedrics_article_choices(request): 

165 colid = request.GET.get("colid") 

166 issue = request.GET.get("issue") 

167 article_choices = get_article_choices(colid, issue) 

168 return render( 

169 request, "cedrics_article_dropdown_list_options.html", {"article_choices": article_choices} 

170 ) 

171 

172 

173class ImportCedricsArticleFormView(FormView): 

174 template_name = "import_article.html" 

175 form_class = ImportArticleForm 

176 

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

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

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

180 

181 def get_success_url(self): 

182 if self.colid: 

183 return reverse("collection-detail", kwargs={"pid": self.colid}) 

184 return "/" 

185 

186 def get_context_data(self, **kwargs): 

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

188 context["colid"] = self.colid 

189 context["helper"] = PtfModalFormHelper 

190 return context 

191 

192 def get_form_kwargs(self): 

193 kwargs = super().get_form_kwargs() 

194 kwargs["colid"] = self.colid 

195 return kwargs 

196 

197 def form_valid(self, form): 

198 self.issue = form.cleaned_data["issue"] 

199 self.article = form.cleaned_data["article"] 

200 return super().form_valid(form) 

201 

202 def import_cedrics_article(self, *args, **kwargs): 

203 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd( 

204 {"container_pid": self.issue_pid, "article_folder_name": self.article_pid} 

205 ) 

206 cmd.do() 

207 

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

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

210 issue = request.POST["issue"] 

211 self.article_pid = request.POST["article"] 

212 self.issue_pid = os.path.basename(os.path.dirname(issue)) 

213 

214 import_args = [self] 

215 import_kwargs = {} 

216 

217 try: 

218 _, status, message = history_views.execute_and_record_func( 

219 "import", 

220 f"{self.issue_pid} / {self.article_pid}", 

221 self.colid, 

222 self.import_cedrics_article, 

223 "", 

224 False, 

225 None, 

226 None, 

227 *import_args, 

228 **import_kwargs, 

229 ) 

230 

231 messages.success( 

232 self.request, f"L'article {self.article_pid} a été importé avec succès" 

233 ) 

234 

235 except Exception as exception: 

236 messages.error( 

237 self.request, 

238 f"Echec de l'import de l'article {self.article_pid} : {str(exception)}", 

239 ) 

240 

241 return redirect(self.get_success_url()) 

242 

243 

244class ImportCedricsIssueView(FormView): 

245 template_name = "import_container.html" 

246 form_class = ImportContainerForm 

247 

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

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

250 self.to_appear = self.request.GET.get("to_appear", False) 

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

252 

253 def get_success_url(self): 

254 if self.filename: 

255 return reverse( 

256 "diff_cedrics_issue", kwargs={"colid": self.colid, "filename": self.filename} 

257 ) 

258 return "/" 

259 

260 def get_context_data(self, **kwargs): 

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

262 context["colid"] = self.colid 

263 context["helper"] = PtfModalFormHelper 

264 return context 

265 

266 def get_form_kwargs(self): 

267 kwargs = super().get_form_kwargs() 

268 kwargs["colid"] = self.colid 

269 kwargs["to_appear"] = self.to_appear 

270 return kwargs 

271 

272 def form_valid(self, form): 

273 self.filename = form.cleaned_data["filename"].split("/")[-1] 

274 return super().form_valid(form) 

275 

276 

277class DiffCedricsIssueView(FormView): 

278 template_name = "diff_container_form.html" 

279 form_class = DiffContainerForm 

280 diffs = None 

281 xissue = None 

282 xissue_encoded = None 

283 

284 def get_success_url(self): 

285 return reverse("collection-detail", kwargs={"pid": self.colid}) 

286 

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

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

289 # self.filename = self.kwargs['filename'] 

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

291 

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

293 self.filename = request.GET["filename"] 

294 self.remove_mail = request.GET.get("remove_email", "off") 

295 self.remove_date_prod = request.GET.get("remove_date_prod", "off") 

296 self.remove_email = self.remove_mail == "on" 

297 self.remove_date_prod = self.remove_date_prod == "on" 

298 

299 try: 

300 result, status, message = history_views.execute_and_record_func( 

301 "import", 

302 os.path.basename(self.filename), 

303 self.colid, 

304 self.diff_cedrics_issue, 

305 "", 

306 True, 

307 ) 

308 except Exception as exception: 

309 pid = self.filename.split("/")[-1] 

310 messages.error(self.request, f"Echec de l'import du volume {pid} : {exception}") 

311 return HttpResponseRedirect(self.get_success_url()) 

312 

313 no_conflict = result[0] 

314 self.diffs = result[1] 

315 self.xissue = result[2] 

316 

317 if no_conflict: 

318 # Proceed with the import 

319 self.form_valid(self.get_form()) 

320 return redirect(self.get_success_url()) 

321 else: 

322 # Display the diff template 

323 self.xissue_encoded = jsonpickle.encode(self.xissue) 

324 

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

326 

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

328 self.filename = request.POST["filename"] 

329 data = request.POST["xissue_encoded"] 

330 self.xissue = jsonpickle.decode(data) 

331 

332 return super().post(request, *args, **kwargs) 

333 

334 def get_context_data(self, **kwargs): 

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

336 context["colid"] = self.colid 

337 context["diff"] = self.diffs 

338 context["filename"] = self.filename 

339 context["xissue_encoded"] = self.xissue_encoded 

340 return context 

341 

342 def get_form_kwargs(self): 

343 kwargs = super().get_form_kwargs() 

344 kwargs["colid"] = self.colid 

345 return kwargs 

346 

347 def diff_cedrics_issue(self, *args, **kwargs): 

348 params = { 

349 "colid": self.colid, 

350 "input_file": self.filename, 

351 "remove_email": self.remove_mail, 

352 "remove_date_prod": self.remove_date_prod, 

353 "diff_only": True, 

354 } 

355 

356 if settings.IMPORT_CEDRICS_DIRECTLY: 

357 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS 

358 params["force_dois"] = self.colid not in settings.NUMDAM_COLLECTIONS 

359 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

360 else: 

361 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

362 

363 result = cmd.do() 

364 if len(cmd.warnings) > 0 and self.request.user.is_superuser: 

365 messages.warning( 

366 self.request, message="Balises non parsées lors de l'import : %s" % cmd.warnings 

367 ) 

368 

369 return result 

370 

371 def import_cedrics_issue(self, *args, **kwargs): 

372 # modify xissue with data_issue if params to override 

373 if "import_choice" in kwargs and kwargs["import_choice"] == "1": 

374 issue = model_helpers.get_container(self.xissue.pid) 

375 if issue: 

376 data_issue = model_data_converter.db_to_issue_data(issue) 

377 for xarticle in self.xissue.articles: 

378 filter_articles = [ 

379 article for article in data_issue.articles if article.doi == xarticle.doi 

380 ] 

381 if len(filter_articles) > 0: 

382 db_article = filter_articles[0] 

383 xarticle.coi_statement = db_article.coi_statement 

384 xarticle.kwds = db_article.kwds 

385 xarticle.contrib_groups = db_article.contrib_groups 

386 

387 params = { 

388 "colid": self.colid, 

389 "xissue": self.xissue, 

390 "input_file": self.filename, 

391 } 

392 

393 if settings.IMPORT_CEDRICS_DIRECTLY: 

394 params["is_seminar"] = self.colid in settings.MERSENNE_SEMINARS 

395 params["add_body_html"] = self.colid not in settings.NUMDAM_COLLECTIONS 

396 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

397 else: 

398 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

399 

400 cmd.do() 

401 

402 def form_valid(self, form): 

403 if "import_choice" in self.kwargs and self.kwargs["import_choice"] == "1": 

404 import_kwargs = {"import_choice": form.cleaned_data["import_choice"]} 

405 else: 

406 import_kwargs = {} 

407 import_args = [self] 

408 

409 try: 

410 _, status, message = history_views.execute_and_record_func( 

411 "import", 

412 self.xissue.pid, 

413 self.kwargs["colid"], 

414 self.import_cedrics_issue, 

415 "", 

416 False, 

417 None, 

418 None, 

419 *import_args, 

420 **import_kwargs, 

421 ) 

422 except Exception as exception: 

423 messages.error( 

424 self.request, f"Echec de l'import du volume {self.xissue.pid} : " + str(exception) 

425 ) 

426 return super().form_invalid(form) 

427 

428 messages.success(self.request, f"Le volume {self.xissue.pid} a été importé avec succès") 

429 return super().form_valid(form) 

430 

431 

432class ImportEditflowArticleFormView(FormView): 

433 template_name = "import_editflow_article.html" 

434 form_class = ImportEditflowArticleForm 

435 

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

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

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

439 

440 def get_context_data(self, **kwargs): 

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

442 context["colid"] = self.kwargs["colid"] 

443 context["helper"] = PtfLargeModalFormHelper 

444 return context 

445 

446 def get_success_url(self): 

447 if self.colid: 

448 return reverse("collection-detail", kwargs={"pid": self.colid}) 

449 return "/" 

450 

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

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

453 try: 

454 if not self.colid: 

455 raise ValueError("Missing collection id") 

456 

457 issue_name = settings.ISSUE_PENDING_PUBLICATION_PIDS.get(self.colid) 

458 if not issue_name: 

459 raise ValueError( 

460 "Issue not found in Pending Publications PIDs. Did you forget to add it?" 

461 ) 

462 

463 issue = model_helpers.get_container(issue_name) 

464 if not issue: 

465 raise ValueError("No issue found") 

466 

467 editflow_xml_file = request.FILES.get("editflow_xml_file") 

468 if not editflow_xml_file: 

469 raise ValueError("The file you specified couldn't be found") 

470 

471 body = editflow_xml_file.read().decode("utf-8") 

472 

473 cmd = xml_cmds.addArticleXmlCmd( 

474 { 

475 "body": body, 

476 "issue": issue, 

477 "assign_doi": True, 

478 "standalone": True, 

479 "from_folder": settings.RESOURCES_ROOT, 

480 } 

481 ) 

482 cmd.set_collection(issue.get_collection()) 

483 cmd.do() 

484 

485 messages.success( 

486 request, 

487 f'Editflow article successfully imported into issue "{issue_name}"', 

488 ) 

489 

490 except Exception as exception: 

491 messages.error( 

492 request, 

493 f"Import failed: {str(exception)}", 

494 ) 

495 

496 return redirect(self.get_success_url()) 

497 

498 

499class BibtexAPIView(View): 

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

501 pid = self.kwargs.get("pid", None) 

502 all_bibtex = "" 

503 if pid: 

504 article = model_helpers.get_article(pid) 

505 if article: 

506 for bibitem in article.bibitem_set.all(): 

507 bibtex_array = bibitem.get_bibtex() 

508 last = len(bibtex_array) 

509 i = 1 

510 for bibtex in bibtex_array: 

511 if i > 1 and i < last: 

512 all_bibtex += " " 

513 all_bibtex += bibtex + "\n" 

514 i += 1 

515 

516 data = {"bibtex": all_bibtex} 

517 return JsonResponse(data) 

518 

519 

520class MatchingAPIView(View): 

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

522 pid = self.kwargs.get("pid", None) 

523 

524 url = settings.MATCHING_URL 

525 headers = {"Content-Type": "application/xml"} 

526 

527 body = ptf_cmds.exportPtfCmd({"pid": pid, "with_body": False}).do() 

528 

529 if settings.DEBUG: 

530 print("Issue exported to /tmp/issue.xml") 

531 f = open("/tmp/issue.xml", "w") 

532 f.write(body.encode("utf8")) 

533 f.close() 

534 

535 r = requests.post(url, data=body.encode("utf8"), headers=headers) 

536 body = r.text.encode("utf8") 

537 data = {"status": r.status_code, "message": body[:1000]} 

538 

539 if settings.DEBUG: 

540 print("Matching received, new issue exported to /tmp/issue1.xml") 

541 f = open("/tmp/issue1.xml", "w") 

542 text = body 

543 f.write(text) 

544 f.close() 

545 

546 resource = model_helpers.get_resource(pid) 

547 obj = resource.cast() 

548 colid = obj.get_collection().pid 

549 

550 full_text_folder = settings.CEDRAM_XML_FOLDER + colid + "/plaintext/" 

551 

552 cmd = xml_cmds.addOrUpdateIssueXmlCmd( 

553 {"body": body, "assign_doi": True, "full_text_folder": full_text_folder} 

554 ) 

555 cmd.do() 

556 

557 print("Matching finished") 

558 return JsonResponse(data) 

559 

560 

561class ImportAllAPIView(View): 

562 def internal_do(self, *args, **kwargs): 

563 pid = self.kwargs.get("pid", None) 

564 

565 root_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, pid) 

566 if not os.path.isdir(root_folder): 

567 raise ValueError(root_folder + " does not exist") 

568 

569 resource = model_helpers.get_resource(pid) 

570 if not resource: 

571 file = os.path.join(root_folder, pid + ".xml") 

572 body = utils.get_file_content_in_utf8(file) 

573 journals = xml_cmds.addCollectionsXmlCmd( 

574 { 

575 "body": body, 

576 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

577 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

578 } 

579 ).do() 

580 if not journals: 

581 raise ValueError(file + " does not contain a collection") 

582 resource = journals[0] 

583 # resolver.copy_binary_files( 

584 # resource, 

585 # settings.MATHDOC_ARCHIVE_FOLDER, 

586 # settings.MERSENNE_TEST_DATA_FOLDER) 

587 

588 obj = resource.cast() 

589 

590 if obj.classname != "Collection": 

591 raise ValueError(pid + " does not contain a collection") 

592 

593 cmd = xml_cmds.collectEntireCollectionXmlCmd( 

594 {"pid": pid, "folder": settings.MATHDOC_ARCHIVE_FOLDER} 

595 ) 

596 pids = cmd.do() 

597 

598 return pids 

599 

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

601 pid = self.kwargs.get("pid", None) 

602 

603 try: 

604 pids, status, message = history_views.execute_and_record_func( 

605 "import", pid, pid, self.internal_do 

606 ) 

607 except Timeout as exception: 

608 return HttpResponse(exception, status=408) 

609 except Exception as exception: 

610 return HttpResponseServerError(exception) 

611 

612 data = {"message": message, "ids": pids, "status": status} 

613 return JsonResponse(data) 

614 

615 

616class DeployAllAPIView(View): 

617 def internal_do(self, *args, **kwargs): 

618 pid = self.kwargs.get("pid", None) 

619 site = self.kwargs.get("site", None) 

620 

621 pids = [] 

622 

623 collection = model_helpers.get_collection(pid) 

624 if not collection: 

625 raise RuntimeError(pid + " does not exist") 

626 

627 if site == "numdam": 

628 server_url = settings.NUMDAM_PRE_URL 

629 elif site != "ptf_tools": 

630 server_url = getattr(collection, site)() 

631 if not server_url: 

632 raise RuntimeError("The collection has no " + site) 

633 

634 if site != "ptf_tools": 

635 # check if the collection exists on the server 

636 # if not, check_collection will upload the collection (XML, 

637 # image...) 

638 check_collection(collection, server_url, site) 

639 

640 for issue in collection.content.all(): 

641 if site != "website" or (site == "website" and issue.are_all_articles_published()): 

642 pids.append(issue.pid) 

643 

644 return pids 

645 

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

647 pid = self.kwargs.get("pid", None) 

648 site = self.kwargs.get("site", None) 

649 

650 try: 

651 pids, status, message = history_views.execute_and_record_func( 

652 "deploy", pid, pid, self.internal_do, site 

653 ) 

654 except Timeout as exception: 

655 return HttpResponse(exception, status=408) 

656 except Exception as exception: 

657 return HttpResponseServerError(exception) 

658 

659 data = {"message": message, "ids": pids, "status": status} 

660 return JsonResponse(data) 

661 

662 

663class AddIssuePDFView(View): 

664 def __init(self, *args, **kwargs): 

665 super().__init__(*args, **kwargs) 

666 self.pid = None 

667 self.issue = None 

668 self.collection = None 

669 self.site = "test_website" 

670 

671 def post_to_site(self, url): 

672 response = requests.post(url, verify=False) 

673 status = response.status_code 

674 if not (199 < status < 205): 

675 messages.error(self.request, response.text) 

676 if status == 503: 

677 raise ServerUnderMaintenance(response.text) 

678 else: 

679 raise RuntimeError(response.text) 

680 

681 def internal_do(self, *args, **kwargs): 

682 """ 

683 Called by history_views.execute_and_record_func to do the actual job. 

684 """ 

685 

686 issue_pid = self.issue.pid 

687 colid = self.collection.pid 

688 

689 if self.site == "website": 

690 # Copy the PDF from the test to the production folder 

691 resolver.copy_binary_files( 

692 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

693 ) 

694 else: 

695 # Copy the PDF from the cedram to the test folder 

696 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid) 

697 from_path = os.path.join(from_folder, issue_pid + ".pdf") 

698 if not os.path.isfile(from_path): 

699 raise Http404(f"{from_path} does not exist") 

700 

701 to_path = resolver.get_disk_location( 

702 settings.MERSENNE_TEST_DATA_FOLDER, colid, "pdf", issue_pid 

703 ) 

704 resolver.copy_file(from_path, to_path) 

705 

706 url = reverse("issue_pdf_upload", kwargs={"pid": self.issue.pid}) 

707 

708 if self.site == "test_website": 

709 # Post to ptf-tools: it will add a Datastream to the issue 

710 absolute_url = self.request.build_absolute_uri(url) 

711 self.post_to_site(absolute_url) 

712 

713 server_url = getattr(self.collection, self.site)() 

714 absolute_url = server_url + url 

715 # Post to the test or production website 

716 self.post_to_site(absolute_url) 

717 

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

719 """ 

720 Send an issue PDF to the test or production website 

721 :param request: pid (mandatory), site (optional) "test_website" (default) or 'website' 

722 :param args: 

723 :param kwargs: 

724 :return: 

725 """ 

726 if check_lock(): 

727 m = "Trammel is under maintenance. Please try again later." 

728 messages.error(self.request, m) 

729 return JsonResponse({"message": m, "status": 503}) 

730 

731 self.pid = self.kwargs.get("pid", None) 

732 self.site = self.kwargs.get("site", "test_website") 

733 

734 self.issue = model_helpers.get_container(self.pid) 

735 if not self.issue: 

736 raise Http404(f"{self.pid} does not exist") 

737 self.collection = self.issue.get_top_collection() 

738 

739 try: 

740 pids, status, message = history_views.execute_and_record_func( 

741 "deploy", 

742 self.pid, 

743 self.collection.pid, 

744 self.internal_do, 

745 f"add issue PDF to {self.site}", 

746 ) 

747 

748 except Timeout as exception: 

749 return HttpResponse(exception, status=408) 

750 except Exception as exception: 

751 return HttpResponseServerError(exception) 

752 

753 data = {"message": message, "status": status} 

754 return JsonResponse(data) 

755 

756 

757class ArchiveAllAPIView(View): 

758 """ 

759 - archive le xml de la collection ainsi que les binaires liés 

760 - renvoie une liste de pid des issues de la collection qui seront ensuite archivés par appel JS 

761 @return array of issues pid 

762 """ 

763 

764 def internal_do(self, *args, **kwargs): 

765 collection = kwargs["collection"] 

766 pids = [] 

767 colid = collection.pid 

768 

769 logfile = os.path.join(settings.LOG_DIR, "archive.log") 

770 if os.path.isfile(logfile): 

771 os.remove(logfile) 

772 

773 ptf_cmds.exportPtfCmd( 

774 { 

775 "pid": colid, 

776 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

777 "with_binary_files": True, 

778 "for_archive": True, 

779 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

780 } 

781 ).do() 

782 

783 cedramcls = os.path.join(settings.CEDRAM_TEX_FOLDER, "cedram.cls") 

784 if os.path.isfile(cedramcls): 

785 dest_folder = os.path.join(settings.MATHDOC_ARCHIVE_FOLDER, collection.pid, "src/tex") 

786 resolver.create_folder(dest_folder) 

787 resolver.copy_file(cedramcls, dest_folder) 

788 

789 for issue in collection.content.all(): 

790 qs = issue.article_set.filter( 

791 date_online_first__isnull=True, date_published__isnull=True 

792 ) 

793 if qs.count() == 0: 

794 pids.append(issue.pid) 

795 

796 return pids 

797 

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

799 pid = self.kwargs.get("pid", None) 

800 

801 collection = model_helpers.get_collection(pid) 

802 if not collection: 

803 return HttpResponse(f"{pid} does not exist", status=400) 

804 

805 dict_ = {"collection": collection} 

806 args_ = [self] 

807 

808 try: 

809 pids, status, message = history_views.execute_and_record_func( 

810 "archive", pid, pid, self.internal_do, "", False, None, None, *args_, **dict_ 

811 ) 

812 except Timeout as exception: 

813 return HttpResponse(exception, status=408) 

814 except Exception as exception: 

815 return HttpResponseServerError(exception) 

816 

817 data = {"message": message, "ids": pids, "status": status} 

818 return JsonResponse(data) 

819 

820 

821class CreateAllDjvuAPIView(View): 

822 def internal_do(self, *args, **kwargs): 

823 issue = kwargs["issue"] 

824 pids = [issue.pid] 

825 

826 for article in issue.article_set.all(): 

827 pids.append(article.pid) 

828 

829 return pids 

830 

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

832 pid = self.kwargs.get("pid", None) 

833 issue = model_helpers.get_container(pid) 

834 if not issue: 

835 raise Http404(f"{pid} does not exist") 

836 

837 try: 

838 dict_ = {"issue": issue} 

839 args_ = [self] 

840 

841 pids, status, message = history_views.execute_and_record_func( 

842 "numdam", 

843 pid, 

844 issue.get_collection().pid, 

845 self.internal_do, 

846 "", 

847 False, 

848 None, 

849 None, 

850 *args_, 

851 **dict_, 

852 ) 

853 except Exception as exception: 

854 return HttpResponseServerError(exception) 

855 

856 data = {"message": message, "ids": pids, "status": status} 

857 return JsonResponse(data) 

858 

859 

860class ImportJatsContainerAPIView(View): 

861 def internal_do(self, *args, **kwargs): 

862 pid = self.kwargs.get("pid", None) 

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

864 

865 if pid and colid: 

866 body = resolver.get_archive_body(settings.MATHDOC_ARCHIVE_FOLDER, colid, pid) 

867 

868 cmd = xml_cmds.addOrUpdateContainerXmlCmd( 

869 { 

870 "body": body, 

871 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

872 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

873 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

874 } 

875 ) 

876 container = cmd.do() 

877 if len(cmd.warnings) > 0: 

878 messages.warning( 

879 self.request, 

880 message="Balises non parsées lors de l'import : %s" % cmd.warnings, 

881 ) 

882 

883 if not container: 

884 raise RuntimeError("Error: the container " + pid + " was not imported") 

885 

886 # resolver.copy_binary_files( 

887 # container, 

888 # settings.MATHDOC_ARCHIVE_FOLDER, 

889 # settings.MERSENNE_TEST_DATA_FOLDER) 

890 # 

891 # for article in container.article_set.all(): 

892 # resolver.copy_binary_files( 

893 # article, 

894 # settings.MATHDOC_ARCHIVE_FOLDER, 

895 # settings.MERSENNE_TEST_DATA_FOLDER) 

896 else: 

897 raise RuntimeError("colid or pid are not defined") 

898 

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

900 pid = self.kwargs.get("pid", None) 

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

902 

903 try: 

904 _, status, message = history_views.execute_and_record_func( 

905 "import", pid, colid, self.internal_do 

906 ) 

907 except Timeout as exception: 

908 return HttpResponse(exception, status=408) 

909 except Exception as exception: 

910 return HttpResponseServerError(exception) 

911 

912 data = {"message": message, "status": status} 

913 return JsonResponse(data) 

914 

915 

916class DeployCollectionAPIView(View): 

917 # Update collection.xml on a site (with its images) 

918 

919 def internal_do(self, *args, **kwargs): 

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

921 site = self.kwargs.get("site", None) 

922 

923 collection = model_helpers.get_collection(colid) 

924 if not collection: 

925 raise RuntimeError(f"{colid} does not exist") 

926 

927 if site == "numdam": 

928 server_url = settings.NUMDAM_PRE_URL 

929 else: 

930 server_url = getattr(collection, site)() 

931 if not server_url: 

932 raise RuntimeError(f"The collection has no {site}") 

933 

934 # check_collection creates or updates the collection (XML, image...) 

935 check_collection(collection, server_url, site) 

936 

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

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

939 site = self.kwargs.get("site", None) 

940 

941 try: 

942 _, status, message = history_views.execute_and_record_func( 

943 "deploy", colid, colid, self.internal_do, site 

944 ) 

945 except Timeout as exception: 

946 return HttpResponse(exception, status=408) 

947 except Exception as exception: 

948 return HttpResponseServerError(exception) 

949 

950 data = {"message": message, "status": status} 

951 return JsonResponse(data) 

952 

953 

954class DeployJatsResourceAPIView(View): 

955 # A RENOMMER aussi DeleteJatsContainerAPIView (mais fonctionne tel quel) 

956 

957 def internal_do(self, *args, **kwargs): 

958 pid = self.kwargs.get("pid", None) 

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

960 site = self.kwargs.get("site", None) 

961 

962 if site == "ptf_tools": 

963 raise RuntimeError("Do not choose to deploy on PTF Tools") 

964 if check_lock(): 

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

966 messages.error(self.request, msg) 

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

968 

969 resource = model_helpers.get_resource(pid) 

970 if not resource: 

971 raise RuntimeError(f"{pid} does not exist") 

972 

973 obj = resource.cast() 

974 article = None 

975 if obj.classname == "Article": 

976 article = obj 

977 container = article.my_container 

978 articles_to_deploy = [article] 

979 else: 

980 container = obj 

981 articles_to_deploy = container.article_set.exclude(do_not_publish=True) 

982 

983 if container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get(colid, None): 

984 raise RuntimeError("Pending publications should not be deployed") 

985 if site == "website" and article is not None and article.do_not_publish: 

986 raise RuntimeError(f"{pid} is marked as Do not publish") 

987 if site == "numdam" and article is not None: 

988 raise RuntimeError("You can only deploy issues to Numdam") 

989 

990 collection = container.get_top_collection() 

991 colid = collection.pid 

992 djvu_exception = None 

993 

994 if site == "numdam": 

995 server_url = settings.NUMDAM_PRE_URL 

996 ResourceInNumdam.objects.get_or_create(pid=container.pid) 

997 

998 # 06/12/2022: DjVu are no longer added with Mersenne articles 

999 # Add Djvu (before exporting the XML) 

1000 if False and int(container.year) < 2020: 

1001 for art in container.article_set.all(): 

1002 try: 

1003 cmd = ptf_cmds.addDjvuPtfCmd() 

1004 cmd.set_resource(art) 

1005 cmd.do() 

1006 except Exception as e: 

1007 # Djvu are optional. 

1008 # Allow the deployment, but record the exception in the history 

1009 djvu_exception = e 

1010 else: 

1011 server_url = getattr(collection, site)() 

1012 if not server_url: 

1013 raise RuntimeError(f"The collection has no {site}") 

1014 

1015 # check if the collection exists on the server 

1016 # if not, check_collection will upload the collection (XML, 

1017 # image...) 

1018 if article is None: 

1019 check_collection(collection, server_url, site) 

1020 

1021 with open(os.path.join(settings.LOG_DIR, "cmds.log"), "w", encoding="utf-8") as file_: 

1022 # Create/update deployed date and published date on all container articles 

1023 if site == "website": 

1024 file_.write( 

1025 "Create/Update deployed_date and date_published on all articles for {}\n".format( 

1026 pid 

1027 ) 

1028 ) 

1029 

1030 # create date_published on articles without date_published (ou date_online_first pour le volume 0) 

1031 cmd = ptf_cmds.publishResourcePtfCmd() 

1032 cmd.set_resource(resource) 

1033 updated_articles = cmd.do() 

1034 

1035 tex.create_frontpage(colid, container, updated_articles, test=False) 

1036 

1037 mersenneSite = model_helpers.get_site_mersenne(colid) 

1038 # create or update deployed_date on container and articles 

1039 model_helpers.update_deployed_date(obj, mersenneSite, None, file_) 

1040 

1041 for art in articles_to_deploy: 

1042 if art.doi and (art.date_published or art.date_online_first): 

1043 if art.my_container.year is None: 

1044 art.my_container.year = datetime.now().strftime("%Y") 

1045 # BUG ? update the container but no save() ? 

1046 

1047 file_.write( 

1048 "Publication date of {} : Online First: {}, Published: {}\n".format( 

1049 art.pid, art.date_online_first, art.date_published 

1050 ) 

1051 ) 

1052 

1053 if article is None: 

1054 resolver.copy_binary_files( 

1055 container, 

1056 settings.MERSENNE_TEST_DATA_FOLDER, 

1057 settings.MERSENNE_PROD_DATA_FOLDER, 

1058 ) 

1059 

1060 for art in articles_to_deploy: 

1061 resolver.copy_binary_files( 

1062 art, 

1063 settings.MERSENNE_TEST_DATA_FOLDER, 

1064 settings.MERSENNE_PROD_DATA_FOLDER, 

1065 ) 

1066 

1067 elif site == "test_website": 

1068 # create date_pre_published on articles without date_pre_published 

1069 cmd = ptf_cmds.publishResourcePtfCmd({"pre_publish": True}) 

1070 cmd.set_resource(resource) 

1071 updated_articles = cmd.do() 

1072 

1073 tex.create_frontpage(colid, container, updated_articles) 

1074 

1075 export_to_website = site == "website" 

1076 

1077 if article is None: 

1078 with_djvu = site == "numdam" 

1079 xml = ptf_cmds.exportPtfCmd( 

1080 { 

1081 "pid": pid, 

1082 "with_djvu": with_djvu, 

1083 "export_to_website": export_to_website, 

1084 } 

1085 ).do() 

1086 body = xml.encode("utf8") 

1087 

1088 if container.ctype == "issue" or container.ctype.startswith("issue_special"): 

1089 url = server_url + reverse("issue_upload") 

1090 else: 

1091 url = server_url + reverse("book_upload") 

1092 

1093 # verify=False: ignore TLS certificate 

1094 response = requests.post(url, data=body, verify=False) 

1095 # response = requests.post(url, files=files, verify=False) 

1096 else: 

1097 xml = ptf_cmds.exportPtfCmd( 

1098 { 

1099 "pid": pid, 

1100 "with_djvu": False, 

1101 "article_standalone": True, 

1102 "collection_pid": collection.pid, 

1103 "export_to_website": export_to_website, 

1104 "export_folder": settings.LOG_DIR, 

1105 } 

1106 ).do() 

1107 # Unlike containers that send their XML as the body of the POST request, 

1108 # articles send their XML as a file, because PCJ editor sends multiple files (XML, PDF, img) 

1109 xml_file = io.StringIO(xml) 

1110 files = {"xml": xml_file} 

1111 

1112 url = server_url + reverse( 

1113 "article_in_issue_upload", kwargs={"pid": container.pid} 

1114 ) 

1115 # verify=False: ignore TLS certificate 

1116 header = {} 

1117 response = requests.post(url, headers=header, files=files, verify=False) 

1118 

1119 status = response.status_code 

1120 

1121 if 199 < status < 205: 

1122 # There is no need to copy files for the test server 

1123 # Files were already copied in /mersenne_test_data during the ptf_tools import 

1124 # We only need to copy files from /mersenne_test_data to 

1125 # /mersenne_prod_data during an upload to prod 

1126 if site == "website": 

1127 # TODO mettre ici le record doi pour un issue publié 

1128 if container.doi: 

1129 recordDOI(container) 

1130 

1131 for art in articles_to_deploy: 

1132 # record DOI automatically when deploying in prod 

1133 

1134 if art.doi and art.allow_crossref(): 

1135 recordDOI(art) 

1136 

1137 if colid == "CRBIOL": 

1138 recordPubmed( 

1139 art, force_update=False, updated_articles=updated_articles 

1140 ) 

1141 

1142 if colid == "PCJ": 

1143 self.update_pcj_editor(updated_articles) 

1144 

1145 # Archive the container or the article 

1146 if article is None: 

1147 archive_resource.delay( 

1148 pid, 

1149 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1150 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1151 ) 

1152 

1153 else: 

1154 archive_resource.delay( 

1155 pid, 

1156 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1157 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1158 article_doi=article.doi, 

1159 ) 

1160 # cmd = ptf_cmds.archiveIssuePtfCmd({ 

1161 # "pid": pid, 

1162 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1163 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER}) 

1164 # cmd.set_article(article) # set_article allows archiving only the article 

1165 # cmd.do() 

1166 

1167 elif site == "numdam": 

1168 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

1169 if colid in settings.NUMDAM_COLLECTIONS: 

1170 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

1171 

1172 resolver.copy_binary_files(container, from_folder, settings.NUMDAM_DATA_ROOT) 

1173 for article in container.article_set.all(): 

1174 resolver.copy_binary_files(article, from_folder, settings.NUMDAM_DATA_ROOT) 

1175 

1176 elif status == 503: 

1177 raise ServerUnderMaintenance(response.text) 

1178 else: 

1179 raise RuntimeError(response.text) 

1180 

1181 if djvu_exception: 

1182 raise djvu_exception 

1183 

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

1185 pid = self.kwargs.get("pid", None) 

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

1187 site = self.kwargs.get("site", None) 

1188 

1189 try: 

1190 _, status, message = history_views.execute_and_record_func( 

1191 "deploy", pid, colid, self.internal_do, site 

1192 ) 

1193 except Timeout as exception: 

1194 return HttpResponse(exception, status=408) 

1195 except Exception as exception: 

1196 return HttpResponseServerError(exception) 

1197 

1198 data = {"message": message, "status": status} 

1199 return JsonResponse(data) 

1200 

1201 def update_pcj_editor(self, updated_articles): 

1202 for article in updated_articles: 

1203 data = { 

1204 "date_published": article.date_published.strftime("%Y-%m-%d"), 

1205 "article_number": article.article_number, 

1206 } 

1207 url = "http://pcj-editor.u-ga.fr/submit/api-article-publish/" + article.doi + "/" 

1208 requests.post(url, json=data, verify=False) 

1209 

1210 

1211class DeployTranslatedArticleAPIView(CsrfExemptMixin, View): 

1212 article = None 

1213 

1214 def internal_do(self, *args, **kwargs): 

1215 lang = self.kwargs.get("lang", None) 

1216 

1217 translation = None 

1218 for trans_article in self.article.translations.all(): 

1219 if trans_article.lang == lang: 

1220 translation = trans_article 

1221 

1222 if translation is None: 

1223 raise RuntimeError(f"{self.article.doi} does not exist in {lang}") 

1224 

1225 collection = self.article.get_top_collection() 

1226 colid = collection.pid 

1227 container = self.article.my_container 

1228 

1229 if translation.date_published is None: 

1230 # Add date posted 

1231 cmd = ptf_cmds.publishResourcePtfCmd() 

1232 cmd.set_resource(translation) 

1233 updated_articles = cmd.do() 

1234 

1235 # Recompile PDF to add the date posted 

1236 try: 

1237 tex.create_frontpage(colid, container, updated_articles, test=False, lang=lang) 

1238 except Exception: 

1239 raise PDFException( 

1240 "Unable to compile the article PDF. Please contact the centre Mersenne" 

1241 ) 

1242 

1243 # Unlike regular articles, binary files of translations need to be copied before uploading the XML. 

1244 # The full text in HTML is read by the JATS parser, so the HTML file needs to be present on disk 

1245 resolver.copy_binary_files( 

1246 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

1247 ) 

1248 

1249 # Deploy in prod 

1250 xml = ptf_cmds.exportPtfCmd( 

1251 { 

1252 "pid": self.article.pid, 

1253 "with_djvu": False, 

1254 "article_standalone": True, 

1255 "collection_pid": colid, 

1256 "export_to_website": True, 

1257 "export_folder": settings.LOG_DIR, 

1258 } 

1259 ).do() 

1260 xml_file = io.StringIO(xml) 

1261 files = {"xml": xml_file} 

1262 

1263 server_url = getattr(collection, "website")() 

1264 if not server_url: 

1265 raise RuntimeError("The collection has no website") 

1266 url = server_url + reverse("article_in_issue_upload", kwargs={"pid": container.pid}) 

1267 header = {} 

1268 

1269 try: 

1270 response = requests.post( 

1271 url, headers=header, files=files, verify=False 

1272 ) # verify: ignore TLS certificate 

1273 status = response.status_code 

1274 except requests.exceptions.ConnectionError: 

1275 raise ServerUnderMaintenance( 

1276 "The journal is under maintenance. Please try again later." 

1277 ) 

1278 

1279 # Register translation in Crossref 

1280 if 199 < status < 205: 

1281 if self.article.allow_crossref(): 

1282 try: 

1283 recordDOI(translation) 

1284 except Exception: 

1285 raise DOIException( 

1286 "Error while recording the DOI. Please contact the centre Mersenne" 

1287 ) 

1288 

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

1290 doi = kwargs.get("doi", None) 

1291 self.article = model_helpers.get_article_by_doi(doi) 

1292 if self.article is None: 

1293 raise Http404(f"{doi} does not exist") 

1294 

1295 try: 

1296 _, status, message = history_views.execute_and_record_func( 

1297 "deploy", 

1298 self.article.pid, 

1299 self.article.get_top_collection().pid, 

1300 self.internal_do, 

1301 "website", 

1302 ) 

1303 except Timeout as exception: 

1304 return HttpResponse(exception, status=408) 

1305 except Exception as exception: 

1306 return HttpResponseServerError(exception) 

1307 

1308 data = {"message": message, "status": status} 

1309 return JsonResponse(data) 

1310 

1311 

1312class DeleteJatsIssueAPIView(View): 

1313 # TODO ? rename in DeleteJatsContainerAPIView mais fonctionne tel quel pour book* 

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

1315 pid = self.kwargs.get("pid", None) 

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

1317 site = self.kwargs.get("site", None) 

1318 message = "Le volume a bien été supprimé" 

1319 status = 200 

1320 

1321 issue = model_helpers.get_container(pid) 

1322 if not issue: 

1323 raise Http404(f"{pid} does not exist") 

1324 try: 

1325 mersenneSite = model_helpers.get_site_mersenne(colid) 

1326 

1327 if site == "ptf_tools": 

1328 if issue.is_deployed(mersenneSite): 

1329 issue.undeploy(mersenneSite) 

1330 for article in issue.article_set.all(): 

1331 article.undeploy(mersenneSite) 

1332 

1333 p = model_helpers.get_provider("mathdoc-id") 

1334 

1335 cmd = ptf_cmds.addContainerPtfCmd( 

1336 { 

1337 "pid": issue.pid, 

1338 "ctype": "issue", 

1339 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

1340 } 

1341 ) 

1342 cmd.set_provider(p) 

1343 cmd.add_collection(issue.get_collection()) 

1344 cmd.set_object_to_be_deleted(issue) 

1345 cmd.undo() 

1346 

1347 else: 

1348 if site == "numdam": 

1349 server_url = settings.NUMDAM_PRE_URL 

1350 else: 

1351 collection = issue.get_collection() 

1352 server_url = getattr(collection, site)() 

1353 

1354 if not server_url: 

1355 message = "The collection has no " + site 

1356 status = 500 

1357 else: 

1358 url = server_url + reverse("issue_delete", kwargs={"pid": pid}) 

1359 response = requests.delete(url, verify=False) 

1360 status = response.status_code 

1361 

1362 if status == 404: 

1363 message = "Le serveur retourne un code 404. Vérifier que le volume soit bien sur le serveur" 

1364 elif status > 204: 

1365 body = response.text.encode("utf8") 

1366 message = body[:1000] 

1367 else: 

1368 status = 200 

1369 # unpublish issue in collection site (site_register.json) 

1370 if site == "website": 

1371 if issue.is_deployed(mersenneSite): 

1372 issue.undeploy(mersenneSite) 

1373 for article in issue.article_set.all(): 

1374 article.undeploy(mersenneSite) 

1375 # delete article binary files 

1376 folder = article.get_relative_folder() 

1377 resolver.delete_object_folder( 

1378 folder, 

1379 to_folder=settings.MERSENNE_PROD_DATA_FORLDER, 

1380 ) 

1381 # delete issue binary files 

1382 folder = issue.get_relative_folder() 

1383 resolver.delete_object_folder( 

1384 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER 

1385 ) 

1386 

1387 except Timeout as exception: 

1388 return HttpResponse(exception, status=408) 

1389 except Exception as exception: 

1390 return HttpResponseServerError(exception) 

1391 

1392 data = {"message": message, "status": status} 

1393 return JsonResponse(data) 

1394 

1395 

1396class ArchiveIssueAPIView(View): 

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

1398 try: 

1399 pid = kwargs["pid"] 

1400 colid = kwargs["colid"] 

1401 except IndexError: 

1402 raise Http404 

1403 

1404 try: 

1405 cmd = ptf_cmds.archiveIssuePtfCmd( 

1406 { 

1407 "pid": pid, 

1408 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1409 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

1410 "needs_publication_date": True, 

1411 } 

1412 ) 

1413 result_, status, message = history_views.execute_and_record_func( 

1414 "archive", pid, colid, cmd.do 

1415 ) 

1416 except Exception as exception: 

1417 return HttpResponseServerError(exception) 

1418 

1419 data = {"message": message, "status": 200} 

1420 return JsonResponse(data) 

1421 

1422 

1423class CreateDjvuAPIView(View): 

1424 def internal_do(self, *args, **kwargs): 

1425 pid = self.kwargs.get("pid", None) 

1426 

1427 resource = model_helpers.get_resource(pid) 

1428 cmd = ptf_cmds.addDjvuPtfCmd() 

1429 cmd.set_resource(resource) 

1430 cmd.do() 

1431 

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

1433 pid = self.kwargs.get("pid", None) 

1434 colid = pid.split("_")[0] 

1435 

1436 try: 

1437 _, status, message = history_views.execute_and_record_func( 

1438 "numdam", pid, colid, self.internal_do 

1439 ) 

1440 except Exception as exception: 

1441 return HttpResponseServerError(exception) 

1442 

1443 data = {"message": message, "status": status} 

1444 return JsonResponse(data) 

1445 

1446 

1447class PTFToolsHomeView(LoginRequiredMixin, View): 

1448 """ 

1449 Home Page. 

1450 - Admin & staff -> Render blank home.html 

1451 - User with unique authorized collection -> Redirect to collection details page 

1452 - User with multiple authorized collections -> Render home.html with data 

1453 - Comment moderator -> Comments dashboard 

1454 - Others -> 404 response 

1455 """ 

1456 

1457 def get(self, request, *args, **kwargs) -> HttpResponse: 

1458 # Staff or user with authorized collections 

1459 if request.user.is_staff or request.user.is_superuser: 

1460 return render(request, "home.html") 

1461 

1462 colids = get_authorized_collections(request.user) 

1463 is_mod = is_comment_moderator(request.user) 

1464 

1465 # The user has no rights 

1466 if not (colids or is_mod): 

1467 raise Http404("No collections associated with your account.") 

1468 # Comment moderator only 

1469 elif not colids: 

1470 return HttpResponseRedirect(reverse("comment_list")) 

1471 

1472 # User with unique collection -> Redirect to collection detail page 

1473 if len(colids) == 1 or getattr(settings, "COMMENTS_DISABLED", False): 

1474 return HttpResponseRedirect(reverse("collection-detail", kwargs={"pid": colids[0]})) 

1475 

1476 # User with multiple authorized collections - Special home 

1477 context = {} 

1478 context["overview"] = True 

1479 

1480 all_collections = Collection.objects.filter(pid__in=colids).values("pid", "title_html") 

1481 all_collections = {c["pid"]: c for c in all_collections} 

1482 

1483 # Comments summary 

1484 try: 

1485 error, comments_data = get_comments_for_home(request.user) 

1486 except AttributeError: 

1487 error, comments_data = True, {} 

1488 

1489 context["comment_server_ok"] = False 

1490 

1491 if not error: 

1492 context["comment_server_ok"] = True 

1493 if comments_data: 

1494 for col_id, comment_nb in comments_data.items(): 

1495 if col_id.upper() in all_collections: 1495 ↛ 1494line 1495 didn't jump to line 1494 because the condition on line 1495 was always true

1496 all_collections[col_id.upper()]["pending_comments"] = comment_nb 

1497 

1498 # TODO: Translations summary 

1499 context["translation_server_ok"] = False 

1500 

1501 # Sort the collections according to the number of pending comments 

1502 context["collections"] = sorted( 

1503 all_collections.values(), key=lambda col: col.get("pending_comments", -1), reverse=True 

1504 ) 

1505 

1506 return render(request, "home.html", context) 

1507 

1508 

1509class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin): 

1510 columns = 5 

1511 

1512 def get_common_context_data(self, **kwargs): 

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

1514 now = timezone.now() 

1515 curyear = now.year 

1516 years = range(curyear - self.columns + 1, curyear + 1) 

1517 

1518 context["collections"] = settings.MERSENNE_COLLECTIONS 

1519 context["containers_to_be_published"] = [] 

1520 context["last_col_events"] = [] 

1521 

1522 event = get_history_last_event_by("clockss", "ALL") 

1523 clockss_gap = get_gap(now, event) 

1524 

1525 context["years"] = years 

1526 context["clockss_gap"] = clockss_gap 

1527 

1528 return context 

1529 

1530 def calculate_articles_and_pages(self, pid, years): 

1531 data_by_year = [] 

1532 total_articles = [0] * len(years) 

1533 total_pages = [0] * len(years) 

1534 

1535 for year in years: 

1536 articles = self.get_articles_for_year(pid, year) 

1537 articles_count = articles.count() 

1538 page_count = sum(article.get_article_page_count() for article in articles) 

1539 

1540 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count}) 

1541 total_articles[year - years[0]] += articles_count 

1542 total_pages[year - years[0]] += page_count 

1543 

1544 return data_by_year, total_articles, total_pages 

1545 

1546 def get_articles_for_year(self, pid, year): 

1547 return Article.objects.filter( 

1548 Q(my_container__my_collection__pid=pid) 

1549 & ( 

1550 Q(date_published__year=year, date_online_first__isnull=True) 

1551 | Q(date_online_first__year=year) 

1552 ) 

1553 ).prefetch_related("resourcecount_set") 

1554 

1555 

1556class PublishedArticlesDashboardView(BaseMersenneDashboardView): 

1557 template_name = "dashboard/published_articles.html" 

1558 

1559 def get_context_data(self, **kwargs): 

1560 context = self.get_common_context_data(**kwargs) 

1561 years = context["years"] 

1562 

1563 published_articles = [] 

1564 total_published_articles = [ 

1565 {"year": year, "total_articles": 0, "total_pages": 0} for year in years 

1566 ] 

1567 

1568 for pid in settings.MERSENNE_COLLECTIONS: 

1569 if pid != "MERSENNE": 

1570 articles_data, total_articles, total_pages = self.calculate_articles_and_pages( 

1571 pid, years 

1572 ) 

1573 published_articles.append({"pid": pid, "years": articles_data}) 

1574 

1575 for i, year in enumerate(years): 

1576 total_published_articles[i]["total_articles"] += total_articles[i] 

1577 total_published_articles[i]["total_pages"] += total_pages[i] 

1578 

1579 context["published_articles"] = published_articles 

1580 context["total_published_articles"] = total_published_articles 

1581 

1582 return context 

1583 

1584 

1585class CreatedVolumesDashboardView(BaseMersenneDashboardView): 

1586 template_name = "dashboard/created_volumes.html" 

1587 

1588 def get_context_data(self, **kwargs): 

1589 context = self.get_common_context_data(**kwargs) 

1590 years = context["years"] 

1591 

1592 created_volumes = [] 

1593 total_created_volumes = [ 

1594 {"year": year, "total_articles": 0, "total_pages": 0} for year in years 

1595 ] 

1596 

1597 for pid in settings.MERSENNE_COLLECTIONS: 

1598 if pid != "MERSENNE": 

1599 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages( 

1600 pid, years 

1601 ) 

1602 created_volumes.append({"pid": pid, "years": volumes_data}) 

1603 

1604 for i, year in enumerate(years): 

1605 total_created_volumes[i]["total_articles"] += total_articles[i] 

1606 total_created_volumes[i]["total_pages"] += total_pages[i] 

1607 

1608 context["created_volumes"] = created_volumes 

1609 context["total_created_volumes"] = total_created_volumes 

1610 

1611 return context 

1612 

1613 def calculate_volumes_and_pages(self, pid, years): 

1614 data_by_year = [] 

1615 total_articles = [0] * len(years) 

1616 total_pages = [0] * len(years) 

1617 

1618 for year in years: 

1619 issues = Container.objects.filter(my_collection__pid=pid, year=year) 

1620 articles_count = 0 

1621 page_count = 0 

1622 

1623 for issue in issues: 

1624 articles = issue.article_set.filter( 

1625 Q(date_published__isnull=False) | Q(date_online_first__isnull=False) 

1626 ).prefetch_related("resourcecount_set") 

1627 

1628 articles_count += articles.count() 

1629 page_count += sum(article.get_article_page_count() for article in articles) 

1630 

1631 data_by_year.append({"year": year, "articles": articles_count, "pages": page_count}) 

1632 total_articles[year - years[0]] += articles_count 

1633 total_pages[year - years[0]] += page_count 

1634 

1635 return data_by_year, total_articles, total_pages 

1636 

1637 

1638class ReferencingChoice(View): 

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

1640 if request.POST.get("optSite") == "ads": 

1641 return redirect( 

1642 reverse("referencingAds", kwargs={"colid": request.POST.get("selectCol")}) 

1643 ) 

1644 elif request.POST.get("optSite") == "wos": 

1645 comp = ReferencingCheckerWos() 

1646 journal = comp.make_journal(request.POST.get("selectCol")) 

1647 if journal is None: 

1648 return render( 

1649 request, 

1650 "dashboard/referencing.html", 

1651 { 

1652 "error": "Collection not found", 

1653 "colid": request.POST.get("selectCol"), 

1654 "optSite": request.POST.get("optSite"), 

1655 }, 

1656 ) 

1657 return render( 

1658 request, 

1659 "dashboard/referencing.html", 

1660 { 

1661 "journal": journal, 

1662 "colid": request.POST.get("selectCol"), 

1663 "optSite": request.POST.get("optSite"), 

1664 }, 

1665 ) 

1666 

1667 

1668class ReferencingWosFileView(View): 

1669 template_name = "dashboard/referencing.html" 

1670 

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

1672 colid = request.POST["colid"] 

1673 if request.FILES.get("risfile") is None: 

1674 message = "No file uploaded" 

1675 return render( 

1676 request, self.template_name, {"message": message, "colid": colid, "optSite": "wos"} 

1677 ) 

1678 uploaded_file = request.FILES["risfile"] 

1679 comp = ReferencingCheckerWos() 

1680 journal = comp.check_references(colid, uploaded_file) 

1681 return render(request, self.template_name, {"journal": journal}) 

1682 

1683 

1684class ReferencingDashboardView(BaseMersenneDashboardView): 

1685 template_name = "dashboard/referencing.html" 

1686 

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

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

1689 comp = ReferencingCheckerAds() 

1690 journal = comp.check_references(colid) 

1691 return render(request, self.template_name, {"journal": journal}) 

1692 

1693 

1694class BaseCollectionView(TemplateView): 

1695 def get_context_data(self, **kwargs): 

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

1697 aid = context.get("aid") 

1698 year = context.get("year") 

1699 

1700 if aid and year: 

1701 context["collection"] = self.get_collection(aid, year) 

1702 

1703 return context 

1704 

1705 def get_collection(self, aid, year): 

1706 """Method to be overridden by subclasses to fetch the appropriate collection""" 

1707 raise NotImplementedError("Subclasses must implement get_collection method") 

1708 

1709 

1710class ArticleListView(BaseCollectionView): 

1711 template_name = "collection-list.html" 

1712 

1713 def get_collection(self, aid, year): 

1714 return Article.objects.filter( 

1715 Q(my_container__my_collection__pid=aid) 

1716 & ( 

1717 Q(date_published__year=year, date_online_first__isnull=True) 

1718 | Q(date_online_first__year=year) 

1719 ) 

1720 ).prefetch_related("resourcecount_set") 

1721 

1722 

1723class VolumeListView(BaseCollectionView): 

1724 template_name = "collection-list.html" 

1725 

1726 def get_collection(self, aid, year): 

1727 return Article.objects.filter( 

1728 Q(my_container__my_collection__pid=aid, my_container__year=year) 

1729 & (Q(date_published__isnull=False) | Q(date_online_first__isnull=False)) 

1730 ).prefetch_related("resourcecount_set") 

1731 

1732 

1733class DOAJResourceRegisterView(View): 

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

1735 pid = kwargs.get("pid", None) 

1736 resource = model_helpers.get_resource(pid) 

1737 if resource is None: 

1738 raise Http404 

1739 if resource.container.pid == settings.ISSUE_PENDING_PUBLICATION_PIDS.get( 

1740 resource.colid, None 

1741 ): 

1742 raise RuntimeError("Pending publications should not be deployed") 

1743 

1744 try: 

1745 data = {} 

1746 doaj_meta, response = doaj_pid_register(pid) 

1747 if response is None: 

1748 return HttpResponse(status=204) 

1749 elif doaj_meta and 200 <= response.status_code <= 299: 

1750 data.update(doaj_meta) 

1751 else: 

1752 return HttpResponse(status=response.status_code, reason=response.text) 

1753 except Timeout as exception: 

1754 return HttpResponse(exception, status=408) 

1755 except Exception as exception: 

1756 return HttpResponseServerError(exception) 

1757 return JsonResponse(data) 

1758 

1759 

1760class ConvertArticleTexToXmlAndUpdateBodyView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

1761 """ 

1762 Launch asynchronous conversion of article TeX -> XML -> body_html/body_xml 

1763 """ 

1764 

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

1766 pid = kwargs.get("pid") 

1767 if not pid: 

1768 raise Http404("Missing pid") 

1769 

1770 article = Article.objects.filter(pid=pid).first() 

1771 if not article: 

1772 raise Http404(f"Article not found: {pid}") 

1773 

1774 colid = article.get_collection().pid 

1775 if colid in settings.EXCLUDED_TEX_CONVERSION_COLLECTIONS: 

1776 return JsonResponse( 

1777 {"status": 403, "message": f"Tex conversions are disabled in {colid}"} 

1778 ) 

1779 

1780 if is_tex_conversion_locked(pid): 

1781 logger.warning("Conversion rejected (lock exists) for %s", pid) 

1782 return JsonResponse( 

1783 {"status": 409, "message": f"A conversion is already running for {pid}"} 

1784 ) 

1785 

1786 logger.info("No lock → scheduling conversion for %s", pid) 

1787 

1788 try: 

1789 convert_article_tex.delay(pid=pid, user_pk=request.user.pk) 

1790 except Exception: 

1791 logger.exception("Failed to enqueue task for %s", pid) 

1792 release_tex_conversion_lock(pid) 

1793 raise 

1794 

1795 return JsonResponse({"status": 200, "message": f"[{pid}]\n → Conversion started"}) 

1796 

1797 

1798class CROSSREFResourceRegisterView(View): 

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

1800 pid = kwargs.get("pid", None) 

1801 # option force for registering doi of articles without date_published (ex; TSG from Numdam) 

1802 force = kwargs.get("force", None) 

1803 if not request.user.is_superuser: 

1804 force = None 

1805 

1806 resource = model_helpers.get_resource(pid) 

1807 if resource is None: 

1808 raise Http404 

1809 

1810 resource = resource.cast() 

1811 meth = getattr(self, "recordDOI" + resource.classname) 

1812 try: 

1813 data = meth(resource, force) 

1814 except Timeout as exception: 

1815 return HttpResponse(exception, status=408) 

1816 except Exception as exception: 

1817 return HttpResponseServerError(exception) 

1818 return JsonResponse(data) 

1819 

1820 def recordDOIArticle(self, article, force=None): 

1821 result = {"status": 404} 

1822 if ( 

1823 article.doi 

1824 and not article.do_not_publish 

1825 and (article.date_published or article.date_online_first or force == "force") 

1826 ): 

1827 if article.my_container.year is None: # or article.my_container.year == '0': 

1828 article.my_container.year = datetime.now().strftime("%Y") 

1829 result = recordDOI(article) 

1830 return result 

1831 

1832 def recordDOICollection(self, collection, force=None): 

1833 return recordDOI(collection) 

1834 

1835 def recordDOIContainer(self, container, force=None): 

1836 data = {"status": 200, "message": "tout va bien"} 

1837 

1838 if container.ctype == "issue": 

1839 if container.doi: 

1840 result = recordDOI(container) 

1841 if result["status"] != 200: 

1842 return result 

1843 if force == "force": 

1844 articles = container.article_set.exclude( 

1845 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True 

1846 ) 

1847 else: 

1848 articles = container.article_set.exclude( 

1849 doi__isnull=True, 

1850 do_not_publish=True, 

1851 date_published__isnull=True, 

1852 date_online_first__isnull=True, 

1853 ) 

1854 

1855 for article in articles: 

1856 result = self.recordDOIArticle(article, force) 

1857 if result["status"] != 200: 

1858 data = result 

1859 else: 

1860 return recordDOI(container) 

1861 return data 

1862 

1863 

1864class CROSSREFResourceCheckStatusView(View): 

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

1866 pid = kwargs.get("pid", None) 

1867 resource = model_helpers.get_resource(pid) 

1868 if resource is None: 

1869 raise Http404 

1870 resource = resource.cast() 

1871 meth = getattr(self, "checkDOI" + resource.classname) 

1872 try: 

1873 meth(resource) 

1874 except Timeout as exception: 

1875 return HttpResponse(exception, status=408) 

1876 except Exception as exception: 

1877 return HttpResponseServerError(exception) 

1878 

1879 data = {"status": 200, "message": "tout va bien"} 

1880 return JsonResponse(data) 

1881 

1882 def checkDOIArticle(self, article): 

1883 if article.my_container.year is None or article.my_container.year == "0": 

1884 article.my_container.year = datetime.now().strftime("%Y") 

1885 checkDOI(article) 

1886 

1887 def checkDOICollection(self, collection): 

1888 checkDOI(collection) 

1889 

1890 def checkDOIContainer(self, container): 

1891 if container.doi is not None: 

1892 checkDOI(container) 

1893 for article in container.article_set.all(): 

1894 self.checkDOIArticle(article) 

1895 

1896 

1897class CROSSREFResourcePendingPublicationRegisterView(View): 

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

1899 pid = kwargs.get("pid", None) 

1900 # option force for registering doi of articles without date_published (ex; TSG from Numdam) 

1901 

1902 resource = model_helpers.get_resource(pid) 

1903 if resource is None: 

1904 raise Http404 

1905 

1906 resource = resource.cast() 

1907 meth = getattr(self, "recordPendingPublication" + resource.classname) 

1908 try: 

1909 data = meth(resource) 

1910 except Timeout as exception: 

1911 return HttpResponse(exception, status=408) 

1912 except Exception as exception: 

1913 return HttpResponseServerError(exception) 

1914 return JsonResponse(data) 

1915 

1916 def recordPendingPublicationArticle(self, article): 

1917 result = {"status": 404} 

1918 if article.doi and not article.date_published and not article.date_online_first: 

1919 if article.my_container.year is None: # or article.my_container.year == '0': 

1920 article.my_container.year = datetime.now().strftime("%Y") 

1921 result = recordPendingPublication(article) 

1922 return result 

1923 

1924 

1925class RegisterPubmedFormView(FormView): 

1926 template_name = "record_pubmed_dialog.html" 

1927 form_class = RegisterPubmedForm 

1928 

1929 def get_context_data(self, **kwargs): 

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

1931 context["pid"] = self.kwargs["pid"] 

1932 context["helper"] = PtfLargeModalFormHelper 

1933 return context 

1934 

1935 

1936class RegisterPubmedView(View): 

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

1938 pid = kwargs.get("pid", None) 

1939 update_article = self.request.GET.get("update_article", "on") == "on" 

1940 

1941 article = model_helpers.get_article(pid) 

1942 if article is None: 

1943 raise Http404 

1944 try: 

1945 recordPubmed(article, update_article) 

1946 except Exception as exception: 

1947 messages.error("Unable to register the article in PubMed") 

1948 return HttpResponseServerError(exception) 

1949 

1950 return HttpResponseRedirect( 

1951 reverse("issue-items", kwargs={"pid": article.my_container.pid}) 

1952 ) 

1953 

1954 

1955class PTFToolsContainerView(TemplateView): 

1956 template_name = "" 

1957 

1958 def get_context_data(self, **kwargs): 

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

1960 

1961 container = model_helpers.get_container(self.kwargs.get("pid")) 

1962 if container is None: 

1963 raise Http404 

1964 citing_articles = container.citations() 

1965 source = self.request.GET.get("source", None) 

1966 if container.ctype.startswith("book"): 

1967 book_parts = ( 

1968 container.article_set.filter(sites__id=settings.SITE_ID).all().order_by("seq") 

1969 ) 

1970 references = False 

1971 if container.ctype == "book-monograph": 

1972 # on regarde si il y a au moins une bibliographie 

1973 for art in container.article_set.all(): 

1974 if art.bibitem_set.count() > 0: 

1975 references = True 

1976 context.update( 

1977 { 

1978 "book": container, 

1979 "book_parts": list(book_parts), 

1980 "source": source, 

1981 "citing_articles": citing_articles, 

1982 "references": references, 

1983 "test_website": container.get_top_collection() 

1984 .extlink_set.get(rel="test_website") 

1985 .location, 

1986 "prod_website": container.get_top_collection() 

1987 .extlink_set.get(rel="website") 

1988 .location, 

1989 } 

1990 ) 

1991 self.template_name = "book-toc.html" 

1992 else: 

1993 articles = container.article_set.all().order_by("seq") 

1994 for article in articles: 

1995 try: 

1996 last_match = ( 

1997 history_models.HistoryEvent.objects.filter( 

1998 pid=article.pid, 

1999 type="matching", 

2000 ) 

2001 .only("created_on") 

2002 .latest("created_on") 

2003 ) 

2004 except history_models.HistoryEvent.DoesNotExist as _: 

2005 article.last_match = None 

2006 else: 

2007 article.last_match = last_match.created_on 

2008 

2009 # article1 = articles.first() 

2010 # date = article1.deployed_date() 

2011 # TODO next_issue, previous_issue 

2012 

2013 # check DOI est maintenant une commande à part 

2014 # # specific PTFTools : on regarde pour chaque article l'état de l'enregistrement DOI 

2015 # articlesWithStatus = [] 

2016 # for article in articles: 

2017 # checkDOIExistence(article) 

2018 # articlesWithStatus.append(article) 

2019 

2020 test_location = prod_location = "" 

2021 qs = container.get_top_collection().extlink_set.filter(rel="test_website") 

2022 if qs: 

2023 test_location = qs.first().location 

2024 qs = container.get_top_collection().extlink_set.filter(rel="website") 

2025 if qs: 

2026 prod_location = qs.first().location 

2027 context.update( 

2028 { 

2029 "issue": container, 

2030 "articles": articles, 

2031 "source": source, 

2032 "citing_articles": citing_articles, 

2033 "test_website": test_location, 

2034 "prod_website": prod_location, 

2035 } 

2036 ) 

2037 

2038 if container.pid in settings.ISSUE_PENDING_PUBLICATION_PIDS.values(): 

2039 context["is_issue_pending_publication"] = True 

2040 self.template_name = "issue-items.html" 

2041 

2042 context["allow_crossref"] = container.allow_crossref() 

2043 context["coltype"] = container.my_collection.coltype 

2044 return context 

2045 

2046 

2047class ExtLinkInline(InlineFormSetFactory): 

2048 model = ExtLink 

2049 form_class = ExtLinkForm 

2050 factory_kwargs = {"extra": 0} 

2051 

2052 

2053class ResourceIdInline(InlineFormSetFactory): 

2054 model = ResourceId 

2055 form_class = ResourceIdForm 

2056 factory_kwargs = {"extra": 0} 

2057 

2058 

2059class IssueDetailAPIView(View): 

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

2061 issue = get_object_or_404(Container, pid=kwargs["pid"]) 

2062 deployed_date = issue.deployed_date() 

2063 result = { 

2064 "deployed_date": timezone.localtime(deployed_date).strftime("%Y-%m-%d %H:%M") 

2065 if deployed_date 

2066 else None, 

2067 "last_modified": timezone.localtime(issue.last_modified).strftime("%Y-%m-%d %H:%M"), 

2068 "all_doi_are_registered": issue.all_doi_are_registered(), 

2069 "registered_in_doaj": issue.registered_in_doaj(), 

2070 "doi": issue.my_collection.doi, 

2071 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(), 

2072 } 

2073 try: 

2074 latest = get_last_unsolved_error(pid=issue.pid, strict=False) 

2075 except history_models.HistoryEvent.DoesNotExist as _: 

2076 pass 

2077 else: 

2078 result["latest"] = latest.message 

2079 result["latest_date"] = timezone.localtime(latest.created_on).strftime( 

2080 "%Y-%m-%d %H:%M" 

2081 ) 

2082 

2083 result["latest_type"] = latest.type.capitalize() 

2084 for event_type in ["matching", "edit", "deploy", "archive", "import"]: 

2085 try: 

2086 result[event_type] = timezone.localtime( 

2087 history_models.HistoryEvent.objects.filter( 

2088 type=event_type, 

2089 status="OK", 

2090 pid__startswith=issue.pid, 

2091 ) 

2092 .latest("created_on") 

2093 .created_on 

2094 ).strftime("%Y-%m-%d %H:%M") 

2095 except history_models.HistoryEvent.DoesNotExist as _: 

2096 result[event_type] = "" 

2097 return JsonResponse(result) 

2098 

2099 

2100class CollectionFormView(LoginRequiredMixin, StaffuserRequiredMixin, NamedFormsetsMixin, View): 

2101 model = Collection 

2102 form_class = CollectionForm 

2103 inlines = [ResourceIdInline, ExtLinkInline] 

2104 inlines_names = ["resource_ids_form", "ext_links_form"] 

2105 

2106 def get_context_data(self, **kwargs): 

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

2108 context["helper"] = PtfFormHelper 

2109 context["formset_helper"] = FormSetHelper 

2110 return context 

2111 

2112 def add_description(self, collection, description, lang, seq): 

2113 if description: 

2114 la = Abstract( 

2115 resource=collection, 

2116 tag="description", 

2117 lang=lang, 

2118 seq=seq, 

2119 value_xml=f'<description xml:lang="{lang}">{replace_html_entities(description)}</description>', 

2120 value_html=description, 

2121 value_tex=description, 

2122 ) 

2123 la.save() 

2124 

2125 def form_valid(self, form): 

2126 if form.instance.abbrev: 

2127 form.instance.title_xml = f"<title-group><title>{form.instance.title_tex}</title><abbrev-title>{form.instance.abbrev}</abbrev-title></title-group>" 

2128 else: 

2129 form.instance.title_xml = ( 

2130 f"<title-group><title>{form.instance.title_tex}</title></title-group>" 

2131 ) 

2132 

2133 form.instance.title_html = form.instance.title_tex 

2134 form.instance.title_sort = form.instance.title_tex 

2135 result = super().form_valid(form) 

2136 

2137 collection = self.object 

2138 collection.abstract_set.all().delete() 

2139 

2140 seq = 1 

2141 description = form.cleaned_data["description_en"] 

2142 if description: 

2143 self.add_description(collection, description, "en", seq) 

2144 seq += 1 

2145 description = form.cleaned_data["description_fr"] 

2146 if description: 

2147 self.add_description(collection, description, "fr", seq) 

2148 

2149 return result 

2150 

2151 def get_success_url(self): 

2152 messages.success(self.request, "La Collection a été modifiée avec succès") 

2153 return reverse("collection-detail", kwargs={"pid": self.object.pid}) 

2154 

2155 

2156class CollectionCreate(CollectionFormView, CreateWithInlinesView): 

2157 """ 

2158 Warning : Not yet finished 

2159 Automatic site membership creation is still missing 

2160 """ 

2161 

2162 

2163class CollectionUpdate(CollectionFormView, UpdateWithInlinesView): 

2164 slug_field = "pid" 

2165 slug_url_kwarg = "pid" 

2166 

2167 

2168def suggest_load_journal_dois(colid): 

2169 articles = ( 

2170 Article.objects.filter(my_container__my_collection__pid=colid) 

2171 .filter(doi__isnull=False) 

2172 .filter(Q(date_published__isnull=False) | Q(date_online_first__isnull=False)) 

2173 .values_list("doi", flat=True) 

2174 ) 

2175 

2176 try: 

2177 articles = sorted( 

2178 articles, 

2179 key=lambda d: ( 

2180 re.search(r"([a-zA-Z]+).\d+$", d).group(1), 

2181 int(re.search(r".(\d+)$", d).group(1)), 

2182 ), 

2183 ) 

2184 except: # noqa: E722 (we'll look later) 

2185 pass 

2186 return [f'<option value="{doi}">' for doi in articles] 

2187 

2188 

2189def get_context_with_volumes(journal): 

2190 result = model_helpers.get_volumes_in_collection(journal) 

2191 volume_count = result["volume_count"] 

2192 collections = [] 

2193 for ancestor in journal.ancestors.all(): 

2194 item = model_helpers.get_volumes_in_collection(ancestor) 

2195 volume_count = max(0, volume_count) 

2196 item.update({"journal": ancestor}) 

2197 collections.append(item) 

2198 

2199 # add the parent collection to its children list and sort it by date 

2200 result.update({"journal": journal}) 

2201 collections.append(result) 

2202 

2203 collections = [c for c in collections if c["sorted_issues"]] 

2204 collections.sort( 

2205 key=lambda ancestor: ancestor["sorted_issues"][0]["volumes"][0]["lyear"], 

2206 reverse=True, 

2207 ) 

2208 

2209 context = { 

2210 "journal": journal, 

2211 "sorted_issues": result["sorted_issues"], 

2212 "volume_count": volume_count, 

2213 "max_width": result["max_width"], 

2214 "collections": collections, 

2215 "choices": "\n".join(suggest_load_journal_dois(journal.pid)), 

2216 } 

2217 return context 

2218 

2219 

2220class CollectionDetail( 

2221 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin 

2222): 

2223 model = Collection 

2224 slug_field = "pid" 

2225 slug_url_kwarg = "pid" 

2226 template_name = "ptf/collection_detail.html" 

2227 

2228 def test_func(self): 

2229 return is_authorized_editor(self.request.user, self.kwargs.get("pid")) 

2230 

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

2232 self.object = self.get_object(queryset=Collection.objects.all()) 

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

2234 

2235 def get_context_data(self, **kwargs): 

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

2237 context["object_list"] = context["object_list"].filter( 

2238 Q(ctype="issue") | Q(ctype="book-lecture-notes") 

2239 ) 

2240 context["special_issues_user"] = self.object.pid in settings.SPECIAL_ISSUES_USERS 

2241 context.update(get_context_with_volumes(self.object)) 

2242 

2243 if self.object.pid in settings.ISSUE_TO_APPEAR_PIDS: 

2244 context["issue_to_appear_pid"] = settings.ISSUE_TO_APPEAR_PIDS[self.object.pid] 

2245 context["issue_to_appear"] = Container.objects.filter( 

2246 pid=context["issue_to_appear_pid"] 

2247 ).exists() 

2248 try: 

2249 latest_error = history_models.HistoryEvent.objects.filter( 

2250 status="ERROR", col=self.object 

2251 ).latest("created_on") 

2252 except history_models.HistoryEvent.DoesNotExist as _: 

2253 pass 

2254 else: 

2255 message = latest_error.message 

2256 if message: 

2257 i = message.find(" - ") 

2258 latest_exception = message[:i] 

2259 latest_error_message = message[i + 3 :] 

2260 context["latest_exception"] = latest_exception 

2261 context["latest_exception_date"] = latest_error.created_on 

2262 context["latest_exception_type"] = latest_error.type 

2263 context["latest_error_message"] = latest_error_message 

2264 

2265 archive_in_error = history_models.HistoryEvent.objects.filter( 

2266 status="ERROR", col=self.object, type="archive" 

2267 ).exists() 

2268 

2269 context["archive_in_error"] = archive_in_error 

2270 

2271 return context 

2272 

2273 def get_queryset(self): 

2274 query = self.object.content.all() 

2275 

2276 for ancestor in self.object.ancestors.all(): 

2277 query |= ancestor.content.all() 

2278 

2279 return query.order_by("-year", "-vseries", "-volume", "-volume_int", "-number_int") 

2280 

2281 

2282class ContainerEditView(FormView): 

2283 template_name = "container_form.html" 

2284 form_class = ContainerForm 

2285 

2286 def get_success_url(self): 

2287 if self.kwargs["pid"]: 

2288 return reverse("issue-items", kwargs={"pid": self.kwargs["pid"]}) 

2289 return reverse("mersenne_dashboard/published_articles") 

2290 

2291 def set_success_message(self): # pylint: disable=no-self-use 

2292 messages.success(self.request, "Le fascicule a été modifié") 

2293 

2294 def get_form_kwargs(self): 

2295 kwargs = super().get_form_kwargs() 

2296 if "pid" not in self.kwargs: 

2297 self.kwargs["pid"] = None 

2298 if "colid" not in self.kwargs: 

2299 self.kwargs["colid"] = None 

2300 if "data" in kwargs and "colid" in kwargs["data"]: 

2301 # colid is passed as a hidden param in the form. 

2302 # It is used when you submit a new container 

2303 self.kwargs["colid"] = kwargs["data"]["colid"] 

2304 

2305 self.kwargs["container"] = kwargs["container"] = model_helpers.get_container( 

2306 self.kwargs["pid"] 

2307 ) 

2308 return kwargs 

2309 

2310 def get_context_data(self, **kwargs): 

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

2312 

2313 context["pid"] = self.kwargs["pid"] 

2314 context["colid"] = self.kwargs["colid"] 

2315 context["container"] = self.kwargs["container"] 

2316 

2317 context["edit_container"] = context["pid"] is not None 

2318 context["name"] = resolve(self.request.path_info).url_name 

2319 

2320 return context 

2321 

2322 def form_valid(self, form): 

2323 new_pid = form.cleaned_data.get("pid") 

2324 new_title = form.cleaned_data.get("title") 

2325 new_trans_title = form.cleaned_data.get("trans_title") 

2326 new_publisher = form.cleaned_data.get("publisher") 

2327 new_year = form.cleaned_data.get("year") 

2328 new_volume = form.cleaned_data.get("volume") 

2329 new_number = form.cleaned_data.get("number") 

2330 

2331 collection = None 

2332 issue = self.kwargs["container"] 

2333 if issue is not None: 

2334 collection = issue.my_collection 

2335 elif self.kwargs["colid"] is not None: 

2336 if "CR" in self.kwargs["colid"]: 

2337 collection = model_helpers.get_collection(self.kwargs["colid"], sites=False) 

2338 else: 

2339 collection = model_helpers.get_collection(self.kwargs["colid"]) 

2340 

2341 if collection is None: 

2342 raise ValueError("Collection for " + new_pid + " does not exist") 

2343 

2344 # Icon 

2345 new_icon_location = "" 

2346 if "icon" in self.request.FILES: 

2347 filename = os.path.basename(self.request.FILES["icon"].name) 

2348 file_extension = filename.split(".")[1] 

2349 

2350 icon_filename = resolver.get_disk_location( 

2351 settings.MERSENNE_TEST_DATA_FOLDER, 

2352 collection.pid, 

2353 file_extension, 

2354 new_pid, 

2355 None, 

2356 True, 

2357 ) 

2358 

2359 with open(icon_filename, "wb+") as destination: 

2360 for chunk in self.request.FILES["icon"].chunks(): 

2361 destination.write(chunk) 

2362 

2363 folder = resolver.get_relative_folder(collection.pid, new_pid) 

2364 new_icon_location = os.path.join(folder, new_pid + "." + file_extension) 

2365 name = resolve(self.request.path_info).url_name 

2366 if name == "special_issue_create": 

2367 self.kwargs["name"] = name 

2368 if self.kwargs["container"]: 

2369 # Edit Issue 

2370 issue = self.kwargs["container"] 

2371 if issue is None: 

2372 raise ValueError(self.kwargs["pid"] + " does not exist") 

2373 

2374 issue.pid = new_pid 

2375 issue.title_tex = issue.title_html = new_title 

2376 issue.title_xml = build_title_xml( 

2377 title=new_title, 

2378 lang=issue.lang, 

2379 title_type="issue-title", 

2380 ) 

2381 

2382 trans_lang = "" 

2383 if issue.trans_lang != "" and issue.trans_lang != "und": 

2384 trans_lang = issue.trans_lang 

2385 elif new_trans_title != "": 

2386 trans_lang = "fr" if issue.lang == "en" else "en" 

2387 issue.trans_lang = trans_lang 

2388 

2389 if trans_lang != "" and new_trans_title != "": 

2390 issue.trans_title_html = "" 

2391 issue.trans_title_tex = "" 

2392 title_xml = build_title_xml( 

2393 title=new_trans_title, lang=trans_lang, title_type="issue-title" 

2394 ) 

2395 try: 

2396 trans_title_object = Title.objects.get(resource=issue, lang=trans_lang) 

2397 trans_title_object.title_html = new_trans_title 

2398 trans_title_object.title_xml = title_xml 

2399 trans_title_object.save() 

2400 except Title.DoesNotExist: 

2401 trans_title = Title( 

2402 resource=issue, 

2403 lang=trans_lang, 

2404 type="main", 

2405 title_html=new_trans_title, 

2406 title_xml=title_xml, 

2407 ) 

2408 trans_title.save() 

2409 issue.year = new_year 

2410 issue.volume = new_volume 

2411 issue.volume_int = make_int(new_volume) 

2412 issue.number = new_number 

2413 issue.number_int = make_int(new_number) 

2414 issue.save() 

2415 else: 

2416 xissue = create_issuedata() 

2417 

2418 xissue.ctype = "issue" 

2419 xissue.pid = new_pid 

2420 xissue.lang = "en" 

2421 xissue.title_tex = new_title 

2422 xissue.title_html = new_title 

2423 xissue.title_xml = build_title_xml( 

2424 title=new_title, lang=xissue.lang, title_type="issue-title" 

2425 ) 

2426 

2427 if new_trans_title != "": 

2428 trans_lang = "fr" 

2429 title_xml = build_title_xml( 

2430 title=new_trans_title, lang=trans_lang, title_type="trans-title" 

2431 ) 

2432 title = create_titledata( 

2433 lang=trans_lang, type="main", title_html=new_trans_title, title_xml=title_xml 

2434 ) 

2435 issue.titles = [title] 

2436 

2437 xissue.year = new_year 

2438 xissue.volume = new_volume 

2439 xissue.number = new_number 

2440 xissue.last_modified_iso_8601_date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 

2441 

2442 cmd = ptf_cmds.addContainerPtfCmd({"xobj": xissue}) 

2443 cmd.add_collection(collection) 

2444 cmd.set_provider(model_helpers.get_provider_by_name("mathdoc")) 

2445 issue = cmd.do() 

2446 

2447 self.kwargs["pid"] = new_pid 

2448 

2449 # Add objects related to the article: contribs, datastream, counts... 

2450 params = { 

2451 "icon_location": new_icon_location, 

2452 } 

2453 cmd = ptf_cmds.updateContainerPtfCmd(params) 

2454 cmd.set_resource(issue) 

2455 cmd.do() 

2456 

2457 publisher = model_helpers.get_publisher(new_publisher) 

2458 if not publisher: 

2459 xpub = create_publisherdata() 

2460 xpub.name = new_publisher 

2461 publisher = ptf_cmds.addPublisherPtfCmd({"xobj": xpub}).do() 

2462 issue.my_publisher = publisher 

2463 issue.save() 

2464 

2465 self.set_success_message() 

2466 

2467 return super().form_valid(form) 

2468 

2469 

2470# class ArticleEditView(FormView): 

2471# template_name = 'article_form.html' 

2472# form_class = ArticleForm 

2473# 

2474# def get_success_url(self): 

2475# if self.kwargs['pid']: 

2476# return reverse('article', kwargs={'aid': self.kwargs['pid']}) 

2477# return reverse('mersenne_dashboard/published_articles') 

2478# 

2479# def set_success_message(self): # pylint: disable=no-self-use 

2480# messages.success(self.request, "L'article a été modifié") 

2481# 

2482# def get_form_kwargs(self): 

2483# kwargs = super(ArticleEditView, self).get_form_kwargs() 

2484# 

2485# if 'pid' not in self.kwargs or self.kwargs['pid'] == 'None': 

2486# # Article creation: pid is None 

2487# self.kwargs['pid'] = None 

2488# if 'issue_id' not in self.kwargs: 

2489# # Article edit: issue_id is not passed 

2490# self.kwargs['issue_id'] = None 

2491# if 'data' in kwargs and 'issue_id' in kwargs['data']: 

2492# # colid is passed as a hidden param in the form. 

2493# # It is used when you submit a new container 

2494# self.kwargs['issue_id'] = kwargs['data']['issue_id'] 

2495# 

2496# self.kwargs['article'] = kwargs['article'] = model_helpers.get_article(self.kwargs['pid']) 

2497# return kwargs 

2498# 

2499# def get_context_data(self, **kwargs): 

2500# context = super(ArticleEditView, self).get_context_data(**kwargs) 

2501# 

2502# context['pid'] = self.kwargs['pid'] 

2503# context['issue_id'] = self.kwargs['issue_id'] 

2504# context['article'] = self.kwargs['article'] 

2505# 

2506# context['edit_article'] = context['pid'] is not None 

2507# 

2508# article = context['article'] 

2509# if article: 

2510# context['author_contributions'] = article.get_author_contributions() 

2511# context['kwds_fr'] = None 

2512# context['kwds_en'] = None 

2513# kwd_gps = article.get_non_msc_kwds() 

2514# for kwd_gp in kwd_gps: 

2515# if kwd_gp.lang == 'fr' or (kwd_gp.lang == 'und' and article.lang == 'fr'): 

2516# if kwd_gp.value_xml: 

2517# kwd_ = types.SimpleNamespace() 

2518# kwd_.value = kwd_gp.value_tex 

2519# context['kwd_unstructured_fr'] = kwd_ 

2520# context['kwds_fr'] = kwd_gp.kwd_set.all() 

2521# elif kwd_gp.lang == 'en' or (kwd_gp.lang == 'und' and article.lang == 'en'): 

2522# if kwd_gp.value_xml: 

2523# kwd_ = types.SimpleNamespace() 

2524# kwd_.value = kwd_gp.value_tex 

2525# context['kwd_unstructured_en'] = kwd_ 

2526# context['kwds_en'] = kwd_gp.kwd_set.all() 

2527# 

2528# # Article creation: init pid 

2529# if context['issue_id'] and context['pid'] is None: 

2530# issue = model_helpers.get_container(context['issue_id']) 

2531# context['pid'] = issue.pid + '_A' + str(issue.article_set.count() + 1) + '_0' 

2532# 

2533# return context 

2534# 

2535# def form_valid(self, form): 

2536# 

2537# new_pid = form.cleaned_data.get('pid') 

2538# new_title = form.cleaned_data.get('title') 

2539# new_fpage = form.cleaned_data.get('fpage') 

2540# new_lpage = form.cleaned_data.get('lpage') 

2541# new_page_range = form.cleaned_data.get('page_range') 

2542# new_page_count = form.cleaned_data.get('page_count') 

2543# new_coi_statement = form.cleaned_data.get('coi_statement') 

2544# new_show_body = form.cleaned_data.get('show_body') 

2545# new_do_not_publish = form.cleaned_data.get('do_not_publish') 

2546# 

2547# # TODO support MathML 

2548# # 27/10/2020: title_xml embeds the trans_title_group in JATS. 

2549# # We need to pass trans_title to get_title_xml 

2550# # Meanwhile, ignore new_title_xml 

2551# new_title_xml = jats_parser.get_title_xml(new_title) 

2552# new_title_html = new_title 

2553# 

2554# authors_count = int(self.request.POST.get('authors_count', "0")) 

2555# i = 1 

2556# new_authors = [] 

2557# old_author_contributions = [] 

2558# if self.kwargs['article']: 

2559# old_author_contributions = self.kwargs['article'].get_author_contributions() 

2560# 

2561# while authors_count > 0: 

2562# prefix = self.request.POST.get('contrib-p-' + str(i), None) 

2563# 

2564# if prefix is not None: 

2565# addresses = [] 

2566# if len(old_author_contributions) >= i: 

2567# old_author_contribution = old_author_contributions[i - 1] 

2568# addresses = [contrib_address.address for contrib_address in 

2569# old_author_contribution.get_addresses()] 

2570# 

2571# first_name = self.request.POST.get('contrib-f-' + str(i), None) 

2572# last_name = self.request.POST.get('contrib-l-' + str(i), None) 

2573# suffix = self.request.POST.get('contrib-s-' + str(i), None) 

2574# orcid = self.request.POST.get('contrib-o-' + str(i), None) 

2575# deceased = self.request.POST.get('contrib-d-' + str(i), None) 

2576# deceased_before_publication = deceased == 'on' 

2577# equal_contrib = self.request.POST.get('contrib-e-' + str(i), None) 

2578# equal_contrib = equal_contrib == 'on' 

2579# corresponding = self.request.POST.get('corresponding-' + str(i), None) 

2580# corresponding = corresponding == 'on' 

2581# email = self.request.POST.get('email-' + str(i), None) 

2582# 

2583# params = jats_parser.get_name_params(first_name, last_name, prefix, suffix, orcid) 

2584# params['deceased_before_publication'] = deceased_before_publication 

2585# params['equal_contrib'] = equal_contrib 

2586# params['corresponding'] = corresponding 

2587# params['addresses'] = addresses 

2588# params['email'] = email 

2589# 

2590# params['contrib_xml'] = xml_utils.get_contrib_xml(params) 

2591# 

2592# new_authors.append(params) 

2593# 

2594# authors_count -= 1 

2595# i += 1 

2596# 

2597# kwds_fr_count = int(self.request.POST.get('kwds_fr_count', "0")) 

2598# i = 1 

2599# new_kwds_fr = [] 

2600# while kwds_fr_count > 0: 

2601# value = self.request.POST.get('kwd-fr-' + str(i), None) 

2602# new_kwds_fr.append(value) 

2603# kwds_fr_count -= 1 

2604# i += 1 

2605# new_kwd_uns_fr = self.request.POST.get('kwd-uns-fr-0', None) 

2606# 

2607# kwds_en_count = int(self.request.POST.get('kwds_en_count', "0")) 

2608# i = 1 

2609# new_kwds_en = [] 

2610# while kwds_en_count > 0: 

2611# value = self.request.POST.get('kwd-en-' + str(i), None) 

2612# new_kwds_en.append(value) 

2613# kwds_en_count -= 1 

2614# i += 1 

2615# new_kwd_uns_en = self.request.POST.get('kwd-uns-en-0', None) 

2616# 

2617# if self.kwargs['article']: 

2618# # Edit article 

2619# container = self.kwargs['article'].my_container 

2620# else: 

2621# # New article 

2622# container = model_helpers.get_container(self.kwargs['issue_id']) 

2623# 

2624# if container is None: 

2625# raise ValueError(self.kwargs['issue_id'] + " does not exist") 

2626# 

2627# collection = container.my_collection 

2628# 

2629# # Copy PDF file & extract full text 

2630# body = '' 

2631# pdf_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER, 

2632# collection.pid, 

2633# "pdf", 

2634# container.pid, 

2635# new_pid, 

2636# True) 

2637# if 'pdf' in self.request.FILES: 

2638# with open(pdf_filename, 'wb+') as destination: 

2639# for chunk in self.request.FILES['pdf'].chunks(): 

2640# destination.write(chunk) 

2641# 

2642# # Extract full text from the PDF 

2643# body = utils.pdf_to_text(pdf_filename) 

2644# 

2645# # Icon 

2646# new_icon_location = '' 

2647# if 'icon' in self.request.FILES: 

2648# filename = os.path.basename(self.request.FILES['icon'].name) 

2649# file_extension = filename.split('.')[1] 

2650# 

2651# icon_filename = resolver.get_disk_location(settings.MERSENNE_TEST_DATA_FOLDER, 

2652# collection.pid, 

2653# file_extension, 

2654# container.pid, 

2655# new_pid, 

2656# True) 

2657# 

2658# with open(icon_filename, 'wb+') as destination: 

2659# for chunk in self.request.FILES['icon'].chunks(): 

2660# destination.write(chunk) 

2661# 

2662# folder = resolver.get_relative_folder(collection.pid, container.pid, new_pid) 

2663# new_icon_location = os.path.join(folder, new_pid + '.' + file_extension) 

2664# 

2665# if self.kwargs['article']: 

2666# # Edit article 

2667# article = self.kwargs['article'] 

2668# article.fpage = new_fpage 

2669# article.lpage = new_lpage 

2670# article.page_range = new_page_range 

2671# article.coi_statement = new_coi_statement 

2672# article.show_body = new_show_body 

2673# article.do_not_publish = new_do_not_publish 

2674# article.save() 

2675# 

2676# else: 

2677# # New article 

2678# params = { 

2679# 'pid': new_pid, 

2680# 'title_xml': new_title_xml, 

2681# 'title_html': new_title_html, 

2682# 'title_tex': new_title, 

2683# 'fpage': new_fpage, 

2684# 'lpage': new_lpage, 

2685# 'page_range': new_page_range, 

2686# 'seq': container.article_set.count() + 1, 

2687# 'body': body, 

2688# 'coi_statement': new_coi_statement, 

2689# 'show_body': new_show_body, 

2690# 'do_not_publish': new_do_not_publish 

2691# } 

2692# 

2693# xarticle = create_articledata() 

2694# xarticle.pid = new_pid 

2695# xarticle.title_xml = new_title_xml 

2696# xarticle.title_html = new_title_html 

2697# xarticle.title_tex = new_title 

2698# xarticle.fpage = new_fpage 

2699# xarticle.lpage = new_lpage 

2700# xarticle.page_range = new_page_range 

2701# xarticle.seq = container.article_set.count() + 1 

2702# xarticle.body = body 

2703# xarticle.coi_statement = new_coi_statement 

2704# params['xobj'] = xarticle 

2705# 

2706# cmd = ptf_cmds.addArticlePtfCmd(params) 

2707# cmd.set_container(container) 

2708# cmd.add_collection(container.my_collection) 

2709# article = cmd.do() 

2710# 

2711# self.kwargs['pid'] = new_pid 

2712# 

2713# # Add objects related to the article: contribs, datastream, counts... 

2714# params = { 

2715# # 'title_xml': new_title_xml, 

2716# # 'title_html': new_title_html, 

2717# # 'title_tex': new_title, 

2718# 'authors': new_authors, 

2719# 'page_count': new_page_count, 

2720# 'icon_location': new_icon_location, 

2721# 'body': body, 

2722# 'use_kwds': True, 

2723# 'kwds_fr': new_kwds_fr, 

2724# 'kwds_en': new_kwds_en, 

2725# 'kwd_uns_fr': new_kwd_uns_fr, 

2726# 'kwd_uns_en': new_kwd_uns_en 

2727# } 

2728# cmd = ptf_cmds.updateArticlePtfCmd(params) 

2729# cmd.set_article(article) 

2730# cmd.do() 

2731# 

2732# self.set_success_message() 

2733# 

2734# return super(ArticleEditView, self).form_valid(form) 

2735 

2736 

2737@require_http_methods(["POST"]) 

2738def do_not_publish_article(request, *args, **kwargs): 

2739 next = request.headers.get("referer") 

2740 

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

2742 

2743 article = model_helpers.get_article(pid) 

2744 if article: 

2745 article.do_not_publish = not article.do_not_publish 

2746 article.save() 

2747 else: 

2748 raise Http404 

2749 

2750 return HttpResponseRedirect(next) 

2751 

2752 

2753@require_http_methods(["POST"]) 

2754def show_article_body(request, *args, **kwargs): 

2755 next = request.headers.get("referer") 

2756 

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

2758 

2759 article = model_helpers.get_article(pid) 

2760 if article: 

2761 article.show_body = not article.show_body 

2762 article.save() 

2763 else: 

2764 raise Http404 

2765 

2766 return HttpResponseRedirect(next) 

2767 

2768 

2769class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView): 

2770 """ 

2771 API to get/post article metadata 

2772 The class is derived from ArticleEditFormWithVueAPIView (see ptf.views) 

2773 """ 

2774 

2775 def __init__(self, *args, **kwargs): 

2776 """ 

2777 we define here what fields we want in the form 

2778 when updating article, lang can change with an impact on xml for (trans_)abstracts and (trans_)title 

2779 so as we iterate on fields to update, lang fields shall be in first position if present in fields_to_update""" 

2780 super().__init__(*args, **kwargs) 

2781 self.fields_to_update = [ 

2782 "lang", 

2783 "atype", 

2784 "contributors", 

2785 "abstracts", 

2786 "kwds", 

2787 "titles", 

2788 "trans_title_html", 

2789 "title_html", 

2790 "title_xml", 

2791 "title_tex", 

2792 "streams", 

2793 "ext_links", 

2794 "date_accepted", 

2795 "history_dates", 

2796 "subjs", 

2797 "bibitems", 

2798 "references", 

2799 ] 

2800 # order between doi and pid is important as for pending article we need doi to create a temporary pid 

2801 self.additional_fields = ["doi", "pid", "container_pid", "pdf", "illustration", "dates"] 

2802 self.editorial_tools = [ 

2803 "translation", 

2804 "sidebar", 

2805 "lang_selection", 

2806 "back_to_article_option", 

2807 ] 

2808 self.article_container_pid = "" 

2809 self.back_url = "trammel" 

2810 

2811 def save_data(self, data_article): 

2812 # On sauvegarde les données additionnelles (extid, deployed_date,...) dans un json 

2813 # The icons are not preserved since we can add/edit/delete them in VueJs 

2814 params = { 

2815 "pid": data_article.pid, 

2816 "export_folder": settings.MERSENNE_TMP_FOLDER, 

2817 "export_all": True, 

2818 "with_binary_files": False, 

2819 } 

2820 ptf_cmds.exportExtraDataPtfCmd(params).do() 

2821 

2822 def restore_data(self, article): 

2823 ptf_cmds.importExtraDataPtfCmd( 

2824 { 

2825 "pid": article.pid, 

2826 "import_folder": settings.MERSENNE_TMP_FOLDER, 

2827 } 

2828 ).do() 

2829 

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

2831 data = super().get(request, *args, **kwargs) 

2832 return data 

2833 

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

2835 response = super().post(request, *args, **kwargs) 

2836 if response["message"] == "OK": 

2837 return redirect( 

2838 "api-edit-article", 

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

2840 containerPid=kwargs.get("containerPid"), 

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

2842 ) 

2843 else: 

2844 raise Http404 

2845 

2846 

2847class ArticleEditWithVueView(LoginRequiredMixin, TemplateView): 

2848 template_name = "article_form.html" 

2849 

2850 def get_success_url(self): 

2851 if self.kwargs["doi"]: 

2852 return reverse("article", kwargs={"aid": self.kwargs["doi"]}) 

2853 return reverse("mersenne_dashboard/published_articles") 

2854 

2855 def get_context_data(self, **kwargs): 

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

2857 if "doi" in self.kwargs: 

2858 context["article"] = model_helpers.get_article_by_doi(self.kwargs["doi"]) 

2859 context["pid"] = context["article"].pid 

2860 

2861 context["container_pid"] = kwargs.get("container_pid", "") 

2862 return context 

2863 

2864 

2865class ArticleDeleteView(View): 

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

2867 pid = self.kwargs.get("pid", None) 

2868 article = get_object_or_404(Article, pid=pid) 

2869 

2870 try: 

2871 mersenneSite = model_helpers.get_site_mersenne(article.get_collection().pid) 

2872 article.undeploy(mersenneSite) 

2873 

2874 cmd = ptf_cmds.addArticlePtfCmd( 

2875 {"pid": article.pid, "to_folder": settings.MERSENNE_TEST_DATA_FOLDER} 

2876 ) 

2877 cmd.set_container(article.my_container) 

2878 cmd.set_object_to_be_deleted(article) 

2879 cmd.undo() 

2880 except Exception as exception: 

2881 return HttpResponseServerError(exception) 

2882 

2883 data = {"message": "L'article a bien été supprimé de ptf-tools", "status": 200} 

2884 return JsonResponse(data) 

2885 

2886 

2887def get_messages_in_queue(): 

2888 app = Celery("ptf-tools") 

2889 # tasks = list(current_app.tasks) 

2890 tasks = list(sorted(name for name in current_app.tasks if name.startswith("celery"))) 

2891 print(tasks) 

2892 # i = app.control.inspect() 

2893 

2894 with app.connection_or_acquire() as conn: 

2895 remaining = conn.default_channel.queue_declare( 

2896 queue="coordinator", passive=True 

2897 ).message_count 

2898 return remaining 

2899 

2900 

2901class NumdamView(TemplateView, history_views.HistoryContextMixin): 

2902 template_name = "numdam.html" 

2903 

2904 def get_context_data(self, **kwargs): 

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

2906 

2907 context["objs"] = ResourceInNumdam.objects.all() 

2908 

2909 pre_issues = [] 

2910 prod_issues = [] 

2911 url = f"{settings.NUMDAM_PRE_URL}/api-all-issues/" 

2912 try: 

2913 response = requests.get(url) 

2914 if response.status_code == 200: 

2915 data = response.json() 

2916 if "issues" in data: 

2917 pre_issues = data["issues"] 

2918 except Exception: 

2919 pass 

2920 

2921 url = f"{settings.NUMDAM_URL}/api-all-issues/" 

2922 response = requests.get(url) 

2923 if response.status_code == 200: 

2924 data = response.json() 

2925 if "issues" in data: 

2926 prod_issues = data["issues"] 

2927 

2928 new = sorted(list(set(pre_issues).difference(prod_issues))) 

2929 removed = sorted(list(set(prod_issues).difference(pre_issues))) 

2930 grouped = [ 

2931 {"colid": k, "issues": list(g)} for k, g in groupby(new, lambda x: x.split("_")[0]) 

2932 ] 

2933 grouped_removed = [ 

2934 {"colid": k, "issues": list(g)} for k, g in groupby(removed, lambda x: x.split("_")[0]) 

2935 ] 

2936 context["added_issues"] = grouped 

2937 context["removed_issues"] = grouped_removed 

2938 

2939 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS 

2940 return context 

2941 

2942 

2943class NumdamArchiveView(RedirectView): 

2944 @staticmethod 

2945 def reset_task_results(): 

2946 TaskResult.objects.all().delete() 

2947 

2948 def get_redirect_url(self, *args, **kwargs): 

2949 self.colid = kwargs["colid"] 

2950 

2951 if self.colid != "ALL" and self.colid in settings.MERSENNE_COLLECTIONS: 

2952 return Http404 

2953 

2954 # we make sure archiving is not already running 

2955 # if not get_messages_in_queue(): 

2956 # self.reset_task_results() 

2957 

2958 if self.colid == "ALL": 

2959 archive_numdam_collections.delay() 

2960 else: 

2961 archive_numdam_collection.s(self.colid).delay() 

2962 

2963 return reverse("numdam") 

2964 

2965 

2966class DeployAllNumdamAPIView(View): 

2967 def internal_do(self, *args, **kwargs): 

2968 pids = [] 

2969 

2970 for obj in ResourceInNumdam.objects.all(): 

2971 pids.append(obj.pid) 

2972 

2973 return pids 

2974 

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

2976 try: 

2977 pids, status, message = history_views.execute_and_record_func( 

2978 "deploy", "numdam", "ALL", self.internal_do, "numdam" 

2979 ) 

2980 except Exception as exception: 

2981 return HttpResponseServerError(exception) 

2982 

2983 data = {"message": message, "ids": pids, "status": status} 

2984 return JsonResponse(data) 

2985 

2986 

2987class NumdamDeleteAPIView(View): 

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

2989 pid = self.kwargs.get("pid", None) 

2990 

2991 try: 

2992 obj = ResourceInNumdam.objects.get(pid=pid) 

2993 obj.delete() 

2994 except Exception as exception: 

2995 return HttpResponseServerError(exception) 

2996 

2997 data = {"message": "Le volume a bien été supprimé de la liste pour Numdam", "status": 200} 

2998 return JsonResponse(data) 

2999 

3000 

3001class ExtIdApiDetail(View): 

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

3003 extid = get_object_or_404( 

3004 ExtId, 

3005 resource__pid=kwargs["pid"], 

3006 id_type=kwargs["what"], 

3007 ) 

3008 return JsonResponse( 

3009 { 

3010 "pk": extid.pk, 

3011 "href": extid.get_href(), 

3012 "fetch": reverse( 

3013 "api-fetch-id", 

3014 args=( 

3015 extid.resource.pk, 

3016 extid.id_value, 

3017 extid.id_type, 

3018 "extid", 

3019 ), 

3020 ), 

3021 "check": reverse("update-extid", args=(extid.pk, "toggle-checked")), 

3022 "uncheck": reverse("update-extid", args=(extid.pk, "toggle-false-positive")), 

3023 "update": reverse("extid-update", kwargs={"pk": extid.pk}), 

3024 "delete": reverse("update-extid", args=(extid.pk, "delete")), 

3025 "is_valid": extid.checked, 

3026 } 

3027 ) 

3028 

3029 

3030class ExtIdFormTemplate(TemplateView): 

3031 template_name = "common/externalid_form.html" 

3032 

3033 def get_context_data(self, **kwargs): 

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

3035 context["sequence"] = kwargs["sequence"] 

3036 return context 

3037 

3038 

3039class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

3040 def get_context_data(self, **kwargs): 

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

3042 context["helper"] = PtfFormHelper 

3043 return context 

3044 

3045 def get_success_url(self): 

3046 self.post_process() 

3047 return self.object.bibitem.resource.get_absolute_url() 

3048 

3049 def post_process(self): 

3050 cmd = updateBibitemCitationXmlCmd() 

3051 cmd.set_bibitem(self.object.bibitem) 

3052 cmd.do() 

3053 model_helpers.post_resource_updated(self.object.bibitem.resource) 

3054 

3055 

3056class BibItemIdCreate(BibItemIdFormView, CreateView): 

3057 model = BibItemId 

3058 form_class = BibItemIdForm 

3059 

3060 def get_context_data(self, **kwargs): 

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

3062 context["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"]) 

3063 return context 

3064 

3065 def get_initial(self): 

3066 initial = super().get_initial() 

3067 initial["bibitem"] = BibItem.objects.get(pk=self.kwargs["bibitem_pk"]) 

3068 return initial 

3069 

3070 def form_valid(self, form): 

3071 form.instance.checked = False 

3072 return super().form_valid(form) 

3073 

3074 

3075class BibItemIdUpdate(BibItemIdFormView, UpdateView): 

3076 model = BibItemId 

3077 form_class = BibItemIdForm 

3078 

3079 def get_context_data(self, **kwargs): 

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

3081 context["bibitem"] = self.object.bibitem 

3082 return context 

3083 

3084 

3085class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

3086 def get_context_data(self, **kwargs): 

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

3088 context["helper"] = PtfFormHelper 

3089 return context 

3090 

3091 def get_success_url(self): 

3092 self.post_process() 

3093 return self.object.resource.get_absolute_url() 

3094 

3095 def post_process(self): 

3096 model_helpers.post_resource_updated(self.object.resource) 

3097 

3098 

3099class ExtIdCreate(ExtIdFormView, CreateView): 

3100 model = ExtId 

3101 form_class = ExtIdForm 

3102 

3103 def get_context_data(self, **kwargs): 

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

3105 context["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"]) 

3106 return context 

3107 

3108 def get_initial(self): 

3109 initial = super().get_initial() 

3110 initial["resource"] = Resource.objects.get(pk=self.kwargs["resource_pk"]) 

3111 return initial 

3112 

3113 def form_valid(self, form): 

3114 form.instance.checked = False 

3115 return super().form_valid(form) 

3116 

3117 

3118class ExtIdUpdate(ExtIdFormView, UpdateView): 

3119 model = ExtId 

3120 form_class = ExtIdForm 

3121 

3122 def get_context_data(self, **kwargs): 

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

3124 context["resource"] = self.object.resource 

3125 return context 

3126 

3127 

3128class BibItemIdApiDetail(View): 

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

3130 bibitemid = get_object_or_404( 

3131 BibItemId, 

3132 bibitem__resource__pid=kwargs["pid"], 

3133 bibitem__sequence=kwargs["seq"], 

3134 id_type=kwargs["what"], 

3135 ) 

3136 return JsonResponse( 

3137 { 

3138 "pk": bibitemid.pk, 

3139 "href": bibitemid.get_href(), 

3140 "fetch": reverse( 

3141 "api-fetch-id", 

3142 args=( 

3143 bibitemid.bibitem.pk, 

3144 bibitemid.id_value, 

3145 bibitemid.id_type, 

3146 "bibitemid", 

3147 ), 

3148 ), 

3149 "check": reverse("update-bibitemid", args=(bibitemid.pk, "toggle-checked")), 

3150 "uncheck": reverse( 

3151 "update-bibitemid", args=(bibitemid.pk, "toggle-false-positive") 

3152 ), 

3153 "update": reverse("bibitemid-update", kwargs={"pk": bibitemid.pk}), 

3154 "delete": reverse("update-bibitemid", args=(bibitemid.pk, "delete")), 

3155 "is_valid": bibitemid.checked, 

3156 } 

3157 ) 

3158 

3159 

3160class UpdateTexmfZipAPIView(View): 

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

3162 def copy_zip_files(src_folder, dest_folder): 

3163 os.makedirs(dest_folder, exist_ok=True) 

3164 

3165 zip_files = [ 

3166 os.path.join(src_folder, f) 

3167 for f in os.listdir(src_folder) 

3168 if os.path.isfile(os.path.join(src_folder, f)) and f.endswith(".zip") 

3169 ] 

3170 for zip_file in zip_files: 

3171 resolver.copy_file(zip_file, dest_folder) 

3172 

3173 # Exceptions: specific zip/gz files 

3174 zip_file = os.path.join(src_folder, "texmf-bsmf.zip") 

3175 resolver.copy_file(zip_file, dest_folder) 

3176 

3177 zip_file = os.path.join(src_folder, "texmf-cg.zip") 

3178 resolver.copy_file(zip_file, dest_folder) 

3179 

3180 gz_file = os.path.join(src_folder, "texmf-mersenne.tar.gz") 

3181 resolver.copy_file(gz_file, dest_folder) 

3182 

3183 src_folder = settings.CEDRAM_DISTRIB_FOLDER 

3184 

3185 dest_folder = os.path.join( 

3186 settings.MERSENNE_TEST_DATA_FOLDER, "MERSENNE", "media", "texmf" 

3187 ) 

3188 

3189 try: 

3190 copy_zip_files(src_folder, dest_folder) 

3191 except Exception as exception: 

3192 return HttpResponseServerError(exception) 

3193 

3194 try: 

3195 dest_folder = os.path.join( 

3196 settings.MERSENNE_PROD_DATA_FOLDER, "MERSENNE", "media", "texmf" 

3197 ) 

3198 copy_zip_files(src_folder, dest_folder) 

3199 except Exception as exception: 

3200 return HttpResponseServerError(exception) 

3201 

3202 data = {"message": "Les texmf*.zip ont bien été mis à jour", "status": 200} 

3203 return JsonResponse(data) 

3204 

3205 

3206class TestView(TemplateView): 

3207 template_name = "mersenne.html" 

3208 

3209 def get_context_data(self, **kwargs): 

3210 super().get_context_data(**kwargs) 

3211 issue = model_helpers.get_container(pid="CRPHYS_0__0_0", prefetch=True) 

3212 model_data_converter.db_to_issue_data(issue) 

3213 

3214 

3215class TrammelTasksProgressView(View): 

3216 def get(self, request, task: str = "archive_numdam_issue", *args, **kwargs): 

3217 """ 

3218 Return a JSON object with the progress of the archiving task Le code permet de récupérer l'état d'avancement 

3219 de la tache celery (archive_trammel_resource) en SSE (Server-Sent Events) 

3220 """ 

3221 task_name = task 

3222 

3223 def get_event_data(): 

3224 # Tasks are typically in the CREATED then SUCCESS or FAILURE state 

3225 

3226 # Some messages (in case of many call to <task>.delay) have not been converted to TaskResult yet 

3227 remaining_messages = get_messages_in_queue() 

3228 

3229 all_tasks = TaskResult.objects.filter(task_name=f"ptf_tools.tasks.{task_name}") 

3230 successed_tasks = all_tasks.filter(status="SUCCESS").order_by("-date_done") 

3231 failed_tasks = all_tasks.filter(status="FAILURE") 

3232 

3233 all_tasks_count = all_tasks.count() 

3234 success_count = successed_tasks.count() 

3235 fail_count = failed_tasks.count() 

3236 

3237 all_count = all_tasks_count + remaining_messages 

3238 remaining_count = all_count - success_count - fail_count 

3239 

3240 success_rate = int(success_count * 100 / all_count) if all_count else 0 

3241 error_rate = int(fail_count * 100 / all_count) if all_count else 0 

3242 status = "consuming_queue" if remaining_count != 0 else "polling" 

3243 

3244 last_task = successed_tasks.first() 

3245 last_task = ( 

3246 " : ".join([last_task.date_done.strftime("%Y-%m-%d"), last_task.task_args]) 

3247 if last_task 

3248 else "" 

3249 ) 

3250 

3251 # SSE event format 

3252 event_data = { 

3253 "status": status, 

3254 "success_rate": success_rate, 

3255 "error_rate": error_rate, 

3256 "all_count": all_count, 

3257 "remaining_count": remaining_count, 

3258 "success_count": success_count, 

3259 "fail_count": fail_count, 

3260 "last_task": last_task, 

3261 } 

3262 

3263 return event_data 

3264 

3265 def stream_response(data): 

3266 # Send initial response headers 

3267 yield f"data: {json.dumps(data)}\n\n" 

3268 

3269 data = get_event_data() 

3270 format = request.GET.get("format", "stream") 

3271 if format == "json": 

3272 response = JsonResponse(data) 

3273 else: 

3274 response = HttpResponse(stream_response(data), content_type="text/event-stream") 

3275 return response 

3276 

3277 

3278user_signed_up.connect(update_user_from_invite)