"""
Data types to describe information sources.
"""
from __future__ import annotations
from typing import final, Iterable, MutableSequence, Iterator, TYPE_CHECKING
from urllib.parse import quote
from typing_extensions import override
from betty.ancestry.date import HasDate
from betty.ancestry.has_file_references import HasFileReferences
from betty.ancestry.has_notes import HasNotes
from betty.ancestry.link import HasLinks, Link
from betty.ancestry.privacy import HasPrivacy, Privacy, merge_privacies
from betty.json.linked_data import dump_context
from betty.locale.localizable import (
_,
OptionalStaticTranslationsLocalizableAttr,
ShorthandStaticTranslations,
Localizable,
StaticTranslationsLocalizableSchema,
)
from betty.model import (
UserFacingEntity,
Entity,
GeneratedEntityId,
EntityReferenceCollectionSchema,
EntityReferenceSchema,
)
from betty.model.association import ManyToOne, OneToMany
from betty.plugin import ShorthandPluginBase
if TYPE_CHECKING:
from betty.ancestry.citation import Citation # noqa F401
from betty.ancestry.note import Note
from betty.ancestry.file_reference import FileReference
from betty.serde.dump import DumpMapping, Dump
from betty.project import Project
from betty.json.schema import Object
from betty.date import Datey
[docs]
@final
class Source(
ShorthandPluginBase,
HasDate,
HasFileReferences,
HasNotes,
HasLinks,
HasPrivacy,
UserFacingEntity,
Entity,
):
"""
A source of information.
"""
_plugin_id = "source"
_plugin_label = _("Source")
#: The source this one is directly contained by.
contained_by = ManyToOne["Source", "Source"](
"betty.ancestry.source:Source",
"contained_by",
"betty.ancestry.source:Source",
"contains",
)
contains = OneToMany["Source", "Source"](
"betty.ancestry.source:Source",
"contains",
"betty.ancestry.source:Source",
"contained_by",
)
citations = OneToMany["Source", "Citation"](
"betty.ancestry.source:Source",
"citations",
"betty.ancestry.citation:Citation",
"source",
)
#: The human-readable source name.
name = OptionalStaticTranslationsLocalizableAttr("name")
#: The human-readable author.
author = OptionalStaticTranslationsLocalizableAttr("author")
#: The human-readable publisher.
publisher = OptionalStaticTranslationsLocalizableAttr("publisher")
[docs]
def __init__(
self,
name: ShorthandStaticTranslations | None = None,
*,
id: str | None = None, # noqa A002 # noqa A002
author: ShorthandStaticTranslations | None = None,
publisher: ShorthandStaticTranslations | None = None,
contained_by: Source | None = None,
contains: Iterable[Source] | None = None,
notes: Iterable[Note] | None = None,
date: Datey | None = None,
file_references: Iterable["FileReference"] | None = None,
links: MutableSequence[Link] | None = None,
privacy: Privacy | None = None,
public: bool | None = None,
private: bool | None = None,
):
super().__init__(
id,
notes=notes,
date=date,
file_references=file_references,
links=links,
privacy=privacy,
public=public,
private=private,
)
if name:
self.name = name
if author:
self.author = author
if publisher:
self.publisher = publisher
if contained_by is not None:
self.contained_by = contained_by
if contains is not None:
self.contains = contains
@override
def _get_effective_privacy(self) -> Privacy:
privacy = super()._get_effective_privacy()
if self.contained_by:
return merge_privacies(privacy, self.contained_by.privacy)
return privacy
@property
def walk_contains(self) -> Iterator[Source]:
"""
All directly and indirectly contained sources.
"""
for source in self.contains:
yield source
yield from source.contains
[docs]
@override
@classmethod
def plugin_label_plural(cls) -> Localizable:
return _("Sources")
@override
@property
def label(self) -> Localizable:
return self.name if self.name else super().label
[docs]
@override
async def dump_linked_data(self, project: Project) -> DumpMapping[Dump]:
dump = await super().dump_linked_data(project)
dump["@type"] = "https://schema.org/Thing"
dump["contains"] = [
project.static_url_generator.generate(
f"/source/{quote(contained.id)}/index.json"
)
for contained in self.contains
if not isinstance(contained.id, GeneratedEntityId)
]
dump["citations"] = [
project.static_url_generator.generate(
f"/citation/{quote(citation.id)}/index.json"
)
for citation in self.citations
if not isinstance(citation.id, GeneratedEntityId)
]
if self.contained_by is not None and not isinstance(
self.contained_by.id, GeneratedEntityId
):
dump["containedBy"] = project.static_url_generator.generate(
f"/source/{quote(self.contained_by.id)}/index.json"
)
if self.public:
if self.name:
dump_context(dump, name="https://schema.org/name")
dump["name"] = await self.name.dump_linked_data(project)
if self.author:
dump["author"] = await self.author.dump_linked_data(project)
if self.publisher:
dump["publisher"] = await self.publisher.dump_linked_data(project)
return dump
[docs]
@override
@classmethod
async def linked_data_schema(cls, project: Project) -> Object:
from betty.ancestry.citation import Citation
schema = await super().linked_data_schema(project)
schema.add_property(
"name", StaticTranslationsLocalizableSchema(title="Name"), False
)
schema.add_property(
"author", StaticTranslationsLocalizableSchema(title="Author"), False
)
schema.add_property(
"publisher", StaticTranslationsLocalizableSchema(title="Publisher"), False
)
schema.add_property("contains", EntityReferenceCollectionSchema(Source))
schema.add_property("citations", EntityReferenceCollectionSchema(Citation))
schema.add_property("containedBy", EntityReferenceSchema(Source), False)
return schema