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

1682 statements  

« prev     ^ index     » next       coverage.py v7.9.0, created at 2025-12-23 16:01 +0000

1import io 

2import json 

3import os 

4import re 

5from datetime import datetime 

6from itertools import groupby 

7 

8import jsonpickle 

9import requests 

10from allauth.account.signals import user_signed_up 

11from braces.views import CsrfExemptMixin, LoginRequiredMixin, StaffuserRequiredMixin 

12from celery import Celery, current_app 

13from django.conf import settings 

14from django.contrib import messages 

15from django.contrib.auth.mixins import UserPassesTestMixin 

16from django.db.models import Q 

17from django.http import ( 

18 Http404, 

19 HttpRequest, 

20 HttpResponse, 

21 HttpResponseRedirect, 

22 HttpResponseServerError, 

23 JsonResponse, 

24) 

25from django.shortcuts import get_object_or_404, redirect, render 

26from django.urls import resolve, reverse, reverse_lazy 

27from django.utils import timezone 

28from django.views.decorators.http import require_http_methods 

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

30from django.views.generic.base import RedirectView 

31from django.views.generic.detail import SingleObjectMixin 

32from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView 

33from django_celery_results.models import TaskResult 

34from extra_views import ( 

35 CreateWithInlinesView, 

36 InlineFormSetFactory, 

37 NamedFormsetsMixin, 

38 UpdateWithInlinesView, 

39) 

40from ptf import model_data_converter, model_helpers, tex, utils 

41from ptf.cmds import ptf_cmds, xml_cmds 

42from ptf.cmds.base_cmds import make_int 

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

44from ptf.cmds.xml.xml_utils import replace_html_entities 

45from ptf.display import resolver 

46from ptf.exceptions import DOIException, PDFException, ServerUnderMaintenance 

47from ptf.model_data import create_issuedata, create_publisherdata, create_titledata 

48from ptf.models import ( 

49 Abstract, 

50 Article, 

51 BibItem, 

52 BibItemId, 

53 Collection, 

54 Container, 

55 ExtId, 

56 ExtLink, 

57 Resource, 

58 ResourceId, 

59 Title, 

60) 

61from ptf.views import ArticleEditFormWithVueAPIView 

62from ptf_back.cmds.xml_cmds import updateBibitemCitationXmlCmd 

63from pubmed.views import recordPubmed 

64from requests import Timeout 

65from task.tasks.archiving_tasks import archive_resource 

66 

67from comments_moderation.utils import get_comments_for_home, is_comment_moderator 

68from history import models as history_models 

69from history import views as history_views 

70from history.utils import get_gap, get_history_last_event_by, get_last_unsolved_error 

71from ptf_tools.doaj import doaj_pid_register 

72from ptf_tools.doi import checkDOI, recordDOI, recordPendingPublication 

73from ptf_tools.forms import ( 

74 BibItemIdForm, 

75 CollectionForm, 

76 ContainerForm, 

77 DiffContainerForm, 

78 ExtIdForm, 

79 ExtLinkForm, 

80 FormSetHelper, 

81 ImportArticleForm, 

82 ImportContainerForm, 

83 ImportEditflowArticleForm, 

84 PtfFormHelper, 

85 PtfLargeModalFormHelper, 

86 PtfModalFormHelper, 

87 RegisterPubmedForm, 

88 ResourceIdForm, 

89 get_article_choices, 

90) 

91from ptf_tools.indexingChecker import ReferencingCheckerAds, ReferencingCheckerWos 

92from ptf_tools.models import ResourceInNumdam 

93from ptf_tools.signals import update_user_from_invite 

94from ptf_tools.tasks import ( 

95 archive_numdam_collection, 

96 archive_numdam_collections, 

97 archive_numdam_resource, 

98) 

99from ptf_tools.templatetags.tools_helpers import get_authorized_collections 

100from ptf_tools.utils import is_authorized_editor 

101 

102 

103def view_404(request: HttpRequest): 

104 """ 

105 Dummy view raising HTTP 404 exception. 

106 """ 

107 raise Http404 

108 

109 

110def check_collection(collection, server_url, server_type): 

111 """ 

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

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

114 """ 

115 

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

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

118 # First, upload the collection XML 

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

120 body = xml.encode("utf8") 

121 

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

123 if response.status_code == 200: 

124 # PUT http verb is used for update 

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

126 else: 

127 # POST http verb is used for creation 

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

129 

130 # Second, copy the collection images 

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

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

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

134 # /mersenne_prod_data during an upload to prod 

135 if server_type == "website": 

136 resolver.copy_binary_files( 

137 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

138 ) 

139 elif server_type == "numdam": 

140 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

141 if collection.pid in settings.NUMDAM_COLLECTIONS: 

142 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

143 

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

145 

146 

147def check_lock(): 

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

149 

150 

151def load_cedrics_article_choices(request): 

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

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

154 article_choices = get_article_choices(colid, issue) 

155 return render( 

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

157 ) 

158 

159 

160class ImportCedricsArticleFormView(FormView): 

161 template_name = "import_article.html" 

162 form_class = ImportArticleForm 

163 

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

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

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

167 

168 def get_success_url(self): 

169 if self.colid: 

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

171 return "/" 

172 

173 def get_context_data(self, **kwargs): 

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

175 context["colid"] = self.colid 

176 context["helper"] = PtfModalFormHelper 

177 return context 

178 

179 def get_form_kwargs(self): 

180 kwargs = super().get_form_kwargs() 

181 kwargs["colid"] = self.colid 

182 return kwargs 

183 

184 def form_valid(self, form): 

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

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

187 return super().form_valid(form) 

188 

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

190 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd( 

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

192 ) 

193 cmd.do() 

194 

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

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

197 issue = request.POST["issue"] 

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

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

200 

201 import_args = [self] 

202 import_kwargs = {} 

203 

204 try: 

205 _, status, message = history_views.execute_and_record_func( 

206 "import", 

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

208 self.colid, 

209 self.import_cedrics_article, 

210 "", 

211 False, 

212 None, 

213 None, 

214 *import_args, 

215 **import_kwargs, 

216 ) 

217 

218 messages.success( 

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

220 ) 

221 

222 except Exception as exception: 

223 messages.error( 

224 self.request, 

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

226 ) 

227 

228 return redirect(self.get_success_url()) 

229 

230 

231class ImportCedricsIssueView(FormView): 

232 template_name = "import_container.html" 

233 form_class = ImportContainerForm 

234 

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

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

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

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

239 

240 def get_success_url(self): 

241 if self.filename: 

242 return reverse( 

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

244 ) 

245 return "/" 

246 

247 def get_context_data(self, **kwargs): 

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

249 context["colid"] = self.colid 

250 context["helper"] = PtfModalFormHelper 

251 return context 

252 

253 def get_form_kwargs(self): 

254 kwargs = super().get_form_kwargs() 

255 kwargs["colid"] = self.colid 

256 kwargs["to_appear"] = self.to_appear 

257 return kwargs 

258 

259 def form_valid(self, form): 

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

261 return super().form_valid(form) 

262 

263 

264class DiffCedricsIssueView(FormView): 

265 template_name = "diff_container_form.html" 

266 form_class = DiffContainerForm 

267 diffs = None 

268 xissue = None 

269 xissue_encoded = None 

270 

271 def get_success_url(self): 

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

273 

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

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

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

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

278 

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

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

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

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

283 self.remove_email = self.remove_mail == "on" 

284 self.remove_date_prod = self.remove_date_prod == "on" 

285 

286 try: 

287 result, status, message = history_views.execute_and_record_func( 

288 "import", 

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

290 self.colid, 

291 self.diff_cedrics_issue, 

292 "", 

293 True, 

294 ) 

295 except Exception as exception: 

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

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

298 return HttpResponseRedirect(self.get_success_url()) 

299 

300 no_conflict = result[0] 

301 self.diffs = result[1] 

302 self.xissue = result[2] 

303 

304 if no_conflict: 

305 # Proceed with the import 

306 self.form_valid(self.get_form()) 

307 return redirect(self.get_success_url()) 

308 else: 

309 # Display the diff template 

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

311 

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

313 

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

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

316 data = request.POST["xissue_encoded"] 

317 self.xissue = jsonpickle.decode(data) 

318 

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

320 

321 def get_context_data(self, **kwargs): 

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

323 context["colid"] = self.colid 

324 context["diff"] = self.diffs 

325 context["filename"] = self.filename 

326 context["xissue_encoded"] = self.xissue_encoded 

327 return context 

328 

329 def get_form_kwargs(self): 

330 kwargs = super().get_form_kwargs() 

331 kwargs["colid"] = self.colid 

332 return kwargs 

333 

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

335 params = { 

336 "colid": self.colid, 

337 "input_file": self.filename, 

338 "remove_email": self.remove_mail, 

339 "remove_date_prod": self.remove_date_prod, 

340 "diff_only": True, 

341 } 

342 

343 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

346 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

347 else: 

348 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

349 

350 result = cmd.do() 

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

352 messages.warning( 

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

354 ) 

355 

356 return result 

357 

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

359 # modify xissue with data_issue if params to override 

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

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

362 if issue: 

363 data_issue = model_data_converter.db_to_issue_data(issue) 

364 for xarticle in self.xissue.articles: 

365 filter_articles = [ 

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

367 ] 

368 if len(filter_articles) > 0: 

369 db_article = filter_articles[0] 

370 xarticle.coi_statement = db_article.coi_statement 

371 xarticle.kwds = db_article.kwds 

372 xarticle.contrib_groups = db_article.contrib_groups 

373 

374 params = { 

375 "colid": self.colid, 

376 "xissue": self.xissue, 

377 "input_file": self.filename, 

378 } 

379 

380 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

383 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

384 else: 

385 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

386 

387 cmd.do() 

388 

389 def form_valid(self, form): 

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

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

392 else: 

393 import_kwargs = {} 

394 import_args = [self] 

395 

396 try: 

397 _, status, message = history_views.execute_and_record_func( 

398 "import", 

399 self.xissue.pid, 

400 self.kwargs["colid"], 

401 self.import_cedrics_issue, 

402 "", 

403 False, 

404 None, 

405 None, 

406 *import_args, 

407 **import_kwargs, 

408 ) 

409 except Exception as exception: 

410 messages.error( 

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

412 ) 

413 return super().form_invalid(form) 

414 

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

416 return super().form_valid(form) 

417 

418 

419class ImportEditflowArticleFormView(FormView): 

420 template_name = "import_editflow_article.html" 

421 form_class = ImportEditflowArticleForm 

422 

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

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

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

426 

427 def get_context_data(self, **kwargs): 

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

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

430 context["helper"] = PtfLargeModalFormHelper 

431 return context 

432 

433 def get_success_url(self): 

434 if self.colid: 

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

436 return "/" 

437 

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

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

440 try: 

441 if not self.colid: 

442 raise ValueError("Missing collection id") 

443 

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

445 if not issue_name: 

446 raise ValueError( 

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

448 ) 

449 

450 issue = model_helpers.get_container(issue_name) 

451 if not issue: 

452 raise ValueError("No issue found") 

453 

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

455 if not editflow_xml_file: 

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

457 

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

459 

460 cmd = xml_cmds.addArticleXmlCmd( 

461 { 

462 "body": body, 

463 "issue": issue, 

464 "assign_doi": True, 

465 "standalone": True, 

466 "from_folder": settings.RESOURCES_ROOT, 

467 } 

468 ) 

469 cmd.set_collection(issue.get_collection()) 

470 cmd.do() 

471 

472 messages.success( 

473 request, 

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

475 ) 

476 

477 except Exception as exception: 

478 messages.error( 

479 request, 

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

481 ) 

482 

483 return redirect(self.get_success_url()) 

484 

485 

486class BibtexAPIView(View): 

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

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

489 all_bibtex = "" 

490 if pid: 

491 article = model_helpers.get_article(pid) 

492 if article: 

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

494 bibtex_array = bibitem.get_bibtex() 

495 last = len(bibtex_array) 

496 i = 1 

497 for bibtex in bibtex_array: 

498 if i > 1 and i < last: 

499 all_bibtex += " " 

500 all_bibtex += bibtex + "\n" 

501 i += 1 

502 

503 data = {"bibtex": all_bibtex} 

504 return JsonResponse(data) 

505 

506 

507class MatchingAPIView(View): 

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

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

510 

511 url = settings.MATCHING_URL 

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

513 

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

515 

516 if settings.DEBUG: 

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

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

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

520 f.close() 

521 

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

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

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

525 

526 if settings.DEBUG: 

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

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

529 text = body 

530 f.write(text) 

531 f.close() 

532 

533 resource = model_helpers.get_resource(pid) 

534 obj = resource.cast() 

535 colid = obj.get_collection().pid 

536 

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

538 

539 cmd = xml_cmds.addOrUpdateIssueXmlCmd( 

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

541 ) 

542 cmd.do() 

543 

544 print("Matching finished") 

545 return JsonResponse(data) 

546 

547 

548class ImportAllAPIView(View): 

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

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

551 

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

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

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

555 

556 resource = model_helpers.get_resource(pid) 

557 if not resource: 

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

559 body = utils.get_file_content_in_utf8(file) 

560 journals = xml_cmds.addCollectionsXmlCmd( 

561 { 

562 "body": body, 

563 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

564 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

565 } 

566 ).do() 

567 if not journals: 

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

569 resource = journals[0] 

570 # resolver.copy_binary_files( 

571 # resource, 

572 # settings.MATHDOC_ARCHIVE_FOLDER, 

573 # settings.MERSENNE_TEST_DATA_FOLDER) 

574 

575 obj = resource.cast() 

576 

577 if obj.classname != "Collection": 

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

579 

580 cmd = xml_cmds.collectEntireCollectionXmlCmd( 

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

582 ) 

583 pids = cmd.do() 

584 

585 return pids 

586 

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

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

589 

590 try: 

591 pids, status, message = history_views.execute_and_record_func( 

592 "import", pid, pid, self.internal_do 

593 ) 

594 except Timeout as exception: 

595 return HttpResponse(exception, status=408) 

596 except Exception as exception: 

597 return HttpResponseServerError(exception) 

598 

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

600 return JsonResponse(data) 

601 

602 

603class DeployAllAPIView(View): 

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

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

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

607 

608 pids = [] 

609 

610 collection = model_helpers.get_collection(pid) 

611 if not collection: 

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

613 

614 if site == "numdam": 

615 server_url = settings.NUMDAM_PRE_URL 

616 elif site != "ptf_tools": 

617 server_url = getattr(collection, site)() 

618 if not server_url: 

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

620 

621 if site != "ptf_tools": 

622 # check if the collection exists on the server 

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

624 # image...) 

625 check_collection(collection, server_url, site) 

626 

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

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

629 pids.append(issue.pid) 

630 

631 return pids 

632 

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

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

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

636 

637 try: 

638 pids, status, message = history_views.execute_and_record_func( 

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

640 ) 

641 except Timeout as exception: 

642 return HttpResponse(exception, status=408) 

643 except Exception as exception: 

644 return HttpResponseServerError(exception) 

645 

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

647 return JsonResponse(data) 

648 

649 

650class AddIssuePDFView(View): 

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

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

653 self.pid = None 

654 self.issue = None 

655 self.collection = None 

656 self.site = "test_website" 

657 

658 def post_to_site(self, url): 

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

660 status = response.status_code 

661 if not (199 < status < 205): 

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

663 if status == 503: 

664 raise ServerUnderMaintenance(response.text) 

665 else: 

666 raise RuntimeError(response.text) 

667 

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

669 """ 

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

671 """ 

672 

673 issue_pid = self.issue.pid 

674 colid = self.collection.pid 

675 

676 if self.site == "website": 

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

678 resolver.copy_binary_files( 

679 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

680 ) 

681 else: 

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

683 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid) 

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

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

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

687 

688 to_path = resolver.get_disk_location( 

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

690 ) 

691 resolver.copy_file(from_path, to_path) 

692 

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

694 

695 if self.site == "test_website": 

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

697 absolute_url = self.request.build_absolute_uri(url) 

698 self.post_to_site(absolute_url) 

699 

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

701 absolute_url = server_url + url 

702 # Post to the test or production website 

703 self.post_to_site(absolute_url) 

704 

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

706 """ 

707 Send an issue PDF to the test or production website 

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

709 :param args: 

710 :param kwargs: 

711 :return: 

712 """ 

713 if check_lock(): 

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

715 messages.error(self.request, m) 

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

717 

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

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

720 

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

722 if not self.issue: 

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

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

725 

726 try: 

727 pids, status, message = history_views.execute_and_record_func( 

728 "deploy", 

729 self.pid, 

730 self.collection.pid, 

731 self.internal_do, 

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

733 ) 

734 

735 except Timeout as exception: 

736 return HttpResponse(exception, status=408) 

737 except Exception as exception: 

738 return HttpResponseServerError(exception) 

739 

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

741 return JsonResponse(data) 

742 

743 

744class ArchiveAllAPIView(View): 

745 """ 

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

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

748 @return array of issues pid 

749 """ 

750 

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

752 collection = kwargs["collection"] 

753 pids = [] 

754 colid = collection.pid 

755 

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

757 if os.path.isfile(logfile): 

758 os.remove(logfile) 

759 

760 ptf_cmds.exportPtfCmd( 

761 { 

762 "pid": colid, 

763 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

764 "with_binary_files": True, 

765 "for_archive": True, 

766 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

767 } 

768 ).do() 

769 

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

771 if os.path.isfile(cedramcls): 

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

773 resolver.create_folder(dest_folder) 

774 resolver.copy_file(cedramcls, dest_folder) 

775 

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

777 qs = issue.article_set.filter( 

778 date_online_first__isnull=True, date_published__isnull=True 

779 ) 

780 if qs.count() == 0: 

781 pids.append(issue.pid) 

782 

783 return pids 

784 

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

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

787 

788 collection = model_helpers.get_collection(pid) 

789 if not collection: 

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

791 

792 dict_ = {"collection": collection} 

793 args_ = [self] 

794 

795 try: 

796 pids, status, message = history_views.execute_and_record_func( 

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

798 ) 

799 except Timeout as exception: 

800 return HttpResponse(exception, status=408) 

801 except Exception as exception: 

802 return HttpResponseServerError(exception) 

803 

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

805 return JsonResponse(data) 

806 

807 

808class CreateAllDjvuAPIView(View): 

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

810 issue = kwargs["issue"] 

811 pids = [issue.pid] 

812 

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

814 pids.append(article.pid) 

815 

816 return pids 

817 

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

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

820 issue = model_helpers.get_container(pid) 

821 if not issue: 

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

823 

824 try: 

825 dict_ = {"issue": issue} 

826 args_ = [self] 

827 

828 pids, status, message = history_views.execute_and_record_func( 

829 "numdam", 

830 pid, 

831 issue.get_collection().pid, 

832 self.internal_do, 

833 "", 

834 False, 

835 None, 

836 None, 

837 *args_, 

838 **dict_, 

839 ) 

840 except Exception as exception: 

841 return HttpResponseServerError(exception) 

842 

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

844 return JsonResponse(data) 

845 

846 

847class ImportJatsContainerAPIView(View): 

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

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

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

851 

852 if pid and colid: 

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

854 

855 cmd = xml_cmds.addOrUpdateContainerXmlCmd( 

856 { 

857 "body": body, 

858 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

859 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

860 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

861 } 

862 ) 

863 container = cmd.do() 

864 if len(cmd.warnings) > 0: 

865 messages.warning( 

866 self.request, 

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

868 ) 

869 

870 if not container: 

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

872 

873 # resolver.copy_binary_files( 

874 # container, 

875 # settings.MATHDOC_ARCHIVE_FOLDER, 

876 # settings.MERSENNE_TEST_DATA_FOLDER) 

877 # 

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

879 # resolver.copy_binary_files( 

880 # article, 

881 # settings.MATHDOC_ARCHIVE_FOLDER, 

882 # settings.MERSENNE_TEST_DATA_FOLDER) 

883 else: 

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

885 

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

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

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

889 

890 try: 

891 _, status, message = history_views.execute_and_record_func( 

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

893 ) 

894 except Timeout as exception: 

895 return HttpResponse(exception, status=408) 

896 except Exception as exception: 

897 return HttpResponseServerError(exception) 

898 

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

900 return JsonResponse(data) 

901 

902 

903class DeployCollectionAPIView(View): 

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

905 

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

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

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

909 

910 collection = model_helpers.get_collection(colid) 

911 if not collection: 

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

913 

914 if site == "numdam": 

915 server_url = settings.NUMDAM_PRE_URL 

916 else: 

917 server_url = getattr(collection, site)() 

918 if not server_url: 

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

920 

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

922 check_collection(collection, server_url, site) 

923 

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

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

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

927 

928 try: 

929 _, status, message = history_views.execute_and_record_func( 

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

931 ) 

932 except Timeout as exception: 

933 return HttpResponse(exception, status=408) 

934 except Exception as exception: 

935 return HttpResponseServerError(exception) 

936 

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

938 return JsonResponse(data) 

939 

940 

941class DeployJatsResourceAPIView(View): 

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

943 

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

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

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

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

948 

949 if site == "ptf_tools": 

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

951 if check_lock(): 

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

953 messages.error(self.request, msg) 

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

955 

956 resource = model_helpers.get_resource(pid) 

957 if not resource: 

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

959 

960 obj = resource.cast() 

961 article = None 

962 if obj.classname == "Article": 

963 article = obj 

964 container = article.my_container 

965 articles_to_deploy = [article] 

966 else: 

967 container = obj 

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

969 

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

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

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

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

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

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

976 

977 collection = container.get_top_collection() 

978 colid = collection.pid 

979 djvu_exception = None 

980 

981 if site == "numdam": 

982 server_url = settings.NUMDAM_PRE_URL 

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

984 

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

986 # Add Djvu (before exporting the XML) 

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

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

989 try: 

990 cmd = ptf_cmds.addDjvuPtfCmd() 

991 cmd.set_resource(art) 

992 cmd.do() 

993 except Exception as e: 

994 # Djvu are optional. 

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

996 djvu_exception = e 

997 else: 

998 server_url = getattr(collection, site)() 

999 if not server_url: 

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

1001 

1002 # check if the collection exists on the server 

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

1004 # image...) 

1005 if article is None: 

1006 check_collection(collection, server_url, site) 

1007 

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

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

1010 if site == "website": 

1011 file_.write( 

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

1013 pid 

1014 ) 

1015 ) 

1016 

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

1018 cmd = ptf_cmds.publishResourcePtfCmd() 

1019 cmd.set_resource(resource) 

1020 updated_articles = cmd.do() 

1021 

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

1023 

1024 mersenneSite = model_helpers.get_site_mersenne(colid) 

1025 # create or update deployed_date on container and articles 

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

1027 

1028 for art in articles_to_deploy: 

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

1030 if art.my_container.year is None: 

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

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

1033 

1034 file_.write( 

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

1036 art.pid, art.date_online_first, art.date_published 

1037 ) 

1038 ) 

1039 

1040 if article is None: 

1041 resolver.copy_binary_files( 

1042 container, 

1043 settings.MERSENNE_TEST_DATA_FOLDER, 

1044 settings.MERSENNE_PROD_DATA_FOLDER, 

1045 ) 

1046 

1047 for art in articles_to_deploy: 

1048 resolver.copy_binary_files( 

1049 art, 

1050 settings.MERSENNE_TEST_DATA_FOLDER, 

1051 settings.MERSENNE_PROD_DATA_FOLDER, 

1052 ) 

1053 

1054 elif site == "test_website": 

1055 # create date_pre_published on articles without date_pre_published 

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

1057 cmd.set_resource(resource) 

1058 updated_articles = cmd.do() 

1059 

1060 tex.create_frontpage(colid, container, updated_articles) 

1061 

1062 export_to_website = site == "website" 

1063 

1064 if article is None: 

1065 with_djvu = site == "numdam" 

1066 xml = ptf_cmds.exportPtfCmd( 

1067 { 

1068 "pid": pid, 

1069 "with_djvu": with_djvu, 

1070 "export_to_website": export_to_website, 

1071 } 

1072 ).do() 

1073 body = xml.encode("utf8") 

1074 

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

1076 url = server_url + reverse("issue_upload") 

1077 else: 

1078 url = server_url + reverse("book_upload") 

1079 

1080 # verify=False: ignore TLS certificate 

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

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

1083 else: 

1084 xml = ptf_cmds.exportPtfCmd( 

1085 { 

1086 "pid": pid, 

1087 "with_djvu": False, 

1088 "article_standalone": True, 

1089 "collection_pid": collection.pid, 

1090 "export_to_website": export_to_website, 

1091 "export_folder": settings.LOG_DIR, 

1092 } 

1093 ).do() 

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

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

1096 xml_file = io.StringIO(xml) 

1097 files = {"xml": xml_file} 

1098 

1099 url = server_url + reverse( 

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

1101 ) 

1102 # verify=False: ignore TLS certificate 

1103 header = {} 

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

1105 

1106 status = response.status_code 

1107 

1108 if 199 < status < 205: 

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

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

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

1112 # /mersenne_prod_data during an upload to prod 

1113 if site == "website": 

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

1115 if container.doi: 

1116 recordDOI(container) 

1117 

1118 for art in articles_to_deploy: 

1119 # record DOI automatically when deploying in prod 

1120 

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

1122 recordDOI(art) 

1123 

1124 if colid == "CRBIOL": 

1125 recordPubmed( 

1126 art, force_update=False, updated_articles=updated_articles 

1127 ) 

1128 

1129 if colid == "PCJ": 

1130 self.update_pcj_editor(updated_articles) 

1131 

1132 # Archive the container or the article 

1133 if article is None: 

1134 archive_resource.delay( 

1135 pid, 

1136 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1137 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1138 ) 

1139 

1140 else: 

1141 archive_resource.delay( 

1142 pid, 

1143 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1144 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1145 article_doi=article.doi, 

1146 ) 

1147 # cmd = ptf_cmds.archiveIssuePtfCmd({ 

1148 # "pid": pid, 

1149 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1150 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER}) 

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

1152 # cmd.do() 

1153 

1154 elif site == "numdam": 

1155 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

1156 if colid in settings.NUMDAM_COLLECTIONS: 

1157 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

1158 

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

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

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

1162 

1163 elif status == 503: 

1164 raise ServerUnderMaintenance(response.text) 

1165 else: 

1166 raise RuntimeError(response.text) 

1167 

1168 if djvu_exception: 

1169 raise djvu_exception 

1170 

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

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

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

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

1175 

1176 try: 

1177 _, status, message = history_views.execute_and_record_func( 

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

1179 ) 

1180 except Timeout as exception: 

1181 return HttpResponse(exception, status=408) 

1182 except Exception as exception: 

1183 return HttpResponseServerError(exception) 

1184 

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

1186 return JsonResponse(data) 

1187 

1188 def update_pcj_editor(self, updated_articles): 

1189 for article in updated_articles: 

1190 data = { 

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

1192 "article_number": article.article_number, 

1193 } 

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

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

1196 

1197 

1198class DeployTranslatedArticleAPIView(CsrfExemptMixin, View): 

1199 article = None 

1200 

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

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

1203 

1204 translation = None 

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

1206 if trans_article.lang == lang: 

1207 translation = trans_article 

1208 

1209 if translation is None: 

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

1211 

1212 collection = self.article.get_top_collection() 

1213 colid = collection.pid 

1214 container = self.article.my_container 

1215 

1216 if translation.date_published is None: 

1217 # Add date posted 

1218 cmd = ptf_cmds.publishResourcePtfCmd() 

1219 cmd.set_resource(translation) 

1220 updated_articles = cmd.do() 

1221 

1222 # Recompile PDF to add the date posted 

1223 try: 

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

1225 except Exception: 

1226 raise PDFException( 

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

1228 ) 

1229 

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

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

1232 resolver.copy_binary_files( 

1233 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

1234 ) 

1235 

1236 # Deploy in prod 

1237 xml = ptf_cmds.exportPtfCmd( 

1238 { 

1239 "pid": self.article.pid, 

1240 "with_djvu": False, 

1241 "article_standalone": True, 

1242 "collection_pid": colid, 

1243 "export_to_website": True, 

1244 "export_folder": settings.LOG_DIR, 

1245 } 

1246 ).do() 

1247 xml_file = io.StringIO(xml) 

1248 files = {"xml": xml_file} 

1249 

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

1251 if not server_url: 

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

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

1254 header = {} 

1255 

1256 try: 

1257 response = requests.post( 

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

1259 ) # verify: ignore TLS certificate 

1260 status = response.status_code 

1261 except requests.exceptions.ConnectionError: 

1262 raise ServerUnderMaintenance( 

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

1264 ) 

1265 

1266 # Register translation in Crossref 

1267 if 199 < status < 205: 

1268 if self.article.allow_crossref(): 

1269 try: 

1270 recordDOI(translation) 

1271 except Exception: 

1272 raise DOIException( 

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

1274 ) 

1275 

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

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

1278 self.article = model_helpers.get_article_by_doi(doi) 

1279 if self.article is None: 

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

1281 

1282 try: 

1283 _, status, message = history_views.execute_and_record_func( 

1284 "deploy", 

1285 self.article.pid, 

1286 self.article.get_top_collection().pid, 

1287 self.internal_do, 

1288 "website", 

1289 ) 

1290 except Timeout as exception: 

1291 return HttpResponse(exception, status=408) 

1292 except Exception as exception: 

1293 return HttpResponseServerError(exception) 

1294 

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

1296 return JsonResponse(data) 

1297 

1298 

1299class DeleteJatsIssueAPIView(View): 

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

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

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

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

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

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

1306 status = 200 

1307 

1308 issue = model_helpers.get_container(pid) 

1309 if not issue: 

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

1311 try: 

1312 mersenneSite = model_helpers.get_site_mersenne(colid) 

1313 

1314 if site == "ptf_tools": 

1315 if issue.is_deployed(mersenneSite): 

1316 issue.undeploy(mersenneSite) 

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

1318 article.undeploy(mersenneSite) 

1319 

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

1321 

1322 cmd = ptf_cmds.addContainerPtfCmd( 

1323 { 

1324 "pid": issue.pid, 

1325 "ctype": "issue", 

1326 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

1327 } 

1328 ) 

1329 cmd.set_provider(p) 

1330 cmd.add_collection(issue.get_collection()) 

1331 cmd.set_object_to_be_deleted(issue) 

1332 cmd.undo() 

1333 

1334 else: 

1335 if site == "numdam": 

1336 server_url = settings.NUMDAM_PRE_URL 

1337 else: 

1338 collection = issue.get_collection() 

1339 server_url = getattr(collection, site)() 

1340 

1341 if not server_url: 

1342 message = "The collection has no " + site 

1343 status = 500 

1344 else: 

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

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

1347 status = response.status_code 

1348 

1349 if status == 404: 

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

1351 elif status > 204: 

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

1353 message = body[:1000] 

1354 else: 

1355 status = 200 

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

1357 if site == "website": 

1358 if issue.is_deployed(mersenneSite): 

1359 issue.undeploy(mersenneSite) 

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

1361 article.undeploy(mersenneSite) 

1362 # delete article binary files 

1363 folder = article.get_relative_folder() 

1364 resolver.delete_object_folder( 

1365 folder, 

1366 to_folder=settings.MERSENNE_PROD_DATA_FORLDER, 

1367 ) 

1368 # delete issue binary files 

1369 folder = issue.get_relative_folder() 

1370 resolver.delete_object_folder( 

1371 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER 

1372 ) 

1373 

1374 except Timeout as exception: 

1375 return HttpResponse(exception, status=408) 

1376 except Exception as exception: 

1377 return HttpResponseServerError(exception) 

1378 

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

1380 return JsonResponse(data) 

1381 

1382 

1383class ArchiveIssueAPIView(View): 

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

1385 try: 

1386 pid = kwargs["pid"] 

1387 colid = kwargs["colid"] 

1388 except IndexError: 

1389 raise Http404 

1390 

1391 try: 

1392 cmd = ptf_cmds.archiveIssuePtfCmd( 

1393 { 

1394 "pid": pid, 

1395 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1396 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

1397 } 

1398 ) 

1399 result_, status, message = history_views.execute_and_record_func( 

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

1401 ) 

1402 except Exception as exception: 

1403 return HttpResponseServerError(exception) 

1404 

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

1406 return JsonResponse(data) 

1407 

1408 

1409class CreateDjvuAPIView(View): 

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

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

1412 

1413 resource = model_helpers.get_resource(pid) 

1414 cmd = ptf_cmds.addDjvuPtfCmd() 

1415 cmd.set_resource(resource) 

1416 cmd.do() 

1417 

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

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

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

1421 

1422 try: 

1423 _, status, message = history_views.execute_and_record_func( 

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

1425 ) 

1426 except Exception as exception: 

1427 return HttpResponseServerError(exception) 

1428 

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

1430 return JsonResponse(data) 

1431 

1432 

1433class PTFToolsHomeView(LoginRequiredMixin, View): 

1434 """ 

1435 Home Page. 

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

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

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

1439 - Comment moderator -> Comments dashboard 

1440 - Others -> 404 response 

1441 """ 

1442 

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

1444 # Staff or user with authorized collections 

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

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

1447 

1448 colids = get_authorized_collections(request.user) 

1449 is_mod = is_comment_moderator(request.user) 

1450 

1451 # The user has no rights 

1452 if not (colids or is_mod): 

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

1454 # Comment moderator only 

1455 elif not colids: 

1456 return HttpResponseRedirect(reverse("comment_list")) 

1457 

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

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

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

1461 

1462 # User with multiple authorized collections - Special home 

1463 context = {} 

1464 context["overview"] = True 

1465 

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

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

1468 

1469 # Comments summary 

1470 try: 

1471 error, comments_data = get_comments_for_home(request.user) 

1472 except AttributeError: 

1473 error, comments_data = True, {} 

1474 

1475 context["comment_server_ok"] = False 

1476 

1477 if not error: 

1478 context["comment_server_ok"] = True 

1479 if comments_data: 

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

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

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

1483 

1484 # TODO: Translations summary 

1485 context["translation_server_ok"] = False 

1486 

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

1488 context["collections"] = sorted( 

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

1490 ) 

1491 

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

1493 

1494 

1495class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin): 

1496 columns = 5 

1497 

1498 def get_common_context_data(self, **kwargs): 

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

1500 now = timezone.now() 

1501 curyear = now.year 

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

1503 

1504 context["collections"] = settings.MERSENNE_COLLECTIONS 

1505 context["containers_to_be_published"] = [] 

1506 context["last_col_events"] = [] 

1507 

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

1509 clockss_gap = get_gap(now, event) 

1510 

1511 context["years"] = years 

1512 context["clockss_gap"] = clockss_gap 

1513 

1514 return context 

1515 

1516 def calculate_articles_and_pages(self, pid, years): 

1517 data_by_year = [] 

1518 total_articles = [0] * len(years) 

1519 total_pages = [0] * len(years) 

1520 

1521 for year in years: 

1522 articles = self.get_articles_for_year(pid, year) 

1523 articles_count = articles.count() 

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

1525 

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

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

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

1529 

1530 return data_by_year, total_articles, total_pages 

1531 

1532 def get_articles_for_year(self, pid, year): 

1533 return Article.objects.filter( 

1534 Q(my_container__my_collection__pid=pid) 

1535 & ( 

1536 Q(date_published__year=year, date_online_first__isnull=True) 

1537 | Q(date_online_first__year=year) 

1538 ) 

1539 ).prefetch_related("resourcecount_set") 

1540 

1541 

1542class PublishedArticlesDashboardView(BaseMersenneDashboardView): 

1543 template_name = "dashboard/published_articles.html" 

1544 

1545 def get_context_data(self, **kwargs): 

1546 context = self.get_common_context_data(**kwargs) 

1547 years = context["years"] 

1548 

1549 published_articles = [] 

1550 total_published_articles = [ 

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

1552 ] 

1553 

1554 for pid in settings.MERSENNE_COLLECTIONS: 

1555 if pid != "MERSENNE": 

1556 articles_data, total_articles, total_pages = self.calculate_articles_and_pages( 

1557 pid, years 

1558 ) 

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

1560 

1561 for i, year in enumerate(years): 

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

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

1564 

1565 context["published_articles"] = published_articles 

1566 context["total_published_articles"] = total_published_articles 

1567 

1568 return context 

1569 

1570 

1571class CreatedVolumesDashboardView(BaseMersenneDashboardView): 

1572 template_name = "dashboard/created_volumes.html" 

1573 

1574 def get_context_data(self, **kwargs): 

1575 context = self.get_common_context_data(**kwargs) 

1576 years = context["years"] 

1577 

1578 created_volumes = [] 

1579 total_created_volumes = [ 

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

1581 ] 

1582 

1583 for pid in settings.MERSENNE_COLLECTIONS: 

1584 if pid != "MERSENNE": 

1585 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages( 

1586 pid, years 

1587 ) 

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

1589 

1590 for i, year in enumerate(years): 

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

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

1593 

1594 context["created_volumes"] = created_volumes 

1595 context["total_created_volumes"] = total_created_volumes 

1596 

1597 return context 

1598 

1599 def calculate_volumes_and_pages(self, pid, years): 

1600 data_by_year = [] 

1601 total_articles = [0] * len(years) 

1602 total_pages = [0] * len(years) 

1603 

1604 for year in years: 

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

1606 articles_count = 0 

1607 page_count = 0 

1608 

1609 for issue in issues: 

1610 articles = issue.article_set.filter( 

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

1612 ).prefetch_related("resourcecount_set") 

1613 

1614 articles_count += articles.count() 

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

1616 

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

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

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

1620 

1621 return data_by_year, total_articles, total_pages 

1622 

1623 

1624class ReferencingChoice(View): 

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

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

1627 return redirect( 

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

1629 ) 

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

1631 comp = ReferencingCheckerWos() 

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

1633 if journal is None: 

1634 return render( 

1635 request, 

1636 "dashboard/referencing.html", 

1637 { 

1638 "error": "Collection not found", 

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

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

1641 }, 

1642 ) 

1643 return render( 

1644 request, 

1645 "dashboard/referencing.html", 

1646 { 

1647 "journal": journal, 

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

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

1650 }, 

1651 ) 

1652 

1653 

1654class ReferencingWosFileView(View): 

1655 template_name = "dashboard/referencing.html" 

1656 

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

1658 colid = request.POST["colid"] 

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

1660 message = "No file uploaded" 

1661 return render( 

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

1663 ) 

1664 uploaded_file = request.FILES["risfile"] 

1665 comp = ReferencingCheckerWos() 

1666 journal = comp.check_references(colid, uploaded_file) 

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

1668 

1669 

1670class ReferencingDashboardView(BaseMersenneDashboardView): 

1671 template_name = "dashboard/referencing.html" 

1672 

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

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

1675 comp = ReferencingCheckerAds() 

1676 journal = comp.check_references(colid) 

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

1678 

1679 

1680class BaseCollectionView(TemplateView): 

1681 def get_context_data(self, **kwargs): 

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

1683 aid = context.get("aid") 

1684 year = context.get("year") 

1685 

1686 if aid and year: 

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

1688 

1689 return context 

1690 

1691 def get_collection(self, aid, year): 

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

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

1694 

1695 

1696class ArticleListView(BaseCollectionView): 

1697 template_name = "collection-list.html" 

1698 

1699 def get_collection(self, aid, year): 

1700 return Article.objects.filter( 

1701 Q(my_container__my_collection__pid=aid) 

1702 & ( 

1703 Q(date_published__year=year, date_online_first__isnull=True) 

1704 | Q(date_online_first__year=year) 

1705 ) 

1706 ).prefetch_related("resourcecount_set") 

1707 

1708 

1709class VolumeListView(BaseCollectionView): 

1710 template_name = "collection-list.html" 

1711 

1712 def get_collection(self, aid, year): 

1713 return Article.objects.filter( 

1714 Q(my_container__my_collection__pid=aid, my_container__year=year) 

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

1716 ).prefetch_related("resourcecount_set") 

1717 

1718 

1719class DOAJResourceRegisterView(View): 

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

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

1722 resource = model_helpers.get_resource(pid) 

1723 if resource is None: 

1724 raise Http404 

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

1726 resource.colid, None 

1727 ): 

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

1729 

1730 try: 

1731 data = {} 

1732 doaj_meta, response = doaj_pid_register(pid) 

1733 if response is None: 

1734 return HttpResponse(status=204) 

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

1736 data.update(doaj_meta) 

1737 else: 

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

1739 except Timeout as exception: 

1740 return HttpResponse(exception, status=408) 

1741 except Exception as exception: 

1742 return HttpResponseServerError(exception) 

1743 return JsonResponse(data) 

1744 

1745 

1746class CROSSREFResourceRegisterView(View): 

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

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

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

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

1751 if not request.user.is_superuser: 

1752 force = None 

1753 

1754 resource = model_helpers.get_resource(pid) 

1755 if resource is None: 

1756 raise Http404 

1757 

1758 resource = resource.cast() 

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

1760 try: 

1761 data = meth(resource, force) 

1762 except Timeout as exception: 

1763 return HttpResponse(exception, status=408) 

1764 except Exception as exception: 

1765 return HttpResponseServerError(exception) 

1766 return JsonResponse(data) 

1767 

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

1769 result = {"status": 404} 

1770 if ( 

1771 article.doi 

1772 and not article.do_not_publish 

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

1774 ): 

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

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

1777 result = recordDOI(article) 

1778 return result 

1779 

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

1781 return recordDOI(collection) 

1782 

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

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

1785 

1786 if container.ctype == "issue": 

1787 if container.doi: 

1788 result = recordDOI(container) 

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

1790 return result 

1791 if force == "force": 

1792 articles = container.article_set.exclude( 

1793 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True 

1794 ) 

1795 else: 

1796 articles = container.article_set.exclude( 

1797 doi__isnull=True, 

1798 do_not_publish=True, 

1799 date_published__isnull=True, 

1800 date_online_first__isnull=True, 

1801 ) 

1802 

1803 for article in articles: 

1804 result = self.recordDOIArticle(article, force) 

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

1806 data = result 

1807 else: 

1808 return recordDOI(container) 

1809 return data 

1810 

1811 

1812class CROSSREFResourceCheckStatusView(View): 

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

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

1815 resource = model_helpers.get_resource(pid) 

1816 if resource is None: 

1817 raise Http404 

1818 resource = resource.cast() 

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

1820 try: 

1821 meth(resource) 

1822 except Timeout as exception: 

1823 return HttpResponse(exception, status=408) 

1824 except Exception as exception: 

1825 return HttpResponseServerError(exception) 

1826 

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

1828 return JsonResponse(data) 

1829 

1830 def checkDOIArticle(self, article): 

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

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

1833 checkDOI(article) 

1834 

1835 def checkDOICollection(self, collection): 

1836 checkDOI(collection) 

1837 

1838 def checkDOIContainer(self, container): 

1839 if container.doi is not None: 

1840 checkDOI(container) 

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

1842 self.checkDOIArticle(article) 

1843 

1844 

1845class CROSSREFResourcePendingPublicationRegisterView(View): 

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

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

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

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

1850 if not request.user.is_superuser: 

1851 force = None 

1852 

1853 resource = model_helpers.get_resource(pid) 

1854 if resource is None: 

1855 raise Http404 

1856 

1857 resource = resource.cast() 

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

1859 try: 

1860 data = meth(resource, force) 

1861 except Timeout as exception: 

1862 return HttpResponse(exception, status=408) 

1863 except Exception as exception: 

1864 return HttpResponseServerError(exception) 

1865 return JsonResponse(data) 

1866 

1867 def recordPendingPublicationArticle(self, article): 

1868 result = {"status": 404} 

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

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

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

1872 result = recordPendingPublication(article) 

1873 return result 

1874 

1875 

1876class RegisterPubmedFormView(FormView): 

1877 template_name = "record_pubmed_dialog.html" 

1878 form_class = RegisterPubmedForm 

1879 

1880 def get_context_data(self, **kwargs): 

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

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

1883 context["helper"] = PtfLargeModalFormHelper 

1884 return context 

1885 

1886 

1887class RegisterPubmedView(View): 

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

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

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

1891 

1892 article = model_helpers.get_article(pid) 

1893 if article is None: 

1894 raise Http404 

1895 try: 

1896 recordPubmed(article, update_article) 

1897 except Exception as exception: 

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

1899 return HttpResponseServerError(exception) 

1900 

1901 return HttpResponseRedirect( 

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

1903 ) 

1904 

1905 

1906class PTFToolsContainerView(TemplateView): 

1907 template_name = "" 

1908 

1909 def get_context_data(self, **kwargs): 

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

1911 

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

1913 if container is None: 

1914 raise Http404 

1915 citing_articles = container.citations() 

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

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

1918 book_parts = ( 

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

1920 ) 

1921 references = False 

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

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

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

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

1926 references = True 

1927 context.update( 

1928 { 

1929 "book": container, 

1930 "book_parts": list(book_parts), 

1931 "source": source, 

1932 "citing_articles": citing_articles, 

1933 "references": references, 

1934 "test_website": container.get_top_collection() 

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

1936 .location, 

1937 "prod_website": container.get_top_collection() 

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

1939 .location, 

1940 } 

1941 ) 

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

1943 else: 

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

1945 for article in articles: 

1946 try: 

1947 last_match = ( 

1948 history_models.HistoryEvent.objects.filter( 

1949 pid=article.pid, 

1950 type="matching", 

1951 ) 

1952 .only("created_on") 

1953 .latest("created_on") 

1954 ) 

1955 except history_models.HistoryEvent.DoesNotExist as _: 

1956 article.last_match = None 

1957 else: 

1958 article.last_match = last_match.created_on 

1959 

1960 # article1 = articles.first() 

1961 # date = article1.deployed_date() 

1962 # TODO next_issue, previous_issue 

1963 

1964 # check DOI est maintenant une commande à part 

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

1966 # articlesWithStatus = [] 

1967 # for article in articles: 

1968 # checkDOIExistence(article) 

1969 # articlesWithStatus.append(article) 

1970 

1971 test_location = prod_location = "" 

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

1973 if qs: 

1974 test_location = qs.first().location 

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

1976 if qs: 

1977 prod_location = qs.first().location 

1978 context.update( 

1979 { 

1980 "issue": container, 

1981 "articles": articles, 

1982 "source": source, 

1983 "citing_articles": citing_articles, 

1984 "test_website": test_location, 

1985 "prod_website": prod_location, 

1986 } 

1987 ) 

1988 

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

1990 context["is_issue_pending_publication"] = True 

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

1992 

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

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

1995 return context 

1996 

1997 

1998class ExtLinkInline(InlineFormSetFactory): 

1999 model = ExtLink 

2000 form_class = ExtLinkForm 

2001 factory_kwargs = {"extra": 0} 

2002 

2003 

2004class ResourceIdInline(InlineFormSetFactory): 

2005 model = ResourceId 

2006 form_class = ResourceIdForm 

2007 factory_kwargs = {"extra": 0} 

2008 

2009 

2010class IssueDetailAPIView(View): 

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

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

2013 deployed_date = issue.deployed_date() 

2014 result = { 

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

2016 if deployed_date 

2017 else None, 

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

2019 "all_doi_are_registered": issue.all_doi_are_registered(), 

2020 "registered_in_doaj": issue.registered_in_doaj(), 

2021 "doi": issue.my_collection.doi, 

2022 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(), 

2023 } 

2024 try: 

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

2026 except history_models.HistoryEvent.DoesNotExist as _: 

2027 pass 

2028 else: 

2029 result["latest"] = latest.message 

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

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

2032 ) 

2033 

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

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

2036 try: 

2037 result[event_type] = timezone.localtime( 

2038 history_models.HistoryEvent.objects.filter( 

2039 type=event_type, 

2040 status="OK", 

2041 pid__startswith=issue.pid, 

2042 ) 

2043 .latest("created_on") 

2044 .created_on 

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

2046 except history_models.HistoryEvent.DoesNotExist as _: 

2047 result[event_type] = "" 

2048 return JsonResponse(result) 

2049 

2050 

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

2052 model = Collection 

2053 form_class = CollectionForm 

2054 inlines = [ResourceIdInline, ExtLinkInline] 

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

2056 

2057 def get_context_data(self, **kwargs): 

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

2059 context["helper"] = PtfFormHelper 

2060 context["formset_helper"] = FormSetHelper 

2061 return context 

2062 

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

2064 if description: 

2065 la = Abstract( 

2066 resource=collection, 

2067 tag="description", 

2068 lang=lang, 

2069 seq=seq, 

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

2071 value_html=description, 

2072 value_tex=description, 

2073 ) 

2074 la.save() 

2075 

2076 def form_valid(self, form): 

2077 if form.instance.abbrev: 

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

2079 else: 

2080 form.instance.title_xml = ( 

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

2082 ) 

2083 

2084 form.instance.title_html = form.instance.title_tex 

2085 form.instance.title_sort = form.instance.title_tex 

2086 result = super().form_valid(form) 

2087 

2088 collection = self.object 

2089 collection.abstract_set.all().delete() 

2090 

2091 seq = 1 

2092 description = form.cleaned_data["description_en"] 

2093 if description: 

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

2095 seq += 1 

2096 description = form.cleaned_data["description_fr"] 

2097 if description: 

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

2099 

2100 return result 

2101 

2102 def get_success_url(self): 

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

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

2105 

2106 

2107class CollectionCreate(CollectionFormView, CreateWithInlinesView): 

2108 """ 

2109 Warning : Not yet finished 

2110 Automatic site membership creation is still missing 

2111 """ 

2112 

2113 

2114class CollectionUpdate(CollectionFormView, UpdateWithInlinesView): 

2115 slug_field = "pid" 

2116 slug_url_kwarg = "pid" 

2117 

2118 

2119def suggest_load_journal_dois(colid): 

2120 articles = ( 

2121 Article.objects.filter(my_container__my_collection__pid=colid) 

2122 .filter(doi__isnull=False) 

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

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

2125 ) 

2126 

2127 try: 

2128 articles = sorted( 

2129 articles, 

2130 key=lambda d: ( 

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

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

2133 ), 

2134 ) 

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

2136 pass 

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

2138 

2139 

2140def get_context_with_volumes(journal): 

2141 result = model_helpers.get_volumes_in_collection(journal) 

2142 volume_count = result["volume_count"] 

2143 collections = [] 

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

2145 item = model_helpers.get_volumes_in_collection(ancestor) 

2146 volume_count = max(0, volume_count) 

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

2148 collections.append(item) 

2149 

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

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

2152 collections.append(result) 

2153 

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

2155 collections.sort( 

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

2157 reverse=True, 

2158 ) 

2159 

2160 context = { 

2161 "journal": journal, 

2162 "sorted_issues": result["sorted_issues"], 

2163 "volume_count": volume_count, 

2164 "max_width": result["max_width"], 

2165 "collections": collections, 

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

2167 } 

2168 return context 

2169 

2170 

2171class CollectionDetail( 

2172 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin 

2173): 

2174 model = Collection 

2175 slug_field = "pid" 

2176 slug_url_kwarg = "pid" 

2177 template_name = "ptf/collection_detail.html" 

2178 

2179 def test_func(self): 

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

2181 

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

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

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

2185 

2186 def get_context_data(self, **kwargs): 

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

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

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

2190 ) 

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

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

2193 

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

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

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

2197 pid=context["issue_to_appear_pid"] 

2198 ).exists() 

2199 try: 

2200 latest_error = history_models.HistoryEvent.objects.filter( 

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

2202 ).latest("created_on") 

2203 except history_models.HistoryEvent.DoesNotExist as _: 

2204 pass 

2205 else: 

2206 message = latest_error.message 

2207 if message: 

2208 i = message.find(" - ") 

2209 latest_exception = message[:i] 

2210 latest_error_message = message[i + 3 :] 

2211 context["latest_exception"] = latest_exception 

2212 context["latest_exception_date"] = latest_error.created_on 

2213 context["latest_exception_type"] = latest_error.type 

2214 context["latest_error_message"] = latest_error_message 

2215 

2216 archive_in_error = history_models.HistoryEvent.objects.filter( 

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

2218 ).exists() 

2219 

2220 context["archive_in_error"] = archive_in_error 

2221 

2222 return context 

2223 

2224 def get_queryset(self): 

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

2226 

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

2228 query |= ancestor.content.all() 

2229 

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

2231 

2232 

2233class ContainerEditView(FormView): 

2234 template_name = "container_form.html" 

2235 form_class = ContainerForm 

2236 

2237 def get_success_url(self): 

2238 if self.kwargs["pid"]: 

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

2240 return reverse("mersenne_dashboard/published_articles") 

2241 

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

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

2244 

2245 def get_form_kwargs(self): 

2246 kwargs = super().get_form_kwargs() 

2247 if "pid" not in self.kwargs: 

2248 self.kwargs["pid"] = None 

2249 if "colid" not in self.kwargs: 

2250 self.kwargs["colid"] = None 

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

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

2253 # It is used when you submit a new container 

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

2255 

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

2257 self.kwargs["pid"] 

2258 ) 

2259 return kwargs 

2260 

2261 def get_context_data(self, **kwargs): 

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

2263 

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

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

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

2267 

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

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

2270 

2271 return context 

2272 

2273 def form_valid(self, form): 

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

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

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

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

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

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

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

2281 

2282 collection = None 

2283 issue = self.kwargs["container"] 

2284 if issue is not None: 

2285 collection = issue.my_collection 

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

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

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

2289 else: 

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

2291 

2292 if collection is None: 

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

2294 

2295 # Icon 

2296 new_icon_location = "" 

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

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

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

2300 

2301 icon_filename = resolver.get_disk_location( 

2302 settings.MERSENNE_TEST_DATA_FOLDER, 

2303 collection.pid, 

2304 file_extension, 

2305 new_pid, 

2306 None, 

2307 True, 

2308 ) 

2309 

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

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

2312 destination.write(chunk) 

2313 

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

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

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

2317 if name == "special_issue_create": 

2318 self.kwargs["name"] = name 

2319 if self.kwargs["container"]: 

2320 # Edit Issue 

2321 issue = self.kwargs["container"] 

2322 if issue is None: 

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

2324 

2325 issue.pid = new_pid 

2326 issue.title_tex = issue.title_html = new_title 

2327 issue.title_xml = build_title_xml( 

2328 title=new_title, 

2329 lang=issue.lang, 

2330 title_type="issue-title", 

2331 ) 

2332 

2333 trans_lang = "" 

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

2335 trans_lang = issue.trans_lang 

2336 elif new_trans_title != "": 

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

2338 issue.trans_lang = trans_lang 

2339 

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

2341 issue.trans_title_html = "" 

2342 issue.trans_title_tex = "" 

2343 title_xml = build_title_xml( 

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

2345 ) 

2346 try: 

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

2348 trans_title_object.title_html = new_trans_title 

2349 trans_title_object.title_xml = title_xml 

2350 trans_title_object.save() 

2351 except Title.DoesNotExist: 

2352 trans_title = Title( 

2353 resource=issue, 

2354 lang=trans_lang, 

2355 type="main", 

2356 title_html=new_trans_title, 

2357 title_xml=title_xml, 

2358 ) 

2359 trans_title.save() 

2360 issue.year = new_year 

2361 issue.volume = new_volume 

2362 issue.volume_int = make_int(new_volume) 

2363 issue.number = new_number 

2364 issue.number_int = make_int(new_number) 

2365 issue.save() 

2366 else: 

2367 xissue = create_issuedata() 

2368 

2369 xissue.ctype = "issue" 

2370 xissue.pid = new_pid 

2371 xissue.lang = "en" 

2372 xissue.title_tex = new_title 

2373 xissue.title_html = new_title 

2374 xissue.title_xml = build_title_xml( 

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

2376 ) 

2377 

2378 if new_trans_title != "": 

2379 trans_lang = "fr" 

2380 title_xml = build_title_xml( 

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

2382 ) 

2383 title = create_titledata( 

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

2385 ) 

2386 issue.titles = [title] 

2387 

2388 xissue.year = new_year 

2389 xissue.volume = new_volume 

2390 xissue.number = new_number 

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

2392 

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

2394 cmd.add_collection(collection) 

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

2396 issue = cmd.do() 

2397 

2398 self.kwargs["pid"] = new_pid 

2399 

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

2401 params = { 

2402 "icon_location": new_icon_location, 

2403 } 

2404 cmd = ptf_cmds.updateContainerPtfCmd(params) 

2405 cmd.set_resource(issue) 

2406 cmd.do() 

2407 

2408 publisher = model_helpers.get_publisher(new_publisher) 

2409 if not publisher: 

2410 xpub = create_publisherdata() 

2411 xpub.name = new_publisher 

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

2413 issue.my_publisher = publisher 

2414 issue.save() 

2415 

2416 self.set_success_message() 

2417 

2418 return super().form_valid(form) 

2419 

2420 

2421# class ArticleEditView(FormView): 

2422# template_name = 'article_form.html' 

2423# form_class = ArticleForm 

2424# 

2425# def get_success_url(self): 

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

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

2428# return reverse('mersenne_dashboard/published_articles') 

2429# 

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

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

2432# 

2433# def get_form_kwargs(self): 

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

2435# 

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

2437# # Article creation: pid is None 

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

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

2440# # Article edit: issue_id is not passed 

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

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

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

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

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

2446# 

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

2448# return kwargs 

2449# 

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

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

2452# 

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

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

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

2456# 

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

2458# 

2459# article = context['article'] 

2460# if article: 

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

2462# context['kwds_fr'] = None 

2463# context['kwds_en'] = None 

2464# kwd_gps = article.get_non_msc_kwds() 

2465# for kwd_gp in kwd_gps: 

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

2467# if kwd_gp.value_xml: 

2468# kwd_ = types.SimpleNamespace() 

2469# kwd_.value = kwd_gp.value_tex 

2470# context['kwd_unstructured_fr'] = kwd_ 

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

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

2473# if kwd_gp.value_xml: 

2474# kwd_ = types.SimpleNamespace() 

2475# kwd_.value = kwd_gp.value_tex 

2476# context['kwd_unstructured_en'] = kwd_ 

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

2478# 

2479# # Article creation: init pid 

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

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

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

2483# 

2484# return context 

2485# 

2486# def form_valid(self, form): 

2487# 

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

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

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

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

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

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

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

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

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

2497# 

2498# # TODO support MathML 

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

2500# # We need to pass trans_title to get_title_xml 

2501# # Meanwhile, ignore new_title_xml 

2502# new_title_xml = jats_parser.get_title_xml(new_title) 

2503# new_title_html = new_title 

2504# 

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

2506# i = 1 

2507# new_authors = [] 

2508# old_author_contributions = [] 

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

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

2511# 

2512# while authors_count > 0: 

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

2514# 

2515# if prefix is not None: 

2516# addresses = [] 

2517# if len(old_author_contributions) >= i: 

2518# old_author_contribution = old_author_contributions[i - 1] 

2519# addresses = [contrib_address.address for contrib_address in 

2520# old_author_contribution.get_addresses()] 

2521# 

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

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

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

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

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

2527# deceased_before_publication = deceased == 'on' 

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

2529# equal_contrib = equal_contrib == 'on' 

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

2531# corresponding = corresponding == 'on' 

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

2533# 

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

2535# params['deceased_before_publication'] = deceased_before_publication 

2536# params['equal_contrib'] = equal_contrib 

2537# params['corresponding'] = corresponding 

2538# params['addresses'] = addresses 

2539# params['email'] = email 

2540# 

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

2542# 

2543# new_authors.append(params) 

2544# 

2545# authors_count -= 1 

2546# i += 1 

2547# 

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

2549# i = 1 

2550# new_kwds_fr = [] 

2551# while kwds_fr_count > 0: 

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

2553# new_kwds_fr.append(value) 

2554# kwds_fr_count -= 1 

2555# i += 1 

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

2557# 

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

2559# i = 1 

2560# new_kwds_en = [] 

2561# while kwds_en_count > 0: 

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

2563# new_kwds_en.append(value) 

2564# kwds_en_count -= 1 

2565# i += 1 

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

2567# 

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

2569# # Edit article 

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

2571# else: 

2572# # New article 

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

2574# 

2575# if container is None: 

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

2577# 

2578# collection = container.my_collection 

2579# 

2580# # Copy PDF file & extract full text 

2581# body = '' 

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

2583# collection.pid, 

2584# "pdf", 

2585# container.pid, 

2586# new_pid, 

2587# True) 

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

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

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

2591# destination.write(chunk) 

2592# 

2593# # Extract full text from the PDF 

2594# body = utils.pdf_to_text(pdf_filename) 

2595# 

2596# # Icon 

2597# new_icon_location = '' 

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

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

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

2601# 

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

2603# collection.pid, 

2604# file_extension, 

2605# container.pid, 

2606# new_pid, 

2607# True) 

2608# 

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

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

2611# destination.write(chunk) 

2612# 

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

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

2615# 

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

2617# # Edit article 

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

2619# article.fpage = new_fpage 

2620# article.lpage = new_lpage 

2621# article.page_range = new_page_range 

2622# article.coi_statement = new_coi_statement 

2623# article.show_body = new_show_body 

2624# article.do_not_publish = new_do_not_publish 

2625# article.save() 

2626# 

2627# else: 

2628# # New article 

2629# params = { 

2630# 'pid': new_pid, 

2631# 'title_xml': new_title_xml, 

2632# 'title_html': new_title_html, 

2633# 'title_tex': new_title, 

2634# 'fpage': new_fpage, 

2635# 'lpage': new_lpage, 

2636# 'page_range': new_page_range, 

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

2638# 'body': body, 

2639# 'coi_statement': new_coi_statement, 

2640# 'show_body': new_show_body, 

2641# 'do_not_publish': new_do_not_publish 

2642# } 

2643# 

2644# xarticle = create_articledata() 

2645# xarticle.pid = new_pid 

2646# xarticle.title_xml = new_title_xml 

2647# xarticle.title_html = new_title_html 

2648# xarticle.title_tex = new_title 

2649# xarticle.fpage = new_fpage 

2650# xarticle.lpage = new_lpage 

2651# xarticle.page_range = new_page_range 

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

2653# xarticle.body = body 

2654# xarticle.coi_statement = new_coi_statement 

2655# params['xobj'] = xarticle 

2656# 

2657# cmd = ptf_cmds.addArticlePtfCmd(params) 

2658# cmd.set_container(container) 

2659# cmd.add_collection(container.my_collection) 

2660# article = cmd.do() 

2661# 

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

2663# 

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

2665# params = { 

2666# # 'title_xml': new_title_xml, 

2667# # 'title_html': new_title_html, 

2668# # 'title_tex': new_title, 

2669# 'authors': new_authors, 

2670# 'page_count': new_page_count, 

2671# 'icon_location': new_icon_location, 

2672# 'body': body, 

2673# 'use_kwds': True, 

2674# 'kwds_fr': new_kwds_fr, 

2675# 'kwds_en': new_kwds_en, 

2676# 'kwd_uns_fr': new_kwd_uns_fr, 

2677# 'kwd_uns_en': new_kwd_uns_en 

2678# } 

2679# cmd = ptf_cmds.updateArticlePtfCmd(params) 

2680# cmd.set_article(article) 

2681# cmd.do() 

2682# 

2683# self.set_success_message() 

2684# 

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

2686 

2687 

2688@require_http_methods(["POST"]) 

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

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

2691 

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

2693 

2694 article = model_helpers.get_article(pid) 

2695 if article: 

2696 article.do_not_publish = not article.do_not_publish 

2697 article.save() 

2698 else: 

2699 raise Http404 

2700 

2701 return HttpResponseRedirect(next) 

2702 

2703 

2704@require_http_methods(["POST"]) 

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

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

2707 

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

2709 

2710 article = model_helpers.get_article(pid) 

2711 if article: 

2712 article.show_body = not article.show_body 

2713 article.save() 

2714 else: 

2715 raise Http404 

2716 

2717 return HttpResponseRedirect(next) 

2718 

2719 

2720class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditFormWithVueAPIView): 

2721 """ 

2722 API to get/post article metadata 

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

2724 """ 

2725 

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

2727 """ 

2728 we define here what fields we want in the form 

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

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

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

2732 self.fields_to_update = [ 

2733 "lang", 

2734 "atype", 

2735 "contributors", 

2736 "abstracts", 

2737 "kwds", 

2738 "titles", 

2739 "trans_title_html", 

2740 "title_html", 

2741 "title_xml", 

2742 "streams", 

2743 "ext_links", 

2744 ] 

2745 self.additional_fields = [ 

2746 "pid", 

2747 "doi", 

2748 "container_pid", 

2749 "pdf", 

2750 "illustration", 

2751 ] 

2752 self.editorial_tools = ["translation", "sidebar", "lang_selection"] 

2753 self.article_container_pid = "" 

2754 self.back_url = "trammel" 

2755 

2756 def save_data(self, data_article): 

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

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

2759 params = { 

2760 "pid": data_article.pid, 

2761 "export_folder": settings.MERSENNE_TMP_FOLDER, 

2762 "export_all": True, 

2763 "with_binary_files": False, 

2764 } 

2765 ptf_cmds.exportExtraDataPtfCmd(params).do() 

2766 

2767 def restore_data(self, article): 

2768 ptf_cmds.importExtraDataPtfCmd( 

2769 { 

2770 "pid": article.pid, 

2771 "import_folder": settings.MERSENNE_TMP_FOLDER, 

2772 } 

2773 ).do() 

2774 

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

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

2777 return data 

2778 

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

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

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

2782 return redirect( 

2783 "api-edit-article", 

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

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

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

2787 ) 

2788 else: 

2789 raise Http404 

2790 

2791 

2792class ArticleEditWithVueView(LoginRequiredMixin, TemplateView): 

2793 template_name = "article_form.html" 

2794 

2795 def get_success_url(self): 

2796 if self.kwargs["doi"]: 

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

2798 return reverse("mersenne_dashboard/published_articles") 

2799 

2800 def get_context_data(self, **kwargs): 

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

2802 if "doi" in self.kwargs: 

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

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

2805 

2806 return context 

2807 

2808 

2809class ArticleDeleteView(View): 

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

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

2812 article = get_object_or_404(Article, pid=pid) 

2813 

2814 try: 

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

2816 article.undeploy(mersenneSite) 

2817 

2818 cmd = ptf_cmds.addArticlePtfCmd( 

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

2820 ) 

2821 cmd.set_container(article.my_container) 

2822 cmd.set_object_to_be_deleted(article) 

2823 cmd.undo() 

2824 except Exception as exception: 

2825 return HttpResponseServerError(exception) 

2826 

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

2828 return JsonResponse(data) 

2829 

2830 

2831def get_messages_in_queue(): 

2832 app = Celery("ptf-tools") 

2833 # tasks = list(current_app.tasks) 

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

2835 print(tasks) 

2836 # i = app.control.inspect() 

2837 

2838 with app.connection_or_acquire() as conn: 

2839 remaining = conn.default_channel.queue_declare( 

2840 queue="coordinator", passive=True 

2841 ).message_count 

2842 return remaining 

2843 

2844 

2845class FailedTasksListView(ListView): 

2846 model = TaskResult 

2847 queryset = TaskResult.objects.filter( 

2848 status="FAILURE", 

2849 task_name="ptf_tools.tasks.archive_numdam_issue", 

2850 ) 

2851 

2852 

2853class FailedTasksDeleteView(DeleteView): 

2854 model = TaskResult 

2855 success_url = reverse_lazy("tasks-failed") 

2856 

2857 

2858class FailedTasksRetryView(SingleObjectMixin, RedirectView): 

2859 model = TaskResult 

2860 

2861 @staticmethod 

2862 def retry_task(task): 

2863 colid, pid = (arg.strip("'") for arg in task.task_args.strip("()").split(", ")) 

2864 archive_numdam_resource.s(colid, pid).set(queue="coordinator").delay() 

2865 task.delete() 

2866 

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

2868 self.retry_task(self.get_object()) 

2869 return reverse("tasks-failed") 

2870 

2871 

2872class NumdamView(TemplateView, history_views.HistoryContextMixin): 

2873 template_name = "numdam.html" 

2874 

2875 def get_context_data(self, **kwargs): 

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

2877 

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

2879 

2880 pre_issues = [] 

2881 prod_issues = [] 

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

2883 try: 

2884 response = requests.get(url) 

2885 if response.status_code == 200: 

2886 data = response.json() 

2887 if "issues" in data: 

2888 pre_issues = data["issues"] 

2889 except Exception: 

2890 pass 

2891 

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

2893 response = requests.get(url) 

2894 if response.status_code == 200: 

2895 data = response.json() 

2896 if "issues" in data: 

2897 prod_issues = data["issues"] 

2898 

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

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

2901 grouped = [ 

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

2903 ] 

2904 grouped_removed = [ 

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

2906 ] 

2907 context["added_issues"] = grouped 

2908 context["removed_issues"] = grouped_removed 

2909 

2910 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS 

2911 return context 

2912 

2913 

2914class TasksProgressView(View): 

2915 def get(self, *args, **kwargs): 

2916 task_name = self.kwargs.get("task", "archive_numdam_issue") 

2917 successes = TaskResult.objects.filter( 

2918 task_name=f"ptf_tools.tasks.{task_name}", status="SUCCESS" 

2919 ).count() 

2920 fails = TaskResult.objects.filter( 

2921 task_name=f"ptf_tools.tasks.{task_name}", status="FAILURE" 

2922 ).count() 

2923 last_task = ( 

2924 TaskResult.objects.filter( 

2925 task_name=f"ptf_tools.tasks.{task_name}", 

2926 status="SUCCESS", 

2927 ) 

2928 .order_by("-date_done") 

2929 .first() 

2930 ) 

2931 if last_task: 

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

2933 remaining = get_messages_in_queue() 

2934 all = successes + remaining 

2935 progress = int(successes * 100 / all) if all else 0 

2936 error_rate = int(fails * 100 / all) if all else 0 

2937 status = "consuming_queue" if (successes or fails) and not progress == 100 else "polling" 

2938 data = { 

2939 "status": status, 

2940 "progress": progress, 

2941 "total": all, 

2942 "remaining": remaining, 

2943 "successes": successes, 

2944 "fails": fails, 

2945 "error_rate": error_rate, 

2946 "last_task": last_task, 

2947 } 

2948 return JsonResponse(data) 

2949 

2950 

2951class NumdamArchiveView(RedirectView): 

2952 @staticmethod 

2953 def reset_task_results(): 

2954 TaskResult.objects.all().delete() 

2955 

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

2957 self.colid = kwargs["colid"] 

2958 

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

2960 return Http404 

2961 

2962 # we make sure archiving is not already running 

2963 # if not get_messages_in_queue(): 

2964 # self.reset_task_results() 

2965 

2966 if self.colid == "ALL": 

2967 archive_numdam_collections.delay() 

2968 else: 

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

2970 

2971 return reverse("numdam") 

2972 

2973 

2974class DeployAllNumdamAPIView(View): 

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

2976 pids = [] 

2977 

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

2979 pids.append(obj.pid) 

2980 

2981 return pids 

2982 

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

2984 try: 

2985 pids, status, message = history_views.execute_and_record_func( 

2986 "deploy", "numdam", "numdam", self.internal_do, "numdam" 

2987 ) 

2988 except Exception as exception: 

2989 return HttpResponseServerError(exception) 

2990 

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

2992 return JsonResponse(data) 

2993 

2994 

2995class NumdamDeleteAPIView(View): 

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

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

2998 

2999 try: 

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

3001 obj.delete() 

3002 except Exception as exception: 

3003 return HttpResponseServerError(exception) 

3004 

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

3006 return JsonResponse(data) 

3007 

3008 

3009class ExtIdApiDetail(View): 

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

3011 extid = get_object_or_404( 

3012 ExtId, 

3013 resource__pid=kwargs["pid"], 

3014 id_type=kwargs["what"], 

3015 ) 

3016 return JsonResponse( 

3017 { 

3018 "pk": extid.pk, 

3019 "href": extid.get_href(), 

3020 "fetch": reverse( 

3021 "api-fetch-id", 

3022 args=( 

3023 extid.resource.pk, 

3024 extid.id_value, 

3025 extid.id_type, 

3026 "extid", 

3027 ), 

3028 ), 

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

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

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

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

3033 "is_valid": extid.checked, 

3034 } 

3035 ) 

3036 

3037 

3038class ExtIdFormTemplate(TemplateView): 

3039 template_name = "common/externalid_form.html" 

3040 

3041 def get_context_data(self, **kwargs): 

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

3043 context["sequence"] = kwargs["sequence"] 

3044 return context 

3045 

3046 

3047class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

3048 def get_context_data(self, **kwargs): 

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

3050 context["helper"] = PtfFormHelper 

3051 return context 

3052 

3053 def get_success_url(self): 

3054 self.post_process() 

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

3056 

3057 def post_process(self): 

3058 cmd = updateBibitemCitationXmlCmd() 

3059 cmd.set_bibitem(self.object.bibitem) 

3060 cmd.do() 

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

3062 

3063 

3064class BibItemIdCreate(BibItemIdFormView, CreateView): 

3065 model = BibItemId 

3066 form_class = BibItemIdForm 

3067 

3068 def get_context_data(self, **kwargs): 

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

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

3071 return context 

3072 

3073 def get_initial(self): 

3074 initial = super().get_initial() 

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

3076 return initial 

3077 

3078 def form_valid(self, form): 

3079 form.instance.checked = False 

3080 return super().form_valid(form) 

3081 

3082 

3083class BibItemIdUpdate(BibItemIdFormView, UpdateView): 

3084 model = BibItemId 

3085 form_class = BibItemIdForm 

3086 

3087 def get_context_data(self, **kwargs): 

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

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

3090 return context 

3091 

3092 

3093class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

3094 def get_context_data(self, **kwargs): 

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

3096 context["helper"] = PtfFormHelper 

3097 return context 

3098 

3099 def get_success_url(self): 

3100 self.post_process() 

3101 return self.object.resource.get_absolute_url() 

3102 

3103 def post_process(self): 

3104 model_helpers.post_resource_updated(self.object.resource) 

3105 

3106 

3107class ExtIdCreate(ExtIdFormView, CreateView): 

3108 model = ExtId 

3109 form_class = ExtIdForm 

3110 

3111 def get_context_data(self, **kwargs): 

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

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

3114 return context 

3115 

3116 def get_initial(self): 

3117 initial = super().get_initial() 

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

3119 return initial 

3120 

3121 def form_valid(self, form): 

3122 form.instance.checked = False 

3123 return super().form_valid(form) 

3124 

3125 

3126class ExtIdUpdate(ExtIdFormView, UpdateView): 

3127 model = ExtId 

3128 form_class = ExtIdForm 

3129 

3130 def get_context_data(self, **kwargs): 

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

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

3133 return context 

3134 

3135 

3136class BibItemIdApiDetail(View): 

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

3138 bibitemid = get_object_or_404( 

3139 BibItemId, 

3140 bibitem__resource__pid=kwargs["pid"], 

3141 bibitem__sequence=kwargs["seq"], 

3142 id_type=kwargs["what"], 

3143 ) 

3144 return JsonResponse( 

3145 { 

3146 "pk": bibitemid.pk, 

3147 "href": bibitemid.get_href(), 

3148 "fetch": reverse( 

3149 "api-fetch-id", 

3150 args=( 

3151 bibitemid.bibitem.pk, 

3152 bibitemid.id_value, 

3153 bibitemid.id_type, 

3154 "bibitemid", 

3155 ), 

3156 ), 

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

3158 "uncheck": reverse( 

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

3160 ), 

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

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

3163 "is_valid": bibitemid.checked, 

3164 } 

3165 ) 

3166 

3167 

3168class UpdateTexmfZipAPIView(View): 

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

3170 def copy_zip_files(src_folder, dest_folder): 

3171 os.makedirs(dest_folder, exist_ok=True) 

3172 

3173 zip_files = [ 

3174 os.path.join(src_folder, f) 

3175 for f in os.listdir(src_folder) 

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

3177 ] 

3178 for zip_file in zip_files: 

3179 resolver.copy_file(zip_file, dest_folder) 

3180 

3181 # Exceptions: specific zip/gz files 

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

3183 resolver.copy_file(zip_file, dest_folder) 

3184 

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

3186 resolver.copy_file(zip_file, dest_folder) 

3187 

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

3189 resolver.copy_file(gz_file, dest_folder) 

3190 

3191 src_folder = settings.CEDRAM_DISTRIB_FOLDER 

3192 

3193 dest_folder = os.path.join( 

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

3195 ) 

3196 

3197 try: 

3198 copy_zip_files(src_folder, dest_folder) 

3199 except Exception as exception: 

3200 return HttpResponseServerError(exception) 

3201 

3202 try: 

3203 dest_folder = os.path.join( 

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

3205 ) 

3206 copy_zip_files(src_folder, dest_folder) 

3207 except Exception as exception: 

3208 return HttpResponseServerError(exception) 

3209 

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

3211 return JsonResponse(data) 

3212 

3213 

3214class TestView(TemplateView): 

3215 template_name = "mersenne.html" 

3216 

3217 def get_context_data(self, **kwargs): 

3218 super().get_context_data(**kwargs) 

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

3220 model_data_converter.db_to_issue_data(issue) 

3221 

3222 

3223class TrammelTasksProgressView(View): 

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

3225 """ 

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

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

3228 """ 

3229 task_name = task 

3230 

3231 def get_event_data(): 

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

3233 

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

3235 remaining_messages = get_messages_in_queue() 

3236 

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

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

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

3240 

3241 all_tasks_count = all_tasks.count() 

3242 success_count = successed_tasks.count() 

3243 fail_count = failed_tasks.count() 

3244 

3245 all_count = all_tasks_count + remaining_messages 

3246 remaining_count = all_count - success_count - fail_count 

3247 

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

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

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

3251 

3252 last_task = successed_tasks.first() 

3253 last_task = ( 

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

3255 if last_task 

3256 else "" 

3257 ) 

3258 

3259 # SSE event format 

3260 event_data = { 

3261 "status": status, 

3262 "success_rate": success_rate, 

3263 "error_rate": error_rate, 

3264 "all_count": all_count, 

3265 "remaining_count": remaining_count, 

3266 "success_count": success_count, 

3267 "fail_count": fail_count, 

3268 "last_task": last_task, 

3269 } 

3270 

3271 return event_data 

3272 

3273 def stream_response(data): 

3274 # Send initial response headers 

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

3276 

3277 data = get_event_data() 

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

3279 if format == "json": 

3280 response = JsonResponse(data) 

3281 else: 

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

3283 return response 

3284 

3285 

3286class TrammelFailedTasksListView(ListView): 

3287 model = TaskResult 

3288 queryset = TaskResult.objects.filter( 

3289 status="FAILURE", 

3290 task_name="ptf_tools.tasks.archive_trammel_collection", 

3291 ) 

3292 

3293 

3294user_signed_up.connect(update_user_from_invite)