diff --git a/dandischema/models.py b/dandischema/models.py index 2de8299..8264472 100644 --- a/dandischema/models.py +++ b/dandischema/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from datetime import date, datetime from enum import Enum import os @@ -845,6 +847,16 @@ class Contributor(DandiBaseModel): "Contributor", validate_default=True, json_schema_extra={"readOnly": True} ) + @model_validator(mode="after") + def ensure_contact_person_has_email(self) -> Contributor: + role_names = self.roleName + + if role_names is not None and RoleType.ContactPerson in role_names: + if self.email is None: + raise ValueError("Contact person must have an email address.") + + return self + class Organization(Contributor): identifier: Optional[RORID] = Field( diff --git a/dandischema/tests/test_datacite.py b/dandischema/tests/test_datacite.py index b02e439..5663ffc 100644 --- a/dandischema/tests/test_datacite.py +++ b/dandischema/tests/test_datacite.py @@ -70,6 +70,7 @@ def metadata_basic() -> Dict[str, Any]: "contributor": [ { "name": "A_last, A_first", + "email": "nemo@example.com", "roleName": [RoleType("dcite:ContactPerson")], } ], diff --git a/dandischema/tests/test_metadata.py b/dandischema/tests/test_metadata.py index a35fa50..61ac2d7 100644 --- a/dandischema/tests/test_metadata.py +++ b/dandischema/tests/test_metadata.py @@ -156,6 +156,7 @@ def test_mismatch_key(schema_version: str, schema_key: str) -> None: { "schemaKey": "Person", "name": "Last, first", + "email": "nobody@example.com", "roleName": ["dcite:ContactPerson"], } ], diff --git a/dandischema/tests/test_models.py b/dandischema/tests/test_models.py index a5bc368..b39af11 100644 --- a/dandischema/tests/test_models.py +++ b/dandischema/tests/test_models.py @@ -18,6 +18,7 @@ Asset, BaseType, CommonModel, + Contributor, DandiBaseModel, Dandiset, DigestType, @@ -378,6 +379,7 @@ def test_dantimeta_1() -> None: "contributor": [ { "name": "last name, first name", + "email": "someone@dandiarchive.org", "roleName": [RoleType("dcite:ContactPerson")], } ], @@ -592,6 +594,7 @@ def test_schemakey_roundtrip() -> None: }, { "name": "last2, first2", + "email": "nospam@dandiarchive.org", "roleName": ["dcite:ContactPerson"], "schemaKey": "Person", "affiliation": [], @@ -689,3 +692,43 @@ def test_embargoedaccess() -> None: ) ] ) + + +_NON_CONTACT_PERSON_ROLES_ARGS: List[List[RoleType]] = [ + [], + [RoleType.Author, RoleType.DataCurator], + [RoleType.Funder], +] + +_CONTACT_PERSON_ROLES_ARGS: List[List[RoleType]] = [ + role_lst + [RoleType.ContactPerson] for role_lst in _NON_CONTACT_PERSON_ROLES_ARGS +] + + +class TestContributor: + + @pytest.mark.parametrize("roles", _CONTACT_PERSON_ROLES_ARGS) + def test_contact_person_without_email(self, roles: List[RoleType]) -> None: + """ + Test creating a `Contributor` instance as a contact person without an email + """ + with pytest.raises( + pydantic.ValidationError, match="Contact person must have an email address" + ): + Contributor(roleName=roles) + + @pytest.mark.parametrize("roles", _NON_CONTACT_PERSON_ROLES_ARGS) + def test_non_contact_person_without_email(self, roles: List[RoleType]) -> None: + """ + Test creating a `Contributor` instance as a non-contact person without an email + """ + Contributor(roleName=roles) + + @pytest.mark.parametrize( + "roles", _NON_CONTACT_PERSON_ROLES_ARGS + _CONTACT_PERSON_ROLES_ARGS + ) + def test_with_email(self, roles: List[RoleType]) -> None: + """ + Test creating a `Contributor` instance with an email + """ + Contributor(email="nemo@dandiarchive.org", roleName=roles)