Skip to content

Commit c5a1767

Browse files
authored
Update organisation (#507)
2 parents 6ddfbad + 0c2c9f9 commit c5a1767

File tree

7 files changed

+167
-5
lines changed

7 files changed

+167
-5
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Add organisation.turnover and organisation.number_of_employees
2+
3+
Revision ID: 5d0d73539c21
4+
Revises: 1662d64ebe23
5+
Create Date: 2025-05-12 14:00:35.644646
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "5d0d73539c21"
17+
down_revision: Union[str, None] = "751c3f34323a"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade():
23+
op.add_column("organisation", sa.Column("turnover_identifier", sa.Integer(), nullable=True))
24+
op.add_column(
25+
"organisation", sa.Column("number_of_employees_identifier", sa.Integer(), nullable=True)
26+
)
27+
28+
op.create_foreign_key(
29+
"fk_organisation_turnover_identifier",
30+
"organisation",
31+
"turnover",
32+
["turnover_identifier"],
33+
["identifier"],
34+
)
35+
36+
op.create_foreign_key(
37+
"fk_organisation_number_of_employees_identifier",
38+
"organisation",
39+
"number_of_employees",
40+
["number_of_employees_identifier"],
41+
["identifier"],
42+
)
43+
44+
45+
def downgrade() -> None:
46+
op.drop_constraint(
47+
"fk_organisation_number_of_employees_identifier", "organisation", type_="foreignkey"
48+
)
49+
op.drop_constraint("fk_organisation_turnover_identifier", "organisation", type_="foreignkey")
50+
51+
op.drop_column("organisation", "number_of_employees_identifier")
52+
op.drop_column("organisation", "turnover_identifier")

src/database/model/agent/organisation.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from datetime import date
2-
from typing import Optional
2+
from typing import Optional, Literal
33

44
from sqlmodel import Field, Relationship
55

6+
from database.model.named_relation import Taxonomy, create_taxonomy
67
from database.model.agent.agent import AgentBase, Agent
78
from database.model.agent.agent_table import AgentTable
89
from database.model.agent.contact import Contact
@@ -18,6 +19,13 @@
1819
)
1920

2021

22+
Turnover: type[Taxonomy] = create_taxonomy(class_name="Turnover", table_name="turnover")
23+
24+
NumberOfEmployees: type[Taxonomy] = create_taxonomy(
25+
class_name="NumberOfEmployees", table_name="number_of_employees"
26+
)
27+
28+
2129
class OrganisationBase(AgentBase):
2230
date_founded: date | None = Field(
2331
description="The date on which the organisation was founded.",
@@ -54,6 +62,20 @@ class Organisation(OrganisationBase, Agent, table=True): # type: ignore [call-a
5462
),
5563
)
5664

65+
turnover_identifier: int | None = Field(
66+
default=None,
67+
foreign_key="turnover.identifier",
68+
description="The revenue bracket of the organisation.",
69+
)
70+
turnover: Optional[Turnover] = Relationship() # type: ignore[valid-type]
71+
72+
number_of_employees_identifier: int | None = Field(
73+
default=None,
74+
foreign_key="number_of_employees.identifier",
75+
description="The employee size bracket of the organisation.",
76+
)
77+
number_of_employees: Optional[NumberOfEmployees] = Relationship() # type: ignore[valid-type]
78+
5779
class RelationshipConfig(Agent.RelationshipConfig):
5880
contact_details: str | None = OneToOne(
5981
description="The identifier of the contact details by which this organisation "
@@ -76,6 +98,24 @@ class RelationshipConfig(Agent.RelationshipConfig):
7698
default_factory_pydantic=list,
7799
)
78100

101+
turnover: Optional[str] = ManyToOne(
102+
description="The approximate revenue bracket of the organisation in euros, see the taxonomy for more details.",
103+
identifier_name="turnover_identifier",
104+
_serializer=AttributeSerializer("name"),
105+
deserializer=FindByNameDeserializer(Turnover),
106+
example=">5 million euros",
107+
)
108+
109+
number_of_employees: Optional[str] = ManyToOne(
110+
description=(
111+
"The number of employees of the organisation, see the taxonomy for more details."
112+
),
113+
identifier_name="number_of_employees_identifier",
114+
_serializer=AttributeSerializer("name"),
115+
deserializer=FindByNameDeserializer(NumberOfEmployees),
116+
example="<10",
117+
)
118+
79119

80120
deserializer = FindByIdentifierDeserializer(Organisation)
81121
Contact.RelationshipConfig.organisation.deserializer = deserializer # type: ignore

src/taxonomies/synchronize_taxonomy.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from database.model.ai_resource.industrial_sector import IndustrialSector
1616
from database.model.ai_resource.scientific_domain import ScientificDomain
1717
from database.model.news.news_category import NewsCategory
18+
from database.model.agent.organisation import NumberOfEmployees, Turnover
1819

1920

2021
def parse_args():
@@ -40,6 +41,8 @@ class Term(NamedTuple):
4041
"Licence": License,
4142
"News Category": NewsCategory,
4243
"Scientific Domain": ScientificDomain,
44+
"Number of Employees": NumberOfEmployees,
45+
"Turnover": Turnover,
4346
}
4447

4548

src/tests/routers/generic/test_router_post.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import pytest
33
from starlette.testclient import TestClient
44

5-
from tests.testutils.users import logged_in_user, kc_connector_with_roles, kc_user_with_roles
5+
from tests.testutils.users import logged_in_user, kc_connector_with_roles
6+
from tests.routers.resource_routers.test_router_organisation import with_organisation_taxonomies
67
from database.model.platform.platform_names import PlatformName
78
from database.model.resource_read_and_create import resource_create
89
from routers import resource_routers
@@ -237,7 +238,7 @@ def test_taxonomy_is_not_enforced_for_connector(
237238
tested_routers := [r for r in resource_routers.router_list if r.resource_name != 'platform'],
238239
ids=map(lambda r: r.resource_name, tested_routers),
239240
)
240-
def test_example_is_valid(router, client: TestClient):
241+
def test_example_is_valid(router, client: TestClient, with_organisation_taxonomies):
241242
example_values = {}
242243
res_create = resource_create(router.resource_class)
243244
for attribute, model_field in res_create.__fields__.items():
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import copy
2+
from http import HTTPStatus
3+
4+
import pytest
5+
6+
from tests.routers.resource_routers.test_router_organisation import with_organisation_taxonomies
7+
from tests.testutils.users import logged_in_user
8+
9+
10+
@pytest.mark.parametrize(
11+
("field", "value"),
12+
[
13+
("turnover", "foo"),
14+
("number_of_employees", "foo"),
15+
]
16+
)
17+
def test_invalid_value_is_rejected(field, value, body_agent, client, with_organisation_taxonomies):
18+
organisation = copy.copy(body_agent)
19+
organisation[field] = value
20+
21+
with logged_in_user():
22+
response = client.post(
23+
"/organisations",
24+
json=organisation,
25+
headers={"Authorization": "Fake token"},
26+
)
27+
assert response.status_code == HTTPStatus.BAD_REQUEST
28+
assert "not part of the taxonomy" in response.json()["detail"]
29+
assert field in response.json()["detail"]
30+
assert value in response.json()["detail"]

src/tests/routers/resource_routers/test_router_organisation.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,37 @@
44
from starlette.testclient import TestClient
55

66
from database.model.agent.contact import Contact
7-
from database.model.agent.organisation import Organisation
7+
from database.model.agent.organisation import Organisation, Turnover,NumberOfEmployees
88
from database.session import DbSession
99

10+
import pytest
11+
12+
from taxonomies.synchronize_taxonomy import synchronize
13+
14+
STANDARD_TURNOVER_VALUES = ["<1 million euros", ">1 million euros", ">3 million euros", ">5 million euros", ">50 million euros", ">1.5 billion euros"]
15+
16+
@pytest.fixture
17+
def with_organisation_taxonomies():
18+
with DbSession() as session:
19+
synchronize(
20+
NumberOfEmployees,
21+
[
22+
NumberOfEmployees(name=value,definition="", official=True, children=[])
23+
for value in ["<10", "<50", "<250", ">=250"]
24+
],
25+
session
26+
)
27+
synchronize(
28+
Turnover,
29+
[
30+
Turnover(name=value,definition="", official=True, children=[])
31+
for value in STANDARD_TURNOVER_VALUES
32+
],
33+
session
34+
)
35+
session.commit()
36+
yield
37+
1038

1139
def test_happy_path(
1240
client: TestClient,
@@ -15,12 +43,14 @@ def test_happy_path(
1543
contact: Contact,
1644
body_agent: dict,
1745
auto_publish: None,
46+
with_organisation_taxonomies,
1847
):
1948
body = copy.copy(body_agent)
2049
body["date_founded"] = "2023-01-01"
2150
body["legal_name"] = "A name for the organisation"
2251
body["ai_relevance"] = "Part of CLAIRE"
2352
body["type"] = "Research Institute"
53+
body["turnover"] = "<1 million euros"
2454
with DbSession() as session:
2555
session.add(organisation) # The new organisation will be a member of this organisation
2656
session.add(contact)
@@ -46,6 +76,7 @@ def test_happy_path(
4676
assert response_json["legal_name"] == "A name for the organisation"
4777
assert response_json["ai_relevance"] == "Part of CLAIRE"
4878
assert response_json["type"] == "research institute"
79+
assert response_json["turnover"] == "<1 million euros"
4980
assert response_json["member"] == body["member"]
5081
assert response_json["contact_details"] == body["contact_details"]
5182
assert response_json["contacts"][0]["name"] == "Aaron Bar"
@@ -71,6 +102,12 @@ def test_happy_path(
71102
response = client.get(f"organisations/{identifier}")
72103
assert response.json()["type"] == "association"
73104

105+
body["number_of_employees"] = "<50"
106+
response = client.put(f"organisations/{identifier}", json=body, headers={"Authorization": "Fake token"})
107+
assert response.status_code == 200, response.json()
108+
response = client.get(f"organisations/{identifier}")
109+
assert response.json()["number_of_employees"] == "<50"
110+
74111
response = client.delete(f"/organisations/{identifier}", headers={"Authorization": "Fake token"})
75112
assert response.status_code == 200, response.json()
76113

src/tests/testutils/default_sqlalchemy.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from database.model.platform.platform_names import PlatformName
2121
from database.session import EngineSingleton
2222
from main import build_app
23-
from database.model.ai_resource.application_area import ApplicationArea
2423
from database.model.ai_resource.industrial_sector import IndustrialSector
2524
from database.model.ai_resource.research_area import ResearchArea
2625
from database.model.ai_resource.scientific_domain import ScientificDomain

0 commit comments

Comments
 (0)