Coverage for datacite/models.py: 99%
328 statements
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-29 15:38 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-29 15:38 +0000
1import logging
2from collections.abc import Mapping
3from typing import Any
5import pycountry
6from django.conf import settings
7from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
8from django.contrib.contenttypes.models import ContentType
9from django.core.validators import MaxValueValidator, MinValueValidator
10from django.db import IntegrityError, models
11from django.urls import reverse
13from datacite.validators import YearValidators, validate_uri
14from network.models import Network
16LANGUAGES = [
17 (lang.alpha_2, lang.name)
18 for lang in pycountry.languages
19 if hasattr(lang, "alpha_2")
20]
21LANGUAGES.insert(0, ("", "--"))
22DEFAULT_LANGUAGE = pycountry.languages.get(name="English").alpha_2
25logger = logging.getLogger(__name__)
28class IdentifierScheme(models.Model):
29 scheme = models.CharField(
30 max_length=255, unique=True, help_text="The identifier scheme."
31 )
32 uri = models.CharField(
33 max_length=255,
34 null=True,
35 blank=True,
36 unique=True,
37 help_text="URI of the identifier scheme.",
38 )
40 def __str__(self) -> str:
41 return f"{self.scheme} ({self.uri})"
44class IdentifierQuerySet(models.QuerySet["Identifier"]):
45 def get_or_create(
46 self, defaults: Mapping[str, Any] | None = None, **kwargs: Any
47 ) -> tuple[Any, bool]:
48 """Takes the information of an identifier, gets it if existing, otherwise
49 creates it.
50 (identifier:str, scheme:dict[str,str], instance:Model|None)
51 -> (Identifier, was_created:bool)"""
52 if defaults is None:
53 defaults = {}
55 if "identifier" in kwargs and "scheme" in kwargs:
56 return self.get_or_create_specific(
57 identifier=kwargs.get("identifier", ""),
58 scheme=kwargs["scheme"].get("scheme"),
59 uri=kwargs["scheme"].get("uri", ""),
60 identified_instance=kwargs.get("instance"),
61 defaults=defaults, # type: ignore[arg-type]
62 )
63 return super().get_or_create(defaults=defaults, **kwargs)
65 def get_or_create_specific(
66 self,
67 identifier: str,
68 scheme: str,
69 uri: str,
70 defaults: dict[str, Any],
71 identified_instance: Any | None = None,
72 ) -> tuple[Any, bool]:
73 # Try to get the identifier
74 try:
75 return self.get(identifier__iexact=identifier), False
76 except Identifier.DoesNotExist:
77 pass
79 # Try to get the scheme
80 try:
81 defaults["scheme"], _ = IdentifierScheme.objects.get_or_create(
82 scheme=scheme, defaults={"uri": uri}
83 )
84 except IntegrityError:
85 # uri exists, but not the scheme. get the corresponding to the uri
86 defaults["scheme"] = IdentifierScheme.objects.get(uri=uri)
88 # Prepare identified instance
89 if identified_instance:
90 defaults["content_type"] = ContentType.objects.get_for_model(
91 identified_instance
92 )
93 defaults["object_id"] = identified_instance.pk
95 return super().get_or_create(identifier=identifier, defaults=defaults)
98class Identifier(models.Model):
99 identifier = models.CharField(
100 max_length=255,
101 unique=True,
102 help_text="The identifier itself (full URI)",
103 )
104 scheme = models.ForeignKey(
105 IdentifierScheme, on_delete=models.PROTECT, help_text="The identifier scheme."
106 )
108 content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
109 object_id = models.PositiveIntegerField()
110 content_object = GenericForeignKey("content_type", "object_id")
112 objects = IdentifierQuerySet.as_manager()
114 class Meta:
115 indexes = [
116 models.Index(fields=["content_type", "object_id"]),
117 ]
119 def __str__(self) -> str:
120 return self.identifier
123class ResourceTypes(models.TextChoices):
124 DEFAULT = "", ""
125 AUDIOVISUAL = "Audiovisual", "Audiovisual"
126 AWARD = "Award", "Award"
127 DATASET = "Dataset", "Dataset"
128 OTHER = "Other", "Other"
131class ResourceType(models.Model):
132 resource_type_general = models.CharField(
133 max_length=31,
134 choices=ResourceTypes,
135 help_text="Type of resource as defined "
136 "by <a href='https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/resourcetype/#a-resourcetypegeneral'>Datacite</a>.",
137 )
138 resource_type = models.CharField(
139 max_length=255, help_text="Single term for subtype."
140 )
142 def __str__(self) -> str:
143 return f"{self.resource_type}"
146class Publisher(models.Model):
147 """https://datacite-metadata-schema.readthedocs.io/en/4/properties/publisher/"""
149 name = models.CharField(max_length=255, help_text="Official name of the publisher.")
150 lang = models.CharField(
151 max_length=60,
152 choices=LANGUAGES,
153 blank=True,
154 help_text="Language of the publisher.",
155 )
156 identifier = GenericRelation(
157 Identifier,
158 related_query_name="publisher",
159 help_text="Identifier of the publisher.",
160 )
162 def __str__(self) -> str:
163 return f"{self.name}"
166class ParticipantTypes(models.TextChoices):
167 DEFAULT = "", ""
168 PERSONAL = "Personal", "Personal"
169 ORGANIZATION = "Organizational", "Organizational"
172class Participant(models.Model):
173 name = models.CharField(
174 max_length=255,
175 help_text="Name of the person or organization. "
176 "This is the name that will appear in the citation.",
177 )
178 given_name = models.CharField(
179 max_length=255,
180 blank=True,
181 default="",
182 help_text="Given name of the participant.",
183 )
184 family_name = models.CharField(
185 max_length=255,
186 blank=True,
187 default="",
188 help_text="Family name of the participant.",
189 )
190 name_type = models.CharField(
191 max_length=15,
192 choices=ParticipantTypes,
193 default=ParticipantTypes.DEFAULT,
194 blank=True,
195 help_text="Type of name as defined by <a href='https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/creator/#a-nametype'>Datacite</a>.",
196 )
197 lang = models.CharField(
198 max_length=60,
199 choices=LANGUAGES,
200 default="",
201 blank=True,
202 help_text="Langue of the organization.",
203 )
204 identifiers = GenericRelation(
205 Identifier,
206 related_query_name="creator",
207 help_text="Identifier of the person/organization.",
208 )
209 affiliations = models.ManyToManyField(
210 "self",
211 blank=True,
212 related_name="affiliation_set",
213 help_text="Affiliation of the person/organization. If it does not exist, "
214 "please create a 'Creator' for this affiliation. ",
215 symmetrical=False,
216 limit_choices_to={"name_type": ParticipantTypes.ORGANIZATION},
217 )
218 is_funder = models.BooleanField(
219 default=False,
220 help_text="Whether the participant is a funder. Only organizations.",
221 )
223 class Meta:
224 constraints = [
225 models.CheckConstraint(
226 condition=(
227 (
228 models.Q(name_type=ParticipantTypes.PERSONAL)
229 & models.Q(is_funder=False)
230 )
231 | models.Q(name_type=ParticipantTypes.ORGANIZATION)
232 | models.Q(name_type=ParticipantTypes.DEFAULT)
233 ),
234 name="person_is_not_funder",
235 ),
236 ]
238 def __str__(self) -> str:
239 return f"{self.name}"
242class MetadataState(models.TextChoices):
243 DRAFT = "draft", "Draft"
244 REGISTERED = "registered", "Registered"
245 FINDABLE = "findable", "Findable"
248class MetadataCreator(models.Model):
249 metadata = models.ForeignKey("Metadata", on_delete=models.CASCADE)
250 creator = models.ForeignKey(Participant, on_delete=models.PROTECT)
251 order = models.IntegerField(
252 null=True, default=-1, help_text="Order of the Creators."
253 )
255 class Meta:
256 constraints = [
257 models.UniqueConstraint(
258 fields=("metadata", "creator"), name="creator_once_per_metadata2"
259 ),
260 ]
262 def __str__(self) -> str:
263 return f"{self.metadata}-{self.creator}"
266class ContributorTypes(models.TextChoices):
267 """
268 https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/contributorType/
269 """
271 DEFAULT = "", ""
272 CONTACT_PERSON = "ContactPerson", "Contact person"
273 DATA_COLLECTOR = "DataCollector", "Data collector"
274 DATA_CURATOR = "DataCurator", "Data curator"
275 DATA_MANAGER = "DataManager", "Data manager"
276 DISTRIBUTOR = "Distributor", "Distributor"
277 HOSTING_INSTITUTION = "HostingInstitution", "Hosting institution"
278 SPONSOR = "Sponsor", "Sponsor"
279 # Present in datacite.prod but not in recommendations.
280 PROJECT_LEADER = "ProjectLeader", "Project leader"
281 PROJECT_MEMBER = "ProjectMember", "Project member"
282 FUNDER = "Funder", "Funder"
283 OTHER = "Other", "Other"
286class MetadataContributor(models.Model):
287 metadata = models.ForeignKey("Metadata", on_delete=models.CASCADE)
288 contributor = models.ForeignKey(Participant, on_delete=models.PROTECT)
289 contributor_type = models.CharField(
290 max_length=31, choices=ContributorTypes, help_text="Role of the participant."
291 )
293 def __str__(self) -> str:
294 return f"{self.metadata}-{self.contributor} ({self.contributor_type})"
297EMBARGOED = "embargoed"
300class Rights(models.Model):
301 """https://datacite-metadata-schema.readthedocs.io/en/4/properties/rights/"""
303 rights = models.CharField(max_length=255)
304 rights_uri = models.URLField(validators=[validate_uri])
305 identifier = GenericRelation(
306 Identifier, related_query_name="rights", help_text="Identifier of the rights."
307 )
308 lang = models.CharField(
309 max_length=60,
310 choices=LANGUAGES,
311 default="",
312 blank=True,
313 )
315 def __str__(self) -> str:
316 return f"{self.rights}"
319class DescriptionTypes(models.TextChoices):
320 """https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/descriptionType/"""
322 DEFAULT = "", ""
323 ABSTRACT = "Abstract", "Abstract"
324 METHODS = "Methods", "Methods"
325 SERIES_INFO = "SeriesInformation", "Series information"
326 TABLE_OF_CONTENTS = "TableOfContents", "Table of contents"
327 TECHNICAL_INFO = "TechnicalInfo", "Technical information"
328 OTHER = "Other", "Other"
331class Description(models.Model):
332 """https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/description/"""
334 description = models.TextField()
335 description_type = models.CharField(max_length=31, choices=DescriptionTypes)
336 lang = models.CharField(
337 max_length=60,
338 choices=LANGUAGES,
339 default="",
340 blank=True,
341 )
343 metadata = models.ForeignKey(
344 "Metadata",
345 null=True,
346 on_delete=models.CASCADE,
347 help_text="A short summary of the network, no more than 200-300 words. "
348 "This should include the number, location, and types of sensors, and the "
349 "type of data collected. Permanent networks frequently gain or lose "
350 "stations, so there is little sense in being too specific concerning "
351 "these. Suggested components : Description, aim and scope, Geodynamic "
352 "setting (for temporary networks), Geographical coverage, Instrument "
353 "types, (number of stations for temporary networks), Data policy "
354 "(e.g. fully open/restricted/embargoed), Any peculiarities of the "
355 "network.",
356 )
358 def __str__(self) -> str:
359 return f"{str(self.description)[:20]}..."
362class Format(models.Model):
363 """https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/format/"""
365 format = models.CharField(
366 max_length=255,
367 help_text=(
368 "Technical format of the resource. Use file extension or MIME type where "
369 "possible, e.g., PDF, XML, MPG or application/pdf, text/xml, video/mpeg."
370 ),
371 )
373 def __str__(self) -> str:
374 return f"{self.format}"
377class Funding(models.Model):
378 """https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/fundingreference/"""
380 funder = models.ForeignKey(
381 Participant, on_delete=models.PROTECT, limit_choices_to={"is_funder": True}
382 )
383 award_number = models.CharField(max_length=255, default="", blank=True)
384 award_uri = models.URLField(max_length=255, default="", blank=True)
385 award_title = models.CharField(max_length=255, default="", blank=True)
387 def __str__(self) -> str:
388 if self.award_title:
389 name = self.award_title
390 elif self.award_number:
391 name = self.award_number
392 elif self.award_uri:
393 name = self.award_uri
394 else:
395 name = "-"
396 return f"{name} ({self.funder})"
399class MetadataManager(models.Manager):
400 def create_from_network(self, network: Network) -> None:
401 metadata = self.create(
402 network=network,
403 url=settings.LANDING_PAGE_BASE_URL + str(network),
404 publication_year=network.start_year,
405 types=ResourceType.objects.get(
406 resource_type=settings.DEFAULT_RESOURCE_TYPE,
407 resource_type_general=ResourceTypes.DATASET,
408 ),
409 publisher=Publisher.objects.get(
410 identifier__identifier=settings.DEFAULT_PUBLISHER_ID
411 ),
412 )
413 metadata.title_set.create(title=network.name, title_type=TitleTypes.MAIN_TITLE) # type: ignore[attr-defined]
414 metadata.add_ordered_creator( # type: ignore[attr-defined]
415 Participant.objects.get(name=settings.DEFAULT_CREATOR_NAME), 0
416 )
417 cc_by_40 = Rights.objects.get(
418 identifier__identifier=settings.DEFAULT_FREE_LICENSE
419 )
420 metadata.rights.add(cc_by_40) # type: ignore[attr-defined]
423class Metadata(models.Model):
424 url = models.URLField(help_text="Landing page of the resource.")
425 publication_year = models.IntegerField(
426 validators=[*YearValidators], help_text="Year of publication."
427 )
428 language = models.CharField(
429 max_length=60,
430 choices=LANGUAGES,
431 default="",
432 blank=True,
433 help_text="Language of the resource.",
434 )
435 state = models.CharField(
436 max_length=15, choices=MetadataState, blank=True, default=MetadataState.DRAFT
437 )
438 creators = models.ManyToManyField(
439 Participant,
440 through=MetadataCreator,
441 help_text="List of creators of this resource.",
442 related_name="creations",
443 )
444 contributors = models.ManyToManyField(
445 Participant,
446 through=MetadataContributor,
447 help_text="List of contributors of this resource.",
448 related_name="contributions",
449 )
451 types = models.ForeignKey(
452 ResourceType, on_delete=models.PROTECT, help_text="Type of resource."
453 )
454 publisher = models.ForeignKey(
455 Publisher, on_delete=models.PROTECT, help_text="Publisher of the metadata"
456 )
457 network = models.OneToOneField(
458 Network,
459 on_delete=models.CASCADE,
460 help_text="Network to which this metadata corresponds.",
461 )
462 formats = models.ManyToManyField(
463 Format,
464 help_text=(
465 "Technical format of the resource. Use file extension or MIME type where "
466 "possible, e.g., PDF, XML, MPG or application/pdf, text/xml, video/mpeg."
467 ),
468 related_name="metadata_set",
469 blank=True,
470 )
471 rights = models.ManyToManyField(
472 Rights,
473 help_text="Rights information for this dataset.",
474 related_name="metadata_set",
475 blank=True,
476 )
477 collected_start = models.DateField(null=True, help_text="Date of the first data.")
478 collected_end = models.DateField(
479 null=True, blank=True, help_text="Date of the last data."
480 )
481 issued = models.DateField(
482 null=True,
483 help_text=(
484 "Start date of the network. It's this date that will appear on citations."
485 ),
486 )
487 available = models.DateField(
488 null=True,
489 blank=True,
490 help_text=(
491 "If needed, the date of the end of the embargo. "
492 "Do not fill if there is no embargo."
493 ),
494 )
495 size_information = models.CharField(
496 max_length=255,
497 blank=True,
498 default="",
499 help_text="Approximate of the number of active stations.",
500 )
501 size_increment = models.CharField(
502 max_length=255,
503 default="",
504 blank=True,
505 help_text="Day increment in 'GB/year' (only for Permanent Networks).",
506 )
507 size_total = models.CharField(
508 default="",
509 blank=True,
510 max_length=255,
511 help_text=(
512 "Total size in TB for a Temporary Network. "
513 "Total size in a given year for a Permanent Network."
514 ),
515 )
516 fundings = models.ManyToManyField(
517 Funding,
518 help_text=(
519 "Information about financial support (funding) for the resource being "
520 "registered."
521 ),
522 related_name="metadata_set",
523 blank=True,
524 )
526 objects = MetadataManager()
528 def __str__(self) -> str:
529 return f"{self.network.doi}"
531 def get_absolute_url(self) -> str:
532 return reverse("datacite:metadata-detail", kwargs={"pk": self.network.pk})
534 def add_ordered_creator(self, participant: Participant, order: int) -> None:
535 MetadataCreator.objects.update_or_create(
536 creator=participant, metadata=self, defaults={"order": order}
537 )
539 def add_ordered_creators(self, participants: list[Participant]) -> None:
540 MetadataCreator.objects.filter(metadata=self).exclude(
541 creator__in=participants
542 ).delete()
543 creators_of_metadata = [
544 MetadataCreator(creator=participant, order=order, metadata=self)
545 for order, participant in enumerate(participants)
546 ]
547 MetadataCreator.objects.bulk_create(
548 creators_of_metadata,
549 update_conflicts=True,
550 update_fields=["order"],
551 unique_fields=["creator", "metadata"],
552 )
554 def add_ordered_creators_from_ids(self, participant_ids: list[int | str]) -> None:
555 participants = []
556 for participant_id in participant_ids:
557 if isinstance(participant_id, str) and participant_id.isdigit():
558 index = int(participant_id)
559 elif isinstance(participant_id, int):
560 index = participant_id
561 else:
562 continue
564 try:
565 participant = Participant.objects.get(pk=index)
566 except Participant.DoesNotExist:
567 continue
569 participants.append(participant)
571 self.add_ordered_creators(participants)
574class TitleTypes(models.TextChoices):
575 DEFAULT = "", ""
576 MAIN_TITLE = "MainTitle", "Main title"
577 ALTERNATIVELY_TITLE = "AlternativeTitle", "Alternative title"
578 SUBTITLE = "Subtitle", "Subtitle"
579 TRANSLATED_TITLE = "TranslatedTitle", "Translated title"
580 OTHER = "Other", "Other"
583class Title(models.Model):
584 """https://datacite-metadata-schema.readthedocs.io/en/4/properties/title/"""
586 title = models.CharField(max_length=255, help_text="Title of the resource.")
587 title_type = models.CharField(
588 max_length=31,
589 choices=TitleTypes,
590 default=TitleTypes.DEFAULT,
591 blank=True,
592 help_text="Type of title as defined by "
593 "<a href='https://datacite-metadata-schema.readthedocs.io/en/4.6/properties"
594 "/title/#a-titletype'>Datacite</a>.",
595 )
596 lang = models.CharField(
597 max_length=60,
598 choices=LANGUAGES,
599 default="",
600 blank=True,
601 help_text="Langue of the title.",
602 )
603 metadata = models.ForeignKey(
604 Metadata,
605 on_delete=models.CASCADE,
606 help_text="Metadata associated to the title.",
607 )
609 def __str__(self) -> str:
610 return f"{self.title}"
613class Subject(models.Model):
614 """https://datacite-metadata-schema.readthedocs.io/en/4/properties/subject/"""
616 subject = models.CharField(max_length=255)
617 lang = models.CharField(
618 max_length=60,
619 choices=LANGUAGES,
620 default="",
621 blank=True,
622 )
623 value_uri = models.URLField()
624 subject_scheme = models.CharField(max_length=255) # maybe auto from value_uri
625 scheme_uri = models.URLField() # maybe auto from value_uri
627 def __str__(self) -> str:
628 return f"{self.subject}"
631class RelatedIdentifierTypes(models.TextChoices):
632 """https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/relatedIdentifierType/"""
634 DEFAULT = "", ""
635 ARK = "ARK", "ARK"
636 ARXIV = "ArXiv", "ArXiv"
637 BIBCODE = "bibcode", "bibcode"
638 CSTR = "CSTR", "CSTR"
639 DOI = "DOI", "DOI"
640 EAN13 = "EAN13", "EAN13"
641 EISSN = "EISSN", "EISSN"
642 HANDLE = "Handle", "Handle"
643 IGSN = "IGSN", "IGSN"
644 ISBN = "ISBN", "ISBN"
645 ISSN = "ISSN", "ISSN"
646 ISTC = "ISTC", "ISTC"
647 LISSN = "LISSN", "LISSN"
648 LSID = "LSID", "LSID"
649 PMID = "PMID", "PMID"
650 PURL = "PURL", "PURL"
651 RRID = "RRID", "RRID"
652 UPC = "UPC", "UPC"
653 URL = "URL", "URL"
654 URN = "URN", "URN"
655 W3ID = "W3ID", "W3ID"
658class RelationTypes(models.TextChoices):
659 """https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/relationType/"""
661 DEFAULT = "", ""
662 IS_CITED_BY = "IsCitedBy", "Is cited by"
663 CITES = "Cites", "Cites"
664 IS_SUPPLEMENT_TO = "IsSupplementTo", "Is supplement to"
665 IS_SUPPLEMENTED_BY = "IsSupplementedBy", "Is supplemented by"
666 IS_CONTINUED_BY = "IsContinuedBy", "Is continued by"
667 CONTINUES = "Continues", "Continues"
668 IS_DESCRIBED_BY = "IsDescribedBy", "Is described by"
669 DESCRIBES = "Describes", "Describes"
670 HAS_METADATA = "HasMetadata", "Has metadata"
671 IS_METADATA_FOR = "IsMetadataFor", "Is metadata for"
672 HAS_VERSION = "HasVersion", "Has version"
673 IS_VERSION_OF = "IsVersionOf", "Is version of"
674 IS_NEW_VERSION_OF = "IsNewVersionOf", "Is new version of"
675 IS_PREVIOUS_VERSION_OF = "IsPreviousVersionOf", "Is previous version of"
676 IS_PART_OF = "IsPartOf", "Is part of"
677 HAS_PART = "HasPart", "Has part"
678 IS_PUBLISHED_IN = "IsPublishedIn", "Is published in"
679 IS_REFERENCED_IN = "IsReferencedBy", "Is referenced by"
680 REFERENCES = "References", "References"
681 IS_DOCUMENTED_BY = "IsDocumentedBy", "Is documented by"
682 DOCUMENTS = "Documents", "Documents"
683 IS_COMPILED_BY = "IsCompiledBy", "Is compiled by"
684 COMPILES = "Compiles", "Compiles"
685 IS_VARIANT_OF = "IsVariantFormOf", "Is variant form of"
686 IS_ORIGINAL_FORM_OF = "IsOriginalFormOf", "Is original form of"
687 IS_IDENTICAL_TO = "IsIdenticalTo", "Is identical to"
688 IS_REVIEWED_BY = "IsReviewedBy", "Is reviewed by"
689 REVIEWS = "Reviews", "Reviews"
690 IS_DERIVED_FROM = "IsDerivedFrom", "Is derived from"
691 IS_SOURCE_OF = "IsSourceOf", "Is source of"
692 IS_REQUIRED_BY = "IsRequiredBy", "Is required by"
693 REQUIRES = "Requires", "Requires"
694 IS_OBSOLETED_BY = "IsObsoletedBy", "Is obsoleted by"
695 OBSOLETES = "Obsoletes", "Obsoletes"
696 IS_COLLECTED_BY = "IsCollectedBy", "Is collected by"
697 COLLECTS = "Collects", "Collects"
698 IS_TRANSLATION_OF = "IsTranslationOf", "Is translation of"
699 HAS_TRANSLATION = "HasTranslation", "Has translation"
702class RelatedIdentifier(models.Model):
703 """https://datacite-metadata-schema.readthedocs.io/en/4/properties/relatedidentifier/"""
705 identifier = GenericRelation(
706 Identifier,
707 related_query_name="related_identifier",
708 help_text="Identifier of the related_identifier.",
709 )
710 relation_type = models.CharField(max_length=31, choices=RelationTypes)
711 resource_type_general = models.CharField(max_length=31, choices=ResourceTypes)
712 related_metadata_scheme = models.CharField(max_length=255, default="", blank=True)
713 scheme_uri = models.URLField(default="", blank=True)
714 scheme_type = models.CharField(max_length=255, default="", blank=True)
716 def __str__(self) -> str:
717 return f"{self.identifier.first().identifier}"
720class Geolocation(models.Model):
721 """https://datacite-metadata-schema.readthedocs.io/en/4.6/properties/geolocation/"""
723 place = models.CharField(
724 max_length=255, default="", help_text="Name of the location."
725 )
726 west_bound_longitude = models.FloatField(
727 null=True,
728 validators=[MaxValueValidator(180), MinValueValidator(-180)],
729 help_text="West bound of the region.",
730 )
731 east_bound_longitude = models.FloatField(
732 null=True,
733 validators=[MaxValueValidator(180), MinValueValidator(-180)],
734 help_text="East bound of the region.",
735 )
736 south_bound_latitude = models.FloatField(
737 null=True,
738 validators=[MaxValueValidator(90), MinValueValidator(-90)],
739 help_text="South bound of the region.",
740 )
741 north_bound_latitude = models.FloatField(
742 null=True,
743 validators=[MaxValueValidator(90), MinValueValidator(-90)],
744 help_text="North bound of the region.",
745 )
746 metadata = models.ForeignKey(
747 Metadata, on_delete=models.CASCADE, help_text="Network metadata associated."
748 )
750 class Meta:
751 constraints = [
752 models.UniqueConstraint(
753 fields=["metadata", "place"], name="unique_place_per_network"
754 ),
755 models.CheckConstraint(
756 condition=(
757 models.Q(west_bound_longitude__isnull=False)
758 & models.Q(east_bound_longitude__isnull=False)
759 & models.Q(north_bound_latitude__isnull=False)
760 & models.Q(south_bound_latitude__isnull=False)
761 )
762 | (
763 models.Q(west_bound_longitude__isnull=True)
764 & models.Q(east_bound_longitude__isnull=True)
765 & models.Q(north_bound_latitude__isnull=True)
766 & models.Q(south_bound_latitude__isnull=True)
767 ),
768 name="all_coords_or_none",
769 ),
770 ]
772 def __str__(self) -> str:
773 coordinates = (
774 f"[{self.latitude_2_str(self.south_bound_latitude)}-"
775 f"{self.latitude_2_str(self.north_bound_latitude)}, "
776 f"{self.longitude_2_str(self.west_bound_longitude)}-"
777 f"{self.longitude_2_str(self.east_bound_longitude)}]"
778 )
779 if self.place:
780 return f"{self.place} {coordinates}"
781 return coordinates
783 @staticmethod
784 def latitude_2_str(latitude: float | None) -> str:
785 if latitude is None:
786 return "?"
787 return f"{abs(latitude)}{'N' if latitude >= 0 else 'S'}"
789 @staticmethod
790 def longitude_2_str(longitude: float | None) -> str:
791 if longitude is None:
792 return "?"
793 return f"{abs(longitude)}{'E' if longitude >= 0 else 'W'}"