Skip to content

Commit b806bdf

Browse files
authored
Add AIResource.contacts with full contact information (#564)
1 parent b0040eb commit b806bdf

File tree

6 files changed

+55
-3
lines changed

6 files changed

+55
-3
lines changed

src/connectors/example/example_connector.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import json
22
import pathlib
3-
from typing import Iterator, TypeVar
3+
from typing import Iterator, TypeVar, cast, Hashable
44

55
from sqlmodel import SQLModel
66

77
from connectors.abstract.resource_connector_on_start_up import ResourceConnectorOnStartUp
88
from database.model.platform.platform_names import PlatformName
99
from database.model.resource_read_and_create import resource_create
10+
from database.model.concept.concept import AIoDConcept
1011

1112
RESOURCE = TypeVar("RESOURCE", bound=SQLModel)
1213

@@ -31,6 +32,6 @@ def platform_name(self) -> PlatformName:
3132
def fetch(self, limit: int | None = None) -> Iterator[RESOURCE]:
3233
with open(self.json_path) as f:
3334
json_data = json.load(f)
34-
pydantic_class = resource_create(self.resource_class)
35+
pydantic_class = resource_create(cast(Hashable, self.resource_class))
3536
for json_item in json_data[:limit]:
3637
yield pydantic_class(**json_item)

src/database/model/agent/organisation.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from datetime import date
22
from typing import Optional
33

4-
from sqlalchemy import Column, Integer, ForeignKey
54
from sqlmodel import Field, Relationship
65

76
from database.model.agent.agent import AgentBase, Agent

src/database/model/ai_resource/resource.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
FindByIdentifierDeserializerList,
3737
FindByNameDeserializerList,
3838
)
39+
from database.model.resource_read_and_create import resource_read
3940

4041

4142
class AIResourceBase(AIoDConceptBase, metaclass=abc.ABCMeta):
@@ -80,6 +81,7 @@ class AIResource(AIResourceBase, AIoDConcept, metaclass=abc.ABCMeta):
8081
scientific_domain: list[ScientificDomain] = Relationship() # type: ignore[valid-type]
8182

8283
contact: list[Contact] = Relationship()
84+
contacts: list[Contact] = Relationship(sa_relationship_kwargs=dict(viewonly=True))
8385
creator: list[Contact] = Relationship()
8486

8587
media: list = Relationship(sa_relationship_kwargs={"cascade": "all, delete"})
@@ -188,6 +190,13 @@ class RelationshipConfig(AIoDConcept.RelationshipConfig):
188190
deserializer=FindByIdentifierDeserializerList(Contact),
189191
default_factory_pydantic=list,
190192
)
193+
194+
contacts: list[Contact] = ManyToMany(
195+
description="Contact information corresponding to the identifiers found in `contact`.",
196+
class_read=list[resource_read(Contact)], # type: ignore
197+
include_in_create=False,
198+
default_factory_pydantic=list,
199+
)
191200
creator: list[str] = ManyToMany(
192201
description="The identifiers of the contact information of the persons and/or "
193202
"organisations that created this resource.",
@@ -272,6 +281,7 @@ def update_relationships(cls, relationships: dict[str, Any]):
272281
to_identifier_type=str,
273282
)
274283
relationships["contact"].link_model = link_model_contact
284+
relationships["contacts"].link_model = link_model_contact
275285
relationships["creator"].link_model = link_model_creator
276286

277287
relationships["description"].sa_relationship_kwargs = dict(

src/database/model/resource_read_and_create.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
request.
66
"""
77

8+
import functools
89
from typing import Type, Tuple, TYPE_CHECKING
910

1011
from pydantic import create_model
@@ -57,6 +58,7 @@ def _get_field_definitions_create(
5758
}
5859

5960

61+
@functools.cache # For Pydantic 'bug', see note at `resource_read`
6062
def resource_create(resource_class: Type["AIoDConcept"] | Type["Platform"]) -> Type[SQLModel]:
6163
"""
6264
Create a SQLModel for a Create class of a resource. This Create class is a Pydantic class
@@ -77,6 +79,11 @@ def resource_create(resource_class: Type["AIoDConcept"] | Type["Platform"]) -> T
7779
return model
7880

7981

82+
# We cache this not for performance reason, but because if the model would be created multiple times,
83+
# this leads to a conflicting state in the Pydantic model map, erasing the older model.
84+
# Related to https://github.com/fastapi/fastapi/issues/4191 and might be fixed by upgrading to the
85+
# latest Pydantic version
86+
@functools.cache
8087
def resource_read(resource_class: Type["AIoDConcept"] | Type["Platform"]) -> Type[SQLModel]:
8188
"""
8289
Create a SQLModel for a Read class of a resource. This Read class is a Pydantic class

src/tests/routers/resource_routers/test_router_organisation.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def test_happy_path(
2828

2929
body["member"] = [organisation.identifier]
3030
body["contact_details"] = contact.identifier
31+
body["contact"] = [contact.identifier]
3132

3233
response = client.post("/organisations", json=body, headers={"Authorization": "Fake token"})
3334
assert response.status_code == 200, response.json()
@@ -47,6 +48,15 @@ def test_happy_path(
4748
assert response_json["type"] == "research institute"
4849
assert response_json["member"] == body["member"]
4950
assert response_json["contact_details"] == body["contact_details"]
51+
assert response_json["contacts"][0]["name"] == "Aaron Bar"
52+
assert response_json["contacts"][0]["telephone"] == ["0032 xxxx xxxx"]
53+
assert response_json["contacts"][0]["email"] == ["a@b.com"]
54+
assert response_json["contacts"][0]["location"] == [
55+
{
56+
"address": {"country": "NED", "street": "Street Name 10", "postal_code": "1234AB"},
57+
"geo": {"latitude": 37.42242, "longitude": -122.08585, "elevation_millimeters": 2000},
58+
}
59+
]
5060

5161
# response = client.delete("/organisations/1", headers={"Authorization": "Fake token"})
5262
# assert response.status_code == 200
@@ -63,3 +73,27 @@ def test_happy_path(
6373

6474
response = client.delete(f"/organisations/{identifier}", headers={"Authorization": "Fake token"})
6575
assert response.status_code == 200, response.json()
76+
77+
78+
def test_ai_resource_contacts_field_is_ignored(
79+
client: TestClient,
80+
mocked_privileged_token: Mock,
81+
organisation: Organisation,
82+
contact: Contact,
83+
body_agent: dict,
84+
auto_publish: None,
85+
):
86+
with DbSession() as session:
87+
session.add(contact)
88+
session.commit()
89+
session.refresh(contact)
90+
91+
body = copy.copy(body_agent)
92+
body["contacts"] = [contact.json()]
93+
response = client.post("/organisations", json=body, headers={"Authorization": "Fake token"})
94+
assert response.status_code == 200, response.json()
95+
identifier = response.json()['identifier']
96+
97+
response = client.get(f"/organisations/{identifier}")
98+
assert response.status_code == 200, response.json()
99+
assert response.json()["contacts"] == []

src/tests/testutils/default_instances.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def publication_factory(body_asset: dict) -> Callable[[], Publication]:
9999
def contact(body_concept, engine: Engine) -> Contact:
100100
body = copy.deepcopy(body_concept)
101101
body["email"] = ["a@b.com"]
102+
body["name"] = "Aaron Bar"
102103
body["telephone"] = ["0032 XXXX XXXX"]
103104
body["location"] = [
104105
{

0 commit comments

Comments
 (0)