Coverage for apps/ptf/models.py: 78%

2037 statements  

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

1import os 

2import re 

3from urllib.parse import urljoin 

4from urllib.parse import urlparse 

5 

6from django.conf import settings 

7from django.contrib.sites.models import Site 

8from django.core.exceptions import MultipleObjectsReturned 

9from django.core.files.storage import FileSystemStorage 

10from django.db import models 

11from django.db.models import Max 

12from django.db.models import Prefetch 

13from django.db.models import Q 

14from django.db.models.signals import pre_delete 

15from django.dispatch import receiver 

16from django.urls import reverse 

17from django.utils import timezone 

18from django.utils.translation import get_language 

19from django.utils.translation import gettext_lazy as _ 

20 

21from ptf import exceptions 

22from ptf.bibtex import append_in_latex 

23from ptf.bibtex import get_bibtex_id 

24from ptf.bibtex import get_bibtex_names 

25from ptf.cmds.xml import xml_utils 

26from ptf.display import resolver 

27from ptf.utils import get_display_name 

28from ptf.utils import volume_display 

29 

30CONTRIB_TYPE_AUTHOR = "author" 

31CONTRIB_TYPE_EDITOR = "editor" 

32CONTRIB_TYPE_CONTRIBUTOR = "contributor" 

33CONTRIB_TYPE_REDAKTOR = "redaktor" 

34CONTRIB_TYPE_ORGANIZER = "organizer" 

35CONTRIB_TYPE_PRESENTER = "presenter" 

36EDITED_BOOK_TYPE = "book-edited-book" 

37 

38 

39# http://stackoverflow.com/questions/929029/ 

40# how-do-i-access-the-child-classes-of-an-object-in-django-without-knowing-the-nam 

41# http://djangosnippets.org/snippets/1031/ 

42 

43 

44class UnknownRelationType(Exception): 

45 pass 

46 

47 

48class Identifier: 

49 """ 

50 descripteur 

51 """ 

52 

53 def __init__(self, id_type): 

54 self.id_type = id_type 

55 

56 def __get__(self, obj, objtype): 

57 value = "" 

58 idobj_qs = obj.resourceid_set.filter(id_type=self.id_type) 

59 if idobj_qs.count() > 0: 

60 value = idobj_qs.first().id_value 

61 

62 return value 

63 

64 def __set__(self, obj, value): 

65 raise NotImplementedError("Operation not implemented") 

66 

67 

68class PtfSite(Site): 

69 """Site hébergé""" 

70 

71 acro = models.CharField(max_length=32, unique=True) 

72 public = models.OneToOneField( 

73 "self", null=True, related_name="test_site", on_delete=models.CASCADE 

74 ) 

75 prev_pub_date = models.DateField(null=True) 

76 last_pub_date = models.DateField(null=True) 

77 

78 

79class ResourceQuerySet(models.QuerySet): 

80 def prefetch_contributors(self): 

81 return self.prefetch_related("contributions", "contributions__contribaddress_set") 

82 

83 def prefetch_references(self): 

84 sorted_ids = BibItemId.objects.filter( 

85 bibitem__resource__pk__in=self.values("pk") 

86 ).order_by("id_type") 

87 return self.prefetch_related( 

88 Prefetch("bibitem_set__bibitemid_set", queryset=sorted_ids), 

89 "bibitem_set__contributions", 

90 "bibitem_set__contributions__contribaddress_set", 

91 ) 

92 

93 def prefetch_work(self): 

94 return self.prefetch_related( 

95 "resourceid_set", 

96 "extid_set", 

97 "abstract_set", 

98 "kwd_set", 

99 "subj_set", 

100 "datastream_set", 

101 "relatedobject_set", 

102 "extlink_set", 

103 "resourcecount_set", 

104 "subject_of", 

105 "object_of", 

106 "award_set", 

107 "frontmatter", 

108 ) 

109 

110 def prefetch_all(self): 

111 return self.prefetch_references().prefetch_contributors().prefetch_work() 

112 

113 

114class Resource(models.Model): 

115 classname = models.CharField(max_length=32, editable=False, db_index=True) 

116 ## 

117 # dans SiteMembership 

118 provider = models.ForeignKey("Provider", null=True, on_delete=models.CASCADE) 

119 ## 

120 # provider id 

121 # pas unique globalement, mais unique par provider 

122 # et par site hébergé 

123 pid = models.CharField(max_length=80, db_index=True, blank=True, default="") 

124 ## 

125 # surrogate provider id -- unique par provider 

126 sid = models.CharField(max_length=64, db_index=True, blank=True, null=True) 

127 doi = models.CharField(max_length=64, unique=True, null=True, blank=True) 

128 right_resources_related_to_me = models.ManyToManyField( 

129 "self", 

130 symmetrical=False, 

131 through="Relationship", 

132 related_name="left_resources_related_to_me", 

133 ) 

134 sites = models.ManyToManyField(PtfSite, symmetrical=False, through="SiteMembership") 

135 

136 title_xml = models.TextField(default="") 

137 

138 lang = models.CharField(max_length=3, default="und") 

139 title_tex = models.TextField(default="") 

140 title_html = models.TextField(default="") 

141 

142 trans_lang = models.CharField(max_length=3, default="und") 

143 trans_title_tex = models.TextField(default="") 

144 trans_title_html = models.TextField(default="") 

145 

146 abbrev = models.CharField(max_length=128, blank=True, db_index=True) 

147 funding_statement_html = models.TextField(default="") 

148 funding_statement_xml = models.TextField(default="") 

149 footnotes_html = models.TextField(default="") 

150 footnotes_xml = models.TextField(default="") 

151 

152 body_html = models.TextField(default="") 

153 body_tex = models.TextField(default="") 

154 body_xml = models.TextField(default="") 

155 

156 objects = ResourceQuerySet.as_manager() 

157 

158 class Meta: 

159 unique_together = ("provider", "pid") 

160 

161 def __str__(self): 

162 return self.pid 

163 

164 def get_absolute_url(self): 

165 """ 

166 @warning : return an absolute path, not an URL 

167 @return: absolute path without scheme or domain name 

168 """ 

169 

170 return reverse("item_id", kwargs={"pid": self.pid}) 

171 

172 def get_url_absolute(self): 

173 if settings.SITE_NAME == "numdam": 173 ↛ 174line 173 didn't jump to line 174, because the condition on line 173 was never true

174 domain = "http://numdam.org/" 

175 else: 

176 col = self.get_collection() 

177 try: 

178 website_extlink = col.extlink_set.get(rel="website", metadata="website") 

179 # add ending / for urljoin 

180 domain = website_extlink.location + "/" 

181 except ExtLink.DoesNotExist: 

182 domain = "/" 

183 

184 # remove beginning / for urljoin 

185 resource_path = re.sub(r"^/*", "", self.get_absolute_url()) 

186 return urljoin(domain, resource_path) 

187 

188 def save(self, *args, **kwargs): 

189 if not self.id: 

190 self.classname = self.__class__.__name__ 

191 super().save(*args, **kwargs) 

192 

193 def cast(self): 

194 base_obj = self 

195 if not hasattr(self, self.classname.lower()): 195 ↛ 196line 195 didn't jump to line 196, because the condition on line 195 was never true

196 base_obj = self.article 

197 return base_obj.__getattribute__(self.classname.lower()) 

198 

199 def get_collection(self): 

200 return None 

201 

202 def get_top_collection(self): 

203 return None 

204 

205 def get_container(self): 

206 return None 

207 

208 def is_deployed(self, site): 

209 try: 

210 site = self.sites.get(id=site.id) 

211 except Site.DoesNotExist: 

212 return False 

213 else: 

214 return True 

215 

216 def deploy(self, site, deployed_date=None): 

217 """ 

218 Warning: As of July 2018, only 1 site id is stored in a SolR document 

219 Although the SolR schema is already OK to store multiple sites ("sites" is an array) 

220 no Solr commands have been written to add/remove sites 

221 We only have add commands. 

222 Search only works if the Solr instance is meant for individual or ALL sites 

223 

224 :param site: 

225 :param deployed_date: 

226 :return: 

227 """ 

228 if deployed_date is None: 

229 deployed_date = timezone.now() 

230 try: 

231 membership = SiteMembership.objects.get(resource=self, site=site) 

232 membership.deployed = deployed_date 

233 membership.save() 

234 except SiteMembership.DoesNotExist: 

235 membership = SiteMembership(resource=self, site=site, deployed=deployed_date) 

236 membership.save() 

237 

238 def undeploy(self, site): 

239 try: 

240 membership = SiteMembership.objects.get(resource=self, site=site) 

241 except SiteMembership.DoesNotExist: 

242 pass 

243 else: 

244 membership.delete() 

245 

246 def date_time_deployed(self, site): 

247 try: 

248 membership = SiteMembership.objects.get(resource=self, site=site) 

249 except SiteMembership.DoesNotExist: 

250 return None 

251 return membership.deployed 

252 

253 def deployed_date(self, site=None): 

254 if site is None and settings.SITE_NAME == "ptf_tools": 

255 # on est sur ptf-tools et dans un template on fait appel à deployed_date 

256 # si le site lié à la collection est créé on renvoie la date de déploiement sur ce site 

257 # sinon None 

258 from ptf import model_helpers 

259 

260 site = model_helpers.get_site_mersenne(self.get_collection().pid) 

261 return self.date_time_deployed(site) 

262 if not site: 

263 site = Site.objects.get_current() 

264 

265 return self.date_time_deployed(site) 

266 

267 def get_id_value(self, id_type): 

268 try: 

269 rid = self.resourceid_set.get(id_type=id_type) 

270 except ResourceId.DoesNotExist: 

271 return None 

272 else: 

273 return rid.id_value 

274 

275 # TODO : doi is in ResourceId and in Resource ? maybe use only one ... 

276 def get_doi_href(self): 

277 href = None 

278 if self.doi: 

279 href = resolver.get_doi_url(self.doi) 

280 return href 

281 

282 def get_link(self, link_type): 

283 href = None 

284 for link in self.extlink_set.all(): 

285 if link.rel == link_type: 

286 href = link.get_href() 

287 

288 return href 

289 

290 def website(self): 

291 return self.get_link("website") 

292 

293 def test_website(self): 

294 return self.get_link("test_website") 

295 

296 def icon(self): 

297 return self.get_link("icon") 

298 

299 def small_icon(self): 

300 return self.get_link("small_icon") 

301 

302 # def relation_names(self): 

303 # names = set() 

304 # for rel in self.subject_of.select_related('rel_info').all(): 

305 # name = rel.rel_info.left 

306 # names.add(name) 

307 # for rel in self.object_of.select_related('rel_info').all(): 

308 # name = rel.rel_info.right 

309 # names.add(name) 

310 # return names 

311 # 

312 # def get_related(self, rel_type, count_only=True): 

313 # is_subject = False 

314 # try: 

315 # rel = RelationName.objects.get(left=rel_type) 

316 # except RelationName.DoesNotExist: 

317 # try: 

318 # rel = RelationName.objects.get(right=rel_type) 

319 # except RelationName.DoesNotExist: 

320 # raise UnknownRelationType(rel_type) 

321 # else: 

322 # pass 

323 # else: 

324 # is_subject = True 

325 # if is_subject: 

326 # qs = self.subject_of.filter(rel_info=rel) 

327 # if count_only: 

328 # return qs.count() 

329 # result = [x.related.cast() for x in qs] 

330 # else: 

331 # qs = self.object_of.filter(rel_info=rel) 

332 # if count_only: 

333 # return qs.count() 

334 # result = [x.resource.cast() for x in qs] 

335 # return result 

336 

337 def is_edited_book(self): 

338 return False 

339 

340 def get_document_type(self): 

341 """ 

342 Is used to classify the resources in the 

343 - (TODO) SOLR Facet Document Type 

344 - Geodesic menu (Journals/Books/Seminars...) 

345 """ 

346 return "" 

347 

348 collection = self.get_top_collection() 

349 if collection.coltype == "journal": 

350 document_type = "Article de revue" 

351 elif collection.coltype == "acta": 

352 document_type = "Acte de séminaire" 

353 elif collection.coltype == "thesis": 

354 self.data["classname"] = "Thèse" 

355 document_type = "Thèse" 

356 elif collection.coltype == "lecture-notes": 

357 self.data["classname"] = "Notes de cours" 

358 document_type = "Notes de cours" 

359 elif collection.coltype == "proceeding": 

360 self.data["classname"] = "Acte de rencontre" 

361 document_type = "Acte de rencontre" 

362 else: 

363 self.data["classname"] = "Livre" 

364 document_type = "Livre" 

365 

366 return document_type 

367 

368 # NOTE - 12/08/2017 - Basile 

369 # utilisé nul part à delete ? 

370 # def has_errata(self): 

371 # return self.get_related('corrected-by') 

372 

373 # def is_erratum_to(self): 

374 # return self.get_related('corrects', count_only=False) 

375 

376 # def errata(self): 

377 # return self.get_related('corrected-by', count_only=False) 

378 

379 # def erratum(self): 

380 # errata = self.get_related('corrected-by', count_only=False) 

381 # if len(errata) == 1: 

382 # return errata[0] 

383 # return None 

384 

385 # def questions(self): 

386 # return self.get_related('resolves', count_only=False) 

387 

388 # def solutions(self): 

389 # return self.get_related('resolved-by', count_only=False) 

390 

391 # def complements(self): 

392 # return self.get_related('complements', count_only=False) 

393 

394 # def completed(self): 

395 # return self.get_related('complemented-by', count_only=False) 

396 

397 # def follows(self): 

398 # followed = self.get_related('follows', count_only=False) 

399 # if len(followed) == 1: 

400 # return followed[0] 

401 # return 0 

402 

403 # def followed(self): 

404 # follows = self.get_related('followed-by', count_only=False) 

405 # if len(follows) == 1: 

406 # return follows[0] 

407 # return 0 

408 

409 # def citations_count(self): 

410 # if not self.pid: 

411 # return 0 

412 # qs = BibItemId.objects.select_related().filter( 

413 # id_type=self.provider.pid_type, 

414 # id_value=self.pid, 

415 # bibitem__resource__sites__id=settings.SITE_ID 

416 # ) 

417 # return qs.count() 

418 

419 def citations(self): 

420 if not self.pid or not self.provider: 

421 return [] 

422 

423 qs = BibItemId.objects.select_related().filter( 

424 id_type=self.provider.pid_type, 

425 id_value=self.pid, 

426 bibitem__resource__sites__id=settings.SITE_ID, 

427 ) 

428 # on ne peut pas trier par date sur la requete car 

429 # on peut avoir soit des containers soit des articles 

430 # comme resource et year est sur le container uniquement 

431 result = [bid.bibitem.resource.cast() for bid in qs] 

432 result.sort(key=lambda item: item.get_year(), reverse=True) 

433 return result 

434 

435 def get_contributions(self, role): 

436 # prefetch probably has already queried the database for the contributions 

437 # Using filter on self.contributions would result in a separate SQL query 

438 return [ 

439 contribution 

440 for contribution in self.contributions.all() 

441 if contribution.role.find(role) == 0 

442 ] 

443 

444 def get_author_contributions(self, strict=True): 

445 authors = self.get_contributions("author") 

446 if not strict and len(authors) == 0: 

447 authors = self.get_editors() 

448 return authors 

449 

450 def get_authors_short(self): 

451 authors = self.get_contributions("author") 

452 if len(authors) > 2: 

453 authors = "; ".join([str(author) for author in authors[:2]]) 

454 authors += " <i>et al.</i>" 

455 return authors 

456 return "; ".join([str(author) for author in authors]) 

457 

458 def get_editors(self): 

459 return self.get_contributions("editor") 

460 

461 def get_contributors(self): 

462 return self.get_contributions("contributor") 

463 

464 def get_redaktors(self): 

465 return self.get_contributions("redaktor") 

466 

467 def get_organizers(self): 

468 return self.get_contributions("organizer") 

469 

470 def get_presenters(self): 

471 return self.get_contributions("presenter") 

472 

473 def get_kwds_by_type(self): 

474 msc = [kwd for kwd in self.kwd_set.all() if kwd.type == "msc"] 

475 kwds = [kwd for kwd in self.kwd_set.all() if kwd.type != "msc" and kwd.lang == self.lang] 

476 trans_kwds = [ 

477 kwd for kwd in self.kwd_set.all() if kwd.type != "msc" and kwd.lang != self.lang 

478 ] 

479 return msc, kwds, trans_kwds 

480 

481 def get_subjs_by_type_and_lang(self): 

482 subjs = {} 

483 for subj in self.subj_set.all(): 

484 if subj.type in subjs: 

485 if subj.lang in subjs[subj.type]: 

486 subjs[subj.type][subj.lang].append(subj) 

487 else: 

488 subjs[subj.type][subj.lang] = [subj] 

489 else: 

490 subjs[subj.type] = {subj.lang: [subj]} 

491 

492 return subjs 

493 

494 def self_uris_no_xml(self): 

495 """ 

496 Returns a list of links to the datastream of the resource (= pdf/djvu of the resource) 

497 This function is only used to export an xml (oai) 

498 HTML templates use get_binary_files_href (see below) 

499 """ 

500 links = [ 

501 { 

502 "mimetype": link.mimetype, 

503 "full_path": self.get_binary_file_href_full_path( 

504 "self", link.mimetype, link.location 

505 ), 

506 "link": link.text, 

507 } 

508 for link in self.datastream_set.all() 

509 ] 

510 return links 

511 

512 @staticmethod 

513 def append_href_to_binary_files(binary_files, key, mimetype, href): 

514 if mimetype == "application/pdf": 

515 if key in binary_files: 515 ↛ 516line 515 didn't jump to line 516, because the condition on line 515 was never true

516 binary_files[key]["pdf"] = href 

517 else: 

518 binary_files[key] = {"pdf": href} 

519 elif mimetype == "image/x.djvu": 519 ↛ 520line 519 didn't jump to line 520, because the condition on line 519 was never true

520 if key in binary_files: 

521 binary_files[key]["djvu"] = href 

522 else: 

523 binary_files[key] = {"djvu": href} 

524 elif mimetype == "application/x-tex": 

525 if key in binary_files: 525 ↛ 528line 525 didn't jump to line 528, because the condition on line 525 was never false

526 binary_files[key]["tex"] = href 

527 else: 

528 binary_files[key] = {"tex": href} 

529 elif mimetype == "video": 529 ↛ 530line 529 didn't jump to line 530, because the condition on line 529 was never true

530 if key in binary_files: 

531 binary_files[key]["video"] = href 

532 else: 

533 binary_files[key] = {"video": href} 

534 

535 def get_binary_files_location(self): 

536 binary_files = [] 

537 

538 for obj in self.extlink_set.all(): 

539 if obj.rel == "icon" or obj.rel == "small_icon": 

540 binary_files.append(obj.location) 

541 

542 for obj in self.relatedobject_set.all(): 

543 if obj.rel != "video" and obj.rel != "html-image": 543 ↛ 544line 543 didn't jump to line 544, because the condition on line 543 was never true

544 binary_files.append(obj.location) 

545 

546 for obj in self.datastream_set.all(): 

547 binary_files.append(obj.location) 

548 

549 if hasattr(self, "translations"): 

550 for translated_article in self.translations.all(): 550 ↛ 551line 550 didn't jump to line 551, because the loop on line 550 never started

551 binary_files.extend(translated_article.get_binary_files_location()) 

552 

553 return binary_files 

554 

555 def get_binary_files_href(self): 

556 """ 

557 Get all the HREFs (without http://site) of the binary files (pdf/djvu) related to the resource 

558 Result: { 'self': {'pdf':href, 'djvu':href, 'tex': href}, 

559 'toc':{'pdf':href, 'djvu':href}, 

560 'frontmatter':{'pdf':href, 'djvu':href}, 

561 'backmatter':{'pdf':href, 'djvu':href}, 

562 'translations': {<lang>: {'pdf':href,...}} } 

563 result['self'] is the main pdf/djvu of the resource (article pdf, 

564 full volume pdf, book part pdf) 

565 The information come from the DataStream 

566 The other results come from RelatedObject 

567 """ 

568 binary_files = {} 

569 

570 for related_object in self.relatedobject_set.all(): 

571 key = related_object.rel 

572 mimetype = related_object.mimetype 

573 location = related_object.location 

574 href = self.get_binary_file_href_full_path(key, mimetype, location) 

575 

576 self.append_href_to_binary_files(binary_files, key, mimetype, href) 

577 

578 allow_local_pdf = not hasattr(settings, "ALLOW_LOCAL_PDF") or settings.ALLOW_LOCAL_PDF 

579 

580 for stream in self.datastream_set.all(): 

581 key = "self" 

582 mimetype = stream.mimetype 

583 location = stream.location 

584 

585 if allow_local_pdf or mimetype != "application/pdf": 585 ↛ 580line 585 didn't jump to line 580, because the condition on line 585 was never false

586 if location.find("http") == 0: 586 ↛ 587line 586 didn't jump to line 587, because the condition on line 586 was never true

587 href = location 

588 else: 

589 href = self.get_binary_file_href_full_path("self", mimetype, location) 

590 

591 self.append_href_to_binary_files(binary_files, key, mimetype, href) 

592 

593 if not allow_local_pdf: 593 ↛ 594line 593 didn't jump to line 594, because the condition on line 593 was never true

594 qs = self.extlink_set.filter(rel="article-pdf") 

595 if qs: 

596 extlink = qs.first() 

597 href = extlink.location 

598 key = "self" 

599 mimetype = "application/pdf" 

600 

601 self.append_href_to_binary_files(binary_files, key, mimetype, href) 

602 

603 translations = {} 

604 if hasattr(self, "translations"): 

605 for trans_article in self.translations.all(): 605 ↛ 606line 605 didn't jump to line 606, because the loop on line 605 never started

606 result = trans_article.get_binary_files_href() 

607 if "self" in result: 

608 translations[trans_article.lang] = result["self"] 

609 

610 binary_files["translations"] = translations 

611 

612 return binary_files 

613 

614 def get_binary_file_href_full_path(self, doctype, mimetype, location): 

615 """ 

616 Returns an encoded URL to a pdf/djvu 

617 URLs in HTML pages do not return the exact path to a file (for safety reason) 

618 An encode URL is used instead 

619 Ex: URL to an article pdf: article/pid.pdf 

620 URL to an issue full pdf: issue/pid.pdf 

621 URL to an issue toc djvu: issue/toc/pid.pdf 

622 URL to an issue frontmatter: issue/frontmatter.pid.djvu 

623 When you click on such a link, ptf/views.py will decode the URL in get_pdf 

624 and use the DataStream/RelatedObject true location to return the file 

625 Input: doctype: 'self', 'toc', 'frontmatter', 'backmatter' 

626 mimetype: 'application/pdf', 'image/x.djvu' 

627 

628 Ex: /article/ALCO_2018__1_1_23_0.pdf 

629 /issue/MSMF_1978__55-56__1_0.pdf 

630 /issue/toc/CSHM_1980__1_.pdf 

631 /article/ALCO_2018__1_1_23_0/tex/src/tex/ALCO_Thiem_31.tex 

632 

633 """ 

634 if self.embargo(): 

635 return "" 

636 

637 pid = self.pid 

638 doi = getattr(self, "doi") 

639 if doi is not None: 

640 pid = doi 

641 

642 prefix = doctype if doctype != "self" else "" 

643 

644 if "annexe" in location: 644 ↛ 648line 644 didn't jump to line 648, because the condition on line 644 was never true

645 # 04/08/2021. Not used anymore ? 

646 

647 # Ex: /article/SMAI-JCM_2015__1__83_0/attach/Allaire-Dapogny-supp.pdf 

648 href = reverse( 

649 "annexe-pdf", kwargs={"pid": pid, "relative_path": location.split("/")[-1]} 

650 ) 

651 

652 elif mimetype in ["application/pdf", "image/x.djvu"] and doctype not in [ 

653 "review", 

654 "supplementary-material", 

655 ]: 

656 extension = "pdf" if mimetype == "application/pdf" else "djvu" 

657 if len(prefix) > 0: 

658 href = reverse( 

659 "issue-relatedobject-pdf", 

660 kwargs={"pid": pid, "extension": extension, "binary_file_type": prefix}, 

661 ) 

662 else: 

663 href = reverse("item-pdf", kwargs={"pid": pid, "extension": extension}) 

664 

665 elif mimetype == "application/x-tex": 

666 to_find = "/src/tex/" 

667 i = location.find(to_find) 

668 if i > 0: 

669 location = location[i + 1 :] 

670 

671 href = reverse("article-binary-files", kwargs={"pid": pid, "relative_path": location}) 

672 

673 else: 

674 # All other attachments (videos, zip, etc...) : 

675 

676 to_find = "/attach/" 

677 i = location.find(to_find) 

678 if i > 0: 678 ↛ 679line 678 didn't jump to line 679, because the condition on line 678 was never true

679 location = location[i + 1 :] 

680 

681 # Ex: /article/ALCO_2018__1_1_23_0/file/review_history.pdf 

682 href = reverse("article-binary-files", kwargs={"pid": pid, "relative_path": location}) 

683 

684 return href 

685 

686 def get_binary_disk_location(self, doctype, mimetype, relativepath): 

687 """ 

688 Returns the path of a binary file (pdf/djvu) on the file server 

689 This function is called when you click on a link like issue/frontmatter/pid.pdf 

690 

691 URLs in HTML pages do not return the exact path to a file (for safety reason) 

692 An encoded URL is used instead (see get_binary_file_href_full_path above) 

693 Ex: URL to an issue toc djvu: issue/toc/pid.pdf 

694 

695 When you click on such a link, ptf/views.py will decode the URL in get_pdf 

696 before calling get_binary_disk_location to get the exact pdf/djvu disk location 

697 based on the DataStream/RelatedObject location 

698 

699 Input: doctype: 'self', 'toc', 'frontmatter', 'backmatter' 

700 mimetype: 'application/pdf', 'image/x.djvu' 

701 relativepath: Ex: 'src/tex/ALCO_Thiem_31.tex' 

702 

703 Returns value: path or None if there is an embargo 

704 May raise exceptions.ResourceDoesNotExist if the resource 

705 (RelatedObject/DataStream) does not exist 

706 """ 

707 if self.embargo(): 

708 return None 

709 

710 filename = None 

711 

712 if ( 712 ↛ 717line 712 didn't jump to line 717

713 doctype not in ["self", "toc", "frontmatter", "backmatter"] 

714 and len(doctype) == 2 

715 and hasattr(self, "translations") 

716 ): 

717 for translated_article in self.translations.all(): 

718 if translated_article.lang == doctype: 

719 filename = translated_article.get_binary_disk_location( 

720 "self", mimetype, relativepath 

721 ) 

722 elif relativepath: 722 ↛ 724line 722 didn't jump to line 724, because the condition on line 722 was never true

723 # relative path are used with supplementary materials or TeX Source file 

724 filename = os.path.join(self.get_relative_folder(), relativepath) 

725 else: 

726 if doctype != "self": 

727 try: 

728 obj = self.relatedobject_set.filter(mimetype=mimetype, rel=doctype).get() 

729 

730 except RelatedObject.DoesNotExist: 

731 # status = 404 

732 raise exceptions.ResourceDoesNotExist("The binary file does not exist") 

733 else: 

734 try: 

735 obj = self.datastream_set.get(mimetype=mimetype) 

736 

737 except DataStream.DoesNotExist: 

738 # status = 404 

739 raise exceptions.ResourceDoesNotExist("The binary file does not exist") 

740 filename = obj.location 

741 

742 return filename 

743 

744 def get_abstracts(self): 

745 return self.abstract_set.filter(tag__endswith="abstract") 

746 

747 def get_avertissements(self): 

748 return self.abstract_set.filter(tag="avertissement") 

749 

750 def get_notes(self): 

751 return self.abstract_set.filter(tag="note") 

752 

753 def get_descriptions(self): 

754 return self.abstract_set.filter(tag="description") 

755 

756 def get_editorial_intros(self): 

757 return self.abstract_set.filter(tag__endswith="intro") 

758 

759 def get_toc(self): 

760 return self.abstract_set.filter(tag__endswith="toc") 

761 

762 def get_biblio(self): 

763 return self.abstract_set.filter(tag="biblio") 

764 

765 def accept(self, visitor): 

766 return visitor.visit(self.cast()) 

767 

768 def natural_key(self): 

769 return (self.pid, self.provider.id) 

770 

771 def get_solr_body(self, field): 

772 from ptf.cmds import solr_cmds 

773 

774 body = None 

775 cmd = solr_cmds.solrGetDocumentByPidCmd({"pid": self.pid}) 

776 result = cmd.do() 

777 if result: 

778 if field in result: 778 ↛ 781line 778 didn't jump to line 781, because the condition on line 778 was never false

779 body = result[field] 

780 

781 return body 

782 

783 def get_body(self): 

784 return self.get_solr_body("body") 

785 

786 def volume_string(self): 

787 return "" 

788 

789 def update_bibtex_with_commons(self, bibtex, container, hostname, scheme, indent): 

790 # TODO chapter, howpublished? 

791 # TODO institution 

792 # if self.institution: 

793 # append_in_latex(bibtex, indent + 'institution = {' + self.institution + '},' ) 

794 

795 to_appear = container.to_appear() 

796 is_cr = container.is_cr() 

797 

798 # pages = self.pages() 

799 publisher = container.my_publisher 

800 

801 if publisher is not None: 

802 if publisher.pub_name: 802 ↛ 804line 802 didn't jump to line 804, because the condition on line 802 was never false

803 append_in_latex(bibtex, indent + "publisher = {" + publisher.pub_name + "},") 

804 if publisher.pub_loc: 

805 append_in_latex(bibtex, indent + "address = {" + publisher.pub_loc + "},") 

806 

807 if not to_appear: 

808 if container.volume: 

809 append_in_latex(bibtex, indent + "volume = {" + self.volume_string() + "},") 

810 if container.number and not (is_cr and container.number[0] == "G"): 

811 append_in_latex(bibtex, indent + "number = {" + container.number + "},") 

812 

813 if container.year != "0": 813 ↛ 815line 813 didn't jump to line 815, because the condition on line 813 was never false

814 append_in_latex(bibtex, indent + "year = {" + container.year + "},") 

815 elif hasattr(self, "date_online_first") and self.date_online_first is not None: 

816 year = self.date_online_first.strftime("%Y") 

817 append_in_latex(bibtex, indent + "year = {" + year + "},") 

818 

819 if self.doi: 

820 append_in_latex(bibtex, indent + "doi = {" + self.doi + "},") 

821 

822 ext_type = {"zbl-item-id": "zbl", "mr-item-id": "mrnumber"} 

823 for extid in self.extid_set.filter(id_type__in=["zbl-item-id", "mr-item-id"]): 

824 append_in_latex( 

825 bibtex, indent + ext_type.get(extid.id_type) + " = {" + extid.id_value + "}," 

826 ) 

827 

828 if self.lang and self.lang != "und": 

829 append_in_latex(bibtex, indent + "language = {" + self.lang + "},") 

830 

831 if to_appear: 

832 append_in_latex(bibtex, indent + "note = {Online first},") 

833 elif not is_cr and len(hostname) > 0: 

834 # No latex encoding, so append directly in the array 

835 url = f"{scheme}://{hostname}{self.get_absolute_url()}" 

836 bibtex.append("{}url = {}{}{}".format(indent, "{", url, "}")) 

837 

838 def update_ris_with_commons(self, items, container, hostname, scheme, sep): 

839 to_appear = container.to_appear() 

840 is_cr = container.is_cr() 

841 

842 if container.year != "0": 842 ↛ 844line 842 didn't jump to line 844, because the condition on line 842 was never false

843 items.append("PY" + sep + container.year) 

844 elif hasattr(self, "date_online_first") and self.date_online_first is not None: 

845 year = self.date_online_first.strftime("%Y") 

846 items.append("PY" + sep + year) 

847 

848 if not to_appear: 

849 if hasattr(self, "pages") and callable(self.pages) and self.pages(): 

850 pages = self.pages(for_bibtex=False).split("-") 

851 items.append("SP" + sep + pages[0]) 

852 if len(pages) > 1: 852 ↛ 854line 852 didn't jump to line 854, because the condition on line 852 was never false

853 items.append("EP" + sep + pages[1]) 

854 if container.volume: 

855 items.append("VL" + sep + container.volume) 

856 if container.number and not (is_cr and container.number[0] == "G"): 856 ↛ 859line 856 didn't jump to line 859, because the condition on line 856 was never false

857 items.append("IS" + sep + container.number) 

858 

859 publisher = container.my_publisher 

860 if publisher is not None: 

861 if publisher.pub_name: 861 ↛ 863line 861 didn't jump to line 863, because the condition on line 861 was never false

862 items.append("PB" + sep + publisher.pub_name) 

863 if publisher.pub_loc: 863 ↛ 866line 863 didn't jump to line 866, because the condition on line 863 was never false

864 items.append("PP" + sep + publisher.pub_loc) 

865 

866 if to_appear: 

867 items.append("N1" + sep + "Online first") 

868 elif not is_cr and len(hostname) > 0: 

869 url = f"{scheme}://{hostname}{self.get_absolute_url()}" 

870 items.append("UR" + sep + url) 

871 

872 if self.doi: 

873 items.append("DO" + sep + self.doi) 

874 

875 if self.lang and self.lang != "und": 

876 items.append("LA" + sep + self.lang) 

877 

878 if self.pid: 878 ↛ 880line 878 didn't jump to line 880, because the condition on line 878 was never false

879 items.append("ID" + sep + self.pid) 

880 items.append("ER" + sep) 

881 

882 def update_endnote_with_commons(self, items, container, hostname, scheme, sep): 

883 to_appear = container.to_appear() 

884 is_cr = container.is_cr() 

885 

886 if container.year != "0": 886 ↛ 888line 886 didn't jump to line 888, because the condition on line 886 was never false

887 items.append("%D" + sep + container.year) 

888 elif hasattr(self, "date_online_first") and self.date_online_first is not None: 

889 year = self.date_online_first.strftime("%Y") 

890 items.append("%D" + sep + year) 

891 

892 if not to_appear: 

893 if hasattr(self, "pages") and callable(self.pages) and self.pages(): 

894 pages = self.pages(for_bibtex=False) 

895 items.append("%P" + sep + pages) 

896 if container.volume: 

897 items.append("%V" + sep + container.volume) 

898 if container.number and not (is_cr and container.number[0] == "G"): 898 ↛ 901line 898 didn't jump to line 901, because the condition on line 898 was never false

899 items.append("%N" + sep + container.number) 

900 

901 publisher = container.my_publisher 

902 if publisher is not None: 

903 if publisher.pub_name: 903 ↛ 905line 903 didn't jump to line 905, because the condition on line 903 was never false

904 items.append("%I" + sep + publisher.pub_name) 

905 if publisher.pub_loc: 905 ↛ 908line 905 didn't jump to line 908, because the condition on line 905 was never false

906 items.append("%C" + sep + publisher.pub_loc) 

907 

908 if to_appear: 

909 items.append("%Z" + sep + "Online first") 

910 elif not is_cr and len(hostname) > 0: 

911 url = f"{scheme}://{hostname}{self.get_absolute_url()}" 

912 items.append("%U" + sep + url) 

913 

914 if self.doi: 

915 items.append("%R" + sep + self.doi) 

916 

917 if self.lang and self.lang != "und": 

918 items.append("%G" + sep + self.lang) 

919 

920 if self.pid: 920 ↛ exitline 920 didn't return from function 'update_endnote_with_commons', because the condition on line 920 was never false

921 items.append("%F" + sep + self.pid) 

922 

923 def _next_in_qs(self, qs): 

924 next_item = None 

925 

926 if qs.count() > 1: 926 ↛ 927line 926 didn't jump to line 927, because the condition on line 926 was never true

927 ready_for_next = False 

928 for item in qs: 

929 if ready_for_next: 

930 next_item = item 

931 ready_for_next = False 

932 if item.pid == self.pid: 

933 ready_for_next = True 

934 return next_item 

935 

936 def get_next_resource(self): 

937 return None 

938 

939 def get_previous_resource(self): 

940 return None 

941 

942 

943class PublisherQuerySet(models.QuerySet): 

944 def get_by_natural_key(self, pub_key, pub_name): 

945 return self.get(pub_key=pub_key) 

946 

947 

948class Publisher(Resource): 

949 """ 

950 les classes Publisher, EventSeries et Event sont un peu à part: 

951 a priori pas de pid ni sid et même pas d'identificateur du tout 

952 d'où les slugs 

953 On peut les sortir de la hiérarchie resource -- les y laisser 

954 permet de leur attache des meta suppléméntaires. Voir Provider 

955 pour la possibilité inverse :-) 

956 """ 

957 

958 pub_key = models.CharField(max_length=128, unique=True) 

959 pub_name = models.CharField(max_length=256, db_index=True) 

960 pub_loc = models.CharField(max_length=128, db_index=True) 

961 

962 # 2016-05-18: Publisher is only used by Container: ManyToOne relation 

963 # publishes = models.ManyToManyField(Resource, related_name='Publisher') 

964 

965 def __str__(self): 

966 return self.pub_key 

967 

968 @staticmethod 

969 def get_collection(): 

970 return None 

971 

972 @staticmethod 

973 def get_top_collection(): 

974 return None 

975 

976 @staticmethod 

977 def get_container(): 

978 return None 

979 

980 def natural_key(self): 

981 return ( 

982 self.pub_key, 

983 self.pub_name, 

984 ) 

985 

986 objects = PublisherQuerySet.as_manager() 

987 

988 

989class CollectionQuerySet(models.QuerySet): 

990 def order_by_date(self): 

991 return self.annotate(year=Max("content__year")).order_by("year") 

992 

993 def get_by_natural_key(self, pid, provider): 

994 return self.get(pid=pid, provider=provider) 

995 

996 

997class Collection(Resource): 

998 # journal, acta, book-series, these, lectures 

999 coltype = models.CharField(max_length=32, db_index=True) 

1000 title_sort = models.CharField(max_length=128, db_index=True) # sort key, not displayed 

1001 issn = Identifier("issn") 

1002 e_issn = Identifier("e-issn") 

1003 wall = models.IntegerField(default=5) 

1004 alive = models.BooleanField(default=True) 

1005 # First/Last year of a collection. Comes from its containers. Is typically 

1006 # updated when a container is imported. 

1007 fyear = models.IntegerField(default=0) 

1008 lyear = models.IntegerField(default=0) 

1009 last_doi = models.IntegerField(default=0) 

1010 

1011 # Ancestors means journals that existed before the creation of the Collection: time-based relation 

1012 # (the name 'ancestor' does not fit with 'parent' as 'parent' refers to a tree structure) 

1013 # We don't really create a tree. 

1014 # Ex: AFST is the root journal. AFST-0996-0481 is the original journal that became AFST-0240-2955 that became AFST. 

1015 # We create a top node (AFST) and 2 ancestors (AFST-0996-0481, AFST-0240-2955) 

1016 parent = models.ForeignKey( 

1017 "self", on_delete=models.CASCADE, null=True, blank=True, related_name="ancestors" 

1018 ) 

1019 objects = CollectionQuerySet.as_manager() 

1020 

1021 class Meta: 

1022 ordering = ["title_sort"] 

1023 

1024 # This function is used by Django: https://docs.djangoproject.com/fr/3.0/ref/models/instances/ 

1025 # Unfortunately, the name is wrong: get_absolute_url must return a relative path !?! 

1026 # => hence the get_url_absolute below that returns an absolute URL 

1027 # def get_absolute_url(self): 

1028 # if self.coltype == "thesis": 

1029 # url = reverse("pretty_thesis", args=[f'"{self.title_html}"-p']) 

1030 # elif self.coltype == "book-series": 

1031 # url = reverse("pretty-series", args=[f'"{self.title_html}"-p']) 

1032 # elif self.coltype == "lectures" or self.coltype == "lecture-notes": 

1033 # url = reverse("pretty-lectures", args=[f'"{self.title_html}"-p']) 

1034 # else: 

1035 # url = reverse(self.coltype + "-issues", kwargs={"jid": self.pid}) 

1036 # return url 

1037 

1038 def bloodline(self): 

1039 # returns parent + siblings, ordered by date (of the last volume published) 

1040 if self.parent or self.ancestors.exists(): 

1041 parent_pid = self.parent.pid if self.parent else self.pid 

1042 return Collection.objects.filter( 

1043 Q(pid=parent_pid) | Q(parent__pid=parent_pid) 

1044 ).order_by_date() 

1045 return Collection.objects.none() 

1046 

1047 def preceding_journal(self): 

1048 # returns my ancestor (timed-based) = the Collection that was published just before me 

1049 bloodline = self.bloodline() 

1050 for index, collection in enumerate(bloodline): 

1051 if collection == self and index: 

1052 return bloodline[index - 1] 

1053 return Collection.objects.none() 

1054 

1055 def get_wall(self): 

1056 return self.wall 

1057 

1058 def get_collection(self): 

1059 return self 

1060 

1061 def get_top_collection(self): 

1062 return self.parent if self.parent else self 

1063 

1064 def get_document_type(self): 

1065 """ 

1066 Is used to classify the resources in the 

1067 - (TODO) SOLR Facet Document Type 

1068 - Geodesic menu (Journals/Books/Seminars...) 

1069 """ 

1070 

1071 if self.coltype == "journal": 

1072 document_type = "Revue" 

1073 elif self.coltype == "acta": 

1074 document_type = "Séminaire" 

1075 elif self.coltype == "thesis": 

1076 document_type = "Thèse" 

1077 elif self.coltype == "lecture-notes": 

1078 document_type = "Notes de cours" 

1079 elif self.coltype == "proceeding": 

1080 self.data["classname"] = "Acte de rencontre" 

1081 document_type = "Actes de rencontres" 

1082 else: 

1083 document_type = "Livre" 

1084 

1085 return document_type 

1086 

1087 def deployed_date(self, site=None): 

1088 # return the last date of metadata deployed date for all of containers 

1089 containers = self.content.all() 

1090 date = None 

1091 if containers: 

1092 site = Site.objects.get_current() 

1093 sms = ( 

1094 SiteMembership.objects.filter(resource__in=containers) 

1095 .filter(site=site) 

1096 .order_by("-deployed") 

1097 ) 

1098 date = sms.first().deployed 

1099 return date 

1100 

1101 def get_relative_folder(self): 

1102 return resolver.get_relative_folder(self.get_top_collection().pid) 

1103 

1104 

1105class ContainerQuerySet(ResourceQuerySet): 

1106 def prefetch_all(self): 

1107 return super().prefetch_all().select_related("my_collection", "my_publisher") 

1108 

1109 def prefetch_for_toc(self): 

1110 return ( 

1111 self.prefetch_contributors() 

1112 .prefetch_work() 

1113 .prefetch_related( 

1114 "article_set__datastream_set", 

1115 "article_set__subj_set", 

1116 "article_set__extlink_set", 

1117 "article_set__resourcecount_set", 

1118 "article_set__contributions", 

1119 ) 

1120 .select_related("my_collection", "my_publisher") 

1121 ) 

1122 

1123 

1124class Container(Resource): 

1125 """ 

1126 mappe issue et book (on pourrait faire deux classes) ou une hiérarchie 

1127 container <--- issue 

1128 <--- book 

1129 """ 

1130 

1131 # issue, book-monograph, book-edited-book (multiple authors, with editors), lecture-notes 

1132 ctype = models.CharField(max_length=32, db_index=True) 

1133 year = models.CharField(max_length=32, db_index=True) 

1134 

1135 last_modified = models.DateTimeField(null=False, blank=False) 

1136 ## 

1137 # if ctype == issue 

1138 number = models.CharField(max_length=32, db_index=True) # issue number 

1139 ## 

1140 # data for relation with enclosing serial, if any 

1141 my_collection = models.ForeignKey( 

1142 Collection, null=True, related_name="content", on_delete=models.CASCADE 

1143 ) 

1144 vseries = models.CharField(max_length=32, db_index=True) 

1145 volume = models.CharField(max_length=64, db_index=True) 

1146 # la même chose pour le tri 

1147 vseries_int = models.IntegerField(default=0, db_index=True) 

1148 volume_int = models.IntegerField(default=0, db_index=True) 

1149 number_int = models.IntegerField(default=0, db_index=True) 

1150 seq = models.IntegerField(db_index=True) 

1151 with_online_first = models.BooleanField(default=False) # Used by ptf-tools only 

1152 

1153 my_publisher = models.ForeignKey( 

1154 Publisher, related_name="publishes", null=True, on_delete=models.CASCADE 

1155 ) 

1156 

1157 # Initially, a container could only be in 1 collection. 

1158 # In 2018, we extended the model so that a container can be 

1159 # in multiple collections (Bourbaki and Asterisque) 

1160 # 

1161 # This is an exception, most containers are in only 1 collection. 

1162 # my_collection is kept to store the main collection, the one used in the "how to cite" field. 

1163 # my_other_collections stores the additional collections. 

1164 # 

1165 # vseries/volume/number/seq of the main collection are kept in Container. 

1166 # As a result, there is no need to fetch the CollectionMembership to get 

1167 # these info for the main collection. 

1168 my_other_collections = models.ManyToManyField( 

1169 Collection, symmetrical=False, through="CollectionMembership" 

1170 ) 

1171 

1172 objects = ContainerQuerySet.as_manager() 

1173 

1174 class Meta: 

1175 ordering = ["seq"] 

1176 get_latest_by = ["year", "vseries_int", "volume_int", "number_int"] 

1177 

1178 def allow_crossref(self): 

1179 # we need at least a doi or an issn to allow crossref record 

1180 result = self.my_collection.doi or self.my_collection.issn or self.my_collection.e_issn 

1181 

1182 # if there are unpublished articles in the volume, we block crossref record 

1183 if self.article_set.filter( 1183 ↛ 1186line 1183 didn't jump to line 1186, because the condition on line 1183 was never true

1184 date_published__isnull=True, date_online_first__isnull=True 

1185 ).exists(): 

1186 result = False 

1187 

1188 return result 

1189 

1190 def all_doi_are_registered(self): 

1191 if self.article_set.count() > 0: 

1192 # il y a des articles, on vérifie qu'ils sont tous enregistrés 

1193 return ( 

1194 self.article_set.filter(doibatch__status="Enregistré").count() 

1195 == self.article_set.count() 

1196 ) 

1197 if self.doi and self.doibatch is not None: 

1198 return self.doibatch.status == "Enregistré" 

1199 # aucun doi associé aux articles ou au container 

1200 return False 

1201 

1202 def registered_in_doaj(self): 

1203 query = Q(date_published__isnull=True, date_online_first__isnull=True) | Q( 

1204 do_not_publish__isnull=True 

1205 ) 

1206 unpublished = self.article_set.filter(query).count() 

1207 registered = self.article_set.filter(doajbatch__status="registered").count() 

1208 all_registered = True if registered == self.article_set.count() - unpublished else False 

1209 if not all_registered and hasattr(self, "doajbatch"): 

1210 self.doajbatch.status = "unregistered" 

1211 self.doajbatch.save() 

1212 

1213 def are_all_articles_published(self): 

1214 from ptf import model_helpers 

1215 

1216 result = True 

1217 

1218 for article in self.article_set.all(): 

1219 year = article.get_year() 

1220 fyear, lyear = model_helpers.get_first_last_years(year) 

1221 try: 

1222 fyear = int(fyear) 

1223 except ValueError: 

1224 fyear = 0 

1225 

1226 if fyear > 2017: 

1227 if not article.date_published: 1227 ↛ 1228line 1227 didn't jump to line 1228, because the condition on line 1227 was never true

1228 result = False 

1229 elif fyear == 0: 

1230 result = False 

1231 

1232 return result 

1233 

1234 def get_wall(self): 

1235 return self.my_collection.get_wall() 

1236 

1237 def previous(self): 

1238 if self.get_top_collection().pid not in settings.COLLECTIONS_SEQUENCED: 

1239 return None 

1240 

1241 issue = None 

1242 qs = self.my_collection.content.filter(seq=self.seq - 1) 

1243 if qs.count() > 0: 

1244 issue = qs.first() 

1245 return issue 

1246 

1247 def next(self): 

1248 if self.get_top_collection().pid not in settings.COLLECTIONS_SEQUENCED: 

1249 return None 

1250 

1251 issue = None 

1252 qs = self.my_collection.content.filter(seq=self.seq + 1) 

1253 if qs.count() > 0: 

1254 issue = qs.first() 

1255 return issue 

1256 

1257 def get_next_resource(self): 

1258 # No Next/Previous for CRAS Special Issues or "Volume articles" 

1259 colid = self.get_top_collection().pid 

1260 if colid in settings.CRAS_COLLECTIONS: 

1261 year = int(self.year) 

1262 if ( 1262 ↛ 1267line 1262 didn't jump to line 1267

1263 self.title_html 

1264 or (colid != "CRBIOL" and year > 2020) 

1265 or (colid == "CRBIOL" and year > 2022) 

1266 ): 

1267 return None 

1268 

1269 collection = self.get_top_collection() 

1270 if collection.pid in settings.COLLECTIONS_SEQUENCED: 1270 ↛ 1271line 1270 didn't jump to line 1271, because the condition on line 1270 was never true

1271 qs = collection.content.order_by("seq") 

1272 else: 

1273 qs = collection.content.order_by("vseries_int", "year", "volume_int", "number_int") 

1274 

1275 next_issue = self._next_in_qs(qs) 

1276 return next_issue 

1277 

1278 def get_previous_resource(self): 

1279 # No Next/Previous for CRAS Special Issues or "Volume articles" 

1280 colid = self.get_top_collection().pid 

1281 if colid in settings.CRAS_COLLECTIONS: 

1282 year = int(self.year) 

1283 if ( 1283 ↛ 1288line 1283 didn't jump to line 1288

1284 self.title_html 

1285 or (colid != "CRBIOL" and year > 2020) 

1286 or (colid == "CRBIOL" and year > 2022) 

1287 ): 

1288 return None 

1289 

1290 collection = self.get_top_collection() 

1291 if collection.pid in settings.COLLECTIONS_SEQUENCED: 1291 ↛ 1292line 1291 didn't jump to line 1292, because the condition on line 1291 was never true

1292 qs = collection.content.order_by("-seq") 

1293 else: 

1294 qs = collection.content.order_by("-vseries_int", "-year", "-volume_int", "-number_int") 

1295 next_issue = self._next_in_qs(qs) 

1296 return next_issue 

1297 

1298 # TODO container in multiple collections 

1299 def get_collection(self): 

1300 return self.my_collection 

1301 

1302 def get_top_collection(self): 

1303 return self.my_collection.get_top_collection() 

1304 

1305 def get_other_collections(self): 

1306 return self.my_other_collections.all() 

1307 

1308 def get_container(self): 

1309 return self 

1310 

1311 def get_document_type(self): 

1312 """ 

1313 Is used to classify the resources in the 

1314 - (TODO) SOLR Facet Document Type 

1315 - Geodesic menu (Journals/Books/Seminars...) 

1316 """ 

1317 return self.get_top_collection().get_document_type() 

1318 

1319 def get_volume(self): 

1320 return self.volume 

1321 

1322 def get_number(self): 

1323 return self.number 

1324 

1325 def embargo(self): 

1326 return resolver.embargo(self.get_wall(), self.year) 

1327 

1328 def to_appear(self): 

1329 return self.with_online_first or ( 

1330 hasattr(settings, "ISSUE_TO_APPEAR_PID") and settings.ISSUE_TO_APPEAR_PID == self.pid 

1331 ) 

1332 

1333 def is_cr(self): 

1334 return ( 

1335 hasattr(settings, "SITE_NAME") 

1336 and len(settings.SITE_NAME) == 6 

1337 and settings.SITE_NAME[0:2] == "cr" 

1338 ) 

1339 

1340 @staticmethod 

1341 def get_base_url(): 

1342 return resolver.get_issue_base_url() 

1343 

1344 def get_relative_folder(self): 

1345 collection = self.get_top_collection() 

1346 return resolver.get_relative_folder(collection.pid, self.pid) 

1347 

1348 def get_vid(self): 

1349 """ 

1350 08/09/2022: support of Collection ancestors 

1351 The collection.pid might no longer be the top_collection.pid 

1352 => The volume URL would change if we were to keep the same vid 

1353 To keep URLs relatively similar, we use the pid of the first_issue of the volume 

1354 

1355 VolumeDetailView (ptf/views.py) handles the vid differently and no longer decrypts the vid 

1356 """ 

1357 # vid = f"{self.my_collection.pid}_{self.year}_{self.vseries}_{self.volume}" 

1358 vid = self.pid 

1359 return vid 

1360 

1361 def get_year(self): 

1362 return self.year 

1363 

1364 def is_edited_book(self): 

1365 return self.ctype == EDITED_BOOK_TYPE 

1366 

1367 def get_citation(self, request): 

1368 citation = "" 

1369 

1370 author_names = get_names(self, "author") 

1371 authors = "" 

1372 if author_names: 

1373 authors = "; ".join(author_names) 

1374 

1375 if not author_names or authors == "Collectif": 

1376 author_names = get_names(self, "editor") 

1377 if author_names: 

1378 authors = "; ".join(author_names) + " (" + str(_("éd.")) + ")" 

1379 else: 

1380 authors = "" 

1381 

1382 if authors != "Collectif": 1382 ↛ 1385line 1382 didn't jump to line 1385, because the condition on line 1382 was never false

1383 citation += authors 

1384 

1385 if citation: 

1386 if not citation.endswith("."): 1386 ↛ 1388line 1386 didn't jump to line 1388, because the condition on line 1386 was never false

1387 citation += "." 

1388 citation += " " 

1389 

1390 citation += self.title_tex + ". " 

1391 citation += self.my_collection.title_tex 

1392 

1393 if self.vseries: 

1394 citation += f", {str(_('Série'))} {self.vseries}" 

1395 

1396 if self.volume: 

1397 citation += ", " + str(volume_display()) + " " + self.volume + " (" + self.year + ") " 

1398 

1399 if self.number: 1399 ↛ 1406line 1399 didn't jump to line 1406, because the condition on line 1399 was never false

1400 citation += "no. " + self.number + ", " 

1401 elif self.number: 

1402 citation += ", no. " + self.number + " (" + self.year + "), " 

1403 else: 

1404 citation += " (" + self.year + "), " 

1405 

1406 redactors = self.get_redaktors() 

1407 if len(redactors) > 0: 

1408 redactors_str = "; ".join(get_names(self, "redaktor")) 

1409 citation += f"{redactors_str} (red.), " 

1410 

1411 for pages in self.resourcecount_set.all(): 

1412 citation += pages.value + " p." 

1413 

1414 for resourceid in self.resourceid_set.all(): 

1415 if resourceid.id_type == "doi": 1415 ↛ 1414line 1415 didn't jump to line 1414, because the condition on line 1415 was never false

1416 citation += " doi : " + resourceid.id_value + "." 

1417 

1418 citation += " " + self.get_url_absolute() 

1419 

1420 return citation 

1421 

1422 def has_detailed_info(self): 

1423 # Ignore citations here. 

1424 

1425 result = False 

1426 

1427 if self.extid_set.exists(): 1427 ↛ 1428line 1427 didn't jump to line 1428, because the condition on line 1427 was never true

1428 result = True 

1429 elif self.kwd_set.exists(): 1429 ↛ 1430line 1429 didn't jump to line 1430, because the condition on line 1429 was never true

1430 result = True 

1431 else: 

1432 for resourceid in self.resourceid_set.all(): 

1433 if resourceid.id_type != "numdam-prod-id": 1433 ↛ 1432line 1433 didn't jump to line 1432, because the condition on line 1433 was never false

1434 result = True 

1435 

1436 return result 

1437 

1438 def get_bibtex(self, request): 

1439 """ 

1440 

1441 :param self: 

1442 :return: a string encoded in latex (with latexcodec) 

1443 """ 

1444 bibtex = [] 

1445 indent = " " 

1446 

1447 collection = self.get_collection() 

1448 

1449 is_phdthesis = False 

1450 

1451 # no bibtex for an issue, only for a book (book, these) 

1452 if self.ctype == "issue": 

1453 return bibtex 

1454 

1455 if collection is not None and collection.coltype == "thesis": 1455 ↛ 1456line 1455 didn't jump to line 1456, because the condition on line 1455 was never true

1456 is_phdthesis = True 

1457 

1458 author_names = get_bibtex_names(self, "author") 

1459 editor_names = get_bibtex_names(self, "editor") 

1460 

1461 type_ = "book" 

1462 if is_phdthesis: 1462 ↛ 1463line 1462 didn't jump to line 1463, because the condition on line 1462 was never true

1463 type_ = "phdthesis" 

1464 

1465 # Numdam meeting: Use the resource pid as the bibtex id => No latex encoding to keep the '_' 

1466 id_ = self.pid 

1467 bibtex.append("@" + type_ + "{" + id_ + ",") 

1468 

1469 if author_names: 

1470 append_in_latex(bibtex, indent + author_names) 

1471 if editor_names: 

1472 append_in_latex(bibtex, indent + editor_names) 

1473 append_in_latex(bibtex, indent + "title = {" + self.title_tex + "},", is_title=True) 

1474 

1475 if collection is not None: 1475 ↛ 1478line 1475 didn't jump to line 1478, because the condition on line 1475 was never false

1476 append_in_latex(bibtex, indent + "series = {" + collection.title_tex + "},") 

1477 

1478 self.update_bibtex_with_commons(bibtex, self, request.get_host(), request.scheme, indent) 

1479 

1480 append_in_latex(bibtex, "}") 

1481 return "\n".join(bibtex) 

1482 

1483 def get_ris(self, request): 

1484 """ 

1485 

1486 :param self: 

1487 :return: a string 

1488 """ 

1489 items = [] 

1490 sep = " - " 

1491 

1492 collection = self.get_collection() 

1493 

1494 is_phdthesis = False 

1495 

1496 # no citation for an issue, only for a book (book, these) 

1497 if self.ctype == "issue": 

1498 return "" 

1499 

1500 if collection is not None and collection.coltype == "these": 1500 ↛ 1501line 1500 didn't jump to line 1501, because the condition on line 1500 was never true

1501 is_phdthesis = True 

1502 

1503 if is_phdthesis: 1503 ↛ 1504line 1503 didn't jump to line 1504, because the condition on line 1503 was never true

1504 items.append("TY" + sep + "THES") 

1505 else: 

1506 items.append("TY" + sep + "BOOK") 

1507 

1508 author_names = get_names(self, CONTRIB_TYPE_AUTHOR) 

1509 for author in author_names: 1509 ↛ 1510line 1509 didn't jump to line 1510, because the loop on line 1509 never started

1510 items.append("AU" + sep + author) 

1511 

1512 editor_names = get_names(self, CONTRIB_TYPE_EDITOR) 

1513 for editor in editor_names: 

1514 items.append("ED" + sep + editor) 

1515 

1516 items.append("TI" + sep + self.title_tex) 

1517 

1518 if collection is not None: 1518 ↛ 1521line 1518 didn't jump to line 1521, because the condition on line 1518 was never false

1519 items.append("T3" + sep + collection.title_tex) 

1520 

1521 self.update_ris_with_commons(items, self, request.get_host(), request.scheme, sep) 

1522 return "\r\n".join(items) 

1523 

1524 def get_endnote(self, request): 

1525 """ 

1526 

1527 :param self: 

1528 :return: a string 

1529 """ 

1530 items = [] 

1531 sep = " " 

1532 

1533 collection = self.get_collection() 

1534 

1535 is_phdthesis = False 

1536 

1537 # no citation for an issue, only for a book (book, these) 

1538 if self.ctype == "issue": 

1539 return "" 

1540 

1541 if collection is not None and collection.coltype == "these": 1541 ↛ 1542line 1541 didn't jump to line 1542, because the condition on line 1541 was never true

1542 is_phdthesis = True 

1543 

1544 if is_phdthesis: 1544 ↛ 1545line 1544 didn't jump to line 1545, because the condition on line 1544 was never true

1545 items.append("%0" + sep + "Thesis") 

1546 else: 

1547 items.append("%0" + sep + "Book") 

1548 

1549 author_names = get_names(self, CONTRIB_TYPE_AUTHOR) 

1550 for author in author_names: 1550 ↛ 1551line 1550 didn't jump to line 1551, because the loop on line 1550 never started

1551 items.append("%A" + sep + author) 

1552 

1553 editor_names = get_names(self, CONTRIB_TYPE_EDITOR) 

1554 for editor in editor_names: 

1555 items.append("%E" + sep + editor) 

1556 

1557 items.append("%T" + sep + self.title_tex) 

1558 

1559 if collection is not None: 1559 ↛ 1562line 1559 didn't jump to line 1562, because the condition on line 1559 was never false

1560 items.append("%S" + sep + collection.title_tex) 

1561 

1562 self.update_endnote_with_commons(items, self, request.get_host(), request.scheme, sep) 

1563 return "\r\n".join(items) 

1564 

1565 def has_articles_excluded_from_publication(self): 

1566 result = self.article_set.filter(do_not_publish=True).count() > 0 

1567 return result 

1568 

1569 

1570class EventSeries(Resource): 

1571 """to do: clé fabriquée voir _manager.make_key done""" 

1572 

1573 slug = models.CharField(max_length=128, unique=True) 

1574 event_type = models.CharField(max_length=32, db_index=True) 

1575 acro = models.CharField(max_length=32, db_index=True) 

1576 title_sort = models.CharField(max_length=128, db_index=True) 

1577 short_title = models.CharField(max_length=64, db_index=True) 

1578 

1579 

1580class Event(Resource): 

1581 """to do: clé fabriquée voir _manager.make_key done""" 

1582 

1583 slug = models.CharField(max_length=128, unique=True) 

1584 event_type = models.CharField(max_length=32, db_index=True) 

1585 title_sort = models.CharField(max_length=128, db_index=True) 

1586 string_event = models.CharField(max_length=128, db_index=True) 

1587 year = models.CharField(max_length=32, db_index=True) 

1588 acro = models.CharField(max_length=32, db_index=True) 

1589 number = models.CharField(max_length=4, db_index=True) 

1590 loc = models.CharField(max_length=64, db_index=True) 

1591 theme = models.CharField(max_length=64, db_index=True) 

1592 contrib = models.TextField() 

1593 proceedings = models.ManyToManyField(Resource, related_name="Events", symmetrical=False) 

1594 series = models.ForeignKey(EventSeries, null=True, on_delete=models.CASCADE) 

1595 

1596 class Meta: 

1597 ordering = ["year"] # seq (int) 

1598 

1599 

1600class ArticleQuerySet(ResourceQuerySet): 

1601 def order_by_published_date(self): 

1602 return self.order_by("-date_published", "-seq") 

1603 

1604 def order_by_sequence(self): 

1605 return self.order_by("seq") 

1606 

1607 def prefetch_for_toc(self): 

1608 return ( 

1609 self.prefetch_contributors() 

1610 .prefetch_work() 

1611 .select_related("my_container", "my_container__my_collection") 

1612 ) 

1613 

1614 

1615class Article(Resource): 

1616 """mappe journal article, book-part""" 

1617 

1618 atype = models.CharField(max_length=32, db_index=True) 

1619 fpage = models.CharField(max_length=32, db_index=True) 

1620 lpage = models.CharField(max_length=32, db_index=True) 

1621 page_range = models.CharField(max_length=32, db_index=True) 

1622 page_type = models.CharField(max_length=64) 

1623 elocation = models.CharField(max_length=32, db_index=True) 

1624 article_number = models.CharField(max_length=32) 

1625 talk_number = models.CharField(max_length=32) 

1626 date_received = models.DateTimeField(null=True, blank=True) 

1627 date_accepted = models.DateTimeField(null=True, blank=True) 

1628 date_revised = models.DateTimeField(null=True, blank=True) 

1629 date_online_first = models.DateTimeField(null=True, blank=True) 

1630 date_published = models.DateTimeField(null=True, blank=True) 

1631 date_pre_published = models.DateTimeField( 

1632 null=True, blank=True 

1633 ) # Used by ptf-tools only to measure delays 

1634 coi_statement = models.TextField(null=True, blank=True) # Conflict of interest 

1635 show_body = models.BooleanField( 

1636 default=True 

1637 ) # Used by ptf-tools only (to export or not the body) 

1638 do_not_publish = models.BooleanField( 

1639 default=False 

1640 ) # Used by ptf-tools only (to export or not the article) 

1641 

1642 ## 

1643 # container 

1644 my_container = models.ForeignKey(Container, null=True, on_delete=models.CASCADE) 

1645 seq = models.IntegerField() 

1646 ## 

1647 # parent part 

1648 parent = models.ForeignKey( 

1649 "self", null=True, related_name="children", on_delete=models.CASCADE 

1650 ) 

1651 pseq = models.IntegerField() 

1652 objects = ArticleQuerySet.as_manager() 

1653 

1654 class Meta: 

1655 ordering = ["seq", "fpage"] 

1656 

1657 def __str__(self): 

1658 return self.pid 

1659 

1660 @staticmethod 

1661 def get_base_url(): 

1662 return resolver.get_article_base_url() 

1663 

1664 def get_absolute_url(self): 

1665 if self.doi is not None: 

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

1667 else: 

1668 return reverse("item_id", kwargs={"pid": self.pid}) 

1669 

1670 def get_relative_folder(self): 

1671 collection = self.get_top_collection() 

1672 return resolver.get_relative_folder(collection.pid, self.my_container.pid, self.pid) 

1673 

1674 def embargo(self): 

1675 if self.my_container is None: 1675 ↛ 1676line 1675 didn't jump to line 1676, because the condition on line 1675 was never true

1676 return False 

1677 

1678 return self.my_container.embargo() 

1679 

1680 def get_wall(self): 

1681 if self.my_container is None: 1681 ↛ 1682line 1681 didn't jump to line 1682, because the condition on line 1681 was never true

1682 return 0 

1683 return self.my_container.get_wall() 

1684 

1685 def get_collection(self): 

1686 return self.my_container.get_collection() 

1687 

1688 def get_top_collection(self): 

1689 return self.my_container.get_top_collection() 

1690 

1691 def get_container(self): 

1692 return self.my_container 

1693 

1694 def get_document_type(self): 

1695 """ 

1696 Is used to classify the resources in the 

1697 - (TODO) SOLR Facet Document Type 

1698 - Geodesic menu (Journals/Books/Seminars...) 

1699 """ 

1700 

1701 collection = self.get_top_collection() 

1702 if collection.coltype == "journal": 

1703 document_type = "Article de revue" 

1704 elif collection.coltype == "acta": 

1705 document_type = "Acte de séminaire" 

1706 elif collection.coltype == "thesis": 

1707 document_type = "Thèse" 

1708 elif collection.coltype == "lecture-notes": 

1709 document_type = "Notes de cours" 

1710 elif collection.coltype == "proceeding": 

1711 document_type = "Acte de rencontre" 

1712 else: 

1713 document_type = "Chapitre de livre" 

1714 

1715 return document_type 

1716 

1717 def get_volume(self): 

1718 return self.my_container.get_volume() 

1719 

1720 def get_number(self): 

1721 return self.my_container.get_number() 

1722 

1723 def get_page_count(self): 

1724 page_count = None 

1725 for resourcecount in self.resourcecount_set.all(): 

1726 if resourcecount.name == "page-count": 1726 ↛ 1725line 1726 didn't jump to line 1725, because the condition on line 1726 was never false

1727 page_count = resourcecount.value 

1728 

1729 return page_count 

1730 

1731 def get_article_page_count(self): 

1732 try: 

1733 page_count = self.get_page_count() or 0 

1734 page_count = int(page_count) 

1735 except ValueError: 

1736 page_count = 0 

1737 

1738 lpage = fpage = 0 

1739 try: 

1740 fpage = int(self.fpage) 

1741 except ValueError: 

1742 pass 

1743 try: 

1744 lpage = int(self.lpage) 

1745 except ValueError: 

1746 pass 

1747 if lpage > 0 and fpage > 0: 

1748 page_count = lpage - fpage + 1 

1749 return page_count 

1750 

1751 def pages(self, for_bibtex=False): 

1752 """ 

1753 Returns a string with the article pages. 

1754 It is used for the different exports (BibTeX, EndNote, RIS) 

1755 

1756 typically "{fpage}-{lpage}" 

1757 For BibTex, 2 '-' are used, ie "{fpage}--{lpage}" 

1758 

1759 Some articles have a page_range. Use this field instead of the fpage/lpage in this case. 

1760 """ 

1761 if self.page_range: 

1762 return self.page_range 

1763 if self.fpage or self.lpage: 

1764 if self.lpage: 

1765 return ( 

1766 f"{self.fpage}--{self.lpage}" if for_bibtex else f"{self.fpage}-{self.lpage}" 

1767 ) 

1768 else: 

1769 return self.fpage 

1770 return None 

1771 

1772 def volume_series(self): 

1773 # Use only for latex 

1774 if not self.my_container.vseries: 

1775 return "" 

1776 if self.lang == "fr": 

1777 return self.my_container.vseries + "e s{\\'e}rie, " 

1778 return f"Ser. {self.my_container.vseries}, " 

1779 

1780 def volume_string(self): 

1781 return self.volume_series() + self.my_container.volume 

1782 

1783 def get_page_text(self, use_pp=False): 

1784 if (self.talk_number or self.article_number) and self.get_page_count(): 

1785 return self.get_page_count() + " p." 

1786 

1787 if not (self.lpage or self.fpage): 

1788 return "" 

1789 

1790 if self.lpage == self.fpage: 

1791 return f"p. {self.lpage}" 

1792 

1793 text = "" 

1794 if not self.page_range: 

1795 if self.fpage and self.lpage and use_pp: 

1796 text = "pp. " 

1797 else: 

1798 text = "p. " 

1799 text += self.fpage 

1800 if self.fpage and self.lpage: 

1801 text += "-" 

1802 if self.lpage: 

1803 text += self.lpage 

1804 elif self.page_range[0] != "p": 1804 ↛ 1807line 1804 didn't jump to line 1807, because the condition on line 1804 was never false

1805 text = "p. " + self.page_range 

1806 

1807 return text 

1808 

1809 def get_summary_page_text(self): 

1810 text = "" 

1811 if self.talk_number: 

1812 text += str(_("Exposé")) + " no. " + str(self.talk_number) + ", " 

1813 if self.article_number: 

1814 text += "article no. " + str(self.article_number) + ", " 

1815 

1816 text += self.get_page_text() 

1817 

1818 return text 

1819 

1820 def get_breadcrumb_page_text(self): 

1821 text = "" 

1822 if self.my_container.with_online_first: 

1823 if self.doi is not None: 

1824 text = self.doi 

1825 else: 

1826 text = str(_("Première publication")) 

1827 elif self.talk_number: 

1828 text += str(_("Exposé")) + " no. " + str(self.talk_number) 

1829 elif self.article_number: 

1830 text += "article no. " + str(self.article_number) 

1831 else: 

1832 text += self.get_page_text() 

1833 

1834 return text 

1835 

1836 def get_year(self): 

1837 return self.my_container.year 

1838 

1839 def previous(self): 

1840 try: 

1841 return self.my_container.article_set.get(seq=(self.seq - 1)) 

1842 except (Article.DoesNotExist, MultipleObjectsReturned): 

1843 return None 

1844 

1845 def next(self): 

1846 try: 

1847 return self.my_container.article_set.get(seq=(self.seq + 1)) 

1848 except (Article.DoesNotExist, MultipleObjectsReturned): 

1849 return None 

1850 

1851 def get_next_resource(self): 

1852 # TODO: rename the next function defined just above by this function 

1853 next_article = None 

1854 

1855 try: 

1856 next_article = self.my_container.article_set.get(seq=(self.seq + 1)) 

1857 except (Article.DoesNotExist, MultipleObjectsReturned): 

1858 pass 

1859 

1860 if next_article is None and not self.my_container.title_html and self.my_container.volume: 1860 ↛ 1861line 1860 didn't jump to line 1861, because the condition on line 1860 was never true

1861 qs = Container.objects.filter(volume=self.my_container.volume) 

1862 if qs.count() > 1: 

1863 qs = qs.order_by("number_int") 

1864 next_issue = self._next_in_qs(qs) 

1865 if next_issue: 

1866 qs = next_issue.article_set.order_by("seq") 

1867 if qs.exists(): 

1868 next_article = qs.first() 

1869 

1870 return next_article 

1871 

1872 def get_previous_resource(self): 

1873 previous_article = None 

1874 

1875 try: 

1876 previous_article = self.my_container.article_set.get(seq=(self.seq - 1)) 

1877 except (Article.DoesNotExist, MultipleObjectsReturned): 

1878 pass 

1879 

1880 if ( 1880 ↛ 1885line 1880 didn't jump to line 1885

1881 previous_article is None 

1882 and not self.my_container.title_html 

1883 and self.my_container.volume 

1884 ): 

1885 qs = Container.objects.filter(volume=self.my_container.volume) 

1886 if qs.count() > 1: 

1887 qs = qs.order_by("-number_int") 

1888 previous_issue = self._next_in_qs(qs) 

1889 if previous_issue: 

1890 qs = previous_issue.article_set.order_by("-seq") 

1891 if qs.exists(): 

1892 previous_article = qs.first() 

1893 

1894 return previous_article 

1895 

1896 def get_citation_base(self, year=None, page_text=None): 

1897 citation = "" 

1898 if year is None: 1898 ↛ 1901line 1898 didn't jump to line 1901, because the condition on line 1898 was never false

1899 year = self.my_container.year 

1900 

1901 to_appear = self.my_container.to_appear() 

1902 is_cr = self.my_container.is_cr() 

1903 

1904 if to_appear and year != "0": 

1905 citation += f", Online first ({year})" 

1906 elif to_appear: 

1907 citation += ", Online first" 

1908 else: 

1909 if self.my_container.vseries: 

1910 citation += f", {str(_('Série'))} {self.my_container.vseries}" 

1911 

1912 if self.my_container.volume: 

1913 citation += f", {str(volume_display())} {self.my_container.volume} ({year})" 

1914 

1915 if self.my_container.number and not (is_cr and self.my_container.number[0] == "G"): 1915 ↛ 1922line 1915 didn't jump to line 1922, because the condition on line 1915 was never false

1916 citation += f" no. {self.my_container.number}" 

1917 elif self.my_container.number: 1917 ↛ 1920line 1917 didn't jump to line 1920, because the condition on line 1917 was never false

1918 citation += f", no. {self.my_container.number} ({year})" 

1919 else: 

1920 citation += f" ({year})" 

1921 

1922 if self.talk_number: 1922 ↛ 1923line 1922 didn't jump to line 1923, because the condition on line 1922 was never true

1923 citation += f", {str(_('Exposé'))} no. {str(self.talk_number)}" 

1924 if self.article_number: 1924 ↛ 1925line 1924 didn't jump to line 1925, because the condition on line 1924 was never true

1925 citation += f", article no. {str(self.article_number)}" 

1926 

1927 if page_text is None: 1927 ↛ 1929line 1927 didn't jump to line 1929, because the condition on line 1927 was never false

1928 page_text = self.get_page_text(True) 

1929 if len(page_text) > 0: 

1930 citation += f", {page_text}" 

1931 

1932 if citation[-1] != ".": 

1933 citation += "." 

1934 

1935 if not to_appear: 

1936 if self.page_type == "volant": 

1937 citation += f" ({str(_('Pages volantes'))})" 

1938 elif self.page_type == "supplement": 

1939 citation += f" ({str(_('Pages supplémentaires'))})" 

1940 elif self.page_type == "preliminaire": 

1941 citation += f" ({str(_('Pages préliminaires'))})" 

1942 elif self.page_type == "special": 

1943 citation += f" ({str(_('Pages spéciales'))})" 

1944 

1945 return citation 

1946 

1947 def get_citation(self, request=None, with_formatting=False): 

1948 citation = "" 

1949 

1950 author_names = get_names(self, "author") 

1951 if author_names: 

1952 authors = "; ".join(author_names) 

1953 else: 

1954 author_names = get_names(self, "editor") 

1955 if author_names: 

1956 authors = "; ".join(author_names) + " (" + str(_("éd.")) + ")" 

1957 else: 

1958 authors = "" 

1959 

1960 if authors != "Collectif": 1960 ↛ 1963line 1960 didn't jump to line 1963, because the condition on line 1960 was never false

1961 citation += authors 

1962 

1963 if citation: 

1964 if not citation.endswith("."): 1964 ↛ 1966line 1964 didn't jump to line 1966, because the condition on line 1964 was never false

1965 citation += "." 

1966 citation += " " 

1967 

1968 if with_formatting: 

1969 citation += f"<strong>{self.title_tex}</strong>" 

1970 else: 

1971 citation += self.title_tex 

1972 if self.my_container.ctype != "issue": 

1973 citation += ", " 

1974 citation += str(_("dans")) + " <em>" + self.my_container.title_tex + "</em>, " 

1975 else: 

1976 citation += ". " 

1977 citation += self.my_container.my_collection.title_tex 

1978 

1979 citation += self.get_citation_base() 

1980 

1981 to_appear = self.my_container.to_appear() 

1982 is_cr = self.my_container.is_cr() 

1983 

1984 if not to_appear or is_cr: 1984 ↛ 1992line 1984 didn't jump to line 1992, because the condition on line 1984 was never false

1985 if self.doi is not None: 1985 ↛ 1988line 1985 didn't jump to line 1988, because the condition on line 1985 was never false

1986 citation += " doi : " + self.doi + "." 

1987 

1988 if not to_appear and request is not None: 

1989 url = f"{request.scheme}://{request.get_host()}{self.get_absolute_url()}" 

1990 citation += " " + url 

1991 

1992 return citation 

1993 

1994 def has_detailed_info(self): 

1995 # Ignore citations here. 

1996 

1997 result = False 

1998 

1999 if ( 

2000 self.date_received 

2001 or self.date_revised 

2002 or self.date_accepted 

2003 or self.date_published 

2004 or self.date_online_first 

2005 ): 

2006 result = True 

2007 elif self.extid_set.exists(): 

2008 result = True 

2009 elif self.kwd_set.exists(): 

2010 result = True 

2011 elif self.doi is not None: 2011 ↛ 2012line 2011 didn't jump to line 2012, because the condition on line 2011 was never true

2012 result = True 

2013 else: 

2014 for resourceid in self.resourceid_set.all(): 

2015 if resourceid.id_type != "numdam-prod-id": 2015 ↛ 2014line 2015 didn't jump to line 2014, because the condition on line 2015 was never false

2016 result = True 

2017 

2018 return result 

2019 

2020 def get_ojs_id(self): 

2021 ojs_id = "" 

2022 qs = self.resourceid_set.filter(id_type="ojs-id") 

2023 if qs.count() > 0: 

2024 resourceid = qs.first() 

2025 ojs_id = resourceid.id_value 

2026 pos = ojs_id.find("$$") 

2027 if pos > 0: 2027 ↛ 2028line 2027 didn't jump to line 2028, because the condition on line 2027 was never true

2028 ojs_id = ojs_id[0:pos] 

2029 return ojs_id 

2030 

2031 def update_bibtex_with_book_contributors( 

2032 self, bibtex, collection, container, indent, author_names, editor_names 

2033 ): 

2034 if container is not None: 2034 ↛ 2046line 2034 didn't jump to line 2046, because the condition on line 2034 was never false

2035 append_in_latex(bibtex, indent + "booktitle = {" + container.title_tex + "},") 

2036 

2037 # @incollection (edited-books): add the book editors 

2038 book_author_names = get_bibtex_names(container, "author") 

2039 book_editor_names = get_bibtex_names(container, "editor") 

2040 

2041 if not author_names and book_author_names: 2041 ↛ 2042line 2041 didn't jump to line 2042, because the condition on line 2041 was never true

2042 append_in_latex(bibtex, indent + book_author_names) 

2043 if not editor_names and book_editor_names: 2043 ↛ 2046line 2043 didn't jump to line 2046, because the condition on line 2043 was never false

2044 append_in_latex(bibtex, indent + book_editor_names) 

2045 

2046 if collection is not None: 2046 ↛ exitline 2046 didn't return from function 'update_bibtex_with_book_contributors', because the condition on line 2046 was never false

2047 append_in_latex(bibtex, indent + "series = {" + collection.title_tex + "},") 

2048 

2049 def get_bibtex(self, request=None, is_title=True): 

2050 """ 

2051 

2052 :param self: 

2053 :return: string encoded in latex (with latexcodec) 

2054 """ 

2055 bibtex = [] 

2056 indent = " " 

2057 

2058 container = self.my_container 

2059 collection = self.get_collection() 

2060 

2061 is_article = True 

2062 is_incollection = False 

2063 is_inbook = False 

2064 is_phdthesis = False 

2065 

2066 if container is not None and container.ctype != "issue": 

2067 is_article = False 

2068 

2069 if collection is not None and collection.coltype == "these": 2069 ↛ 2070line 2069 didn't jump to line 2070, because the condition on line 2069 was never true

2070 is_phdthesis = True 

2071 elif container.ctype == "book-monograph": 2071 ↛ 2072line 2071 didn't jump to line 2072, because the condition on line 2071 was never true

2072 is_inbook = True 

2073 elif container.ctype in ["book-edited-book", "lecture-notes"]: 2073 ↛ 2078line 2073 didn't jump to line 2078, because the condition on line 2073 was never false

2074 is_incollection = True 

2075 elif collection.coltype == "proceeding": 2075 ↛ 2076line 2075 didn't jump to line 2076, because the condition on line 2075 was never true

2076 is_incollection = True 

2077 

2078 to_appear = container.to_appear() 

2079 is_cr = container.is_cr() 

2080 

2081 # No bibtex at the article level for a these 

2082 if is_phdthesis: 2082 ↛ 2083line 2082 didn't jump to line 2083, because the condition on line 2082 was never true

2083 return "" 

2084 

2085 author_names = get_bibtex_names(self, "author") 

2086 editor_names = get_bibtex_names(self, "editor") 

2087 

2088 type_ = "article" 

2089 if to_appear and not is_cr: 2089 ↛ 2090line 2089 didn't jump to line 2090, because the condition on line 2089 was never true

2090 type_ = "unpublished" 

2091 elif is_inbook: 2091 ↛ 2092line 2091 didn't jump to line 2092, because the condition on line 2091 was never true

2092 type_ = "inbook" 

2093 elif is_incollection: 

2094 type_ = "incollection" 

2095 elif len(author_names) == 0 and len(editor_names) == 0: 

2096 type_ = "misc" 

2097 

2098 # Numdam meeting: Use the resource pid as the bibtex id => No latex encoding to keep the '_' 

2099 bibtex.append("@" + type_ + "{" + self.pid + ",") 

2100 

2101 if author_names: 

2102 append_in_latex(bibtex, indent + author_names) 

2103 if editor_names: 

2104 append_in_latex(bibtex, indent + editor_names) 

2105 

2106 title = xml_utils.normalise_span(self.title_tex) 

2107 append_in_latex(bibtex, indent + "title = {" + title + "},", is_title=is_title) 

2108 

2109 if is_article and not is_incollection: 

2110 title = xml_utils.normalise_span(collection.title_tex) 

2111 append_in_latex(bibtex, indent + "journal = {" + title + "},") 

2112 elif is_article: 2112 ↛ 2113line 2112 didn't jump to line 2113, because the condition on line 2112 was never true

2113 title = xml_utils.normalise_span(container.title_tex) 

2114 append_in_latex(bibtex, indent + "booktitle = {" + title + "},") 

2115 title = xml_utils.normalise_span(collection.title_tex) 

2116 append_in_latex(bibtex, indent + "series = {" + title + "},") 

2117 else: 

2118 self.update_bibtex_with_book_contributors( 

2119 bibtex, collection, container, indent, author_names, editor_names 

2120 ) 

2121 

2122 if not to_appear: 

2123 if self.talk_number: 

2124 append_in_latex(bibtex, indent + "note = {talk:" + self.talk_number + "},") 

2125 if self.article_number: 2125 ↛ 2126line 2125 didn't jump to line 2126, because the condition on line 2125 was never true

2126 append_in_latex(bibtex, indent + "eid = {" + self.article_number + "},") 

2127 if self.pages(): 

2128 append_in_latex(bibtex, indent + "pages = {" + self.pages(for_bibtex=True) + "},") 

2129 

2130 hostname = request.get_host() if request is not None else "" 

2131 scheme = request.scheme if request is not None else "https" 

2132 self.update_bibtex_with_commons(bibtex, container, hostname, scheme, indent) 

2133 

2134 append_in_latex(bibtex, "}") 

2135 

2136 return "\n".join(bibtex) 

2137 

2138 def get_ris(self, request=None): 

2139 """ 

2140 

2141 :param self: 

2142 :return: string 

2143 """ 

2144 items = [] 

2145 sep = " - " 

2146 

2147 container = self.my_container 

2148 collection = self.get_collection() 

2149 

2150 is_article = True 

2151 is_incollection = False 

2152 is_inbook = False 

2153 is_phdthesis = False 

2154 

2155 if container is not None and container.ctype != "issue": 

2156 is_article = False 

2157 

2158 if collection is not None and collection.coltype == "these": 2158 ↛ 2159line 2158 didn't jump to line 2159, because the condition on line 2158 was never true

2159 is_phdthesis = True 

2160 elif container.ctype == "book-monograph": 2160 ↛ 2161line 2160 didn't jump to line 2161, because the condition on line 2160 was never true

2161 is_inbook = True 

2162 elif container.ctype == "book-edited-book": 2162 ↛ 2164line 2162 didn't jump to line 2164, because the condition on line 2162 was never false

2163 is_incollection = True 

2164 elif container.ctype == "lecture-notes": 

2165 is_incollection = True 

2166 

2167 to_appear = container.to_appear() 

2168 is_cr = container.is_cr() 

2169 

2170 # no citation at the article level for a these 

2171 if is_phdthesis: 2171 ↛ 2172line 2171 didn't jump to line 2172, because the condition on line 2171 was never true

2172 return "" 

2173 

2174 type_ = "JOUR" # "article" 

2175 if to_appear and not is_cr: 2175 ↛ 2176line 2175 didn't jump to line 2176, because the condition on line 2175 was never true

2176 type_ = "UNPB" # "unpublished" 

2177 elif is_inbook: 2177 ↛ 2178line 2177 didn't jump to line 2178, because the condition on line 2177 was never true

2178 type_ = "CHAP" 

2179 elif is_incollection: 

2180 type_ = "CHAP" 

2181 items.append("TY" + sep + type_) 

2182 

2183 author_names = get_names(self, CONTRIB_TYPE_AUTHOR) 

2184 for author in author_names: 

2185 items.append("AU" + sep + author) 

2186 

2187 editor_names = get_names(self, CONTRIB_TYPE_EDITOR) 

2188 for editor in editor_names: 

2189 items.append("ED" + sep + editor) 

2190 

2191 title = xml_utils.remove_html(self.title_tex) 

2192 items.append("TI" + sep + title) 

2193 

2194 collection_title = xml_utils.remove_html(collection.title_tex) 

2195 if collection is not None and is_article: 

2196 items.append("JO" + sep + collection_title) 

2197 else: 

2198 if container is not None: 2198 ↛ 2207line 2198 didn't jump to line 2207, because the condition on line 2198 was never false

2199 items.append("BT" + sep + container.title_tex) 

2200 author_names = get_names(container, CONTRIB_TYPE_AUTHOR) 

2201 for author in author_names: 2201 ↛ 2202line 2201 didn't jump to line 2202, because the loop on line 2201 never started

2202 items.append("AU" + sep + author) 

2203 

2204 editor_names = get_names(container, CONTRIB_TYPE_EDITOR) 

2205 for editor in editor_names: 

2206 items.append("ED" + sep + editor) 

2207 if collection is not None: 2207 ↛ 2210line 2207 didn't jump to line 2210, because the condition on line 2207 was never false

2208 items.append("T3" + sep + collection_title) 

2209 

2210 if not to_appear: 

2211 if self.talk_number: 2211 ↛ 2212line 2211 didn't jump to line 2212, because the condition on line 2211 was never true

2212 items.append("N1" + sep + "talk:" + self.talk_number) 

2213 # if self.article_number: 

2214 # items.append("M1" + sep + "eid = " + self.article_number) 

2215 

2216 hostname = request.get_host() if request is not None else "" 

2217 scheme = request.scheme if request is not None else "https" 

2218 self.update_ris_with_commons(items, container, hostname, scheme, sep) 

2219 return "\r\n".join(items) 

2220 

2221 def get_endnote(self, request=None): 

2222 """ 

2223 

2224 :param self: 

2225 :return: string 

2226 """ 

2227 items = [] 

2228 sep = " " 

2229 

2230 container = self.my_container 

2231 collection = self.get_collection() 

2232 

2233 is_article = True 

2234 is_incollection = False 

2235 is_inbook = False 

2236 is_phdthesis = False 

2237 

2238 if container is not None and container.ctype != "issue": 

2239 is_article = False 

2240 

2241 if collection is not None and collection.coltype == "these": 2241 ↛ 2242line 2241 didn't jump to line 2242, because the condition on line 2241 was never true

2242 is_phdthesis = True 

2243 elif container.ctype == "book-monograph": 2243 ↛ 2244line 2243 didn't jump to line 2244, because the condition on line 2243 was never true

2244 is_inbook = True 

2245 elif container.ctype == "book-edited-book": 2245 ↛ 2247line 2245 didn't jump to line 2247, because the condition on line 2245 was never false

2246 is_incollection = True 

2247 elif container.ctype == "lecture-notes": 

2248 is_incollection = True 

2249 

2250 to_appear = container.to_appear() 

2251 is_cr = container.is_cr() 

2252 

2253 # no citation at the article level for a these 

2254 if is_phdthesis: 2254 ↛ 2255line 2254 didn't jump to line 2255, because the condition on line 2254 was never true

2255 return "" 

2256 

2257 type_ = "Journal Article" # "article" 

2258 if to_appear and not is_cr: 2258 ↛ 2259line 2258 didn't jump to line 2259, because the condition on line 2258 was never true

2259 type_ = "Unpublished Work" # "unpublished" 

2260 elif is_inbook: 2260 ↛ 2261line 2260 didn't jump to line 2261, because the condition on line 2260 was never true

2261 type_ = "Book Section" 

2262 elif is_incollection: 

2263 type_ = "Book Section" 

2264 items.append("%0" + sep + type_) 

2265 

2266 author_names = get_names(self, CONTRIB_TYPE_AUTHOR) 

2267 for author in author_names: 

2268 items.append("%A" + sep + author) 

2269 

2270 editor_names = get_names(self, CONTRIB_TYPE_EDITOR) 

2271 for editor in editor_names: 

2272 items.append("%E" + sep + editor) 

2273 

2274 title = xml_utils.remove_html(self.title_tex) 

2275 items.append("%T" + sep + title) 

2276 

2277 collection_title = xml_utils.remove_html(collection.title_tex) 

2278 if collection is not None and is_article: 

2279 items.append("%J" + sep + collection_title) 

2280 else: 

2281 if container is not None: 2281 ↛ 2290line 2281 didn't jump to line 2290, because the condition on line 2281 was never false

2282 items.append("%B" + sep + container.title_tex) 

2283 author_names = get_names(container, CONTRIB_TYPE_AUTHOR) 

2284 for author in author_names: 2284 ↛ 2285line 2284 didn't jump to line 2285, because the loop on line 2284 never started

2285 items.append("%A" + sep + author) 

2286 

2287 editor_names = get_names(container, CONTRIB_TYPE_EDITOR) 

2288 for editor in editor_names: 

2289 items.append("%E" + sep + editor) 

2290 if collection is not None: 2290 ↛ 2293line 2290 didn't jump to line 2293, because the condition on line 2290 was never false

2291 items.append("%S" + sep + collection_title) 

2292 

2293 if not to_appear: 

2294 if self.talk_number: 2294 ↛ 2295line 2294 didn't jump to line 2295, because the condition on line 2294 was never true

2295 items.append("%Z" + sep + "talk:" + self.talk_number) 

2296 # if self.article_number: 

2297 # items.append("%1" + sep + "eid = " + self.article_number) 

2298 

2299 hostname = request.get_host() if request is not None else "" 

2300 scheme = request.scheme if request is not None else "https" 

2301 self.update_endnote_with_commons(items, container, hostname, scheme, sep) 

2302 return "\r\n".join(items) 

2303 

2304 def get_conference(self): 

2305 text = "" 

2306 

2307 subjs = [] 

2308 for subj in self.subj_set.all(): 

2309 if subj.type == "conference": 

2310 subjs.append(subj.value) 

2311 

2312 text = ", ".join(subjs) 

2313 

2314 return text 

2315 

2316 def get_topics(self): 

2317 text = "" 

2318 

2319 subjs = [] 

2320 for subj in self.subj_set.all(): 

2321 if subj.type == "topic": 

2322 subjs.append(subj.value) 

2323 

2324 text = ", ".join(subjs) 

2325 

2326 return text 

2327 

2328 def get_subj_text(self): 

2329 text = "" 

2330 lang = get_language() 

2331 

2332 subj_types = ["subject", "type", "pci", "heading", "conference"] 

2333 if self.my_container.my_collection.pid == "CRMATH": 

2334 subj_types = ["subject", "heading"] 

2335 

2336 subj_groups = self.get_subjs_by_type_and_lang() 

2337 for type_ in subj_types: 

2338 if type_ in subj_groups: 

2339 sg_type_langs = subj_groups[type_] 

2340 if type_ == "pci": 

2341 subtext = ", ".join( 

2342 list( 

2343 [ 

2344 resolver.get_pci(subj.value) 

2345 for lang_ in sg_type_langs 

2346 for subj in sg_type_langs[lang_] 

2347 ] 

2348 ) 

2349 ) 

2350 else: 

2351 if lang in sg_type_langs: 

2352 subtext = ", ".join( 

2353 list([subj.value for subj in subj_groups[type_][lang]]) 

2354 ) 

2355 else: 

2356 subtext = ", ".join( 

2357 list( 

2358 [ 

2359 subj.value 

2360 for lang_ in sg_type_langs 

2361 if lang_ != lang 

2362 for subj in sg_type_langs[lang_] 

2363 ] 

2364 ) 

2365 ) 

2366 

2367 if text: 

2368 text += " - " 

2369 text += subtext 

2370 

2371 return text 

2372 

2373 def get_pci_section(self): 

2374 pci = "" 

2375 for subj in self.subj_set.all(): 

2376 if subj.type == "pci": 

2377 pci = subj.value 

2378 

2379 return pci 

2380 

2381 def get_pci_value(self): 

2382 return resolver.get_pci(self.get_pci_section()) 

2383 

2384 def is_uga_pci(self): 

2385 return self.get_pci_section() in resolver.PCJ_UGA_SECTION 

2386 

2387 def allow_crossref(self): 

2388 # we need at least a doi or an issn to allow crossref record 

2389 doi = self.my_container.my_collection.doi 

2390 issn = self.my_container.my_collection.issn 

2391 e_issn = self.my_container.my_collection.e_issn 

2392 result = bool(doi) or bool(issn) or bool(e_issn) 

2393 

2394 # and we need a published date (online_first or final) 

2395 result = result and (self.date_published is not None or self.date_online_first is not None) 

2396 

2397 return result 

2398 

2399 def get_illustrations(self): 

2400 return GraphicalAbstract.objects.filter(resource=self).first() 

2401 

2402 def has_graphical_abstract(self): 

2403 collections = ["CRCHIM"] 

2404 return True if self.my_container.my_collection.pid in collections else False 

2405 

2406 

2407class ResourceCategory(models.Model): 

2408 """non utilisé""" 

2409 

2410 category = models.CharField(max_length=32, db_index=True) 

2411 

2412 

2413class Provider(models.Model): 

2414 """ 

2415 en faire une resource permettrait d'attacher des metadonnées 

2416 supplémentaires -- à voir 

2417 """ 

2418 

2419 name = models.CharField(max_length=32, unique=True) 

2420 pid_type = models.CharField(max_length=32, unique=True) 

2421 sid_type = models.CharField(max_length=64, unique=True, null=True, blank=True) 

2422 

2423 def __str__(self): 

2424 return self.name 

2425 

2426 

2427class SiteMembership(models.Model): 

2428 """ 

2429 Warning: As of July 3018, only 1 site id is stored in a SolR document 

2430 Although the SolR schema is already OK to store multiple sites ("sites" is an array) 

2431 no Solr commands have been written to add/remove sites 

2432 We only have add commands. 

2433 Search only works if the Solr instance is meant for individual or ALL sites 

2434 """ 

2435 

2436 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2437 deployed = models.DateTimeField(null=True) 

2438 online = models.DateTimeField(db_index=True, null=True) # pour cedram 

2439 site = models.ForeignKey(PtfSite, on_delete=models.CASCADE) 

2440 

2441 class Meta: 

2442 unique_together = ( 

2443 "resource", 

2444 "site", 

2445 ) 

2446 

2447 

2448class CollectionMembership(models.Model): 

2449 collection = models.ForeignKey(Collection, on_delete=models.CASCADE) 

2450 container = models.ForeignKey(Container, on_delete=models.CASCADE) 

2451 

2452 vseries = models.CharField(max_length=32, db_index=True) 

2453 volume = models.CharField(max_length=64, db_index=True) 

2454 number = models.CharField(max_length=32, db_index=True) # issue number 

2455 # la même chose pour le tri 

2456 vseries_int = models.IntegerField(default=0, db_index=True) 

2457 volume_int = models.IntegerField(default=0, db_index=True) 

2458 number_int = models.IntegerField(default=0, db_index=True) 

2459 

2460 seq = models.IntegerField(db_index=True) 

2461 

2462 class Meta: 

2463 unique_together = ( 

2464 "collection", 

2465 "container", 

2466 ) 

2467 

2468 def __str__(self): 

2469 return f"{self.collection} - {self.container}" 

2470 

2471 

2472class RelationName(models.Model): 

2473 """ 

2474 Triple store ;-) 

2475 """ 

2476 

2477 left = models.CharField(max_length=32, unique=True) 

2478 right = models.CharField(max_length=32, unique=True) 

2479 gauche = models.CharField(max_length=64, unique=True) 

2480 droite = models.CharField(max_length=64, unique=True) 

2481 

2482 

2483class Relationship(models.Model): 

2484 resource = models.ForeignKey( 

2485 Resource, null=True, related_name="subject_of", on_delete=models.CASCADE 

2486 ) 

2487 related = models.ForeignKey( 

2488 Resource, null=True, related_name="object_of", on_delete=models.CASCADE 

2489 ) 

2490 subject_pid = models.CharField(max_length=64, db_index=True) 

2491 object_pid = models.CharField(max_length=64, db_index=True) 

2492 rel_info = models.ForeignKey(RelationName, null=True, on_delete=models.CASCADE) 

2493 

2494 

2495class ExtRelationship(models.Model): 

2496 """ 

2497 Triple store (resources externes) 

2498 """ 

2499 

2500 resource = models.ForeignKey(Resource, related_name="subject", on_delete=models.CASCADE) 

2501 ext_object = models.CharField(max_length=256) # uri 

2502 predicate = models.CharField(max_length=256, db_index=True) # predicate uri 

2503 

2504 

2505class XmlBase(models.Model): 

2506 base = models.CharField(max_length=200, unique=True) 

2507 

2508 def __str__(self): 

2509 return self.base 

2510 

2511 

2512class DataStream(models.Model): 

2513 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2514 rel = models.CharField(max_length=32) 

2515 mimetype = models.CharField(max_length=32) 

2516 base = models.ForeignKey(XmlBase, blank=True, null=True, on_delete=models.CASCADE) 

2517 location = models.URLField() 

2518 text = models.CharField(max_length=32, default="Link") 

2519 seq = models.IntegerField(db_index=True) 

2520 

2521 class Meta: 

2522 ordering = ["seq"] 

2523 

2524 def __str__(self): 

2525 return " - ".join([self.resource.pid, self.mimetype]) 

2526 

2527 

2528class ExtLink(models.Model): 

2529 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2530 rel = models.CharField(max_length=50) 

2531 mimetype = models.CharField(max_length=32) 

2532 base = models.ForeignKey(XmlBase, blank=True, null=True, on_delete=models.CASCADE) 

2533 location = models.CharField(max_length=300) 

2534 metadata = models.TextField() 

2535 seq = models.IntegerField(db_index=True) 

2536 

2537 class Meta: 

2538 ordering = ["seq"] 

2539 

2540 def __str__(self): 

2541 return f"{self.rel}: {self.get_href()}" 

2542 

2543 def save(self, *args, **kwargs): 

2544 if "website" in self.rel: 

2545 self.metadata = "website" 

2546 if not self.seq: 2546 ↛ 2547line 2546 didn't jump to line 2547, because the condition on line 2546 was never true

2547 self.seq = self.generate_seq() 

2548 super().save(*args, **kwargs) 

2549 

2550 def generate_seq(self): 

2551 max_seq = ExtLink.objects.filter(resource=self.resource).aggregate(Max("seq"))["seq__max"] 

2552 if max_seq: 

2553 return max_seq + 1 

2554 else: 

2555 return 1 

2556 

2557 def get_href(self): 

2558 if self.rel in ("small_icon", "icon"): 

2559 # construction du chemin vers l'icone 

2560 # filename = os.path.basename(self.location) 

2561 resource_pid = self.resource.pid 

2562 href = resolver.get_icon_url(resource_pid, self.location) 

2563 return href 

2564 return self.location 

2565 

2566 

2567class RelatedObject(models.Model): 

2568 """ 

2569 Related Objects are used to store pdf/djvu related to an issue (tdm, preliminary pages) 

2570 """ 

2571 

2572 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2573 rel = models.CharField(max_length=32) 

2574 mimetype = models.CharField(max_length=32) 

2575 base = models.ForeignKey(XmlBase, blank=True, null=True, on_delete=models.CASCADE) 

2576 location = models.CharField(max_length=200) 

2577 metadata = models.TextField() 

2578 seq = models.IntegerField(db_index=True) 

2579 

2580 class Meta: 

2581 ordering = ["seq"] 

2582 

2583 def __str__(self): 

2584 return " - ".join([self.resource.pid, self.mimetype]) 

2585 

2586 def get_href(self): 

2587 the_dynamic_object = self.resource.cast() 

2588 href = the_dynamic_object.get_binary_file_href_full_path( 

2589 self.rel, self.mimetype, self.location 

2590 ) 

2591 return href 

2592 

2593 

2594class SupplementaryMaterial(RelatedObject): 

2595 caption = models.TextField() 

2596 

2597 def __str__(self): 

2598 return self.location.split("/")[-1] 

2599 

2600 def embeded_link(self): 

2601 if "youtube" in self.location: 

2602 video_id = urlparse(self.location).query.split("=")[1] 

2603 return f"https://www.youtube-nocookie.com/embed/{video_id}" 

2604 return self.get_href() 

2605 

2606 

2607class MetaDataPart(models.Model): 

2608 """ 

2609 stockage des metadonnées 

2610 qui ne sont pas aiilleurs (!) 

2611 non utilisé - à voir 

2612 3 classes qui font sans doute doublon: 

2613 MetadataPart, ResourceAttribute, CustomMeta -- à voir 

2614 """ 

2615 

2616 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2617 name = models.CharField(max_length=32, db_index=True) 

2618 seq = models.IntegerField(db_index=True) 

2619 data = models.TextField() 

2620 

2621 class Meta: 

2622 ordering = ["seq"] 

2623 

2624 

2625class ResourceAttribute(models.Model): 

2626 """ 

2627 not used 

2628 """ 

2629 

2630 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2631 name = models.CharField(max_length=32, db_index=True) 

2632 value = models.TextField() 

2633 

2634 

2635class CustomMeta(models.Model): 

2636 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2637 name = models.CharField(max_length=32, db_index=True) 

2638 value = models.CharField(max_length=128, db_index=True) 

2639 

2640 

2641class ResourceId(models.Model): 

2642 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2643 id_type = models.CharField(max_length=32, db_index=True) 

2644 id_value = models.CharField(max_length=64, db_index=True) 

2645 

2646 class Meta: 

2647 unique_together = ("id_type", "id_value") 

2648 

2649 def __str__(self): 

2650 return f"{self.id_type} {self.id_value}" 

2651 

2652 def get_href(self): 

2653 href = "" 

2654 if self.id_type == "doi": 2654 ↛ 2656line 2654 didn't jump to line 2656, because the condition on line 2654 was never false

2655 href = resolver.get_doi_url(self.id_value) 

2656 return href 

2657 

2658 

2659class ExtId(models.Model): 

2660 """ 

2661 zbl, mr, jfm, etc.. 

2662 mis à part car non uniques 

2663 """ 

2664 

2665 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2666 id_type = models.CharField(max_length=32, db_index=True) 

2667 id_value = models.CharField(max_length=64, db_index=True) 

2668 checked = models.BooleanField(default=True) 

2669 false_positive = models.BooleanField(default=False) 

2670 

2671 class Meta: 

2672 unique_together = ["resource", "id_type"] 

2673 

2674 def __str__(self): 

2675 return f"{self.resource} - {self.id_type}:{self.id_value}" 

2676 

2677 def get_href(self): 

2678 return resolver.resolve_id(self.id_type, self.id_value) 

2679 

2680 

2681class Abstract(models.Model): 

2682 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2683 lang = models.CharField(max_length=3, default="und") 

2684 tag = models.CharField(max_length=32, db_index=True) 

2685 # specific use, content_type, label ? 

2686 seq = models.IntegerField(db_index=True) 

2687 value_xml = models.TextField(default="") 

2688 value_tex = models.TextField(default="") 

2689 value_html = models.TextField(default="") 

2690 

2691 class Meta: 

2692 ordering = ["seq"] 

2693 

2694 def __str__(self): 

2695 return f"{self.resource} - {self.tag}" 

2696 

2697 

2698class Kwd(models.Model): 

2699 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2700 lang = models.CharField(max_length=3, default="und") 

2701 type = models.CharField(max_length=32, db_index=True) 

2702 value = models.TextField(default="") 

2703 seq = models.IntegerField(db_index=True) 

2704 

2705 class Meta: 

2706 ordering = ["seq"] 

2707 

2708 def __str__(self): 

2709 return f"{self.type} - {self.lang} - {self.value}" 

2710 

2711 

2712class Subj(models.Model): 

2713 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2714 lang = models.CharField(max_length=3, default="und") 

2715 type = models.CharField(max_length=32, db_index=True) 

2716 value = models.TextField(default="") 

2717 seq = models.IntegerField(db_index=True) 

2718 

2719 class Meta: 

2720 ordering = ["seq"] 

2721 

2722 def __str__(self): 

2723 return f"{self.type} - {self.lang} - {self.value}" 

2724 

2725 

2726class Award(models.Model): 

2727 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2728 abbrev = models.CharField(max_length=600, db_index=True) 

2729 award_id = models.CharField(max_length=600, db_index=True) 

2730 seq = models.IntegerField(db_index=True) 

2731 

2732 class Meta: 

2733 ordering = ["seq"] 

2734 

2735 

2736class BibItem(models.Model): 

2737 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

2738 sequence = models.PositiveIntegerField(db_index=True) 

2739 label = models.CharField(max_length=128, default="") 

2740 citation_xml = models.TextField(default="") 

2741 citation_tex = models.TextField(default="") 

2742 citation_html = models.TextField(default="") 

2743 

2744 type = models.CharField(max_length=32, default="", db_index=True) 

2745 user_id = models.TextField(default="") 

2746 

2747 # article_title, chapter_title and source can contain formulas. Save the tex version. 

2748 # artile_title, chapter_title and source were created from title, booktitle and chapter. 

2749 # We need to do reverse engineering when exporting the bibtex (see 

2750 # get_bibtex) 

2751 article_title_tex = models.TextField(default="") 

2752 chapter_title_tex = models.TextField(default="") 

2753 source_tex = models.TextField(default="") 

2754 

2755 publisher_name = models.TextField(default="") 

2756 publisher_loc = models.TextField(default="") 

2757 institution = models.TextField(default="") 

2758 series = models.TextField(default="") 

2759 volume = models.TextField(default="") 

2760 issue = models.TextField(default="") 

2761 month = models.CharField(max_length=32, default="") 

2762 year = models.TextField(default="") 

2763 comment = models.TextField(default="") 

2764 annotation = models.CharField(max_length=255, default="") 

2765 fpage = models.TextField(default="") 

2766 lpage = models.TextField(default="") 

2767 page_range = models.TextField(default="") 

2768 size = models.TextField(default="") 

2769 

2770 class Meta: 

2771 ordering = ["sequence"] 

2772 

2773 def __str__(self): 

2774 return f"{self.resource} - {self.label}" 

2775 

2776 def get_ref_id(self): 

2777 ref_id = "" 

2778 # self.resource.body_html is for PCJ case when SHOW_BODY is at false but we have a body html to display 

2779 if (hasattr(settings, "SHOW_BODY") and settings.SHOW_BODY) or self.resource.body_html: 2779 ↛ 2780line 2779 didn't jump to line 2780, because the condition on line 2779 was never true

2780 ref_id = "r" + str(self.sequence) 

2781 else: 

2782 ref_id = self.user_id 

2783 return ref_id 

2784 

2785 def get_doi(self): 

2786 bibitemId_doi = BibItemId.objects.filter(bibitem=self, id_type="doi") 

2787 if bibitemId_doi: 2787 ↛ 2788line 2787 didn't jump to line 2788, because the condition on line 2787 was never true

2788 return bibitemId_doi.get().id_value 

2789 return None 

2790 

2791 def get_bibtex(self): 

2792 """ 

2793 

2794 :param self: 

2795 :return: an array of strings encoded in latex (with latexcodec) 

2796 """ 

2797 bibtex = [] 

2798 indent = " " 

2799 

2800 author_names = get_bibtex_names(self, "author") 

2801 editor_names = get_bibtex_names(self, "editor") 

2802 

2803 if self.user_id: 2803 ↛ 2806line 2803 didn't jump to line 2806, because the condition on line 2803 was never false

2804 id_ = self.user_id 

2805 else: 

2806 id_ = get_bibtex_id(self, self.year) 

2807 

2808 append_in_latex(bibtex, "@" + self.type + "{" + id_ + ",") 

2809 

2810 if author_names: 2810 ↛ 2812line 2810 didn't jump to line 2812, because the condition on line 2810 was never false

2811 append_in_latex(bibtex, indent + author_names) 

2812 if editor_names: 2812 ↛ 2822line 2812 didn't jump to line 2822, because the condition on line 2812 was never false

2813 append_in_latex(bibtex, indent + editor_names) 

2814 

2815 # From numdam+/ptf-xsl/cedram/structured-biblio.xsl 

2816 # < xsl:template match = "bib_entry[@doctype='article']/title| 

2817 # bib_entry[@doctype='misc']/title| 

2818 # bib_entry[@doctype='inproceedings']/title| 

2819 # bib_entry[@doctype='booklet']/title| 

2820 # bib_entry[@doctype='conference']/title" > 

2821 # => All article_title in JATS become title in bibtex 

2822 if self.article_title_tex: 2822 ↛ 2830line 2822 didn't jump to line 2830, because the condition on line 2822 was never false

2823 title = xml_utils.normalise_span(self.article_title_tex) 

2824 append_in_latex(bibtex, indent + "title = {" + title + "},", is_title=True) 

2825 

2826 # <chapter-title> in JATS becomes title or chapter according to the type 

2827 # From numdam+/ptf-xsl/cedram/structured-biblio.xsl 

2828 # <xsl:template match="bib_entry[@doctype='incollection']/title|bibentry/chapter"> 

2829 

2830 if self.chapter_title_tex: 2830 ↛ 2859line 2830 didn't jump to line 2859, because the condition on line 2830 was never false

2831 keyword = "" 

2832 if self.type == "incollection": 2832 ↛ 2833line 2832 didn't jump to line 2833, because the condition on line 2832 was never true

2833 keyword += "title" 

2834 else: 

2835 keyword += "chapter" 

2836 title = xml_utils.normalise_span(self.chapter_title_tex) 

2837 append_in_latex(bibtex, indent + keyword + " = {" + title + "},") 

2838 

2839 # <source> in JATS becomes title, journal, booktitle, or howpublished according to the type 

2840 # From numdam+/ptf-xsl/cedram/structured-biblio.xsl 

2841 # <xsl:template match="bib_entry[@doctype='article']/journal| 

2842 # bib_entry[@doctype='incollection']/booktitle| 

2843 # bib_entry[@doctype='inproceedings']/booktitle| 

2844 # bib_entry[@doctype='conference']/booktitle| 

2845 # bib_entry[@doctype='misc']/howpublished| 

2846 # bib_entry[@doctype='booklet']/howpublished| 

2847 # bib_entry[@doctype='book']/title| 

2848 # bib_entry[@doctype='unpublished']/title| 

2849 # bib_entry[@doctype='inbook']/title| 

2850 # bib_entry[@doctype='phdthesis']/title| 

2851 # bib_entry[@doctype='mastersthesis']/title| 

2852 # bib_entry[@doctype='manual']/title| 

2853 # bib_entry[@doctype='techreport']/title| 

2854 # bib_entry[@doctype='masterthesis']/title| 

2855 # bib_entry[@doctype='coursenotes']/title| 

2856 # bib_entry[@doctype='proceedings']/title"> 

2857 # < xsl:template match = "bib_entry[@doctype='misc']/booktitle" > : Unable to reverse correctly ! 2 choices 

2858 

2859 if self.source_tex: 2859 ↛ 2876line 2859 didn't jump to line 2876, because the condition on line 2859 was never false

2860 keyword = "" 

2861 if self.type == "article": 2861 ↛ 2863line 2861 didn't jump to line 2863, because the condition on line 2861 was never false

2862 keyword += "journal" 

2863 elif ( 

2864 self.type == "incollection" 

2865 or self.type == "inproceedings" 

2866 or self.type == "conference" 

2867 ): 

2868 keyword += "booktitle" 

2869 elif self.type == "misc" or self.type == "booklet": 

2870 keyword += "howpublished" 

2871 else: 

2872 keyword += "title" 

2873 title = xml_utils.normalise_span(self.source_tex) 

2874 append_in_latex(bibtex, indent + keyword + " = {" + title + "},") 

2875 

2876 if self.publisher_name: 2876 ↛ 2878line 2876 didn't jump to line 2878, because the condition on line 2876 was never false

2877 append_in_latex(bibtex, indent + "publisher = {" + self.publisher_name + "},") 

2878 if self.publisher_loc: 2878 ↛ 2880line 2878 didn't jump to line 2880, because the condition on line 2878 was never false

2879 append_in_latex(bibtex, indent + "address = {" + self.publisher_loc + "},") 

2880 if self.institution: 2880 ↛ 2882line 2880 didn't jump to line 2882, because the condition on line 2880 was never false

2881 append_in_latex(bibtex, indent + "institution = {" + self.institution + "},") 

2882 if self.series: 2882 ↛ 2884line 2882 didn't jump to line 2884, because the condition on line 2882 was never false

2883 append_in_latex(bibtex, indent + "series = {" + self.series + "},") 

2884 if self.volume: 2884 ↛ 2886line 2884 didn't jump to line 2886, because the condition on line 2884 was never false

2885 append_in_latex(bibtex, indent + "volume = {" + self.volume + "},") 

2886 if self.issue: 2886 ↛ 2888line 2886 didn't jump to line 2888, because the condition on line 2886 was never false

2887 append_in_latex(bibtex, indent + "number = {" + self.issue + "},") 

2888 if self.year: 2888 ↛ 2891line 2888 didn't jump to line 2891, because the condition on line 2888 was never false

2889 append_in_latex(bibtex, indent + "year = {" + self.year + "},") 

2890 

2891 if self.page_range: 2891 ↛ 2892line 2891 didn't jump to line 2892, because the condition on line 2891 was never true

2892 append_in_latex(bibtex, indent + "pages = {" + self.page_range + "},") 

2893 elif self.fpage or self.lpage: 2893 ↛ 2901line 2893 didn't jump to line 2901, because the condition on line 2893 was never false

2894 text = self.fpage 

2895 if self.fpage and self.lpage: 2895 ↛ 2897line 2895 didn't jump to line 2897, because the condition on line 2895 was never false

2896 text += "--" 

2897 if self.lpage: 2897 ↛ 2899line 2897 didn't jump to line 2899, because the condition on line 2897 was never false

2898 text += self.lpage 

2899 append_in_latex(bibtex, indent + "pages = {" + text + "},") 

2900 

2901 if self.size: 2901 ↛ 2904line 2901 didn't jump to line 2904, because the condition on line 2901 was never false

2902 append_in_latex(bibtex, indent + "pagetotal = {" + self.size + "},") 

2903 

2904 if self.comment: 2904 ↛ 2907line 2904 didn't jump to line 2907, because the condition on line 2904 was never false

2905 append_in_latex(bibtex, indent + "note = {" + self.comment + "},") 

2906 

2907 for extid in BibItemId.objects.filter(bibitem=self): 2907 ↛ 2908line 2907 didn't jump to line 2908, because the loop on line 2907 never started

2908 type_ = "" 

2909 if extid.id_type == "zbl-item-id": 

2910 type_ = "zbl" 

2911 elif extid.id_type == "mr-item-id": 

2912 type_ = "mrnumber" 

2913 elif extid.id_type == "doi": 

2914 type_ = "doi" 

2915 elif extid.id_type == "eid": 

2916 type_ = "eid" 

2917 

2918 if type_: 

2919 append_in_latex(bibtex, indent + type_ + " = {" + extid.id_value + "},") 

2920 

2921 append_in_latex(bibtex, "}") 

2922 

2923 return bibtex 

2924 

2925 

2926class BibItemId(models.Model): 

2927 bibitem = models.ForeignKey(BibItem, on_delete=models.CASCADE) 

2928 id_type = models.CharField(max_length=32, db_index=True) 

2929 id_value = models.CharField(max_length=256, db_index=True) 

2930 checked = models.BooleanField(default=True) 

2931 false_positive = models.BooleanField(default=False) 

2932 

2933 class Meta: 

2934 unique_together = ["bibitem", "id_type"] 

2935 

2936 def __str__(self): 

2937 return f"{self.bibitem} - {self.id_type}:{self.id_value}" 

2938 

2939 def get_href_display(self): 

2940 value = "Article" 

2941 if settings.SITE_ID == 3: 

2942 value = "Numdam" 

2943 else: 

2944 if self.id_type in ["numdam-id", "mathdoc-id"]: 

2945 value = "Numdam" 

2946 

2947 return value 

2948 

2949 def get_href(self): 

2950 force_numdam = False 

2951 if self.id_type in ["numdam-id", "mathdoc-id"] and settings.SITE_ID != 3: 

2952 force_numdam = True 

2953 

2954 return resolver.resolve_id(self.id_type, self.id_value, force_numdam) 

2955 

2956 

2957class ContribGroup(models.Model): 

2958 resource = models.ForeignKey(Resource, blank=True, null=True, on_delete=models.CASCADE) 

2959 bibitem = models.ForeignKey(BibItem, blank=True, null=True, on_delete=models.CASCADE) 

2960 content_type = models.CharField(max_length=32, db_index=True) 

2961 # specific_use ?! 

2962 seq = models.IntegerField() 

2963 

2964 class Meta: 

2965 ordering = ["seq"] 

2966 

2967 

2968class Contrib(models.Model): 

2969 group = models.ForeignKey(ContribGroup, on_delete=models.CASCADE) 

2970 contrib_type = models.CharField(max_length=32, db_index=True) 

2971 

2972 last_name = models.CharField(max_length=128, db_index=True) 

2973 first_name = models.CharField(max_length=128, db_index=True) 

2974 prefix = models.CharField(max_length=32) 

2975 suffix = models.CharField(max_length=32) 

2976 string_name = models.CharField(max_length=256, db_index=True) 

2977 reference_name = models.CharField(max_length=256, db_index=True) 

2978 deceased = models.BooleanField(default=False) 

2979 orcid = models.CharField(max_length=64, db_index=True, blank=True, default="") 

2980 equal_contrib = models.BooleanField(default=True) 

2981 email = models.EmailField(max_length=254, db_index=True, blank=True, default="") 

2982 

2983 # Could be used to export the contrib 

2984 contrib_xml = models.TextField() 

2985 

2986 seq = models.IntegerField() 

2987 

2988 class Meta: 

2989 ordering = ["seq"] 

2990 

2991 @classmethod 

2992 def get_fields_list(cls): 

2993 return [ 

2994 item.attname 

2995 for item in cls._meta.get_fields() 

2996 if not item.auto_created 

2997 and not item.many_to_many 

2998 and not item.many_to_one 

2999 and not item.one_to_many 

3000 and not item.one_to_one 

3001 ] 

3002 

3003 def __str__(self): 

3004 return "{} {} / {} / {}".format( 

3005 self.last_name, self.first_name, self.reference_name, self.contrib_type or "None" 

3006 ) 

3007 

3008 def get_absolute_url(self): 

3009 from ptf.templatetags.helpers import pretty_search 

3010 

3011 return pretty_search("search", "c", f'"{self.reference_name()}"') 

3012 

3013 def display_name(self): 

3014 display_name = self.string_name 

3015 

3016 if self.is_etal(): 

3017 display_name = "et al." 

3018 elif getattr(settings, "DISPLAY_FIRST_NAME_FIRST", False) and ( 

3019 len(str(self.first_name)) > 0 or len(str(self.last_name)) > 0 

3020 ): 

3021 display_name = f"{self.first_name} {self.last_name}" 

3022 

3023 return display_name 

3024 

3025 def orcid_href(self): 

3026 return resolver.resolve_id("orcid", self.orcid) 

3027 

3028 def idref_href(self): 

3029 return resolver.resolve_id("idref", self.idref) 

3030 

3031 def get_addresses(self): 

3032 return self.contribaddress_set.all() # .order_by('address') 

3033 

3034 def is_etal(self): 

3035 return self.contrib_xml.startswith("<etal") 

3036 

3037 

3038class Author(models.Model): 

3039 """ 

3040 Count the number of documents (articles,books...) written by an author 

3041 """ 

3042 

3043 name = models.CharField(max_length=200, db_index=True, unique=True) 

3044 first_letter = models.CharField(max_length=1, db_index=True) 

3045 count = models.IntegerField() 

3046 

3047 

3048class FrontMatter(models.Model): 

3049 resource = models.OneToOneField(Resource, on_delete=models.CASCADE) 

3050 

3051 value_xml = models.TextField(default="") 

3052 value_html = models.TextField(default="") 

3053 foreword_html = models.TextField(default="") 

3054 

3055 

3056class LangTable(models.Model): 

3057 code = models.CharField(max_length=3, db_index=True) 

3058 name = models.CharField(max_length=64) 

3059 

3060 

3061class ResourceCount(models.Model): 

3062 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

3063 name = models.CharField(max_length=32, db_index=True) 

3064 value = models.CharField(max_length=32, db_index=True) 

3065 seq = models.IntegerField() 

3066 

3067 class Meta: 

3068 ordering = ["seq"] 

3069 

3070 

3071class Stats(models.Model): 

3072 name = models.CharField(max_length=128, db_index=True, unique=True) 

3073 value = models.IntegerField() 

3074 

3075 

3076class History(models.Model): 

3077 """pas utilisé pour l'heure""" 

3078 

3079 resource = models.ForeignKey(Resource, on_delete=models.CASCADE) 

3080 date_type = models.CharField(max_length=32, db_index=True) 

3081 date_value = models.DateTimeField() 

3082 comment = models.TextField() 

3083 

3084 

3085class SerialHistory(models.Model): 

3086 """à revoir""" 

3087 

3088 serial = models.ForeignKey("Collection", on_delete=models.CASCADE) 

3089 date_deployed = models.DateField(auto_now_add=True) 

3090 first_year = models.CharField(max_length=32) 

3091 last_year = models.CharField(max_length=32) 

3092 

3093 

3094# def matches(id_type, id_value): 

3095# if id_type in ('mr-item-id', 'zbl-item-id', 'jfm-item-id'): 

3096# qs = ExtId.objects.filter( 

3097# id_type=id_type, 

3098# id_value=id_value, 

3099# ).select_related('resource', 'resource__resourceid') 

3100# counter = 0 

3101# match_list = [] 

3102# for extid in qs: 

3103# resource = extid.resource 

3104# for idext in resource.extid_set.all(): 

3105# match_list.append({'type': idext.id_type, 'value': idext.id_value}) 

3106# for rid in resource.resourceid_set.all(): 

3107# match_list.append({'type': rid.id_type, 'value': rid.id_value}) 

3108# counter += 1 

3109# return { 

3110# 'id': {'type': id_type, 'value': id_value}, 

3111# 'count': counter, 

3112# 'ids': match_list, 

3113# } 

3114# resource = Resource.objects.get(resourceid__id_type=id_type, 

3115# resourceid__id_value=id_value) 

3116# counter = 0 

3117# match_list = [] 

3118# for idext in resource.extid_set.all(): 

3119# match_list.append({'type': idext.id_type, 'value': idext.id_value}) 

3120# for rid in resource.resourceid_set.all(): 

3121# match_list.append({'type': rid.id_type, 'value': rid.id_value}) 

3122# counter += 1 

3123# return { 

3124# 'id': {'type': id_type, 'value': id_value}, 

3125# 'count': counter, 

3126# 'ids': match_list, 

3127# } 

3128 

3129 

3130def parse_page_count(page_count): 

3131 """ 

3132 page-count is not an integer but a string to be displayed. 

3133 page_count may sometimes mix roman and arabic values. Ex "iv-121" 

3134 """ 

3135 if "-" in page_count: 

3136 result = 0 

3137 values = page_count.split("-") 

3138 for value in values: 

3139 try: 

3140 result += int(value) 

3141 except ValueError: 

3142 pass 

3143 else: 

3144 result = int(page_count) 

3145 

3146 return result 

3147 

3148 

3149@receiver(pre_delete, sender=ResourceCount) 

3150def delete_resourcecount_handler(sender, instance, **kwargs): 

3151 """ 

3152 pre_delete ResourceCount signal 

3153 

3154 Stats are added manually during a addResourceCountDatabaseCmd 

3155 but ResourceCount are deleted automatically by Django when its 

3156 related resource is deleted 

3157 (resource = models.ForeignKey(Resource) of ResourceCount) 

3158 To update Stats, we use the Django signal mechanism 

3159 """ 

3160 if instance and instance.name == "page-count" and instance.resource.classname == "Container": 

3161 # page-count may sometimes mix roman and arabic values. Ex "iv-121" 

3162 value = parse_page_count(instance.value) 

3163 

3164 total = Stats.objects.get(name=instance.name) 

3165 total.value -= value 

3166 total.save() 

3167 

3168 

3169# class Volume: 

3170# 

3171# def get_absolute_url(self): 

3172# return reverse('volume-items', kwargs={'vid': self.id_}) 

3173# 

3174# def __init__(self, id_): 

3175# self.id_ = id_ 

3176# try: 

3177# journal_pid, year_id, self.vseries_id, self.volume_id = id_.split( 

3178# '_') 

3179# except ValueError: 

3180# raise self.DoesNotExist(_('Volume {} does not exist').format(id_)) 

3181# try: 

3182# self.collection = Collection.objects.get( 

3183# pid=journal_pid, sites__id=settings.SITE_ID) 

3184# except Collection.DoesNotExist: 

3185# self.issues = None 

3186# try: 

3187# self.issues = Container.objects.filter( 

3188# my_collection__pid=journal_pid, 

3189# year=year_id, 

3190# vseries=self.vseries_id, 

3191# volume=self.volume_id, 

3192# ).order_by('number_int').all() 

3193# except Container.DoesNotExist: 

3194# self.issues = None 

3195# 

3196# class DoesNotExist(Exception): 

3197# pass 

3198 

3199 

3200class ContribAddress(models.Model): 

3201 contrib = models.ForeignKey("Contrib", blank=True, null=True, on_delete=models.CASCADE) 

3202 contribution = models.ForeignKey( 

3203 "Contribution", blank=True, null=True, on_delete=models.CASCADE 

3204 ) 

3205 address = models.TextField(null=True, blank=True) 

3206 

3207 class Meta: 

3208 ordering = ["pk"] 

3209 

3210 

3211def cmp_container_base(a, b): 

3212 return ( 

3213 a.year < b.year 

3214 or (a.year == b.year and a.vseries_int < b.vseries_int) 

3215 or (a.year == b.year and a.vseries_int == b.vseries_int and a.volume_int < b.volume_int) 

3216 or ( 

3217 a.year == b.year 

3218 and a.vseries_int == b.vseries_int 

3219 and a.volume_int == b.volume_int 

3220 and a.number_int < b.number_int 

3221 ) 

3222 ) 

3223 

3224 

3225class PersonManager(models.Manager): 

3226 @staticmethod 

3227 def clean(): 

3228 pass 

3229 # Person.objects.filter(contributions=None).delete() 

3230 

3231 

3232class Person(models.Model): 

3233 """ 

3234 A Person is a contributor (author/editor...) of a Resource (Article/Book) or a BibItem. 

3235 A Person can appear with different names (ex: "John Smith", "J. Smith") and we want to preserve the differences, 

3236 in particular with print papers that are digitized. 

3237 A Person is unique for a person (a Person is basically the key). 

3238 A Person has one or many PersonInfo which stores the different names 

3239 

3240 TODO: signal similar to delete_contrib_handler 

3241 """ 

3242 

3243 # blank=True and null=True allow unique=True with null values (on multiple Persons) 

3244 orcid = models.CharField(max_length=20, blank=True, null=True) 

3245 idref = models.CharField(max_length=10, blank=True, null=True) 

3246 # mid (Mathdoc id) is the key set by numdam-plus 

3247 mid = models.CharField(max_length=256, blank=True, null=True) 

3248 

3249 last_name = models.CharField(max_length=128, blank=True, default="") 

3250 first_name = models.CharField(max_length=128, blank=True, default="") 

3251 prefix = models.CharField(max_length=32, blank=True, default="") 

3252 suffix = models.CharField(max_length=32, blank=True, default="") 

3253 first_letter = models.CharField(max_length=1, blank=True, default="") 

3254 

3255 # Used when a person is not fully tagged in the XML 

3256 string_name = models.CharField(max_length=256, blank=True, default="") 

3257 

3258 objects = PersonManager() 

3259 

3260 @classmethod 

3261 def get_fields_list(cls): 

3262 return [ 

3263 item.attname 

3264 for item in cls._meta.get_fields() 

3265 if not item.auto_created 

3266 and not item.many_to_many 

3267 and not item.many_to_one 

3268 and not item.one_to_many 

3269 and not item.one_to_one 

3270 ] 

3271 

3272 def __str__(self): 

3273 return get_display_name( 

3274 self.prefix, self.first_name, self.last_name, self.suffix, self.string_name 

3275 ) 

3276 

3277 

3278class PersonInfoManager(models.Manager): 

3279 @staticmethod 

3280 def clean(): 

3281 PersonInfo.objects.filter(contributions=None).delete() 

3282 Person.objects.filter(personinfo=None).delete() 

3283 

3284 

3285class PersonInfo(models.Model): 

3286 person = models.ForeignKey(Person, on_delete=models.CASCADE) 

3287 

3288 last_name = models.CharField(max_length=128) 

3289 first_name = models.CharField(max_length=128) 

3290 prefix = models.CharField(max_length=32) 

3291 suffix = models.CharField(max_length=32) 

3292 

3293 # Used when a person is not fully tagged in the XML 

3294 string_name = models.CharField(max_length=256, blank=True, default="") 

3295 

3296 objects = PersonInfoManager() 

3297 

3298 @classmethod 

3299 def get_fields_list(cls): 

3300 return [ 

3301 item.attname 

3302 for item in cls._meta.get_fields() 

3303 if not item.auto_created 

3304 and not item.many_to_many 

3305 and not item.many_to_one 

3306 and not item.one_to_many 

3307 and not item.one_to_one 

3308 ] 

3309 

3310 def __str__(self): 

3311 return get_display_name( 

3312 self.prefix, self.first_name, self.last_name, self.suffix, self.string_name 

3313 ) 

3314 

3315 

3316class Contribution(models.Model): 

3317 resource = models.ForeignKey( 

3318 Resource, blank=True, null=True, on_delete=models.CASCADE, related_name="contributions" 

3319 ) 

3320 bibitem = models.ForeignKey( 

3321 BibItem, blank=True, null=True, on_delete=models.CASCADE, related_name="contributions" 

3322 ) 

3323 

3324 # blank=True and null=True allow unique=True with null values (on multiple Persons) 

3325 orcid = models.CharField(max_length=20, blank=True, null=True) 

3326 idref = models.CharField(max_length=10, blank=True, null=True) 

3327 # mid (Mathdoc id) is the key set by numdam-plus 

3328 mid = models.CharField(max_length=256, blank=True, null=True) 

3329 

3330 last_name = models.CharField(max_length=128, blank=True, default="") 

3331 first_name = models.CharField(max_length=128, blank=True, default="") 

3332 prefix = models.CharField(max_length=32, blank=True, default="") 

3333 suffix = models.CharField(max_length=32, blank=True, default="") 

3334 first_letter = models.CharField(max_length=1, blank=True, default="") 

3335 

3336 # Used when a person is not fully tagged in the XML 

3337 string_name = models.CharField(max_length=256, blank=True, default="") 

3338 

3339 role = models.CharField(max_length=64) 

3340 email = models.EmailField(max_length=254, blank=True, default="") 

3341 deceased_before_publication = models.BooleanField(default=False) 

3342 equal_contrib = models.BooleanField(default=True) 

3343 corresponding = models.BooleanField(default=False) 

3344 

3345 # Used to export the contribution 

3346 contrib_xml = models.TextField() 

3347 

3348 seq = models.IntegerField() 

3349 

3350 class Meta: 

3351 ordering = ["seq"] 

3352 

3353 @classmethod 

3354 def get_fields_list(cls): 

3355 return [ 

3356 item.attname 

3357 for item in cls._meta.get_fields() 

3358 if not item.auto_created 

3359 and not item.many_to_many 

3360 and not item.many_to_one 

3361 and not item.one_to_many 

3362 and not item.one_to_one 

3363 ] 

3364 

3365 def __str__(self): 

3366 return self.display_name() 

3367 

3368 def is_etal(self): 

3369 return self.contrib_xml.startswith("<etal") 

3370 

3371 def display_name(self): 

3372 return ( 

3373 "et al." 

3374 if self.is_etal() 

3375 else get_display_name( 

3376 self.prefix, self.first_name, self.last_name, self.suffix, self.string_name 

3377 ) 

3378 ) 

3379 

3380 def get_absolute_url(self): 

3381 from ptf.templatetags.helpers import pretty_search 

3382 

3383 return pretty_search("search", "c", f'"{self.display_name()}"') 

3384 

3385 def orcid_href(self): 

3386 return resolver.resolve_id("orcid", self.orcid) 

3387 

3388 def idref_href(self): 

3389 return resolver.resolve_id("idref", self.idref) 

3390 

3391 def is_equal(self, contribution): 

3392 """ 

3393 return True if the contribution is identical, based on orcif/idref or homonimy 

3394 TODO: override the __eq__ operator ? Not sure since homonimy is not bullet proof 

3395 """ 

3396 equal = False 

3397 if self.orcid and self.orcid == contribution.orcid: 

3398 equal = True 

3399 elif self.idref and self.idref == contribution.idref: 

3400 equal = True 

3401 else: 

3402 equal = self.display_name() == contribution.display_name() 

3403 

3404 return True 

3405 return equal 

3406 

3407 

3408@receiver(pre_delete, sender=Contribution) 

3409def delete_contrib_handler(sender, instance, **kwargs): 

3410 """ 

3411 pre_delete Contrib signal 

3412 

3413 Contrib and Author are added manually during a 

3414 addResourceDatabaseCmd (mainly addArticleDatabaseCmd) 

3415 but Contrib are deleted automatically by Django when its 

3416 related resource is deleted 

3417 (resource = models.ForeignKey(Resource) of ContribGroup) 

3418 To update Author, we use the Django signal mechanism 

3419 """ 

3420 if instance and instance.role == "author": 

3421 try: 

3422 ref_name = instance.mid if instance.mid else str(instance) 

3423 author = Author.objects.get(name=ref_name) 

3424 author.count -= 1 

3425 if author.count == 0: 

3426 author.delete() 

3427 else: 

3428 author.save() 

3429 except Author.DoesNotExist: 

3430 pass 

3431 

3432 

3433def get_names(item, role): 

3434 """ 

3435 item: resource or bibitem 

3436 """ 

3437 return [ 

3438 str(contribution) for contribution in item.contributions.all() if contribution.role == role 

3439 ] 

3440 

3441 

3442def are_all_equal_contrib(contributions): 

3443 if len(contributions) == 0: 

3444 return False 

3445 

3446 are_all_equal = True 

3447 for contribution in contributions: 

3448 are_all_equal = are_all_equal and contribution.equal_contrib 

3449 

3450 return are_all_equal 

3451 

3452 

3453def are_all_false_equal_contrib(contributions): 

3454 are_all_equal = True 

3455 for contribution in contributions: 

3456 are_all_equal = are_all_equal and not contribution.equal_contrib 

3457 

3458 return are_all_equal 

3459 

3460 

3461class ResourceInSpecialIssue(models.Model): 

3462 my_container = models.ForeignKey( 

3463 Container, null=False, related_name="resources_in_special_issue", on_delete=models.CASCADE 

3464 ) 

3465 resource = models.ForeignKey( 

3466 Resource, null=True, related_name="used_in_special_issues", on_delete=models.CASCADE 

3467 ) 

3468 resource_doi = models.CharField(max_length=64, unique=True) 

3469 seq = models.IntegerField(db_index=True) 

3470 citation = models.TextField(null=True, blank=True) 

3471 

3472 def __str__(self): 

3473 if self.resource and hasattr(self.resource, "doi"): 

3474 return f"{self.resource.doi}, {self.my_container}" 

3475 

3476 

3477class RelatedArticles(models.Model): 

3478 resource = models.ForeignKey(Resource, null=True, blank=True, on_delete=models.CASCADE) 

3479 # Used during reimport/redeploy to find back the RelatedArticles 

3480 resource_doi = models.CharField(max_length=64, unique=True, null=True, blank=True) 

3481 date_modified = models.DateTimeField(null=True, blank=True) 

3482 doi_list = models.TextField(null=True, blank=True) 

3483 exclusion_list = models.TextField(null=True, blank=True) 

3484 automatic_list = models.BooleanField(default=True) 

3485 

3486 def __str__(self): 

3487 if self.resource and hasattr(self.resource, "doi"): 

3488 return f"{self.resource.doi}" 

3489 

3490 

3491def image_path_graphical_abstract(instance, filename): 

3492 path = "images" 

3493 return os.path.join(path, str(instance.id), "graphical_abstract", filename) 

3494 

3495 

3496def image_path_illustration(instance, filename): 

3497 path = "images" 

3498 return os.path.join(path, str(instance.id), "illustration", filename) 

3499 

3500 

3501class OverwriteStorage(FileSystemStorage): 

3502 def get_available_name(self, name, max_length=None): 

3503 if self.exists(name): 

3504 os.remove(os.path.join(settings.MEDIA_ROOT, name)) 

3505 return name 

3506 

3507 

3508class GraphicalAbstract(models