Coverage for sites/ptf_tools/ptf_tools/views/base_views.py: 20%

1640 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-11-04 17:46 +0000

1import io 

2import json 

3import os 

4import re 

5from datetime import datetime 

6from itertools import groupby 

7 

8import jsonpickle 

9import requests 

10from braces.views import CsrfExemptMixin 

11from braces.views import LoginRequiredMixin 

12from braces.views import StaffuserRequiredMixin 

13from celery import Celery 

14from celery import current_app 

15from django_celery_results.models import TaskResult 

16from extra_views import CreateWithInlinesView 

17from extra_views import InlineFormSetFactory 

18from extra_views import NamedFormsetsMixin 

19from extra_views import UpdateWithInlinesView 

20from requests import Timeout 

21 

22from django.conf import settings 

23from django.contrib import messages 

24from django.contrib.auth.mixins import UserPassesTestMixin 

25from django.db.models import Q 

26from django.http import Http404 

27from django.http import HttpRequest 

28from django.http import HttpResponse 

29from django.http import HttpResponseRedirect 

30from django.http import HttpResponseServerError 

31from django.http import JsonResponse 

32from django.shortcuts import get_object_or_404 

33from django.shortcuts import redirect 

34from django.shortcuts import render 

35from django.urls import resolve 

36from django.urls import reverse 

37from django.urls import reverse_lazy 

38from django.utils import timezone 

39from django.views.decorators.http import require_http_methods 

40from django.views.generic import ListView 

41from django.views.generic import TemplateView 

42from django.views.generic import View 

43from django.views.generic.base import RedirectView 

44from django.views.generic.detail import SingleObjectMixin 

45from django.views.generic.edit import CreateView 

46from django.views.generic.edit import DeleteView 

47from django.views.generic.edit import FormView 

48from django.views.generic.edit import UpdateView 

49 

50from comments_moderation.utils import get_comments_for_home 

51from comments_moderation.utils import is_comment_moderator 

52from history import models as history_models 

53from history import views as history_views 

54from ptf import model_data_converter 

55from ptf import model_helpers 

56from ptf import tex 

57from ptf import utils 

58from ptf.cmds import ptf_cmds 

59from ptf.cmds import xml_cmds 

60from ptf.cmds.base_cmds import make_int 

61from ptf.cmds.xml.jats.builder.issue import get_issue_title_xml 

62from ptf.cmds.xml.xml_utils import replace_html_entities 

63from ptf.display import resolver 

64from ptf.exceptions import DOIException 

65from ptf.exceptions import PDFException 

66from ptf.exceptions import ServerUnderMaintenance 

67from ptf.model_data import create_issuedata 

68from ptf.model_data import create_publisherdata 

69from ptf.models import Abstract 

70from ptf.models import Article 

71from ptf.models import BibItem 

72from ptf.models import BibItemId 

73from ptf.models import Collection 

74from ptf.models import Container 

75from ptf.models import ExtId 

76from ptf.models import ExtLink 

77from ptf.models import Resource 

78from ptf.models import ResourceId 

79from ptf.views import ArticleEditAPIView 

80from ptf_tools.doaj import doaj_pid_register 

81from ptf_tools.doi import get_or_create_doibatch 

82from ptf_tools.doi import recordDOI 

83from ptf_tools.forms import BibItemIdForm 

84from ptf_tools.forms import CollectionForm 

85from ptf_tools.forms import ContainerForm 

86from ptf_tools.forms import DiffContainerForm 

87from ptf_tools.forms import ExtIdForm 

88from ptf_tools.forms import ExtLinkForm 

89from ptf_tools.forms import FormSetHelper 

90from ptf_tools.forms import ImportArticleForm 

91from ptf_tools.forms import ImportContainerForm 

92from ptf_tools.forms import PtfFormHelper 

93from ptf_tools.forms import PtfLargeModalFormHelper 

94from ptf_tools.forms import PtfModalFormHelper 

95from ptf_tools.forms import RegisterPubmedForm 

96from ptf_tools.forms import ResourceIdForm 

97from ptf_tools.forms import get_article_choices 

98from ptf_tools.models import ResourceInNumdam 

99from ptf_tools.tasks import archive_numdam_collection 

100from ptf_tools.tasks import archive_numdam_issue 

101from ptf_tools.tasks import archive_trammel_collection 

102from ptf_tools.tasks import archive_trammel_resource 

103from ptf_tools.templatetags.tools_helpers import get_authorized_collections 

104from ptf_tools.utils import is_authorized_editor 

105from pubmed.views import recordPubmed 

106 

107 

108def view_404(request: HttpRequest): 

109 """ 

110 Dummy view raising HTTP 404 exception. 

111 """ 

112 raise Http404 

113 

114 

115def check_collection(collection, server_url, server_type): 

116 """ 

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

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

119 """ 

120 

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

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

123 # First, upload the collection XML 

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

125 body = xml.encode("utf8") 

126 

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

128 if response.status_code == 200: 

129 # PUT http verb is used for update 

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

131 else: 

132 # POST http verb is used for creation 

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

134 

135 # Second, copy the collection images 

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

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

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

139 # /mersenne_prod_data during an upload to prod 

140 if server_type == "website": 

141 resolver.copy_binary_files( 

142 collection, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

143 ) 

144 elif server_type == "numdam": 

145 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

146 if collection.pid in settings.NUMDAM_COLLECTIONS: 

147 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

148 

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

150 

151 

152def check_lock(): 

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

154 

155 

156def load_cedrics_article_choices(request): 

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

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

159 article_choices = get_article_choices(colid, issue) 

160 return render( 

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

162 ) 

163 

164 

165class ImportCedricsArticleFormView(FormView): 

166 template_name = "import_article.html" 

167 form_class = ImportArticleForm 

168 

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

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

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

172 

173 def get_success_url(self): 

174 if self.colid: 

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

176 return "/" 

177 

178 def get_context_data(self, **kwargs): 

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

180 context["colid"] = self.colid 

181 context["helper"] = PtfModalFormHelper 

182 return context 

183 

184 def get_form_kwargs(self): 

185 kwargs = super().get_form_kwargs() 

186 kwargs["colid"] = self.colid 

187 return kwargs 

188 

189 def form_valid(self, form): 

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

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

192 return super().form_valid(form) 

193 

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

195 cmd = xml_cmds.addorUpdateCedricsArticleXmlCmd( 

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

197 ) 

198 cmd.do() 

199 

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

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

202 issue = request.POST["issue"] 

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

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

205 

206 import_args = [self] 

207 import_kwargs = {} 

208 

209 try: 

210 _, status, message = history_views.execute_and_record_func( 

211 "import", 

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

213 self.colid, 

214 self.import_cedrics_article, 

215 "", 

216 False, 

217 *import_args, 

218 **import_kwargs, 

219 ) 

220 

221 messages.success( 

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

223 ) 

224 

225 except Exception as exception: 

226 messages.error( 

227 self.request, 

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

229 ) 

230 

231 return redirect(self.get_success_url()) 

232 

233 

234class ImportCedricsIssueView(FormView): 

235 template_name = "import_container.html" 

236 form_class = ImportContainerForm 

237 

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

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

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

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

242 

243 def get_success_url(self): 

244 if self.filename: 

245 return reverse( 

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

247 ) 

248 return "/" 

249 

250 def get_context_data(self, **kwargs): 

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

252 context["colid"] = self.colid 

253 context["helper"] = PtfModalFormHelper 

254 return context 

255 

256 def get_form_kwargs(self): 

257 kwargs = super().get_form_kwargs() 

258 kwargs["colid"] = self.colid 

259 kwargs["to_appear"] = self.to_appear 

260 return kwargs 

261 

262 def form_valid(self, form): 

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

264 return super().form_valid(form) 

265 

266 

267class DiffCedricsIssueView(FormView): 

268 template_name = "diff_container_form.html" 

269 form_class = DiffContainerForm 

270 diffs = None 

271 xissue = None 

272 xissue_encoded = None 

273 

274 def get_success_url(self): 

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

276 

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

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

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

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

281 

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

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

284 self.remove_mail = request.GET["remove_email"] 

285 self.remove_date_prod = request.GET["remove_date_prod"] 

286 

287 try: 

288 result, status, message = history_views.execute_and_record_func( 

289 "import", 

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

291 self.colid, 

292 self.diff_cedrics_issue, 

293 "", 

294 True, 

295 ) 

296 except Exception as exception: 

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

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

299 return HttpResponseRedirect(self.get_success_url()) 

300 

301 no_conflict = result[0] 

302 self.diffs = result[1] 

303 self.xissue = result[2] 

304 

305 if no_conflict: 

306 # Proceed with the import 

307 self.form_valid(self.get_form()) 

308 return redirect(self.get_success_url()) 

309 else: 

310 # Display the diff template 

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

312 

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

314 

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

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

317 data = request.POST["xissue_encoded"] 

318 self.xissue = jsonpickle.decode(data) 

319 

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

321 

322 def get_context_data(self, **kwargs): 

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

324 context["colid"] = self.colid 

325 context["diff"] = self.diffs 

326 context["filename"] = self.filename 

327 context["xissue_encoded"] = self.xissue_encoded 

328 return context 

329 

330 def get_form_kwargs(self): 

331 kwargs = super().get_form_kwargs() 

332 kwargs["colid"] = self.colid 

333 return kwargs 

334 

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

336 params = { 

337 "colid": self.colid, 

338 "input_file": self.filename, 

339 "remove_email": self.remove_mail, 

340 "remove_date_prod": self.remove_date_prod, 

341 "diff_only": True, 

342 } 

343 

344 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

347 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

348 else: 

349 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

350 

351 result = cmd.do() 

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

353 messages.warning( 

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

355 ) 

356 

357 return result 

358 

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

360 # modify xissue with data_issue if params to override 

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

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

363 if issue: 

364 data_issue = model_data_converter.db_to_issue_data(issue) 

365 for xarticle in self.xissue.articles: 

366 filter_articles = [ 

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

368 ] 

369 if len(filter_articles) > 0: 

370 db_article = filter_articles[0] 

371 xarticle.coi_statement = db_article.coi_statement 

372 xarticle.kwds = db_article.kwds 

373 xarticle.contrib_groups = db_article.contrib_groups 

374 

375 params = { 

376 "colid": self.colid, 

377 "xissue": self.xissue, 

378 "input_file": self.filename, 

379 } 

380 

381 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

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

384 cmd = xml_cmds.importCedricsIssueDirectlyXmlCmd(params) 

385 else: 

386 cmd = xml_cmds.importCedricsIssueXmlCmd(params) 

387 

388 cmd.do() 

389 

390 def form_valid(self, form): 

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

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

393 else: 

394 import_kwargs = {} 

395 import_args = [self] 

396 

397 try: 

398 _, status, message = history_views.execute_and_record_func( 

399 "import", 

400 self.xissue.pid, 

401 self.kwargs["colid"], 

402 self.import_cedrics_issue, 

403 "", 

404 False, 

405 *import_args, 

406 **import_kwargs, 

407 ) 

408 except Exception as exception: 

409 messages.error( 

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

411 ) 

412 return super().form_invalid(form) 

413 

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

415 return super().form_valid(form) 

416 

417 

418class BibtexAPIView(View): 

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

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

421 all_bibtex = "" 

422 if pid: 

423 article = model_helpers.get_article(pid) 

424 if article: 

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

426 bibtex_array = bibitem.get_bibtex() 

427 last = len(bibtex_array) 

428 i = 1 

429 for bibtex in bibtex_array: 

430 if i > 1 and i < last: 

431 all_bibtex += " " 

432 all_bibtex += bibtex + "\n" 

433 i += 1 

434 

435 data = {"bibtex": all_bibtex} 

436 return JsonResponse(data) 

437 

438 

439class MatchingAPIView(View): 

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

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

442 

443 url = settings.MATCHING_URL 

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

445 

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

447 

448 if settings.DEBUG: 

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

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

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

452 f.close() 

453 

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

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

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

457 

458 if settings.DEBUG: 

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

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

461 text = body 

462 f.write(text) 

463 f.close() 

464 

465 resource = model_helpers.get_resource(pid) 

466 obj = resource.cast() 

467 colid = obj.get_collection().pid 

468 

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

470 

471 cmd = xml_cmds.addOrUpdateIssueXmlCmd( 

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

473 ) 

474 cmd.do() 

475 

476 print("Matching finished") 

477 return JsonResponse(data) 

478 

479 

480class ImportAllAPIView(View): 

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

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

483 

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

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

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

487 

488 resource = model_helpers.get_resource(pid) 

489 if not resource: 

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

491 body = utils.get_file_content_in_utf8(file) 

492 journals = xml_cmds.addCollectionsXmlCmd( 

493 { 

494 "body": body, 

495 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

496 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

497 } 

498 ).do() 

499 if not journals: 

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

501 resource = journals[0] 

502 # resolver.copy_binary_files( 

503 # resource, 

504 # settings.MATHDOC_ARCHIVE_FOLDER, 

505 # settings.MERSENNE_TEST_DATA_FOLDER) 

506 

507 obj = resource.cast() 

508 

509 if obj.classname != "Collection": 

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

511 

512 cmd = xml_cmds.collectEntireCollectionXmlCmd( 

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

514 ) 

515 pids = cmd.do() 

516 

517 return pids 

518 

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

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

521 

522 try: 

523 pids, status, message = history_views.execute_and_record_func( 

524 "import", pid, pid, self.internal_do 

525 ) 

526 except Timeout as exception: 

527 return HttpResponse(exception, status=408) 

528 except Exception as exception: 

529 return HttpResponseServerError(exception) 

530 

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

532 return JsonResponse(data) 

533 

534 

535class DeployAllAPIView(View): 

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

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

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

539 

540 pids = [] 

541 

542 collection = model_helpers.get_collection(pid) 

543 if not collection: 

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

545 

546 if site == "numdam": 

547 server_url = settings.NUMDAM_PRE_URL 

548 elif site != "ptf_tools": 

549 server_url = getattr(collection, site)() 

550 if not server_url: 

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

552 

553 if site != "ptf_tools": 

554 # check if the collection exists on the server 

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

556 # image...) 

557 check_collection(collection, server_url, site) 

558 

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

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

561 pids.append(issue.pid) 

562 

563 return pids 

564 

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

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

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

568 

569 try: 

570 pids, status, message = history_views.execute_and_record_func( 

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

572 ) 

573 except Timeout as exception: 

574 return HttpResponse(exception, status=408) 

575 except Exception as exception: 

576 return HttpResponseServerError(exception) 

577 

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

579 return JsonResponse(data) 

580 

581 

582class AddIssuePDFView(View): 

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

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

585 self.pid = None 

586 self.issue = None 

587 self.collection = None 

588 self.site = "test_website" 

589 

590 def post_to_site(self, url): 

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

592 status = response.status_code 

593 if not (199 < status < 205): 

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

595 if status == 503: 

596 raise ServerUnderMaintenance(response.text) 

597 else: 

598 raise RuntimeError(response.text) 

599 

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

601 """ 

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

603 """ 

604 

605 issue_pid = self.issue.pid 

606 colid = self.collection.pid 

607 

608 if self.site == "website": 

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

610 resolver.copy_binary_files( 

611 self.issue, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

612 ) 

613 else: 

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

615 from_folder = resolver.get_cedram_issue_tex_folder(colid, issue_pid) 

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

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

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

619 

620 to_path = resolver.get_disk_location( 

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

622 ) 

623 resolver.copy_file(from_path, to_path) 

624 

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

626 

627 if self.site == "test_website": 

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

629 absolute_url = self.request.build_absolute_uri(url) 

630 self.post_to_site(absolute_url) 

631 

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

633 absolute_url = server_url + url 

634 # Post to the test or production website 

635 self.post_to_site(absolute_url) 

636 

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

638 """ 

639 Send an issue PDF to the test or production website 

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

641 :param args: 

642 :param kwargs: 

643 :return: 

644 """ 

645 if check_lock(): 

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

647 messages.error(self.request, m) 

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

649 

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

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

652 

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

654 if not self.issue: 

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

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

657 

658 try: 

659 pids, status, message = history_views.execute_and_record_func( 

660 "deploy", 

661 self.pid, 

662 self.collection.pid, 

663 self.internal_do, 

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

665 ) 

666 

667 except Timeout as exception: 

668 return HttpResponse(exception, status=408) 

669 except Exception as exception: 

670 return HttpResponseServerError(exception) 

671 

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

673 return JsonResponse(data) 

674 

675 

676class ArchiveAllAPIView(View): 

677 """ 

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

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

680 @return array of issues pid 

681 """ 

682 

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

684 collection = kwargs["collection"] 

685 pids = [] 

686 colid = collection.pid 

687 

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

689 if os.path.isfile(logfile): 

690 os.remove(logfile) 

691 

692 ptf_cmds.exportPtfCmd( 

693 { 

694 "pid": colid, 

695 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

696 "with_binary_files": True, 

697 "for_archive": True, 

698 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

699 } 

700 ).do() 

701 

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

703 if os.path.isfile(cedramcls): 

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

705 resolver.create_folder(dest_folder) 

706 resolver.copy_file(cedramcls, dest_folder) 

707 

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

709 qs = issue.article_set.filter( 

710 date_online_first__isnull=True, date_published__isnull=True 

711 ) 

712 if qs.count() == 0: 

713 pids.append(issue.pid) 

714 

715 return pids 

716 

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

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

719 

720 collection = model_helpers.get_collection(pid) 

721 if not collection: 

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

723 

724 dict_ = {"collection": collection} 

725 args_ = [self] 

726 

727 try: 

728 pids, status, message = history_views.execute_and_record_func( 

729 "archive", pid, pid, self.internal_do, "", False, *args_, **dict_ 

730 ) 

731 except Timeout as exception: 

732 return HttpResponse(exception, status=408) 

733 except Exception as exception: 

734 return HttpResponseServerError(exception) 

735 

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

737 return JsonResponse(data) 

738 

739 

740class CreateAllDjvuAPIView(View): 

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

742 issue = kwargs["issue"] 

743 pids = [issue.pid] 

744 

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

746 pids.append(article.pid) 

747 

748 return pids 

749 

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

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

752 issue = model_helpers.get_container(pid) 

753 if not issue: 

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

755 

756 try: 

757 dict_ = {"issue": issue} 

758 args_ = [self] 

759 

760 pids, status, message = history_views.execute_and_record_func( 

761 "numdam", 

762 pid, 

763 issue.get_collection().pid, 

764 self.internal_do, 

765 "", 

766 False, 

767 *args_, 

768 **dict_, 

769 ) 

770 except Exception as exception: 

771 return HttpResponseServerError(exception) 

772 

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

774 return JsonResponse(data) 

775 

776 

777class ImportJatsContainerAPIView(View): 

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

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

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

781 

782 if pid and colid: 

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

784 

785 cmd = xml_cmds.addOrUpdateContainerXmlCmd( 

786 { 

787 "body": body, 

788 "from_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

789 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

790 "backup_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

791 } 

792 ) 

793 container = cmd.do() 

794 if len(cmd.warnings) > 0: 

795 messages.warning( 

796 self.request, 

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

798 ) 

799 

800 if not container: 

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

802 

803 # resolver.copy_binary_files( 

804 # container, 

805 # settings.MATHDOC_ARCHIVE_FOLDER, 

806 # settings.MERSENNE_TEST_DATA_FOLDER) 

807 # 

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

809 # resolver.copy_binary_files( 

810 # article, 

811 # settings.MATHDOC_ARCHIVE_FOLDER, 

812 # settings.MERSENNE_TEST_DATA_FOLDER) 

813 else: 

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

815 

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

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

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

819 

820 try: 

821 _, status, message = history_views.execute_and_record_func( 

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

823 ) 

824 except Timeout as exception: 

825 return HttpResponse(exception, status=408) 

826 except Exception as exception: 

827 return HttpResponseServerError(exception) 

828 

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

830 return JsonResponse(data) 

831 

832 

833class DeployCollectionAPIView(View): 

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

835 

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

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

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

839 

840 collection = model_helpers.get_collection(colid) 

841 if not collection: 

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

843 

844 if site == "numdam": 

845 server_url = settings.NUMDAM_PRE_URL 

846 else: 

847 server_url = getattr(collection, site)() 

848 if not server_url: 

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

850 

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

852 check_collection(collection, server_url, site) 

853 

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

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

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

857 

858 try: 

859 _, status, message = history_views.execute_and_record_func( 

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

861 ) 

862 except Timeout as exception: 

863 return HttpResponse(exception, status=408) 

864 except Exception as exception: 

865 return HttpResponseServerError(exception) 

866 

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

868 return JsonResponse(data) 

869 

870 

871class DeployJatsResourceAPIView(View): 

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

873 

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

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

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

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

878 

879 if site == "ptf_tools": 

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

881 

882 resource = model_helpers.get_resource(pid) 

883 if not resource: 

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

885 

886 obj = resource.cast() 

887 article = None 

888 if obj.classname == "Article": 

889 article = obj 

890 container = article.my_container 

891 articles_to_deploy = [article] 

892 else: 

893 container = obj 

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

895 

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

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

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

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

900 

901 collection = container.get_top_collection() 

902 colid = collection.pid 

903 djvu_exception = None 

904 

905 if site == "numdam": 

906 server_url = settings.NUMDAM_PRE_URL 

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

908 

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

910 # Add Djvu (before exporting the XML) 

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

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

913 try: 

914 cmd = ptf_cmds.addDjvuPtfCmd() 

915 cmd.set_resource(art) 

916 cmd.do() 

917 except Exception as e: 

918 # Djvu are optional. 

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

920 djvu_exception = e 

921 else: 

922 server_url = getattr(collection, site)() 

923 if not server_url: 

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

925 

926 # check if the collection exists on the server 

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

928 # image...) 

929 if article is None: 

930 check_collection(collection, server_url, site) 

931 

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

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

934 if site == "website": 

935 file_.write( 

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

937 pid 

938 ) 

939 ) 

940 

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

942 cmd = ptf_cmds.publishResourcePtfCmd() 

943 cmd.set_resource(resource) 

944 updated_articles = cmd.do() 

945 

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

947 

948 mersenneSite = model_helpers.get_site_mersenne(colid) 

949 # create or update deployed_date on container and articles 

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

951 

952 for art in articles_to_deploy: 

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

954 if art.my_container.year is None: 

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

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

957 

958 file_.write( 

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

960 art.pid, art.date_online_first, art.date_published 

961 ) 

962 ) 

963 

964 if article is None: 

965 resolver.copy_binary_files( 

966 container, 

967 settings.MERSENNE_TEST_DATA_FOLDER, 

968 settings.MERSENNE_PROD_DATA_FOLDER, 

969 ) 

970 

971 for art in articles_to_deploy: 

972 resolver.copy_binary_files( 

973 art, 

974 settings.MERSENNE_TEST_DATA_FOLDER, 

975 settings.MERSENNE_PROD_DATA_FOLDER, 

976 ) 

977 

978 elif site == "test_website": 

979 # create date_pre_published on articles without date_pre_published 

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

981 cmd.set_resource(resource) 

982 updated_articles = cmd.do() 

983 

984 tex.create_frontpage(colid, container, updated_articles) 

985 

986 export_to_website = site == "website" 

987 

988 if article is None: 

989 with_djvu = site == "numdam" 

990 # if obj.ctype == 'issue_special': 

991 # 

992 xml = ptf_cmds.exportPtfCmd( 

993 { 

994 "pid": pid, 

995 "with_djvu": with_djvu, 

996 "export_to_website": export_to_website, 

997 } 

998 ).do() 

999 body = xml.encode("utf8") 

1000 

1001 if container.ctype == "issue" or container.ctype == "issue_special": 

1002 url = server_url + reverse("issue_upload") 

1003 else: 

1004 url = server_url + reverse("book_upload") 

1005 

1006 # verify=False: ignore TLS certificate 

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

1008 else: 

1009 xml = ptf_cmds.exportPtfCmd( 

1010 { 

1011 "pid": pid, 

1012 "with_djvu": False, 

1013 "article_standalone": True, 

1014 "collection_pid": collection.pid, 

1015 "export_to_website": export_to_website, 

1016 "export_folder": settings.LOG_DIR, 

1017 } 

1018 ).do() 

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

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

1021 xml_file = io.StringIO(xml) 

1022 files = {"xml": xml_file} 

1023 

1024 url = server_url + reverse( 

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

1026 ) 

1027 # verify=False: ignore TLS certificate 

1028 header = {} 

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

1030 

1031 status = response.status_code 

1032 

1033 if 199 < status < 205: 

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

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

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

1037 # /mersenne_prod_data during an upload to prod 

1038 if site == "website": 

1039 for art in articles_to_deploy: 

1040 # record DOI automatically when deploying in prod 

1041 

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

1043 recordDOI(art) 

1044 

1045 if colid == "CRBIOL": 

1046 recordPubmed( 

1047 art, force_update=False, updated_articles=updated_articles 

1048 ) 

1049 

1050 if colid == "PCJ": 

1051 self.update_pcj_editor(updated_articles) 

1052 

1053 # Archive the container or the article 

1054 if article is None: 

1055 archive_trammel_resource.delay( 

1056 colid=colid, 

1057 pid=pid, 

1058 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1059 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1060 ) 

1061 else: 

1062 archive_trammel_resource.delay( 

1063 colid=colid, 

1064 pid=pid, 

1065 mathdoc_archive=settings.MATHDOC_ARCHIVE_FOLDER, 

1066 binary_files_folder=settings.MERSENNE_PROD_DATA_FOLDER, 

1067 article_doi=article.doi, 

1068 ) 

1069 # cmd = ptf_cmds.archiveIssuePtfCmd({ 

1070 # "pid": pid, 

1071 # "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1072 # "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER}) 

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

1074 # cmd.do() 

1075 

1076 elif site == "numdam": 

1077 from_folder = settings.MERSENNE_PROD_DATA_FOLDER 

1078 if colid in settings.NUMDAM_COLLECTIONS: 

1079 from_folder = settings.MERSENNE_TEST_DATA_FOLDER 

1080 

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

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

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

1084 

1085 elif status == 503: 

1086 raise ServerUnderMaintenance(response.text) 

1087 else: 

1088 raise RuntimeError(response.text) 

1089 

1090 if djvu_exception: 

1091 raise djvu_exception 

1092 

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

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

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

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

1097 

1098 try: 

1099 _, status, message = history_views.execute_and_record_func( 

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

1101 ) 

1102 except Timeout as exception: 

1103 return HttpResponse(exception, status=408) 

1104 except Exception as exception: 

1105 return HttpResponseServerError(exception) 

1106 

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

1108 return JsonResponse(data) 

1109 

1110 def update_pcj_editor(self, updated_articles): 

1111 for article in updated_articles: 

1112 data = { 

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

1114 "article_number": article.article_number, 

1115 } 

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

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

1118 

1119 

1120class DeployTranslatedArticleAPIView(CsrfExemptMixin, View): 

1121 article = None 

1122 

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

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

1125 

1126 translation = None 

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

1128 if trans_article.lang == lang: 

1129 translation = trans_article 

1130 

1131 if translation is None: 

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

1133 

1134 collection = self.article.get_top_collection() 

1135 colid = collection.pid 

1136 container = self.article.my_container 

1137 

1138 if translation.date_published is None: 

1139 # Add date posted 

1140 cmd = ptf_cmds.publishResourcePtfCmd() 

1141 cmd.set_resource(translation) 

1142 updated_articles = cmd.do() 

1143 

1144 # Recompile PDF to add the date posted 

1145 try: 

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

1147 except Exception: 

1148 raise PDFException( 

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

1150 ) 

1151 

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

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

1154 resolver.copy_binary_files( 

1155 self.article, settings.MERSENNE_TEST_DATA_FOLDER, settings.MERSENNE_PROD_DATA_FOLDER 

1156 ) 

1157 

1158 # Deploy in prod 

1159 xml = ptf_cmds.exportPtfCmd( 

1160 { 

1161 "pid": self.article.pid, 

1162 "with_djvu": False, 

1163 "article_standalone": True, 

1164 "collection_pid": colid, 

1165 "export_to_website": True, 

1166 "export_folder": settings.LOG_DIR, 

1167 } 

1168 ).do() 

1169 xml_file = io.StringIO(xml) 

1170 files = {"xml": xml_file} 

1171 

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

1173 if not server_url: 

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

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

1176 header = {} 

1177 

1178 try: 

1179 response = requests.post( 

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

1181 ) # verify: ignore TLS certificate 

1182 status = response.status_code 

1183 except requests.exceptions.ConnectionError: 

1184 raise ServerUnderMaintenance( 

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

1186 ) 

1187 

1188 # Register translation in Crossref 

1189 if 199 < status < 205: 

1190 if self.article.allow_crossref(): 

1191 try: 

1192 recordDOI(translation) 

1193 except Exception: 

1194 raise DOIException( 

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

1196 ) 

1197 

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

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

1200 self.article = model_helpers.get_article_by_doi(doi) 

1201 if self.article is None: 

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

1203 

1204 try: 

1205 _, status, message = history_views.execute_and_record_func( 

1206 "deploy", 

1207 self.article.pid, 

1208 self.article.get_top_collection().pid, 

1209 self.internal_do, 

1210 "website", 

1211 ) 

1212 except Timeout as exception: 

1213 return HttpResponse(exception, status=408) 

1214 except Exception as exception: 

1215 return HttpResponseServerError(exception) 

1216 

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

1218 return JsonResponse(data) 

1219 

1220 

1221class DeleteJatsIssueAPIView(View): 

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

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

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

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

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

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

1228 status = 200 

1229 

1230 issue = model_helpers.get_container(pid) 

1231 if not issue: 

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

1233 try: 

1234 mersenneSite = model_helpers.get_site_mersenne(colid) 

1235 

1236 if site == "ptf_tools": 

1237 if issue.is_deployed(mersenneSite): 

1238 issue.undeploy(mersenneSite) 

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

1240 article.undeploy(mersenneSite) 

1241 

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

1243 

1244 cmd = ptf_cmds.addContainerPtfCmd( 

1245 { 

1246 "pid": issue.pid, 

1247 "ctype": "issue", 

1248 "to_folder": settings.MERSENNE_TEST_DATA_FOLDER, 

1249 } 

1250 ) 

1251 cmd.set_provider(p) 

1252 cmd.add_collection(issue.get_collection()) 

1253 cmd.set_object_to_be_deleted(issue) 

1254 cmd.undo() 

1255 

1256 else: 

1257 if site == "numdam": 

1258 server_url = settings.NUMDAM_PRE_URL 

1259 else: 

1260 collection = issue.get_collection() 

1261 server_url = getattr(collection, site)() 

1262 

1263 if not server_url: 

1264 message = "The collection has no " + site 

1265 status = 500 

1266 else: 

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

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

1269 status = response.status_code 

1270 

1271 if status == 404: 

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

1273 elif status > 204: 

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

1275 message = body[:1000] 

1276 else: 

1277 status = 200 

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

1279 if site == "website": 

1280 if issue.is_deployed(mersenneSite): 

1281 issue.undeploy(mersenneSite) 

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

1283 article.undeploy(mersenneSite) 

1284 # delete article binary files 

1285 folder = article.get_relative_folder() 

1286 resolver.delete_object_folder( 

1287 folder, 

1288 to_folder=settings.MERSENNE_PROD_DATA_FORLDER, 

1289 ) 

1290 # delete issue binary files 

1291 folder = issue.get_relative_folder() 

1292 resolver.delete_object_folder( 

1293 folder, to_folder=settings.MERSENNE_PROD_DATA_FORLDER 

1294 ) 

1295 

1296 except Timeout as exception: 

1297 return HttpResponse(exception, status=408) 

1298 except Exception as exception: 

1299 return HttpResponseServerError(exception) 

1300 

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

1302 return JsonResponse(data) 

1303 

1304 

1305class ArchiveIssueAPIView(View): 

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

1307 try: 

1308 pid = kwargs["pid"] 

1309 colid = kwargs["colid"] 

1310 except IndexError: 

1311 raise Http404 

1312 

1313 try: 

1314 cmd = ptf_cmds.archiveIssuePtfCmd( 

1315 { 

1316 "pid": pid, 

1317 "export_folder": settings.MATHDOC_ARCHIVE_FOLDER, 

1318 "binary_files_folder": settings.MERSENNE_PROD_DATA_FOLDER, 

1319 } 

1320 ) 

1321 result_, status, message = history_views.execute_and_record_func( 

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

1323 ) 

1324 except Exception as exception: 

1325 return HttpResponseServerError(exception) 

1326 

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

1328 return JsonResponse(data) 

1329 

1330 

1331class CreateDjvuAPIView(View): 

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

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

1334 

1335 resource = model_helpers.get_resource(pid) 

1336 cmd = ptf_cmds.addDjvuPtfCmd() 

1337 cmd.set_resource(resource) 

1338 cmd.do() 

1339 

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

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

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

1343 

1344 try: 

1345 _, status, message = history_views.execute_and_record_func( 

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

1347 ) 

1348 except Exception as exception: 

1349 return HttpResponseServerError(exception) 

1350 

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

1352 return JsonResponse(data) 

1353 

1354 

1355class PTFToolsHomeView(LoginRequiredMixin, View): 

1356 """ 

1357 Home Page. 

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

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

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

1361 - Comment moderator -> Comments dashboard 

1362 - Others -> 404 response 

1363 """ 

1364 

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

1366 # Staff or user with authorized collections 

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

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

1369 

1370 colids = get_authorized_collections(request.user) 

1371 is_mod = is_comment_moderator(request.user) 

1372 

1373 # The user has no rights 

1374 if not (colids or is_mod): 

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

1376 # Comment moderator only 

1377 elif not colids: 

1378 return HttpResponseRedirect(reverse("comment_list")) 

1379 

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

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

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

1383 

1384 # User with multiple authorized collections - Special home 

1385 context = {} 

1386 context["overview"] = True 

1387 

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

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

1390 

1391 # Comments summary 

1392 try: 

1393 error, comments_data = get_comments_for_home(request.user) 

1394 except AttributeError: 

1395 error, comments_data = True, {} 

1396 

1397 context["comment_server_ok"] = False 

1398 

1399 if not error: 

1400 context["comment_server_ok"] = True 

1401 if comments_data: 

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

1403 if col_id.upper() in all_collections: 1403 ↛ 1402line 1403 didn't jump to line 1402, because the condition on line 1403 was never false

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

1405 

1406 # TODO: Translations summary 

1407 context["translation_server_ok"] = False 

1408 

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

1410 context["collections"] = sorted( 

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

1412 ) 

1413 

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

1415 

1416 

1417class BaseMersenneDashboardView(TemplateView, history_views.HistoryContextMixin): 

1418 columns = 5 

1419 

1420 def get_common_context_data(self, **kwargs): 

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

1422 now = timezone.now() 

1423 curyear = now.year 

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

1425 

1426 context["collections"] = settings.MERSENNE_COLLECTIONS 

1427 context["containers_to_be_published"] = [] 

1428 context["last_col_events"] = [] 

1429 

1430 event = history_models.get_history_last_event_by("clockss", "ALL") 

1431 clockss_gap = history_models.get_gap(now, event) 

1432 

1433 context["years"] = years 

1434 context["clockss_gap"] = clockss_gap 

1435 

1436 return context 

1437 

1438 def calculate_articles_and_pages(self, pid, years): 

1439 data_by_year = [] 

1440 total_articles = [0] * len(years) 

1441 total_pages = [0] * len(years) 

1442 

1443 for year in years: 

1444 articles = self.get_articles_for_year(pid, year) 

1445 articles_count = articles.count() 

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

1447 

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

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

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

1451 

1452 return data_by_year, total_articles, total_pages 

1453 

1454 def get_articles_for_year(self, pid, year): 

1455 return Article.objects.filter( 

1456 Q(my_container__my_collection__pid=pid) 

1457 & ( 

1458 Q(date_published__year=year, date_online_first__isnull=True) 

1459 | Q(date_online_first__year=year) 

1460 ) 

1461 ).prefetch_related("resourcecount_set") 

1462 

1463 

1464class PublishedArticlesDashboardView(BaseMersenneDashboardView): 

1465 template_name = "dashboard/published_articles.html" 

1466 

1467 def get_context_data(self, **kwargs): 

1468 context = self.get_common_context_data(**kwargs) 

1469 years = context["years"] 

1470 

1471 published_articles = [] 

1472 total_published_articles = [ 

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

1474 ] 

1475 

1476 for pid in settings.MERSENNE_COLLECTIONS: 

1477 if pid != "MERSENNE": 

1478 articles_data, total_articles, total_pages = self.calculate_articles_and_pages( 

1479 pid, years 

1480 ) 

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

1482 

1483 for i, year in enumerate(years): 

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

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

1486 

1487 context["published_articles"] = published_articles 

1488 context["total_published_articles"] = total_published_articles 

1489 

1490 return context 

1491 

1492 

1493class CreatedVolumesDashboardView(BaseMersenneDashboardView): 

1494 template_name = "dashboard/created_volumes.html" 

1495 

1496 def get_context_data(self, **kwargs): 

1497 context = self.get_common_context_data(**kwargs) 

1498 years = context["years"] 

1499 

1500 created_volumes = [] 

1501 total_created_volumes = [ 

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

1503 ] 

1504 

1505 for pid in settings.MERSENNE_COLLECTIONS: 

1506 if pid != "MERSENNE": 

1507 volumes_data, total_articles, total_pages = self.calculate_volumes_and_pages( 

1508 pid, years 

1509 ) 

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

1511 

1512 for i, year in enumerate(years): 

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

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

1515 

1516 context["created_volumes"] = created_volumes 

1517 context["total_created_volumes"] = total_created_volumes 

1518 

1519 return context 

1520 

1521 def calculate_volumes_and_pages(self, pid, years): 

1522 data_by_year = [] 

1523 total_articles = [0] * len(years) 

1524 total_pages = [0] * len(years) 

1525 

1526 for year in years: 

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

1528 articles_count = 0 

1529 page_count = 0 

1530 

1531 for issue in issues: 

1532 articles = issue.article_set.filter( 

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

1534 ).prefetch_related("resourcecount_set") 

1535 

1536 articles_count += articles.count() 

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

1538 

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

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

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

1542 

1543 return data_by_year, total_articles, total_pages 

1544 

1545 

1546class BaseCollectionView(TemplateView): 

1547 def get_context_data(self, **kwargs): 

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

1549 aid = context.get("aid") 

1550 year = context.get("year") 

1551 

1552 if aid and year: 

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

1554 

1555 return context 

1556 

1557 def get_collection(self, aid, year): 

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

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

1560 

1561 

1562class ArticleListView(BaseCollectionView): 

1563 template_name = "collection-list.html" 

1564 

1565 def get_collection(self, aid, year): 

1566 return Article.objects.filter( 

1567 Q(my_container__my_collection__pid=aid) 

1568 & ( 

1569 Q(date_published__year=year, date_online_first__isnull=True) 

1570 | Q(date_online_first__year=year) 

1571 ) 

1572 ).prefetch_related("resourcecount_set") 

1573 

1574 

1575class VolumeListView(BaseCollectionView): 

1576 template_name = "collection-list.html" 

1577 

1578 def get_collection(self, aid, year): 

1579 return Article.objects.filter( 

1580 Q(my_container__my_collection__pid=aid, my_container__year=year) 

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

1582 ).prefetch_related("resourcecount_set") 

1583 

1584 

1585class DOAJResourceRegisterView(View): 

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

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

1588 resource = model_helpers.get_resource(pid) 

1589 if resource is None: 

1590 raise Http404 

1591 

1592 try: 

1593 data = {} 

1594 doaj_meta, response = doaj_pid_register(pid) 

1595 if response is None: 

1596 return HttpResponse(status=204) 

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

1598 data.update(doaj_meta) 

1599 else: 

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

1601 except Timeout as exception: 

1602 return HttpResponse(exception, status=408) 

1603 except Exception as exception: 

1604 return HttpResponseServerError(exception) 

1605 return JsonResponse(data) 

1606 

1607 

1608class CROSSREFResourceRegisterView(View): 

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

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

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

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

1613 if not request.user.is_superuser: 

1614 force = None 

1615 

1616 resource = model_helpers.get_resource(pid) 

1617 if resource is None: 

1618 raise Http404 

1619 

1620 resource = resource.cast() 

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

1622 try: 

1623 data = meth(resource, force) 

1624 except Timeout as exception: 

1625 return HttpResponse(exception, status=408) 

1626 except Exception as exception: 

1627 return HttpResponseServerError(exception) 

1628 return JsonResponse(data) 

1629 

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

1631 result = {"status": 404} 

1632 if ( 

1633 article.doi 

1634 and not article.do_not_publish 

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

1636 ): 

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

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

1639 result = recordDOI(article) 

1640 return result 

1641 

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

1643 return recordDOI(collection) 

1644 

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

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

1647 

1648 if container.ctype == "issue": 

1649 if container.doi: 

1650 result = recordDOI(container) 

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

1652 return result 

1653 if force == "force": 

1654 articles = container.article_set.exclude( 

1655 doi__isnull=True, do_not_publish=True, date_online_first__isnull=True 

1656 ) 

1657 else: 

1658 articles = container.article_set.exclude( 

1659 doi__isnull=True, 

1660 do_not_publish=True, 

1661 date_published__isnull=True, 

1662 date_online_first__isnull=True, 

1663 ) 

1664 

1665 for article in articles: 

1666 result = self.recordDOIArticle(article, force) 

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

1668 data = result 

1669 else: 

1670 return recordDOI(container) 

1671 return data 

1672 

1673 

1674class CROSSREFResourceCheckStatusView(View): 

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

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

1677 resource = model_helpers.get_resource(pid) 

1678 if resource is None: 

1679 raise Http404 

1680 resource = resource.cast() 

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

1682 try: 

1683 meth(resource) 

1684 except Timeout as exception: 

1685 return HttpResponse(exception, status=408) 

1686 except Exception as exception: 

1687 return HttpResponseServerError(exception) 

1688 

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

1690 return JsonResponse(data) 

1691 

1692 def checkDOIArticle(self, article): 

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

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

1695 get_or_create_doibatch(article) 

1696 

1697 def checkDOICollection(self, collection): 

1698 get_or_create_doibatch(collection) 

1699 

1700 def checkDOIContainer(self, container): 

1701 if container.doi is not None: 

1702 get_or_create_doibatch(container) 

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

1704 self.checkDOIArticle(article) 

1705 

1706 

1707class RegisterPubmedFormView(FormView): 

1708 template_name = "record_pubmed_dialog.html" 

1709 form_class = RegisterPubmedForm 

1710 

1711 def get_context_data(self, **kwargs): 

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

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

1714 context["helper"] = PtfLargeModalFormHelper 

1715 return context 

1716 

1717 

1718class RegisterPubmedView(View): 

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

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

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

1722 

1723 article = model_helpers.get_article(pid) 

1724 if article is None: 

1725 raise Http404 

1726 try: 

1727 recordPubmed(article, update_article) 

1728 except Exception as exception: 

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

1730 return HttpResponseServerError(exception) 

1731 

1732 return HttpResponseRedirect( 

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

1734 ) 

1735 

1736 

1737class PTFToolsContainerView(TemplateView): 

1738 template_name = "" 

1739 

1740 def get_context_data(self, **kwargs): 

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

1742 

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

1744 if container is None: 

1745 raise Http404 

1746 citing_articles = container.citations() 

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

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

1749 book_parts = ( 

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

1751 ) 

1752 references = False 

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

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

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

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

1757 references = True 

1758 context.update( 

1759 { 

1760 "book": container, 

1761 "book_parts": list(book_parts), 

1762 "source": source, 

1763 "citing_articles": citing_articles, 

1764 "references": references, 

1765 "test_website": container.get_top_collection() 

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

1767 .location, 

1768 "prod_website": container.get_top_collection() 

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

1770 .location, 

1771 } 

1772 ) 

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

1774 else: 

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

1776 for article in articles: 

1777 try: 

1778 last_match = ( 

1779 history_models.HistoryEvent.objects.filter( 

1780 pid=article.pid, 

1781 type="matching", 

1782 ) 

1783 .only("created_on") 

1784 .latest("created_on") 

1785 ) 

1786 except history_models.HistoryEvent.DoesNotExist as _: 

1787 article.last_match = None 

1788 else: 

1789 article.last_match = last_match.created_on 

1790 

1791 # article1 = articles.first() 

1792 # date = article1.deployed_date() 

1793 # TODO next_issue, previous_issue 

1794 

1795 # check DOI est maintenant une commande à part 

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

1797 # articlesWithStatus = [] 

1798 # for article in articles: 

1799 # get_or_create_doibatch(article) 

1800 # articlesWithStatus.append(article) 

1801 

1802 test_location = prod_location = "" 

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

1804 if qs: 

1805 test_location = qs.first().location 

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

1807 if qs: 

1808 prod_location = qs.first().location 

1809 context.update( 

1810 { 

1811 "issue": container, 

1812 "articles": articles, 

1813 "source": source, 

1814 "citing_articles": citing_articles, 

1815 "test_website": test_location, 

1816 "prod_website": prod_location, 

1817 } 

1818 ) 

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

1820 

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

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

1823 return context 

1824 

1825 

1826class ExtLinkInline(InlineFormSetFactory): 

1827 model = ExtLink 

1828 form_class = ExtLinkForm 

1829 factory_kwargs = {"extra": 0} 

1830 

1831 

1832class ResourceIdInline(InlineFormSetFactory): 

1833 model = ResourceId 

1834 form_class = ResourceIdForm 

1835 factory_kwargs = {"extra": 0} 

1836 

1837 

1838class IssueDetailAPIView(View): 

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

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

1841 deployed_date = issue.deployed_date() 

1842 result = { 

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

1844 if deployed_date 

1845 else None, 

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

1847 "all_doi_are_registered": issue.all_doi_are_registered(), 

1848 "registered_in_doaj": issue.registered_in_doaj(), 

1849 "doi": issue.my_collection.doi, 

1850 "has_articles_excluded_from_publication": issue.has_articles_excluded_from_publication(), 

1851 } 

1852 try: 

1853 latest = history_models.HistoryEvent.objects.get_last_unsolved_error( 

1854 pid=issue.pid, strict=False 

1855 ) 

1856 except history_models.HistoryEvent.DoesNotExist as _: 

1857 pass 

1858 else: 

1859 result["latest"] = latest.data["message"] 

1860 result["latest_target"] = latest.data.get("target", "") 

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

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

1863 ) 

1864 

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

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

1867 try: 

1868 result[event_type] = timezone.localtime( 

1869 history_models.HistoryEvent.objects.filter( 

1870 type=event_type, 

1871 status="OK", 

1872 pid__startswith=issue.pid, 

1873 ) 

1874 .latest("created_on") 

1875 .created_on 

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

1877 except history_models.HistoryEvent.DoesNotExist as _: 

1878 result[event_type] = "" 

1879 return JsonResponse(result) 

1880 

1881 

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

1883 model = Collection 

1884 form_class = CollectionForm 

1885 inlines = [ResourceIdInline, ExtLinkInline] 

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

1887 

1888 def get_context_data(self, **kwargs): 

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

1890 context["helper"] = PtfFormHelper 

1891 context["formset_helper"] = FormSetHelper 

1892 return context 

1893 

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

1895 if description: 

1896 la = Abstract( 

1897 resource=collection, 

1898 tag="description", 

1899 lang=lang, 

1900 seq=seq, 

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

1902 value_html=description, 

1903 value_tex=description, 

1904 ) 

1905 la.save() 

1906 

1907 def form_valid(self, form): 

1908 if form.instance.abbrev: 

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

1910 else: 

1911 form.instance.title_xml = ( 

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

1913 ) 

1914 

1915 form.instance.title_html = form.instance.title_tex 

1916 form.instance.title_sort = form.instance.title_tex 

1917 result = super().form_valid(form) 

1918 

1919 collection = self.object 

1920 collection.abstract_set.all().delete() 

1921 

1922 seq = 1 

1923 description = form.cleaned_data["description_en"] 

1924 if description: 

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

1926 seq += 1 

1927 description = form.cleaned_data["description_fr"] 

1928 if description: 

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

1930 

1931 return result 

1932 

1933 def get_success_url(self): 

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

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

1936 

1937 

1938class CollectionCreate(CollectionFormView, CreateWithInlinesView): 

1939 """ 

1940 Warning : Not yet finished 

1941 Automatic site membership creation is still missing 

1942 """ 

1943 

1944 

1945class CollectionUpdate(CollectionFormView, UpdateWithInlinesView): 

1946 slug_field = "pid" 

1947 slug_url_kwarg = "pid" 

1948 

1949 

1950def suggest_load_journal_dois(colid): 

1951 articles = ( 

1952 Article.objects.filter(my_container__my_collection__pid=colid) 

1953 .filter(doi__isnull=False) 

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

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

1956 ) 

1957 

1958 try: 

1959 articles = sorted( 

1960 articles, 

1961 key=lambda d: ( 

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

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

1964 ), 

1965 ) 

1966 except: 

1967 pass 

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

1969 

1970 

1971def get_context_with_volumes(journal): 

1972 result = model_helpers.get_volumes_in_collection(journal) 

1973 volume_count = result["volume_count"] 

1974 collections = [] 

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

1976 item = model_helpers.get_volumes_in_collection(ancestor) 

1977 volume_count = max(0, volume_count) 

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

1979 collections.append(item) 

1980 

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

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

1983 collections.append(result) 

1984 

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

1986 collections.sort( 

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

1988 reverse=True, 

1989 ) 

1990 

1991 context = { 

1992 "journal": journal, 

1993 "sorted_issues": result["sorted_issues"], 

1994 "volume_count": volume_count, 

1995 "max_width": result["max_width"], 

1996 "collections": collections, 

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

1998 } 

1999 return context 

2000 

2001 

2002class CollectionDetail( 

2003 UserPassesTestMixin, SingleObjectMixin, ListView, history_views.HistoryContextMixin 

2004): 

2005 model = Collection 

2006 slug_field = "pid" 

2007 slug_url_kwarg = "pid" 

2008 template_name = "ptf/collection_detail.html" 

2009 

2010 def test_func(self): 

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

2012 

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

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

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

2016 

2017 def get_context_data(self, **kwargs): 

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

2019 context["object_list"] = context["object_list"].filter(ctype="issue") 

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

2021 

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

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

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

2025 pid=context["issue_to_appear_pid"] 

2026 ).exists() 

2027 try: 

2028 latest_error = history_models.HistoryEvent.objects.get_last_unsolved_error( 

2029 self.object.pid, 

2030 strict=True, 

2031 ) 

2032 except history_models.HistoryEvent.DoesNotExist as _: 

2033 pass 

2034 else: 

2035 message = latest_error.data["message"] 

2036 i = message.find(" - ") 

2037 latest_exception = message[:i] 

2038 latest_error_message = message[i + 3 :] 

2039 context["latest_exception"] = latest_exception 

2040 context["latest_exception_date"] = latest_error.created_on 

2041 context["latest_exception_type"] = latest_error.type 

2042 context["latest_error_message"] = latest_error_message 

2043 return context 

2044 

2045 def get_queryset(self): 

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

2047 

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

2049 query |= ancestor.content.all() 

2050 

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

2052 

2053 

2054class ContainerEditView(FormView): 

2055 template_name = "container_form.html" 

2056 form_class = ContainerForm 

2057 

2058 def get_success_url(self): 

2059 if self.kwargs["name"] == "special_issue_create": 

2060 return reverse("special_issues_index", kwargs={"colid": self.kwargs["colid"]}) 

2061 if self.kwargs["pid"]: 

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

2063 return reverse("mersenne_dashboard/published_articles") 

2064 

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

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

2067 

2068 def get_form_kwargs(self): 

2069 kwargs = super().get_form_kwargs() 

2070 if "pid" not in self.kwargs: 

2071 self.kwargs["pid"] = None 

2072 if "colid" not in self.kwargs: 

2073 self.kwargs["colid"] = None 

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

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

2076 # It is used when you submit a new container 

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

2078 

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

2080 self.kwargs["pid"] 

2081 ) 

2082 return kwargs 

2083 

2084 def get_context_data(self, **kwargs): 

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

2086 

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

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

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

2090 

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

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

2093 

2094 return context 

2095 

2096 def form_valid(self, form): 

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

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

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

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

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

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

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

2104 

2105 collection = None 

2106 issue = self.kwargs["container"] 

2107 if issue is not None: 

2108 collection = issue.my_collection 

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

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

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

2112 else: 

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

2114 

2115 if collection is None: 

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

2117 

2118 # Icon 

2119 new_icon_location = "" 

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

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

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

2123 

2124 icon_filename = resolver.get_disk_location( 

2125 settings.MERSENNE_TEST_DATA_FOLDER, 

2126 collection.pid, 

2127 file_extension, 

2128 new_pid, 

2129 None, 

2130 True, 

2131 ) 

2132 

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

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

2135 destination.write(chunk) 

2136 

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

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

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

2140 if name == "special_issue_create": 

2141 self.kwargs["name"] = name 

2142 if self.kwargs["container"]: 

2143 # Edit Issue 

2144 issue = self.kwargs["container"] 

2145 if issue is None: 

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

2147 

2148 if new_trans_title and (not issue.trans_lang or issue.trans_lang == "und"): 

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

2150 

2151 issue.pid = new_pid 

2152 issue.title_tex = issue.title_html = new_title 

2153 issue.trans_title_tex = issue.trans_title_html = new_trans_title 

2154 issue.title_xml = get_issue_title_xml( 

2155 new_title, issue.lang, new_trans_title, issue.trans_lang 

2156 ) 

2157 issue.year = new_year 

2158 issue.volume = new_volume 

2159 issue.volume_int = make_int(new_volume) 

2160 issue.number = new_number 

2161 issue.number_int = make_int(new_number) 

2162 issue.save() 

2163 else: 

2164 xissue = create_issuedata() 

2165 

2166 if name == "special_issue_create": 

2167 xissue.ctype = "issue_special" 

2168 if not new_year: 

2169 new_year = datetime.now().year 

2170 else: 

2171 xissue.ctype = "issue" 

2172 xissue.pid = new_pid 

2173 # TODO: add lang + trans_lang 

2174 xissue.title_tex = new_title 

2175 xissue.title_html = new_title 

2176 xissue.trans_title_tex = new_trans_title 

2177 xissue.title_xml = get_issue_title_xml(new_title) 

2178 

2179 # new_title, new_trans_title, issue.trans_lang 

2180 # ) 

2181 xissue.year = new_year 

2182 xissue.volume = new_volume 

2183 xissue.number = new_number 

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

2185 

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

2187 cmd.add_collection(collection) 

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

2189 issue = cmd.do() 

2190 

2191 self.kwargs["pid"] = new_pid 

2192 

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

2194 params = { 

2195 "icon_location": new_icon_location, 

2196 } 

2197 cmd = ptf_cmds.updateContainerPtfCmd(params) 

2198 cmd.set_resource(issue) 

2199 cmd.do() 

2200 

2201 publisher = model_helpers.get_publisher(new_publisher) 

2202 if not publisher: 

2203 xpub = create_publisherdata() 

2204 xpub.name = new_publisher 

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

2206 issue.my_publisher = publisher 

2207 issue.save() 

2208 

2209 self.set_success_message() 

2210 

2211 return super().form_valid(form) 

2212 

2213 

2214# class ArticleEditView(FormView): 

2215# template_name = 'article_form.html' 

2216# form_class = ArticleForm 

2217# 

2218# def get_success_url(self): 

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

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

2221# return reverse('mersenne_dashboard/published_articles') 

2222# 

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

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

2225# 

2226# def get_form_kwargs(self): 

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

2228# 

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

2230# # Article creation: pid is None 

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

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

2233# # Article edit: issue_id is not passed 

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

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

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

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

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

2239# 

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

2241# return kwargs 

2242# 

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

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

2245# 

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

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

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

2249# 

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

2251# 

2252# article = context['article'] 

2253# if article: 

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

2255# context['kwds_fr'] = None 

2256# context['kwds_en'] = None 

2257# kwd_gps = article.get_non_msc_kwds() 

2258# for kwd_gp in kwd_gps: 

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

2260# if kwd_gp.value_xml: 

2261# kwd_ = types.SimpleNamespace() 

2262# kwd_.value = kwd_gp.value_tex 

2263# context['kwd_unstructured_fr'] = kwd_ 

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

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

2266# if kwd_gp.value_xml: 

2267# kwd_ = types.SimpleNamespace() 

2268# kwd_.value = kwd_gp.value_tex 

2269# context['kwd_unstructured_en'] = kwd_ 

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

2271# 

2272# # Article creation: init pid 

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

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

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

2276# 

2277# return context 

2278# 

2279# def form_valid(self, form): 

2280# 

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

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

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

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

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

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

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

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

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

2290# 

2291# # TODO support MathML 

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

2293# # We need to pass trans_title to get_title_xml 

2294# # Meanwhile, ignore new_title_xml 

2295# new_title_xml = jats_parser.get_title_xml(new_title) 

2296# new_title_html = new_title 

2297# 

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

2299# i = 1 

2300# new_authors = [] 

2301# old_author_contributions = [] 

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

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

2304# 

2305# while authors_count > 0: 

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

2307# 

2308# if prefix is not None: 

2309# addresses = [] 

2310# if len(old_author_contributions) >= i: 

2311# old_author_contribution = old_author_contributions[i - 1] 

2312# addresses = [contrib_address.address for contrib_address in 

2313# old_author_contribution.get_addresses()] 

2314# 

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

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

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

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

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

2320# deceased_before_publication = deceased == 'on' 

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

2322# equal_contrib = equal_contrib == 'on' 

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

2324# corresponding = corresponding == 'on' 

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

2326# 

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

2328# params['deceased_before_publication'] = deceased_before_publication 

2329# params['equal_contrib'] = equal_contrib 

2330# params['corresponding'] = corresponding 

2331# params['addresses'] = addresses 

2332# params['email'] = email 

2333# 

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

2335# 

2336# new_authors.append(params) 

2337# 

2338# authors_count -= 1 

2339# i += 1 

2340# 

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

2342# i = 1 

2343# new_kwds_fr = [] 

2344# while kwds_fr_count > 0: 

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

2346# new_kwds_fr.append(value) 

2347# kwds_fr_count -= 1 

2348# i += 1 

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

2350# 

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

2352# i = 1 

2353# new_kwds_en = [] 

2354# while kwds_en_count > 0: 

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

2356# new_kwds_en.append(value) 

2357# kwds_en_count -= 1 

2358# i += 1 

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

2360# 

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

2362# # Edit article 

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

2364# else: 

2365# # New article 

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

2367# 

2368# if container is None: 

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

2370# 

2371# collection = container.my_collection 

2372# 

2373# # Copy PDF file & extract full text 

2374# body = '' 

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

2376# collection.pid, 

2377# "pdf", 

2378# container.pid, 

2379# new_pid, 

2380# True) 

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

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

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

2384# destination.write(chunk) 

2385# 

2386# # Extract full text from the PDF 

2387# body = utils.pdf_to_text(pdf_filename) 

2388# 

2389# # Icon 

2390# new_icon_location = '' 

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

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

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

2394# 

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

2396# collection.pid, 

2397# file_extension, 

2398# container.pid, 

2399# new_pid, 

2400# True) 

2401# 

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

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

2404# destination.write(chunk) 

2405# 

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

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

2408# 

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

2410# # Edit article 

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

2412# article.fpage = new_fpage 

2413# article.lpage = new_lpage 

2414# article.page_range = new_page_range 

2415# article.coi_statement = new_coi_statement 

2416# article.show_body = new_show_body 

2417# article.do_not_publish = new_do_not_publish 

2418# article.save() 

2419# 

2420# else: 

2421# # New article 

2422# params = { 

2423# 'pid': new_pid, 

2424# 'title_xml': new_title_xml, 

2425# 'title_html': new_title_html, 

2426# 'title_tex': new_title, 

2427# 'fpage': new_fpage, 

2428# 'lpage': new_lpage, 

2429# 'page_range': new_page_range, 

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

2431# 'body': body, 

2432# 'coi_statement': new_coi_statement, 

2433# 'show_body': new_show_body, 

2434# 'do_not_publish': new_do_not_publish 

2435# } 

2436# 

2437# xarticle = create_articledata() 

2438# xarticle.pid = new_pid 

2439# xarticle.title_xml = new_title_xml 

2440# xarticle.title_html = new_title_html 

2441# xarticle.title_tex = new_title 

2442# xarticle.fpage = new_fpage 

2443# xarticle.lpage = new_lpage 

2444# xarticle.page_range = new_page_range 

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

2446# xarticle.body = body 

2447# xarticle.coi_statement = new_coi_statement 

2448# params['xobj'] = xarticle 

2449# 

2450# cmd = ptf_cmds.addArticlePtfCmd(params) 

2451# cmd.set_container(container) 

2452# cmd.add_collection(container.my_collection) 

2453# article = cmd.do() 

2454# 

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

2456# 

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

2458# params = { 

2459# # 'title_xml': new_title_xml, 

2460# # 'title_html': new_title_html, 

2461# # 'title_tex': new_title, 

2462# 'authors': new_authors, 

2463# 'page_count': new_page_count, 

2464# 'icon_location': new_icon_location, 

2465# 'body': body, 

2466# 'use_kwds': True, 

2467# 'kwds_fr': new_kwds_fr, 

2468# 'kwds_en': new_kwds_en, 

2469# 'kwd_uns_fr': new_kwd_uns_fr, 

2470# 'kwd_uns_en': new_kwd_uns_en 

2471# } 

2472# cmd = ptf_cmds.updateArticlePtfCmd(params) 

2473# cmd.set_article(article) 

2474# cmd.do() 

2475# 

2476# self.set_success_message() 

2477# 

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

2479 

2480 

2481@require_http_methods(["POST"]) 

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

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

2484 

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

2486 

2487 article = model_helpers.get_article(pid) 

2488 if article: 

2489 article.do_not_publish = not article.do_not_publish 

2490 article.save() 

2491 else: 

2492 raise Http404 

2493 

2494 return HttpResponseRedirect(next) 

2495 

2496 

2497@require_http_methods(["POST"]) 

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

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

2500 

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

2502 

2503 article = model_helpers.get_article(pid) 

2504 if article: 

2505 article.show_body = not article.show_body 

2506 article.save() 

2507 else: 

2508 raise Http404 

2509 

2510 return HttpResponseRedirect(next) 

2511 

2512 

2513class ArticleEditWithVueAPIView(CsrfExemptMixin, ArticleEditAPIView): 

2514 """ 

2515 API to get/post article metadata 

2516 The class is derived from ArticleEditAPIView (see ptf.views) 

2517 """ 

2518 

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

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

2521 self.fields_to_update = [ 

2522 "lang", 

2523 "title_xml", 

2524 "title_tex", 

2525 "title_html", 

2526 "trans_lang", 

2527 "trans_title_html", 

2528 "trans_title_tex", 

2529 "trans_title_xml", 

2530 "atype", 

2531 "contributors", 

2532 "abstracts", 

2533 "subjs", 

2534 "kwds", 

2535 "ext_links", 

2536 ] 

2537 

2538 def convert_data_for_editor(self, data_article): 

2539 super().convert_data_for_editor(data_article) 

2540 data_article.is_staff = self.request.user.is_staff 

2541 

2542 def save_data(self, data_article): 

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

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

2545 params = { 

2546 "pid": data_article.pid, 

2547 "export_folder": settings.MERSENNE_TMP_FOLDER, 

2548 "export_all": True, 

2549 "with_binary_files": False, 

2550 } 

2551 ptf_cmds.exportExtraDataPtfCmd(params).do() 

2552 

2553 def restore_data(self, article): 

2554 ptf_cmds.importExtraDataPtfCmd( 

2555 { 

2556 "pid": article.pid, 

2557 "import_folder": settings.MERSENNE_TMP_FOLDER, 

2558 } 

2559 ).do() 

2560 

2561 

2562class ArticleEditWithVueView(LoginRequiredMixin, TemplateView): 

2563 template_name = "article_form.html" 

2564 

2565 def get_success_url(self): 

2566 if self.kwargs["doi"]: 

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

2568 return reverse("mersenne_dashboard/published_articles") 

2569 

2570 def get_context_data(self, **kwargs): 

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

2572 if "doi" in self.kwargs: 

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

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

2575 

2576 return context 

2577 

2578 

2579class ArticleDeleteView(View): 

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

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

2582 article = get_object_or_404(Article, pid=pid) 

2583 

2584 try: 

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

2586 article.undeploy(mersenneSite) 

2587 

2588 cmd = ptf_cmds.addArticlePtfCmd( 

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

2590 ) 

2591 cmd.set_container(article.my_container) 

2592 cmd.set_object_to_be_deleted(article) 

2593 cmd.undo() 

2594 except Exception as exception: 

2595 return HttpResponseServerError(exception) 

2596 

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

2598 return JsonResponse(data) 

2599 

2600 

2601def get_messages_in_queue(): 

2602 app = Celery("ptf-tools") 

2603 # tasks = list(current_app.tasks) 

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

2605 print(tasks) 

2606 # i = app.control.inspect() 

2607 

2608 with app.connection_or_acquire() as conn: 

2609 remaining = conn.default_channel.queue_declare(queue="celery", passive=True).message_count 

2610 return remaining 

2611 

2612 

2613class FailedTasksListView(ListView): 

2614 model = TaskResult 

2615 queryset = TaskResult.objects.filter( 

2616 status="FAILURE", 

2617 task_name="ptf_tools.tasks.archive_numdam_issue", 

2618 ) 

2619 

2620 

2621class FailedTasksDeleteView(DeleteView): 

2622 model = TaskResult 

2623 success_url = reverse_lazy("tasks-failed") 

2624 

2625 

2626class FailedTasksRetryView(SingleObjectMixin, RedirectView): 

2627 model = TaskResult 

2628 

2629 @staticmethod 

2630 def retry_task(task): 

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

2632 archive_numdam_issue.delay(colid, pid) 

2633 task.delete() 

2634 

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

2636 self.retry_task(self.get_object()) 

2637 return reverse("tasks-failed") 

2638 

2639 

2640class NumdamView(TemplateView, history_views.HistoryContextMixin): 

2641 template_name = "numdam.html" 

2642 

2643 def get_context_data(self, **kwargs): 

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

2645 

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

2647 

2648 pre_issues = [] 

2649 prod_issues = [] 

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

2651 try: 

2652 response = requests.get(url) 

2653 if response.status_code == 200: 

2654 data = response.json() 

2655 if "issues" in data: 

2656 pre_issues = data["issues"] 

2657 except Exception: 

2658 pass 

2659 

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

2661 response = requests.get(url) 

2662 if response.status_code == 200: 

2663 data = response.json() 

2664 if "issues" in data: 

2665 prod_issues = data["issues"] 

2666 

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

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

2669 grouped = [ 

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

2671 ] 

2672 grouped_removed = [ 

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

2674 ] 

2675 context["added_issues"] = grouped 

2676 context["removed_issues"] = grouped_removed 

2677 

2678 context["numdam_collections"] = settings.NUMDAM_COLLECTIONS 

2679 return context 

2680 

2681 

2682class TasksProgressView(View): 

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

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

2685 successes = TaskResult.objects.filter( 

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

2687 ).count() 

2688 fails = TaskResult.objects.filter( 

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

2690 ).count() 

2691 last_task = ( 

2692 TaskResult.objects.filter( 

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

2694 status="SUCCESS", 

2695 ) 

2696 .order_by("-date_done") 

2697 .first() 

2698 ) 

2699 if last_task: 

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

2701 remaining = get_messages_in_queue() 

2702 all = successes + remaining 

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

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

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

2706 data = { 

2707 "status": status, 

2708 "progress": progress, 

2709 "total": all, 

2710 "remaining": remaining, 

2711 "successes": successes, 

2712 "fails": fails, 

2713 "error_rate": error_rate, 

2714 "last_task": last_task, 

2715 } 

2716 return JsonResponse(data) 

2717 

2718 

2719class TasksView(TemplateView): 

2720 template_name = "tasks.html" 

2721 

2722 def get_context_data(self, **kwargs): 

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

2724 context["tasks"] = TaskResult.objects.all() 

2725 return context 

2726 

2727 

2728class NumdamArchiveView(RedirectView): 

2729 @staticmethod 

2730 def reset_task_results(): 

2731 TaskResult.objects.all().delete() 

2732 

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

2734 self.colid = kwargs["colid"] 

2735 

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

2737 return Http404 

2738 

2739 # we make sure archiving is not already running 

2740 if not get_messages_in_queue(): 

2741 self.reset_task_results() 

2742 response = requests.get(f"{settings.NUMDAM_URL}/api-all-collections/") 

2743 if response.status_code == 200: 

2744 data = sorted(response.json()["collections"]) 

2745 

2746 if self.colid != "ALL" and self.colid not in data: 

2747 return Http404 

2748 

2749 colids = [self.colid] if self.colid != "ALL" else data 

2750 

2751 with open( 

2752 os.path.join(settings.LOG_DIR, "archive.log"), "w", encoding="utf-8" 

2753 ) as file_: 

2754 file_.write("Archive " + " ".join([colid for colid in colids]) + "\n") 

2755 

2756 for colid in colids: 

2757 if colid not in settings.MERSENNE_COLLECTIONS: 

2758 archive_numdam_collection.delay(colid) 

2759 return reverse("numdam") 

2760 

2761 

2762class DeployAllNumdamAPIView(View): 

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

2764 pids = [] 

2765 

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

2767 pids.append(obj.pid) 

2768 

2769 return pids 

2770 

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

2772 try: 

2773 pids, status, message = history_views.execute_and_record_func( 

2774 "deploy", "numdam", "numdam", self.internal_do, "numdam" 

2775 ) 

2776 except Exception as exception: 

2777 return HttpResponseServerError(exception) 

2778 

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

2780 return JsonResponse(data) 

2781 

2782 

2783class NumdamDeleteAPIView(View): 

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

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

2786 

2787 try: 

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

2789 obj.delete() 

2790 except Exception as exception: 

2791 return HttpResponseServerError(exception) 

2792 

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

2794 return JsonResponse(data) 

2795 

2796 

2797class ExtIdApiDetail(View): 

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

2799 extid = get_object_or_404( 

2800 ExtId, 

2801 resource__pid=kwargs["pid"], 

2802 id_type=kwargs["what"], 

2803 ) 

2804 return JsonResponse( 

2805 { 

2806 "pk": extid.pk, 

2807 "href": extid.get_href(), 

2808 "fetch": reverse( 

2809 "api-fetch-id", 

2810 args=( 

2811 extid.resource.pk, 

2812 extid.id_value, 

2813 extid.id_type, 

2814 "extid", 

2815 ), 

2816 ), 

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

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

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

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

2821 "is_valid": extid.checked, 

2822 } 

2823 ) 

2824 

2825 

2826class ExtIdFormTemplate(TemplateView): 

2827 template_name = "common/externalid_form.html" 

2828 

2829 def get_context_data(self, **kwargs): 

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

2831 context["sequence"] = kwargs["sequence"] 

2832 return context 

2833 

2834 

2835class BibItemIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

2836 def get_context_data(self, **kwargs): 

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

2838 context["helper"] = PtfFormHelper 

2839 return context 

2840 

2841 def get_success_url(self): 

2842 self.post_process() 

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

2844 

2845 def post_process(self): 

2846 cmd = xml_cmds.updateBibitemCitationXmlCmd() 

2847 cmd.set_bibitem(self.object.bibitem) 

2848 cmd.do() 

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

2850 

2851 

2852class BibItemIdCreate(BibItemIdFormView, CreateView): 

2853 model = BibItemId 

2854 form_class = BibItemIdForm 

2855 

2856 def get_context_data(self, **kwargs): 

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

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

2859 return context 

2860 

2861 def get_initial(self): 

2862 initial = super().get_initial() 

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

2864 return initial 

2865 

2866 def form_valid(self, form): 

2867 form.instance.checked = False 

2868 return super().form_valid(form) 

2869 

2870 

2871class BibItemIdUpdate(BibItemIdFormView, UpdateView): 

2872 model = BibItemId 

2873 form_class = BibItemIdForm 

2874 

2875 def get_context_data(self, **kwargs): 

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

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

2878 return context 

2879 

2880 

2881class ExtIdFormView(LoginRequiredMixin, StaffuserRequiredMixin, View): 

2882 def get_context_data(self, **kwargs): 

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

2884 context["helper"] = PtfFormHelper 

2885 return context 

2886 

2887 def get_success_url(self): 

2888 self.post_process() 

2889 return self.object.resource.get_absolute_url() 

2890 

2891 def post_process(self): 

2892 model_helpers.post_resource_updated(self.object.resource) 

2893 

2894 

2895class ExtIdCreate(ExtIdFormView, CreateView): 

2896 model = ExtId 

2897 form_class = ExtIdForm 

2898 

2899 def get_context_data(self, **kwargs): 

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

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

2902 return context 

2903 

2904 def get_initial(self): 

2905 initial = super().get_initial() 

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

2907 return initial 

2908 

2909 def form_valid(self, form): 

2910 form.instance.checked = False 

2911 return super().form_valid(form) 

2912 

2913 

2914class ExtIdUpdate(ExtIdFormView, UpdateView): 

2915 model = ExtId 

2916 form_class = ExtIdForm 

2917 

2918 def get_context_data(self, **kwargs): 

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

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

2921 return context 

2922 

2923 

2924class BibItemIdApiDetail(View): 

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

2926 bibitemid = get_object_or_404( 

2927 BibItemId, 

2928 bibitem__resource__pid=kwargs["pid"], 

2929 bibitem__sequence=kwargs["seq"], 

2930 id_type=kwargs["what"], 

2931 ) 

2932 return JsonResponse( 

2933 { 

2934 "pk": bibitemid.pk, 

2935 "href": bibitemid.get_href(), 

2936 "fetch": reverse( 

2937 "api-fetch-id", 

2938 args=( 

2939 bibitemid.bibitem.pk, 

2940 bibitemid.id_value, 

2941 bibitemid.id_type, 

2942 "bibitemid", 

2943 ), 

2944 ), 

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

2946 "uncheck": reverse( 

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

2948 ), 

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

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

2951 "is_valid": bibitemid.checked, 

2952 } 

2953 ) 

2954 

2955 

2956class UpdateTexmfZipAPIView(View): 

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

2958 def copy_zip_files(src_folder, dest_folder): 

2959 os.makedirs(dest_folder, exist_ok=True) 

2960 

2961 zip_files = [ 

2962 os.path.join(src_folder, f) 

2963 for f in os.listdir(src_folder) 

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

2965 ] 

2966 for zip_file in zip_files: 

2967 resolver.copy_file(zip_file, dest_folder) 

2968 

2969 # Exceptions: specific zip/gz files 

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

2971 resolver.copy_file(zip_file, dest_folder) 

2972 

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

2974 resolver.copy_file(zip_file, dest_folder) 

2975 

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

2977 resolver.copy_file(gz_file, dest_folder) 

2978 

2979 src_folder = settings.CEDRAM_DISTRIB_FOLDER 

2980 

2981 dest_folder = os.path.join( 

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

2983 ) 

2984 

2985 try: 

2986 copy_zip_files(src_folder, dest_folder) 

2987 except Exception as exception: 

2988 return HttpResponseServerError(exception) 

2989 

2990 try: 

2991 dest_folder = os.path.join( 

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

2993 ) 

2994 copy_zip_files(src_folder, dest_folder) 

2995 except Exception as exception: 

2996 return HttpResponseServerError(exception) 

2997 

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

2999 return JsonResponse(data) 

3000 

3001 

3002class TestView(TemplateView): 

3003 template_name = "mersenne.html" 

3004 

3005 def get_context_data(self, **kwargs): 

3006 super().get_context_data(**kwargs) 

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

3008 model_data_converter.db_to_issue_data(issue) 

3009 

3010 

3011class TrammelArchiveView(RedirectView): 

3012 @staticmethod 

3013 def reset_task_results(): 

3014 TaskResult.objects.all().delete() 

3015 

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

3017 self.colid = kwargs["colid"] 

3018 self.mathdoc_archive = settings.MATHDOC_ARCHIVE_FOLDER 

3019 self.binary_files_folder = settings.MERSENNE_PROD_DATA_FOLDER 

3020 # Make sure archiving is not already running 

3021 if not get_messages_in_queue(): 

3022 self.reset_task_results() 

3023 if "progress/" in self.colid: 

3024 self.colid = self.colid.replace("progress/", "") 

3025 if "/progress" in self.colid: 

3026 self.colid = self.colid.replace("/progress", "") 

3027 

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

3029 return Http404 

3030 

3031 colids = [self.colid] if self.colid != "ALL" else settings.MERSENNE_COLLECTIONS 

3032 

3033 with open( 

3034 os.path.join(settings.LOG_DIR, "archive.log"), "w", encoding="utf-8" 

3035 ) as file_: 

3036 file_.write("Archive " + " ".join([colid for colid in colids]) + "\n") 

3037 

3038 for colid in colids: 

3039 archive_trammel_collection.delay( 

3040 colid, self.mathdoc_archive, self.binary_files_folder 

3041 ) 

3042 

3043 if self.colid == "ALL": 

3044 return reverse("home") 

3045 else: 

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

3047 

3048 

3049class TrammelTasksProgressView(View): 

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

3051 """ 

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

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

3054 """ 

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

3056 

3057 def get_event_data(): 

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

3059 

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

3061 remaining_messages = get_messages_in_queue() 

3062 

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

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

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

3066 

3067 all_tasks_count = all_tasks.count() 

3068 success_count = successed_tasks.count() 

3069 fail_count = failed_tasks.count() 

3070 

3071 all_count = all_tasks_count + remaining_messages 

3072 remaining_count = all_count - success_count - fail_count 

3073 

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

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

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

3077 

3078 last_task = successed_tasks.first() 

3079 last_task = ( 

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

3081 if last_task 

3082 else "" 

3083 ) 

3084 

3085 # SSE event format 

3086 event_data = { 

3087 "status": status, 

3088 "success_rate": success_rate, 

3089 "error_rate": error_rate, 

3090 "all_count": all_count, 

3091 "remaining_count": remaining_count, 

3092 "success_count": success_count, 

3093 "fail_count": fail_count, 

3094 "last_task": last_task, 

3095 } 

3096 

3097 return event_data 

3098 

3099 def stream_response(data): 

3100 # Send initial response headers 

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

3102 

3103 data = get_event_data() 

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

3105 if format == "json": 

3106 response = JsonResponse(data) 

3107 else: 

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

3109 return response 

3110 

3111 

3112class TrammelFailedTasksListView(ListView): 

3113 model = TaskResult 

3114 queryset = TaskResult.objects.filter( 

3115 status="FAILURE", 

3116 task_name="ptf_tools.tasks.archive_trammel_resource", 

3117 )