"""
Data types to describe events.
"""
from __future__ import annotations
from reprlib import recursive_repr
from typing import final, Iterable, Mapping, TYPE_CHECKING
from urllib.parse import quote
from typing_extensions import override
from betty.ancestry.date import HasDate
from betty.ancestry.description import HasDescription
from betty.ancestry.event_type import EVENT_TYPE_REPOSITORY, EventType
from betty.ancestry.event_type.event_types import Unknown as UnknownEventType
from betty.ancestry.has_citations import HasCitations
from betty.ancestry.has_file_references import HasFileReferences
from betty.ancestry.has_notes import HasNotes
from betty.ancestry.link import HasLinks
from betty.ancestry.person import Person
from betty.ancestry.place import Place
from betty.ancestry.presence import Presence
from betty.ancestry.presence_role import PresenceRoleSchema
from betty.ancestry.presence_role.presence_roles import Subject
from betty.ancestry.privacy import HasPrivacy, Privacy
from betty.asyncio import wait_to_thread
from betty.json.linked_data import dump_context, JsonLdObject
from betty.json.schema import Object, Enum, Array, String
from betty.locale.localizable import _, ShorthandStaticTranslations, Localizable, call
from betty.model import UserFacingEntity, GeneratedEntityId, EntityReferenceSchema
from betty.model.association import ManyToOne, OneToMany
from betty.plugin import ShorthandPluginBase
from betty.repr import repr_instance
if TYPE_CHECKING:
from betty.ancestry.citation import Citation
from betty.ancestry.note import Note
from betty.ancestry.file_reference import FileReference
from betty.date import Datey
from betty.project import Project
from betty.serde.dump import DumpMapping, Dump
[docs]
@final
class Event(
ShorthandPluginBase,
HasDate,
HasFileReferences,
HasCitations,
HasNotes,
HasDescription,
HasPrivacy,
HasLinks,
UserFacingEntity,
):
"""
An event that took place.
"""
_plugin_id = "event"
_plugin_label = _("Event")
#: The place the event happened.
place = ManyToOne["Event", Place](
"betty.ancestry.event:Event", "place", "betty.ancestry.place:Place", "events"
)
presences = OneToMany["Event", Presence](
"betty.ancestry.event:Event",
"presences",
"betty.ancestry.presence:Presence",
"event",
)
[docs]
def __init__(
self,
*,
id: str | None = None, # noqa A002
event_type: EventType | None = None,
date: Datey | None = None,
file_references: Iterable[FileReference] | None = None,
citations: Iterable[Citation] | None = None,
notes: Iterable[Note] | None = None,
privacy: Privacy | None = None,
public: bool | None = None,
private: bool | None = None,
place: Place | None = None,
description: ShorthandStaticTranslations | None = None,
):
super().__init__(
id,
date=date,
file_references=file_references,
citations=citations,
notes=notes,
privacy=privacy,
public=public,
private=private,
description=description,
)
self._event_type = event_type or UnknownEventType()
if place is not None:
self.place = place
[docs]
@override
def dated_linked_data_contexts(self) -> tuple[str | None, str | None, str | None]:
return (
"https://schema.org/startDate",
"https://schema.org/startDate",
"https://schema.org/endDate",
)
@override
@property
def label(self) -> Localizable:
format_kwargs: Mapping[str, str | Localizable] = {
"event_type": self._event_type.plugin_label(),
}
subjects = [
presence.person
for presence in self.presences
if presence.public
and isinstance(presence.role, Subject)
and presence.person is not None
and presence.person.public
]
if subjects:
format_kwargs["subjects"] = call(
lambda localizer: ", ".join(
person.label.localize(localizer) for person in subjects
)
)
if self.description:
format_kwargs["event_description"] = self.description
if subjects:
if self.description:
return _("{event_type} ({event_description}) of {subjects}").format(
**format_kwargs
)
else:
return _("{event_type} of {subjects}").format(**format_kwargs)
if self.description:
return _("{event_type} ({event_description})").format(**format_kwargs)
else:
return _("{event_type}").format(**format_kwargs)
@override # type: ignore[callable-functiontype]
@recursive_repr()
def __repr__(self) -> str:
return repr_instance(self, id=self._id, type=self._event_type)
[docs]
@override
@classmethod
def plugin_label_plural(cls) -> Localizable:
return _("Events")
@property
def event_type(self) -> EventType:
"""
The type of event.
"""
return self._event_type
[docs]
@override
async def dump_linked_data(self, project: Project) -> DumpMapping[Dump]:
dump = await super().dump_linked_data(project)
dump_context(dump, presences="https://schema.org/performer")
dump["@type"] = "https://schema.org/Event"
dump["type"] = self.event_type.plugin_id()
dump["eventAttendanceMode"] = "https://schema.org/OfflineEventAttendanceMode"
dump["eventStatus"] = "https://schema.org/EventScheduled"
dump["presences"] = presences = []
for presence in self.presences:
if presence.person and not isinstance(
presence.person.id, GeneratedEntityId
):
presences.append(self._dump_event_presence(presence, project))
if self.place is not None and not isinstance(self.place.id, GeneratedEntityId):
dump["place"] = project.static_url_generator.generate(
f"/place/{quote(self.place.id)}/index.json"
)
dump_context(dump, place="https://schema.org/location")
return dump
def _dump_event_presence(
self, presence: Presence, project: Project
) -> DumpMapping[Dump]:
assert presence.person
dump: DumpMapping[Dump] = {
"@type": "https://schema.org/Person",
"person": project.static_url_generator.generate(
f"/person/{quote(presence.person.id)}/index.json"
),
}
if presence.public:
dump["role"] = presence.role.plugin_id()
return dump
[docs]
@override
@classmethod
async def linked_data_schema(cls, project: Project) -> Object:
schema = await super().linked_data_schema(project)
schema.add_property(
"type",
Enum(
*[
presence_role.plugin_id()
for presence_role in wait_to_thread(EVENT_TYPE_REPOSITORY.select())
],
title="Event type",
),
)
schema.add_property("place", EntityReferenceSchema(Place), False)
schema.add_property(
"presences", Array(_EventPresenceSchema(), title="Presences")
)
schema.add_property("eventStatus", String(title="Event status"))
schema.add_property(
"eventAttendanceMode", String(title="Event attendance mode")
)
return schema
class _EventPresenceSchema(JsonLdObject):
"""
A schema for the :py:class:`betty.ancestry.presence.Presence` associations on a :py:class:`betty.ancestry.event.Event`.
"""
def __init__(self):
super().__init__(title="Presence (event)")
self.add_property("role", PresenceRoleSchema(), False)
self.add_property("person", EntityReferenceSchema(Person))