diff --git a/betty/ancestry/event_type.py b/betty/ancestry/event_type.py
index 6bcb2bf6d..5cfd3a617 100644
--- a/betty/ancestry/event_type.py
+++ b/betty/ancestry/event_type.py
@@ -10,6 +10,7 @@
from betty.locale.localizable import _, Localizable
from betty.locale.localizer import DEFAULT_LOCALIZER
+from betty.machine_name import MachineName
from betty.plugin import Plugin, PluginRepository
from betty.plugin.entry_point import EntryPointPluginRepository
diff --git a/betty/assertion/__init__.py b/betty/assertion/__init__.py
index 526825868..7e9272b1e 100644
--- a/betty/assertion/__init__.py
+++ b/betty/assertion/__init__.py
@@ -85,6 +85,9 @@ def __call__(self, value: _AssertionValueT) -> _AssertionReturnT:
Invoke the chain with a value.
This method may be called more than once.
+
+ :raises betty.assertion.error.AssertionFailed: Raised if any part of the
+ assertion chain fails.
"""
return self._assertion(value)
diff --git a/betty/config/collections/mapping.py b/betty/config/collections/mapping.py
index 39d395f31..17e5b102c 100644
--- a/betty/config/collections/mapping.py
+++ b/betty/config/collections/mapping.py
@@ -118,11 +118,7 @@ class ConfigurationMapping(
"""
@abstractmethod
- def _load_key(
- self,
- item_dump: Dump,
- key_dump: str,
- ) -> Dump:
+ def _load_key(self, item_dump: Dump, key_dump: str) -> Dump:
pass
@abstractmethod
diff --git a/betty/extension/gramps/__init__.py b/betty/extension/gramps/__init__.py
index 41ca92ad8..1f51698c4 100644
--- a/betty/extension/gramps/__init__.py
+++ b/betty/extension/gramps/__init__.py
@@ -26,14 +26,20 @@ async def _load_ancestry(event: LoadAncestryEvent) -> None:
Gramps
].extension_configuration
assert isinstance(gramps_configuration, GrampsConfiguration)
- for family_tree in gramps_configuration.family_trees:
- file_path = family_tree.file_path
+ for family_tree_configuration in gramps_configuration.family_trees:
+ file_path = family_tree_configuration.file_path
if file_path:
await GrampsLoader(
project.ancestry,
attribute_prefix_key=project.configuration.name,
factory=project.new,
localizer=project.app.localizer,
+ event_type_map={
+ event_type_configuration.gramps_event_type: await project.event_types.get(
+ event_type_configuration.event_type_id
+ )
+ for event_type_configuration in family_tree_configuration.event_types.values()
+ },
).load_file(file_path)
diff --git a/betty/extension/gramps/config.py b/betty/extension/gramps/config.py
index f83997486..40f96d34f 100644
--- a/betty/extension/gramps/config.py
+++ b/betty/extension/gramps/config.py
@@ -15,20 +15,159 @@
assert_record,
assert_path,
assert_setattr,
+ assert_mapping,
+ assert_len,
+ assert_str,
)
from betty.config import Configuration
+from betty.config.collections.mapping import ConfigurationMapping
from betty.config.collections.sequence import ConfigurationSequence
+from betty.machine_name import assert_machine_name, MachineName
from betty.serde.dump import minimize, Dump, VoidableDump
+def _assert_gramps_event_type(value: Any) -> str:
+ event_type = assert_str()(value)
+ assert_len(minimum=1)(event_type)
+ return event_type
+
+
+class FamilyTreeEventTypeConfiguration(Configuration):
+ """
+ Configure for loading Gramps events.
+ """
+
+ _gramps_event_type: str
+ _event_type_id: MachineName
+
+ def __init__(self, gramps_event_type: str, event_type_id: MachineName):
+ super().__init__()
+ self.gramps_event_type = gramps_event_type
+ self.event_type_id = event_type_id
+
+ @property
+ def gramps_event_type(self) -> str:
+ """
+ The Gramps event type this configuration applies to.
+ """
+ return self._gramps_event_type
+
+ @gramps_event_type.setter
+ def gramps_event_type(self, event_type: str) -> None:
+ self._gramps_event_type = _assert_gramps_event_type(event_type)
+
+ @property
+ def event_type_id(self) -> MachineName:
+ """
+ The ID of the Betty event type to load Gramps events of type :py:attr:`betty.extension.gramps.config.FamilyTreeEventTypeConfiguration.gramps_event_type` as.
+ """
+ return self._event_type_id
+
+ @event_type_id.setter
+ def event_type_id(self, event_type_id: MachineName) -> None:
+ self._event_type_id = assert_machine_name()(event_type_id)
+
+ @override
+ def load(self, dump: Dump) -> None:
+ assert_record(
+ RequiredField(
+ "gramps_event_type", assert_setattr(self, "gramps_event_type")
+ ),
+ RequiredField("event_type", assert_setattr(self, "event_type_id")),
+ )(dump)
+
+ @override
+ def dump(self) -> VoidableDump:
+ return {
+ "gramps_event_type": self.gramps_event_type,
+ "event_type": self.event_type_id,
+ }
+
+ @override
+ def update(self, other: Self) -> None:
+ self.gramps_event_type = other.gramps_event_type
+ self.event_type_id = other.event_type_id
+
+
+class FamilyTreeEventTypeConfigurationMapping(
+ ConfigurationMapping[str, FamilyTreeEventTypeConfiguration]
+):
+ """
+ Configure how to map Gramps events to Betty events.
+ """
+
+ def __init__(
+ self, configurations: Iterable[FamilyTreeEventTypeConfiguration] | None = None
+ ):
+ if configurations is None:
+ configurations = [
+ FamilyTreeEventTypeConfiguration("Adopted", "adoption"),
+ FamilyTreeEventTypeConfiguration("Baptism", "baptism"),
+ FamilyTreeEventTypeConfiguration("Birth", "birth"),
+ FamilyTreeEventTypeConfiguration("Burial", "burial"),
+ FamilyTreeEventTypeConfiguration("Confirmation", "confirmation"),
+ FamilyTreeEventTypeConfiguration("Cremation", "cremation"),
+ FamilyTreeEventTypeConfiguration("Death", "death"),
+ FamilyTreeEventTypeConfiguration("Divorce", "divorce"),
+ FamilyTreeEventTypeConfiguration(
+ "Divorce Filing", "divorce-announcement"
+ ),
+ FamilyTreeEventTypeConfiguration("Emigration", "emigration"),
+ FamilyTreeEventTypeConfiguration("Engagement", "engagement"),
+ FamilyTreeEventTypeConfiguration("Immigration", "immigration"),
+ FamilyTreeEventTypeConfiguration("Marriage", "marriage"),
+ FamilyTreeEventTypeConfiguration(
+ "Marriage Banns", "marriage-announcement"
+ ),
+ FamilyTreeEventTypeConfiguration("Occupation", "occupation"),
+ FamilyTreeEventTypeConfiguration("Residence", "residence"),
+ FamilyTreeEventTypeConfiguration("Retirement", "retirement"),
+ FamilyTreeEventTypeConfiguration("Will", "will"),
+ ]
+ super().__init__(configurations)
+
+ @override
+ def _minimize_item_dump(self) -> bool:
+ return True
+
+ @override
+ def _get_key(self, configuration: FamilyTreeEventTypeConfiguration) -> str:
+ return configuration.gramps_event_type
+
+ @override
+ def _load_key(self, item_dump: Dump, key_dump: str) -> Dump:
+ mapping_dump = assert_mapping()(item_dump)
+ mapping_dump["gramps_event_type"] = _assert_gramps_event_type(key_dump)
+ return mapping_dump
+
+ @override
+ def _dump_key(self, item_dump: VoidableDump) -> tuple[VoidableDump, str]:
+ mapping_dump = assert_mapping()(item_dump)
+ return mapping_dump, mapping_dump.pop("entity_type")
+
+ @override
+ def load_item(self, dump: Dump) -> FamilyTreeEventTypeConfiguration:
+ # Use dummy configuration for now to satisfy the initializer.
+ # It will be overridden when loading the dump.
+ configuration = FamilyTreeEventTypeConfiguration("-", "-")
+ configuration.load(dump)
+ return configuration
+
+
class FamilyTreeConfiguration(Configuration):
"""
Configure a single Gramps family tree.
"""
- def __init__(self, file_path: Path):
+ def __init__(
+ self,
+ file_path: Path,
+ *,
+ event_types: Iterable[FamilyTreeEventTypeConfiguration] | None = None,
+ ):
super().__init__()
self.file_path = file_path
+ self._event_types = FamilyTreeEventTypeConfigurationMapping(event_types)
@override
def __eq__(self, other: Any) -> bool:
@@ -47,6 +186,13 @@ def file_path(self) -> Path | None:
def file_path(self, file_path: Path | None) -> None:
self._file_path = file_path
+ @property
+ def event_types(self) -> FamilyTreeEventTypeConfigurationMapping:
+ """
+ How to map event types.
+ """
+ return self._event_types
+
@override
def load(self, dump: Dump) -> None:
assert_record(
@@ -82,9 +228,7 @@ class GrampsConfiguration(Configuration):
"""
def __init__(
- self,
- *,
- family_trees: Iterable[FamilyTreeConfiguration] | None = None,
+ self, *, family_trees: Iterable[FamilyTreeConfiguration] | None = None
):
super().__init__()
self._family_trees = FamilyTreeConfigurationSequence(family_trees)
@@ -106,9 +250,4 @@ def load(self, dump: Dump) -> None:
@override
def dump(self) -> VoidableDump:
- return minimize(
- {
- "family_trees": self.family_trees.dump(),
- },
- True,
- )
+ return minimize({"family_trees": self.family_trees.dump()}, True)
diff --git a/betty/gramps/loader.py b/betty/gramps/loader.py
index 692fd6bdb..6a53af97c 100644
--- a/betty/gramps/loader.py
+++ b/betty/gramps/loader.py
@@ -44,30 +44,8 @@
Ancestry,
)
from betty.ancestry.event_type import (
- Birth,
- Baptism,
- Adoption,
- Cremation,
- Death,
- Funeral,
- Burial,
- Will,
- Engagement,
- Marriage,
- MarriageAnnouncement,
- Divorce,
- DivorceAnnouncement,
- Residence,
- Immigration,
- Emigration,
- Occupation,
- Retirement,
- Correspondence,
- Confirmation,
- Missing,
UnknownEventType,
EventType,
- Conference,
)
from betty.ancestry.presence_role import (
Subject,
@@ -156,6 +134,7 @@ def __init__(
factory: Factory[Any],
localizer: Localizer,
attribute_prefix_key: str | None = None,
+ event_type_map: Mapping[str, type[EventType]] | None = None,
):
super().__init__()
self._ancestry = ancestry
@@ -169,6 +148,7 @@ def __init__(
self._gramps_tree_directory_path: Path | None = None
self._loaded = False
self._localizer = localizer
+ self._event_type_map = event_type_map or {}
async def load_file(self, file_path: Path) -> None:
"""
@@ -759,31 +739,6 @@ def _load_events(self, database: ElementTree.Element) -> None:
for element in self._xpath(database, "./ns:events/ns:event"):
self._load_event(element)
- _EVENT_TYPE_MAP = {
- "Birth": Birth,
- "Baptism": Baptism,
- "Adopted": Adoption,
- "Cremation": Cremation,
- "Death": Death,
- "Funeral": Funeral,
- "Burial": Burial,
- "Will": Will,
- "Engagement": Engagement,
- "Marriage": Marriage,
- "Marriage Banns": MarriageAnnouncement,
- "Divorce": Divorce,
- "Divorce Filing": DivorceAnnouncement,
- "Residence": Residence,
- "Immigration": Immigration,
- "Emigration": Emigration,
- "Occupation": Occupation,
- "Retirement": Retirement,
- "Correspondence": Correspondence,
- "Confirmation": Confirmation,
- "Missing": Missing,
- "Conference": Conference,
- }
-
def _load_event(self, element: ElementTree.Element) -> None:
event_handle = element.get("handle")
event_id = element.get("id")
@@ -792,7 +747,7 @@ def _load_event(self, element: ElementTree.Element) -> None:
assert gramps_type is not None
try:
- event_type: EventType = self._EVENT_TYPE_MAP[gramps_type]()
+ event_type: EventType = self._event_type_map[gramps_type]()
except KeyError:
event_type = UnknownEventType()
getLogger(__name__).warning(
diff --git a/betty/locale/localizable/__init__.py b/betty/locale/localizable/__init__.py
index 9f1f776d0..4344a2b94 100644
--- a/betty/locale/localizable/__init__.py
+++ b/betty/locale/localizable/__init__.py
@@ -15,14 +15,15 @@
from betty.classtools import repr_instance
from betty.json.linked_data import LinkedDataDumpable
from betty.json.schema import Object
-from betty.locale import negotiate_locale, to_locale, UNDETERMINED_LOCALE
+from betty.locale import UNDETERMINED_LOCALE
from betty.locale.localized import LocalizedStr
-from betty.locale.localizer import DEFAULT_LOCALIZER
-from betty.locale.localizer import Localizer
if TYPE_CHECKING:
from betty.serde.dump import DumpMapping, Dump
from betty.project import Project
+from betty.locale import negotiate_locale, to_locale
+from betty.locale.localizer import DEFAULT_LOCALIZER
+from betty.locale.localizer import Localizer
class Localizable(ABC):
diff --git a/betty/plugin/config.py b/betty/plugin/config.py
new file mode 100644
index 000000000..16d08ad96
--- /dev/null
+++ b/betty/plugin/config.py
@@ -0,0 +1,153 @@
+"""
+Provide plugin configuration.
+"""
+
+from collections.abc import Sequence
+from typing import Self, TypeVar, Generic
+
+from typing_extensions import override
+
+from betty.assertion import (
+ RequiredField,
+ assert_str,
+ assert_record,
+ OptionalField,
+ assert_setattr,
+ assert_mapping,
+)
+from betty.config import Configuration
+from betty.config.collections.mapping import ConfigurationMapping
+from betty.locale.localizable import ShorthandStaticTranslations
+from betty.locale.localizable.config import (
+ StaticTranslationsLocalizableConfigurationAttr,
+)
+from betty.machine_name import assert_machine_name, MachineName
+from betty.plugin import Plugin
+from betty.serde.dump import Dump, minimize, VoidableDump
+
+_PluginCoT = TypeVar("_PluginCoT", bound=Plugin, covariant=True)
+
+
+class PluginConfiguration(Configuration):
+ """
+ Configure a single plugin.
+ """
+
+ label = StaticTranslationsLocalizableConfigurationAttr("label")
+ description = StaticTranslationsLocalizableConfigurationAttr(
+ "description", required=False
+ )
+
+ def __init__(
+ self,
+ plugin_id: MachineName,
+ label: ShorthandStaticTranslations,
+ *,
+ description: ShorthandStaticTranslations | None = None,
+ ):
+ super().__init__()
+ self._id = assert_machine_name()(plugin_id)
+ self.label = label
+ if description is not None:
+ self.description = description
+
+ @property
+ def id(self) -> str:
+ """
+ The configured plugin ID.
+ """
+ return self._id
+
+ @override
+ def update(self, other: Self) -> None:
+ self._id = other.id
+ self.label.update(other.label)
+ self.description.update(other.description)
+
+ @override
+ def load(self, dump: Dump) -> None:
+ assert_record(
+ RequiredField("id", assert_machine_name() | assert_setattr(self, "_id")),
+ RequiredField("label", self.label.load),
+ OptionalField("description", self.description.load),
+ )(dump)
+
+ @override
+ def dump(self) -> VoidableDump:
+ return minimize(
+ {
+ "id": self.id,
+ "label": self.label.dump(),
+ "description": self.description.dump(),
+ }
+ )
+
+
+_PluginConfigurationT = TypeVar("_PluginConfigurationT", bound=PluginConfiguration)
+
+
+class PluginConfigurationMapping(
+ ConfigurationMapping[str, _PluginConfigurationT],
+ Generic[_PluginCoT, _PluginConfigurationT],
+):
+ """
+ Configure a collection of plugins.
+ """
+
+ @property
+ def plugins(self) -> Sequence[type[_PluginCoT]]:
+ """
+ The plugins for this configuration.
+
+ You SHOULD NOT cache the value anywhere, as it *will* change
+ when this configuration changes.
+ """
+ return tuple(
+ self._create_plugin(plugin_configuration)
+ for plugin_configuration in self.values()
+ )
+
+ def _create_plugin(self, configuration: _PluginConfigurationT) -> type[_PluginCoT]:
+ """
+ The plugin (class) for the given configuration.
+ """
+ raise NotImplementedError
+
+ @override
+ def _minimize_item_dump(self) -> bool:
+ return True
+
+ @override
+ def _get_key(self, configuration: _PluginConfigurationT) -> str:
+ return configuration.id
+
+ @override
+ @classmethod
+ def _load_key(cls, item_dump: Dump, key_dump: str) -> Dump:
+ dump_mapping = assert_mapping()(item_dump)
+ assert_str()(key_dump)
+ dump_mapping["id"] = key_dump
+ return dump_mapping
+
+ @override
+ def _dump_key(self, item_dump: VoidableDump) -> tuple[VoidableDump, str]:
+ dump_mapping = assert_mapping()(item_dump)
+ return dump_mapping, dump_mapping.pop("id")
+
+
+class PluginConfigurationPluginConfigurationMapping(
+ PluginConfigurationMapping[_PluginCoT, PluginConfiguration], Generic[_PluginCoT]
+):
+ """
+ Configure a collection of plugins using :py:class:`betty.plugin.config.PluginConfiguration`.
+ """
+
+ @override
+ def load_item(self, dump: Dump) -> PluginConfiguration:
+ item = PluginConfiguration("-", "")
+ item.load(dump)
+ return item
+
+ @classmethod
+ def _create_default_item(cls, configuration_key: str) -> PluginConfiguration:
+ return PluginConfiguration(configuration_key, {})
diff --git a/betty/project/__init__.py b/betty/project/__init__.py
index 9f44636c6..cc17f04c0 100644
--- a/betty/project/__init__.py
+++ b/betty/project/__init__.py
@@ -27,6 +27,7 @@
from betty import fs, event_dispatcher
from betty.ancestry import Ancestry
+from betty.ancestry.event_type import EVENT_TYPE_REPOSITORY
from betty.assets import AssetRepository
from betty.asyncio import wait_to_thread
from betty.config import (
@@ -44,6 +45,8 @@
from betty.locale.localizable import _
from betty.locale.localizer import LocalizerRepository
from betty.model import Entity, EntityReferenceCollectionSchema
+from betty.plugin.proxy import ProxyPluginRepository
+from betty.plugin.static import StaticPluginRepository
from betty.project import extension
from betty.project.config import ProjectConfiguration
from betty.project.extension import (
@@ -58,6 +61,7 @@
from betty.typing import internal
if TYPE_CHECKING:
+ from betty.ancestry.event_type import EventType
from betty.machine_name import MachineName
from betty.plugin import PluginIdentifier
from collections.abc import Sequence
@@ -65,9 +69,11 @@
from betty.app import App
from betty.url import LocalizedUrlGenerator, StaticUrlGenerator
from betty.jinja2 import Environment
+ from betty.plugin import PluginRepository
_EntityT = TypeVar("_EntityT", bound=Entity)
+
_ProjectDependentT = TypeVar("_ProjectDependentT")
@@ -100,6 +106,7 @@ def __init__(
self._extensions: ProjectExtensions | None = None
self._event_dispatcher: EventDispatcher | None = None
self._entity_types: set[type[Entity]] | None = None
+ self._event_types: PluginRepository[EventType] | None = None
@classmethod
@asynccontextmanager
@@ -328,6 +335,20 @@ def logo(self) -> Path:
or fs.ASSETS_DIRECTORY_PATH / "public" / "static" / "betty-512x512.png"
)
+ @property
+ def event_types(self) -> PluginRepository[EventType]:
+ """
+ The event types available to this project.
+ """
+ if self._event_types is None:
+ self._assert_bootstrapped()
+ self._event_types = ProxyPluginRepository(
+ EVENT_TYPE_REPOSITORY,
+ StaticPluginRepository(*self.configuration.event_types.plugins),
+ )
+
+ return self._event_types
+
_ExtensionT = TypeVar("_ExtensionT", bound=Extension)
diff --git a/betty/project/config.py b/betty/project/config.py
index 3bf0d25d3..d0a641c7a 100644
--- a/betty/project/config.py
+++ b/betty/project/config.py
@@ -12,6 +12,8 @@
from betty import model
from betty.ancestry import Person, Event, Place, Source
+from betty.ancestry.event_type import EventType
+from betty.ancestry.event_type import _EventTypeShorthandBase
from betty.assertion import (
assert_record,
RequiredField,
@@ -36,12 +38,17 @@
)
from betty.config.collections.sequence import ConfigurationSequence
from betty.locale import DEFAULT_LOCALE, UNDETERMINED_LOCALE
-from betty.locale.localizable import _, ShorthandStaticTranslations
+from betty.locale.localizable import _, ShorthandStaticTranslations, Localizable
from betty.locale.localizable.config import (
StaticTranslationsLocalizableConfigurationAttr,
)
from betty.model import Entity, UserFacingEntity
from betty.plugin.assertion import assert_plugin
+from betty.plugin.config import (
+ PluginConfigurationPluginConfigurationMapping,
+ PluginConfiguration,
+ PluginConfigurationMapping,
+)
from betty.project import extension
from betty.project.extension import Extension, ConfigurableExtension
from betty.serde.dump import (
@@ -374,11 +381,7 @@ def _get_key(self, configuration: ExtensionConfiguration) -> type[Extension]:
return configuration.extension_type
@override
- def _load_key(
- self,
- item_dump: Dump,
- key_dump: str,
- ) -> Dump:
+ def _load_key(self, item_dump: Dump, key_dump: str) -> Dump:
mapping_dump = assert_mapping()(item_dump)
mapping_dump["extension"] = key_dump
return mapping_dump
@@ -488,11 +491,7 @@ def _get_key(self, configuration: EntityTypeConfiguration) -> type[Entity]:
return configuration.entity_type
@override
- def _load_key(
- self,
- item_dump: Dump,
- key_dump: str,
- ) -> Dump:
+ def _load_key(self, item_dump: Dump, key_dump: str) -> Dump:
mapping_dump = assert_mapping()(item_dump)
assert_plugin(model.ENTITY_TYPE_REPOSITORY)(key_dump)
mapping_dump["entity_type"] = key_dump
@@ -634,6 +633,26 @@ def multilingual(self) -> bool:
return len(self) > 1
+class EventTypeConfigurationMapping(
+ PluginConfigurationPluginConfigurationMapping[EventType]
+):
+ """
+ A configuration mapping for event types.
+ """
+
+ @override
+ def _create_plugin(self, configuration: PluginConfiguration) -> type[EventType]:
+ class _ProjectConfigurationEventType(_EventTypeShorthandBase):
+ _plugin_id = configuration.id
+ _plugin_label = configuration.label
+
+ @classmethod
+ def plugin_description(cls) -> Localizable | None:
+ return configuration.description
+
+ return _ProjectConfigurationEventType
+
+
@final
class ProjectConfiguration(Configuration):
"""
@@ -652,6 +671,7 @@ def __init__(
title: ShorthandStaticTranslations = "Betty",
author: ShorthandStaticTranslations | None = None,
entity_types: Iterable[EntityTypeConfiguration] | None = None,
+ event_types: Iterable[PluginConfiguration] | None = None,
extensions: Iterable[ExtensionConfiguration] | None = None,
debug: bool = False,
locales: Iterable[LocaleConfiguration] | None = None,
@@ -689,6 +709,9 @@ def __init__(
),
]
)
+ self._event_types = EventTypeConfigurationMapping()
+ if event_types is not None:
+ self._event_types.append(*event_types)
self._extensions = ExtensionConfigurationMapping(extensions or ())
self._debug = debug
self._locales = LocaleConfigurationMapping(locales or ())
@@ -880,6 +903,13 @@ def logo(self) -> Path | None:
def logo(self, logo: Path | None) -> None:
self._logo = logo
+ @property
+ def event_types(self) -> PluginConfigurationMapping[EventType, PluginConfiguration]:
+ """
+ The event types.
+ """
+ return self._event_types
+
@override
def update(self, other: Self) -> None:
self._url = other._url
@@ -913,6 +943,7 @@ def load(self, dump: Dump) -> None:
OptionalField("locales", self.locales.load),
OptionalField("extensions", self.extensions.load),
OptionalField("entity_types", self.entity_types.load),
+ OptionalField("event_types", self.event_types.load),
)(dump)
@override
@@ -930,6 +961,7 @@ def dump(self) -> VoidableDumpMapping[Dump]:
"locales": self.locales.dump(),
"extensions": self.extensions.dump(),
"entity_types": self.entity_types.dump(),
+ "event_types": self.event_types.dump(),
},
True,
)
diff --git a/betty/test_utils/plugin/config.py b/betty/test_utils/plugin/config.py
new file mode 100644
index 000000000..090532963
--- /dev/null
+++ b/betty/test_utils/plugin/config.py
@@ -0,0 +1,26 @@
+"""
+Test utilities for :py:mod:`betty.plugin.config`.
+"""
+
+from typing import TypeVar, Generic
+
+from betty.machine_name import MachineName
+from betty.plugin.config import PluginConfiguration
+from betty.test_utils.config.collections.mapping import ConfigurationMappingTestBase
+
+_PluginConfigurationT = TypeVar("_PluginConfigurationT", bound=PluginConfiguration)
+
+
+class PluginConfigurationMappingTestBase(
+ ConfigurationMappingTestBase[MachineName, _PluginConfigurationT],
+ Generic[_PluginConfigurationT],
+):
+ """
+ A base class for testing :py:class:`betty.plugin.config.PluginConfigurationMapping` implementations.
+ """
+
+ def test_plugins(self) -> None:
+ """
+ Tests :py:meth:`betty.plugin.config.PluginConfigurationMapping.plugins` implementations.
+ """
+ raise AssertionError
diff --git a/betty/tests/app/test___init__.py b/betty/tests/app/test___init__.py
index 68dbd6cf4..e4ea73418 100644
--- a/betty/tests/app/test___init__.py
+++ b/betty/tests/app/test___init__.py
@@ -1,6 +1,7 @@
from __future__ import annotations
from typing import Self
+
from typing_extensions import override
from betty.app import App
diff --git a/betty/tests/config/collections/test_mapping.py b/betty/tests/config/collections/test_mapping.py
index 613bec97b..f32950b22 100644
--- a/betty/tests/config/collections/test_mapping.py
+++ b/betty/tests/config/collections/test_mapping.py
@@ -7,6 +7,8 @@
TYPE_CHECKING,
)
+from typing_extensions import override
+
from betty.assertion import (
assert_record,
RequiredField,
@@ -22,7 +24,6 @@
)
from betty.test_utils.config.collections.mapping import ConfigurationMappingTestBase
from betty.typing import Void
-from typing_extensions import override
if TYPE_CHECKING:
from betty.serde.dump import Dump, VoidableDump
@@ -127,11 +128,7 @@ def load_item(self, dump: Dump) -> ConfigurationMappingTestConfiguration:
def _get_key(self, configuration: ConfigurationMappingTestConfiguration) -> str:
return configuration.key
- def _load_key(
- self,
- item_dump: Dump,
- key_dump: str,
- ) -> Dump:
+ def _load_key(self, item_dump: Dump, key_dump: str) -> Dump:
mapping_item_dump = assert_mapping()(item_dump)
mapping_item_dump["key"] = key_dump
return mapping_item_dump
diff --git a/betty/tests/coverage/test_coverage.py b/betty/tests/coverage/test_coverage.py
index c41e419ba..6c1c3ab73 100644
--- a/betty/tests/coverage/test_coverage.py
+++ b/betty/tests/coverage/test_coverage.py
@@ -523,6 +523,10 @@ class TestKnownToBeMissing:
"betty/plugin/assertion.py": {
"assert_plugin": TestKnownToBeMissing,
},
+ "betty/plugin/config.py": {
+ # This is tested as part of PluginConfigurationPluginConfigurationMapping.
+ "PluginConfigurationMapping": TestKnownToBeMissing,
+ },
"betty/plugin/lazy.py": TestKnownToBeMissing,
"betty/privatizer.py": {
"Privatizer": {
@@ -616,6 +620,7 @@ class TestKnownToBeMissing:
"betty/test_utils/locale.py": TestKnownToBeMissing,
"betty/test_utils/model/__init__.py": TestKnownToBeMissing,
"betty/test_utils/plugin/__init__.py": TestKnownToBeMissing,
+ "betty/test_utils/plugin/config.py": TestKnownToBeMissing,
"betty/test_utils/project/extension/__init__.py": TestKnownToBeMissing,
"betty/test_utils/serve.py": TestKnownToBeMissing,
},
diff --git a/betty/tests/extension/gramps/test___init__.py b/betty/tests/extension/gramps/test___init__.py
index 58129089c..2e8dede29 100644
--- a/betty/tests/extension/gramps/test___init__.py
+++ b/betty/tests/extension/gramps/test___init__.py
@@ -5,9 +5,14 @@
from typing_extensions import override
from betty.ancestry import Citation, Note, Source, File, Event, Person, Place
+from betty.ancestry.event_type import Birth
from betty.app import App
from betty.extension.gramps import Gramps
-from betty.extension.gramps.config import FamilyTreeConfiguration, GrampsConfiguration
+from betty.extension.gramps.config import (
+ FamilyTreeConfiguration,
+ GrampsConfiguration,
+ FamilyTreeEventTypeConfiguration,
+)
from betty.load import load
from betty.project import Project
from betty.project.config import ExtensionConfiguration
@@ -19,6 +24,51 @@ class TestGramps(ExtensionTestBase):
def get_sut_class(self) -> type[Gramps]:
return Gramps
+ async def test_load_with_event_type_map(
+ self, new_temporary_app: App, tmp_path: Path
+ ) -> None:
+ family_tree_xml = """
+
+
+
+
+
+
+ Birth
+
+
+
+
+""".strip()
+ gramps_family_tree_path = tmp_path / "gramps.xml"
+ async with aiofiles.open(gramps_family_tree_path, mode="w") as f:
+ await f.write(family_tree_xml)
+
+ async with Project.new_temporary(new_temporary_app) as project:
+ project.configuration.extensions.append(
+ ExtensionConfiguration(
+ Gramps,
+ extension_configuration=GrampsConfiguration(
+ family_trees=[
+ FamilyTreeConfiguration(
+ file_path=gramps_family_tree_path,
+ event_types=[
+ FamilyTreeEventTypeConfiguration("Birth", "birth")
+ ],
+ )
+ ],
+ ),
+ )
+ )
+ async with project:
+ await load(project)
+ assert isinstance(project.ancestry[Event]["E0000"].event_type, Birth)
+
async def test_load_multiple_family_trees(self, new_temporary_app: App) -> None:
family_tree_one_xml = """
diff --git a/betty/tests/extension/gramps/test_config.py b/betty/tests/extension/gramps/test_config.py
index 889656816..f63d8a1cb 100644
--- a/betty/tests/extension/gramps/test_config.py
+++ b/betty/tests/extension/gramps/test_config.py
@@ -1,20 +1,24 @@
from collections.abc import Iterable, Mapping
from pathlib import Path
-from typing import Any, TYPE_CHECKING
+from typing import Any
+from typing_extensions import override
+
+import pytest
from betty.assertion.error import AssertionFailed
from betty.extension.gramps.config import (
FamilyTreeConfiguration,
GrampsConfiguration,
FamilyTreeConfigurationSequence,
+ FamilyTreeEventTypeConfiguration,
+ FamilyTreeEventTypeConfigurationMapping,
)
+from betty.serde.dump import Dump
from betty.test_utils.assertion.error import raises_error
+from betty.test_utils.config.collections.mapping import ConfigurationMappingTestBase
from betty.test_utils.config.collections.sequence import ConfigurationSequenceTestBase
from betty.typing import Void
-if TYPE_CHECKING:
- from betty.serde.dump import Dump
-
class TestFamilyTreeConfigurationSequence(
ConfigurationSequenceTestBase[FamilyTreeConfiguration]
@@ -41,6 +45,10 @@ def get_configurations(
class TestFamilyTreeConfiguration:
+ def test_event_types(self, tmp_path: Path) -> None:
+ sut = FamilyTreeConfiguration(tmp_path)
+ assert len(sut.event_types)
+
async def test_load_with_minimal_configuration(self, tmp_path: Path) -> None:
file_path = tmp_path / "ancestry.gramps"
dump: Mapping[str, Any] = {"file": str(file_path)}
@@ -91,6 +99,98 @@ async def test___eq___is_not_equal(self, tmp_path: Path) -> None:
assert sut != other
+class TestFamilyTreeEventTypeConfiguration:
+ async def test_gramps_event_type(self) -> None:
+ gramps_event_type = "my-first-gramps-event-type"
+ sut = FamilyTreeEventTypeConfiguration(
+ gramps_event_type, "my-first-betty-event-type"
+ )
+ assert sut.gramps_event_type == gramps_event_type
+
+ async def test_event_type_id(self) -> None:
+ event_type_id = "my-first-betty-event-type"
+ sut = FamilyTreeEventTypeConfiguration(
+ "my-first-gramps-event-type", event_type_id
+ )
+ assert sut.event_type_id == event_type_id
+
+ async def test_load(self) -> None:
+ gramps_event_type = "my-first-gramps-event-type"
+ event_type_id = "my-first-betty-event-type"
+ dump: Dump = {
+ "gramps_event_type": gramps_event_type,
+ "event_type": event_type_id,
+ }
+ sut = FamilyTreeEventTypeConfiguration("-", "-")
+ sut.load(dump)
+ assert sut.gramps_event_type == gramps_event_type
+ assert sut.event_type_id == event_type_id
+
+ @pytest.mark.parametrize(
+ "dump",
+ [
+ {},
+ {"gramps_event_type": "-"},
+ {"event_type": "-"},
+ ],
+ )
+ async def test_load_with_invalid_dump_should_error(self, dump: Dump) -> None:
+ sut = FamilyTreeEventTypeConfiguration("-", "-")
+ with pytest.raises(AssertionFailed):
+ sut.load(dump)
+
+ async def test_dump(self) -> None:
+ gramps_event_type = "my-first-gramps-event-type"
+ event_type_id = "my-first-betty-event-type"
+ sut = FamilyTreeEventTypeConfiguration(gramps_event_type, event_type_id)
+ dump = sut.dump()
+ assert dump == {
+ "gramps_event_type": gramps_event_type,
+ "event_type": event_type_id,
+ }
+
+ async def test_update(self) -> None:
+ gramps_event_type = "my-first-gramps-event-type"
+ event_type_id = "my-first-betty-event-type"
+ other = FamilyTreeEventTypeConfiguration(gramps_event_type, event_type_id)
+ sut = FamilyTreeEventTypeConfiguration("-", "-")
+ sut.update(other)
+ assert sut.gramps_event_type == gramps_event_type
+ assert sut.event_type_id == event_type_id
+
+
+class TestFamilyTreeEventTypeConfigurationMapping(
+ ConfigurationMappingTestBase[str, FamilyTreeEventTypeConfiguration]
+):
+ @override
+ def get_configuration_keys(
+ self,
+ ) -> tuple[str, str, str, str]:
+ return "gramps-foo", "gramps-bar", "gramps-baz", "gramps-qux"
+
+ @override
+ def get_configurations(
+ self,
+ ) -> tuple[
+ FamilyTreeEventTypeConfiguration,
+ FamilyTreeEventTypeConfiguration,
+ FamilyTreeEventTypeConfiguration,
+ FamilyTreeEventTypeConfiguration,
+ ]:
+ return (
+ FamilyTreeEventTypeConfiguration("gramps-foo", "betty-foo"),
+ FamilyTreeEventTypeConfiguration("gramps-bar", "betty-bar"),
+ FamilyTreeEventTypeConfiguration("gramps-baz", "betty-baz"),
+ FamilyTreeEventTypeConfiguration("gramps-qux", "betty-qux"),
+ )
+
+ @override
+ def get_sut(
+ self, configurations: Iterable[FamilyTreeEventTypeConfiguration] | None = None
+ ) -> FamilyTreeEventTypeConfigurationMapping:
+ return FamilyTreeEventTypeConfigurationMapping(configurations)
+
+
class TestGrampsConfiguration:
async def test_load_with_minimal_configuration(self) -> None:
dump: Mapping[str, Any] = {}
diff --git a/betty/tests/gramps/test_loader.py b/betty/tests/gramps/test_loader.py
index 99538d734..a212877ec 100644
--- a/betty/tests/gramps/test_loader.py
+++ b/betty/tests/gramps/test_loader.py
@@ -1,6 +1,7 @@
from __future__ import annotations
from pathlib import Path
+from typing import TYPE_CHECKING
import aiofiles
import pytest
@@ -17,7 +18,7 @@
Place,
Privacy,
)
-from betty.ancestry.event_type import Birth, Death, UnknownEventType
+from betty.ancestry.event_type import Birth, Death, UnknownEventType, EventType
from betty.ancestry.presence_role import Attendee
from betty.app import App
from betty.gramps.error import UserFacingGrampsError
@@ -29,6 +30,9 @@
from betty.path import rootname
from betty.project import Project
+if TYPE_CHECKING:
+ from collections.abc import Mapping
+
class TestGrampsLoader:
ATTRIBUTE_PREFIX_KEY = "pre3f1x"
@@ -127,7 +131,9 @@ async def test_load_file_with_invalid_file(
Path(__file__).parent / "assets" / "minimal.invalid"
)
- async def _load(self, xml: str) -> Ancestry:
+ async def _load(
+ self, xml: str, *, event_type_map: Mapping[str, type[EventType]] | None = None
+ ) -> Ancestry:
async with (
App.new_temporary() as app,
app,
@@ -135,12 +141,12 @@ async def _load(self, xml: str) -> Ancestry:
):
project.configuration.name = TestGrampsLoader.__name__
async with project:
- # @todo We need to be able to customize the loader
loader = GrampsLoader(
project.ancestry,
factory=project.new,
localizer=DEFAULT_LOCALIZER,
attribute_prefix_key=self.ATTRIBUTE_PREFIX_KEY,
+ event_type_map=event_type_map,
)
async with TemporaryDirectory() as tree_directory_path_str:
await loader.load_xml(
@@ -149,7 +155,9 @@ async def _load(self, xml: str) -> Ancestry:
)
return project.ancestry
- async def _load_partial(self, xml: str) -> Ancestry:
+ async def _load_partial(
+ self, xml: str, *, event_type_map: Mapping[str, type[EventType]] | None = None
+ ) -> Ancestry:
return await self._load(
f"""
@@ -163,7 +171,8 @@ async def _load_partial(self, xml: str) -> Ancestry:
{xml}
-"""
+""",
+ event_type_map=event_type_map,
)
async def test_load_xml(self, new_temporary_app: App) -> None:
@@ -350,7 +359,8 @@ async def test_person_should_include_birth(self) -> None:
-"""
+""",
+ event_type_map={"Birth": Birth},
)
person = ancestry[Person]["I0000"]
birth = [
@@ -378,7 +388,8 @@ async def test_person_should_include_death(self) -> None:
-"""
+""",
+ event_type_map={"Death": Death},
)
person = ancestry[Person]["I0000"]
death = [
@@ -567,7 +578,8 @@ async def test_event_should_be_birth(self) -> None:
Birth
-"""
+""",
+ event_type_map={"Birth": Birth},
)
assert isinstance(ancestry[Event]["E0000"].event_type, Birth)
@@ -579,7 +591,8 @@ async def test_event_should_be_death(self) -> None:
Death
-"""
+""",
+ event_type_map={"Death": Death},
)
assert isinstance(ancestry[Event]["E0000"].event_type, Death)
diff --git a/betty/tests/plugin/test_config.py b/betty/tests/plugin/test_config.py
new file mode 100644
index 000000000..93ab81070
--- /dev/null
+++ b/betty/tests/plugin/test_config.py
@@ -0,0 +1,212 @@
+from collections.abc import Iterable
+from typing import TYPE_CHECKING
+
+import pytest
+
+from betty.config.collections import ConfigurationCollection
+from betty.locale import UNDETERMINED_LOCALE
+from betty.locale.localizable import ShorthandStaticTranslations
+from betty.locale.localizer import DEFAULT_LOCALIZER
+from betty.machine_name import MachineName
+from betty.plugin.config import (
+ PluginConfiguration,
+ PluginConfigurationPluginConfigurationMapping,
+)
+from betty.test_utils.config.collections.mapping import ConfigurationMappingTestBase
+
+if TYPE_CHECKING:
+ from betty.serde.dump import Dump
+
+
+class TestPluginConfiguration:
+ async def test_update(self) -> None:
+ sut = PluginConfiguration("hello-world", "")
+ other = PluginConfiguration(
+ "hello-other-world",
+ "Hello, other world!",
+ description="Hello, very big other world!",
+ )
+ sut.update(other)
+ assert sut.id == "hello-other-world"
+ assert sut.label[UNDETERMINED_LOCALE] == "Hello, other world!"
+ assert sut.description[UNDETERMINED_LOCALE] == "Hello, very big other world!"
+
+ async def test_load(self) -> None:
+ plugin_id = "hello-world"
+ dump: Dump = {
+ "id": plugin_id,
+ "label": "",
+ }
+ sut = PluginConfiguration("-", "")
+ sut.load(dump)
+ assert sut.id == plugin_id
+
+ async def test_load_with_undetermined_label(self) -> None:
+ label = "Hello, world!"
+ dump: Dump = {
+ "id": "hello-world",
+ "label": label,
+ }
+ sut = PluginConfiguration("-", "")
+ sut.load(dump)
+ assert sut.label[UNDETERMINED_LOCALE] == label
+
+ async def test_load_with_expanded_label(self) -> None:
+ label = "Hello, world!"
+ dump: Dump = {
+ "id": "hello-world",
+ "label": {
+ DEFAULT_LOCALIZER.locale: label,
+ },
+ }
+ sut = PluginConfiguration("-", "")
+ sut.load(dump)
+ assert sut.label[DEFAULT_LOCALIZER.locale] == label
+
+ async def test_load_with_undetermined_description(self) -> None:
+ description = "Hello, world!"
+ dump: Dump = {
+ "id": "hello-world",
+ "label": "",
+ "description": description,
+ }
+ sut = PluginConfiguration("-", "")
+ sut.load(dump)
+ assert sut.description[UNDETERMINED_LOCALE] == description
+
+ async def test_load_with_expanded_description(self) -> None:
+ description = "Hello, world!"
+ dump: Dump = {
+ "id": "hello-world",
+ "label": "",
+ "description": {
+ DEFAULT_LOCALIZER.locale: description,
+ },
+ }
+ sut = PluginConfiguration("-", "")
+ sut.load(dump)
+ assert sut.description[DEFAULT_LOCALIZER.locale] == description
+
+ async def test_dump(self) -> None:
+ plugin_id = "hello-world"
+ sut = PluginConfiguration(plugin_id, "")
+ dump = sut.dump()
+ assert isinstance(dump, dict)
+ assert dump["id"] == plugin_id
+
+ async def test_dump_with_undetermined_label(self) -> None:
+ label = "Hello, world!"
+ sut = PluginConfiguration("hello-world", label)
+ dump = sut.dump()
+ assert isinstance(dump, dict)
+ assert dump["label"] == label
+
+ async def test_dump_with_expanded_label(self) -> None:
+ label = "Hello, world!"
+ sut = PluginConfiguration("hello-world", {DEFAULT_LOCALIZER.locale: label})
+ dump = sut.dump()
+ assert isinstance(dump, dict)
+ assert dump["label"] == {
+ DEFAULT_LOCALIZER.locale: label,
+ }
+
+ async def test_dump_with_undetermined_description(self) -> None:
+ description = "Hello, world!"
+ sut = PluginConfiguration("hello-world", "", description=description)
+ dump = sut.dump()
+ assert isinstance(dump, dict)
+ assert dump["description"] == description
+
+ async def test_dump_with_expanded_description(self) -> None:
+ description = "Hello, world!"
+ sut = PluginConfiguration(
+ "hello-world",
+ "",
+ description={DEFAULT_LOCALIZER.locale: description},
+ )
+ dump = sut.dump()
+ assert isinstance(dump, dict)
+ assert dump["description"] == {
+ DEFAULT_LOCALIZER.locale: description,
+ }
+
+ async def test_id(self) -> None:
+ plugin_id = "hello-world"
+ sut = PluginConfiguration(plugin_id, "")
+ assert sut.id == plugin_id
+
+ @pytest.mark.parametrize(
+ ("expected_locale", "expected_label", "init_label"),
+ [
+ ("und", "Hello, world!", "Hello, world!"),
+ (
+ DEFAULT_LOCALIZER.locale,
+ "Hello, world!",
+ {DEFAULT_LOCALIZER.locale: "Hello, world!"},
+ ),
+ ],
+ )
+ async def test_label(
+ self,
+ expected_locale: str,
+ expected_label: str,
+ init_label: ShorthandStaticTranslations,
+ ) -> None:
+ plugin_id = "hello-world"
+ sut = PluginConfiguration(plugin_id, init_label)
+ assert sut.label[expected_locale] == expected_label
+
+ @pytest.mark.parametrize(
+ ("expected_locale", "expected_description", "init_description"),
+ [
+ ("und", "Hello, world!", "Hello, world!"),
+ (
+ DEFAULT_LOCALIZER.locale,
+ "Hello, world!",
+ {DEFAULT_LOCALIZER.locale: "Hello, world!"},
+ ),
+ ],
+ )
+ async def test_description(
+ self,
+ expected_locale: str,
+ expected_description: str,
+ init_description: ShorthandStaticTranslations,
+ ) -> None:
+ plugin_id = "hello-world"
+ sut = PluginConfiguration(plugin_id, "", description=init_description)
+ assert sut.description[expected_locale] == expected_description
+
+
+class TestPluginConfigurationPluginConfigurationMapping(
+ ConfigurationMappingTestBase[MachineName, PluginConfiguration]
+):
+ def get_sut(
+ self, configurations: Iterable[PluginConfiguration] | None = None
+ ) -> ConfigurationCollection[MachineName, PluginConfiguration]:
+ return PluginConfigurationPluginConfigurationMapping(configurations)
+
+ def get_configuration_keys(
+ self,
+ ) -> tuple[MachineName, MachineName, MachineName, MachineName]:
+ return (
+ "hello-world-1",
+ "hello-world-2",
+ "hello-world-3",
+ "hello-world-4",
+ )
+
+ def get_configurations(
+ self,
+ ) -> tuple[
+ PluginConfiguration,
+ PluginConfiguration,
+ PluginConfiguration,
+ PluginConfiguration,
+ ]:
+ return (
+ PluginConfiguration(self.get_configuration_keys()[0], ""),
+ PluginConfiguration(self.get_configuration_keys()[1], ""),
+ PluginConfiguration(self.get_configuration_keys()[2], ""),
+ PluginConfiguration(self.get_configuration_keys()[3], ""),
+ )
diff --git a/betty/tests/project/test___init__.py b/betty/tests/project/test___init__.py
index 3a4c5728b..63948fcba 100644
--- a/betty/tests/project/test___init__.py
+++ b/betty/tests/project/test___init__.py
@@ -10,6 +10,7 @@
from betty.app import App
from betty.app.factory import AppDependentFactory
from betty.json.schema import JsonSchemaSchema
+from betty.plugin.config import PluginConfiguration
from betty.plugin.static import StaticPluginRepository
from betty.project import (
Project,
@@ -393,6 +394,12 @@ async def test_logo_without_configuration(self, new_temporary_app: App) -> None:
async with Project.new_temporary(new_temporary_app) as sut, sut:
assert sut.logo.exists()
+ async def test_event_types(self, new_temporary_app: App) -> None:
+ async with Project.new_temporary(new_temporary_app) as sut:
+ sut.configuration.event_types.append(PluginConfiguration("foo", "Foo"))
+ async with sut:
+ assert await sut.event_types.get("foo")
+
class TestProjectContext:
async def test_project(self, new_temporary_app: App) -> None:
diff --git a/betty/tests/project/test_config.py b/betty/tests/project/test_config.py
index c7a897c17..2e7db0a63 100644
--- a/betty/tests/project/test_config.py
+++ b/betty/tests/project/test_config.py
@@ -10,6 +10,7 @@
from betty.locale import DEFAULT_LOCALE, UNDETERMINED_LOCALE
from betty.locale.localizer import DEFAULT_LOCALIZER
from betty.model import Entity, UserFacingEntity
+from betty.plugin.config import PluginConfiguration
from betty.plugin.static import StaticPluginRepository
from betty.project.config import (
EntityReference,
@@ -20,6 +21,7 @@
ExtensionConfigurationMapping,
EntityTypeConfiguration,
EntityTypeConfigurationMapping,
+ EventTypeConfigurationMapping,
)
from betty.project.config import ProjectConfiguration
from betty.project.extension import Extension
@@ -35,7 +37,7 @@
from betty.typing import Void
if TYPE_CHECKING:
- from betty.serde.dump import Dump, VoidableDump
+ from betty.serde.dump import Dump, VoidableDump, DumpMapping
from pytest_mock import MockerFixture
from pathlib import Path
@@ -359,9 +361,7 @@ def get_sut(
return LocaleConfigurationMapping(configurations) # type: ignore[arg-type]
@override
- def get_configuration_keys(
- self,
- ) -> tuple[str, str, str, str]:
+ def get_configuration_keys(self) -> tuple[str, str, str, str]:
return ("en", "nl", "uk", "fr")
@override
@@ -821,6 +821,36 @@ def get_configurations(
)
+class TestEventTypeConfigurationMapping(
+ ConfigurationMappingTestBase[str, PluginConfiguration]
+):
+ @override
+ def get_configuration_keys(self) -> tuple[str, str, str, str]:
+ return "foo", "bar", "baz", "qux"
+
+ @override
+ def get_configurations(
+ self,
+ ) -> tuple[
+ PluginConfiguration,
+ PluginConfiguration,
+ PluginConfiguration,
+ PluginConfiguration,
+ ]:
+ return (
+ PluginConfiguration("foo", "Foo"),
+ PluginConfiguration("bar", "Bar"),
+ PluginConfiguration("baz", "Baz"),
+ PluginConfiguration("qux", "Qux"),
+ )
+
+ @override
+ def get_sut(
+ self, configurations: Iterable[PluginConfiguration] | None = None
+ ) -> EventTypeConfigurationMapping:
+ return EventTypeConfigurationMapping(configurations)
+
+
class TestProjectConfiguration:
async def test_configuration_file_path(self, tmp_path: Path) -> None:
old_configuration_file_path = tmp_path / "betty.json"
@@ -977,6 +1007,10 @@ async def test_logo(self, tmp_path: Path) -> None:
sut.logo = logo
assert sut.logo == logo
+ async def test_event_types(self, tmp_path: Path) -> None:
+ sut = ProjectConfiguration(tmp_path / "betty.json")
+ assert sut.event_types is sut.event_types
+
async def test_load_should_load_minimal(self, tmp_path: Path) -> None:
dump: Any = ProjectConfiguration(tmp_path / "betty.json").dump()
sut = ProjectConfiguration(tmp_path / "betty.json")
@@ -1136,6 +1170,23 @@ async def test_load_not_an_extension_type_name_should_error(
with raises_error(error_type=AssertionFailed):
sut.load(dump)
+ @pytest.mark.parametrize(
+ "event_types_configuration",
+ [
+ {},
+ {"foo": {"label": "Foo"}},
+ ],
+ )
+ async def test_load_should_load_event_types(
+ self, event_types_configuration: DumpMapping[Dump], tmp_path: Path
+ ) -> None:
+ dump: Any = ProjectConfiguration(tmp_path / "betty.json").dump()
+ dump["event_types"] = event_types_configuration
+ sut = ProjectConfiguration(tmp_path / "betty.json")
+ sut.load(dump)
+ if event_types_configuration:
+ assert sut.dump()["event_types"] == event_types_configuration
+
async def test_load_should_error_if_invalid_config(self, tmp_path: Path) -> None:
dump: Dump = {}
sut = ProjectConfiguration(tmp_path / "betty.json")
@@ -1257,6 +1308,13 @@ async def test_dump_should_dump_one_extension_without_configuration(
expected == dump["extensions"][_DummyNonConfigurableExtension.plugin_id()]
)
+ async def test_dump_should_dump_event_types(self, tmp_path: Path) -> None:
+ sut = ProjectConfiguration(tmp_path / "betty.json")
+ sut.event_types.append(PluginConfiguration("foo", "Foo"))
+ dump: Any = sut.dump()
+ expected = {"foo": {"label": "Foo"}}
+ assert expected == dump["event_types"]
+
async def test_dump_should_error_if_invalid_config(self, tmp_path: Path) -> None:
dump: Dump = {}
sut = ProjectConfiguration(tmp_path / "betty.json")
diff --git a/documentation/usage/extension/gramps.rst b/documentation/usage/extension/gramps.rst
index 2f34c0b1f..1fd2ea829 100644
--- a/documentation/usage/extension/gramps.rst
+++ b/documentation/usage/extension/gramps.rst
@@ -38,6 +38,11 @@ This extension is configurable:
configuration:
family_trees:
- file: ./gramps.gpkg
+ event-types:
+ GrampsEventType:
+ event-type: betty-event-type
+ AnotherGrampsEventType:
+ event-type: another-betty-event-type
.. tab-item:: JSON
@@ -50,6 +55,14 @@ This extension is configurable:
"family_trees": [
{
"file": "./gramps.gpkg"
+ "event-types": {
+ "GrampsEventType: {
+ "event-type": "betty-event-type"
+ },
+ "AnotherGrampsEventType: {
+ "event-type": "another-betty-event-type"
+ }
+ }
}
]
}
@@ -64,6 +77,10 @@ All configuration options
the following keys:
- ``file`` (required): the path to a *Gramps XML* or *Gramps XML Package* file.
+ - ``event_types`` (optional): how to map Gramps event types to Betty event types. Keys are Gramps event types, and
+ values are objects with the following keys:
+
+ - ``event_type``: (required): the plugin ID of th Gramps event type to import this Gramps event type as.
If multiple family trees contain entities of the same type and with the same ID (e.g. a person with ID ``I1234``) each
entity will overwrite any previously loaded entity.
@@ -136,30 +153,50 @@ For unknown date parts, set those to all zeroes and Betty will ignore them. For
Event types
-----------
-Betty supports the following Gramps event types:
-
-- ``Adopted``
-- ``Birth``
-- ``Burial``
-- ``Baptism``
-- ``Conference``
-- ``Confirmation``
-- ``Correspondence``
-- ``Cremation``
-- ``Emigration``
-- ``Engagement``
-- ``Death``
-- ``Divorce``
-- ``Divorce Filing`` (imported as ``DivorceAnnouncement``)
-- ``Funeral``
-- ``Immigration``
-- ``Marriage``
-- ``Marriage Banns`` (imported as ``MarriageAnnouncement``)
-- ``Missing``
-- ``Occupation``
-- ``Residence``
-- ``Will``
-- ``Retirement``
+Betty supports the following Gramps event types without any additional configuration:
+
+.. list-table:: Event types
+ :align: left
+ :header-rows: 1
+
+ * - Gramps event type
+ - Betty event type
+ * - ``Adopted``
+ - ``adoption``
+ * - ``Baptism``
+ - ``baptism``
+ * - ``Birth``
+ - ``birth``
+ * - ``Burial``
+ - ``burial``
+ * - ``Confirmation``
+ - ``confirmation``
+ * - ``Cremation``
+ - ``cremation``
+ * - ``Death``
+ - ``death``
+ * - ``Divorce``
+ - ``divorce``
+ * - ``Divorce Filing``
+ - ``divorce-announcement``
+ * - ``Emigration``
+ - ``emigration``
+ * - ``Engagement``
+ - ``engagement``
+ * - ``Immigration``
+ - ``immigration``
+ * - ``Marriage``
+ - ``marriage``
+ * - ``Marriage Banns``
+ - ``marriage-announcement``
+ * - ``Occupation``
+ - ``occupation``
+ * - ``Residence``
+ - ``residence``
+ * - ``Retirement``
+ - ``retirement``
+ * - ``Will``
+ - ``will``
Event roles
-----------
diff --git a/documentation/usage/project/configuration.rst b/documentation/usage/project/configuration.rst
index cacc2ad13..575ae796b 100644
--- a/documentation/usage/project/configuration.rst
+++ b/documentation/usage/project/configuration.rst
@@ -28,6 +28,9 @@ structure. Example configuration:
generate_html_list: true
file:
generate_html_list: false
+ event_types:
+ moon-landing:
+ label: Moon Landing
extensions: {}
.. tab-item:: JSON
@@ -60,6 +63,11 @@ structure. Example configuration:
"generate_html_list": false
}
},
+ "event_types": {
+ "moon-landing": {
+ "label": "Moon Landing"
+ }
+ },
"extensions": {}
}
@@ -83,6 +91,11 @@ All configuration options
- ``entity_types`` (optional): Keys are entity type (plugin) IDs, and values are objects containing the following keys:
- ``generate_html_list`` (optional): Whether to generate the HTML page to list entities of this type. Defaults to ``false``.
+- ``event_types`` (optional): Keys are event type (plugin) IDs, and values are objects containing the following keys:
+
+ - ``id`` (required): The event type (plugin) ID.
+ - ``label`` (required): The event type's human-readable label. This can be a string or :doc:`multiple translations `.
+ - ``description`` (optional): The event's human-readable long description. This can be a string or :doc:`multiple translations `.
- ``extensions`` (optional): The :doc:`extensions ` to enable. Keys are extension names, and values are objects containing the
following keys: