Skip to content

Commit 63eac38

Browse files
authored
✨ Support center: extend response model of get profile endpoint with support group info (#8294)
1 parent 1a721f8 commit 63eac38

File tree

23 files changed

+1605
-1222
lines changed

23 files changed

+1605
-1222
lines changed

packages/models-library/src/models_library/api_schemas_webserver/groups.py

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,48 @@ class GroupAccessRights(BaseModel):
5454
)
5555

5656

57-
class GroupGet(OutputSchema):
58-
gid: GroupID = Field(..., description="the group ID")
59-
label: str = Field(..., description="the group name")
60-
description: str = Field(..., description="the group description")
61-
thumbnail: AnyUrl | None = Field(
62-
default=None, description="url to the group thumbnail"
63-
)
64-
access_rights: GroupAccessRights = Field(..., alias="accessRights")
57+
class GroupGetBase(OutputSchema):
58+
gid: Annotated[GroupID, Field(description="the group's unique ID")]
59+
label: Annotated[str, Field(description="the group's display name")]
60+
description: str
61+
thumbnail: Annotated[
62+
AnyUrl | None, Field(description="a link to the group's thumbnail")
63+
] = None
64+
65+
@field_validator("thumbnail", mode="before")
66+
@classmethod
67+
def _sanitize_thumbnail_input(cls, v):
68+
if v:
69+
# Enforces null if thumbnail is not valid URL or empty
70+
with suppress(ValidationError):
71+
return TypeAdapter(AnyHttpUrl).validate_python(v)
72+
return None
73+
74+
@classmethod
75+
def dump_basic_group_data(cls, group: Group) -> dict:
76+
"""Helper function to extract common group data for schema conversion"""
77+
return remap_keys(
78+
group.model_dump(
79+
include={
80+
"gid",
81+
"name",
82+
"description",
83+
"thumbnail",
84+
},
85+
exclude={
86+
"inclusion_rules", # deprecated
87+
},
88+
exclude_unset=True,
89+
by_alias=False,
90+
),
91+
rename={
92+
"name": "label",
93+
},
94+
)
95+
96+
97+
class GroupGet(GroupGetBase):
98+
access_rights: Annotated[GroupAccessRights, Field(alias="accessRights")]
6599

66100
inclusion_rules: Annotated[
67101
dict[str, str],
@@ -77,24 +111,7 @@ def from_domain_model(cls, group: Group, access_rights: AccessRightsDict) -> Sel
77111
# Adapts these domain models into this schema
78112
return cls.model_validate(
79113
{
80-
**remap_keys(
81-
group.model_dump(
82-
include={
83-
"gid",
84-
"name",
85-
"description",
86-
"thumbnail",
87-
},
88-
exclude={
89-
"inclusion_rules", # deprecated
90-
},
91-
exclude_unset=True,
92-
by_alias=False,
93-
),
94-
rename={
95-
"name": "label",
96-
},
97-
),
114+
**cls.dump_basic_group_data(group),
98115
"access_rights": access_rights,
99116
}
100117
)
@@ -136,15 +153,6 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
136153

137154
model_config = ConfigDict(json_schema_extra=_update_json_schema_extra)
138155

139-
@field_validator("thumbnail", mode="before")
140-
@classmethod
141-
def _sanitize_legacy_data(cls, v):
142-
if v:
143-
# Enforces null if thumbnail is not valid URL or empty
144-
with suppress(ValidationError):
145-
return TypeAdapter(AnyHttpUrl).validate_python(v)
146-
return None
147-
148156

149157
class GroupCreate(InputSchema):
150158
label: str
@@ -187,6 +195,12 @@ class MyGroupsGet(OutputSchema):
187195
organizations: list[GroupGet] | None = None
188196
all: GroupGet
189197
product: GroupGet | None = None
198+
support: Annotated[
199+
GroupGetBase | None,
200+
Field(
201+
description="Group ID of the app support team or None if no support is defined for this product"
202+
),
203+
] = None
190204

191205
model_config = ConfigDict(
192206
json_schema_extra={
@@ -225,6 +239,12 @@ class MyGroupsGet(OutputSchema):
225239
"description": "Open to all users",
226240
"accessRights": {"read": True, "write": False, "delete": False},
227241
},
242+
"support": {
243+
"gid": "2",
244+
"label": "Support Team",
245+
"description": "The support team of the application",
246+
"thumbnail": "https://placekitten.com/15/15",
247+
},
228248
}
229249
}
230250
)
@@ -234,6 +254,7 @@ def from_domain_model(
234254
cls,
235255
groups_by_type: GroupsByTypeTuple,
236256
my_product_group: tuple[Group, AccessRightsDict] | None,
257+
product_support_group: Group | None,
237258
) -> Self:
238259
assert groups_by_type.primary # nosec
239260
assert groups_by_type.everyone # nosec
@@ -249,6 +270,13 @@ def from_domain_model(
249270
if my_product_group
250271
else None
251272
),
273+
support=(
274+
GroupGetBase.model_validate(
275+
GroupGetBase.dump_basic_group_data(product_support_group)
276+
)
277+
if product_support_group
278+
else None
279+
),
252280
)
253281

254282

packages/models-library/src/models_library/api_schemas_webserver/users.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def from_domain_model(
132132
my_groups_by_type: GroupsByTypeTuple,
133133
my_product_group: tuple[Group, AccessRightsDict] | None,
134134
my_preferences: AggregatedPreferences,
135+
my_support_group: Group | None,
135136
) -> Self:
136137
data = remap_keys(
137138
my_profile.model_dump(
@@ -152,7 +153,9 @@ def from_domain_model(
152153
)
153154
return cls(
154155
**data,
155-
groups=MyGroupsGet.from_domain_model(my_groups_by_type, my_product_group),
156+
groups=MyGroupsGet.from_domain_model(
157+
my_groups_by_type, my_product_group, my_support_group
158+
),
156159
preferences=my_preferences,
157160
)
158161

packages/models-library/src/models_library/groups.py

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -39,38 +39,51 @@ class Group(BaseModel):
3939

4040
@staticmethod
4141
def _update_json_schema_extra(schema: JsonDict) -> None:
42+
everyone: JsonDict = {
43+
"gid": 1,
44+
"name": "Everyone",
45+
"type": "everyone",
46+
"description": "all users",
47+
"thumbnail": None,
48+
}
49+
user: JsonDict = {
50+
"gid": 2,
51+
"name": "User",
52+
"description": "primary group",
53+
"type": "primary",
54+
"thumbnail": None,
55+
}
56+
organization: JsonDict = {
57+
"gid": 3,
58+
"name": "Organization",
59+
"description": "standard group",
60+
"type": "standard",
61+
"thumbnail": None,
62+
"inclusionRules": {},
63+
}
64+
product: JsonDict = {
65+
"gid": 4,
66+
"name": "Product",
67+
"description": "standard group for products",
68+
"type": "standard",
69+
"thumbnail": None,
70+
}
71+
support: JsonDict = {
72+
"gid": 5,
73+
"name": "Support",
74+
"description": "support group",
75+
"type": "standard",
76+
"thumbnail": None,
77+
}
78+
4279
schema.update(
4380
{
4481
"examples": [
45-
{
46-
"gid": 1,
47-
"name": "Everyone",
48-
"type": "everyone",
49-
"description": "all users",
50-
"thumbnail": None,
51-
},
52-
{
53-
"gid": 2,
54-
"name": "User",
55-
"description": "primary group",
56-
"type": "primary",
57-
"thumbnail": None,
58-
},
59-
{
60-
"gid": 3,
61-
"name": "Organization",
62-
"description": "standard group",
63-
"type": "standard",
64-
"thumbnail": None,
65-
"inclusionRules": {},
66-
},
67-
{
68-
"gid": 4,
69-
"name": "Product",
70-
"description": "standard group for products",
71-
"type": "standard",
72-
"thumbnail": None,
73-
},
82+
everyone,
83+
user,
84+
organization,
85+
product,
86+
support,
7487
]
7588
}
7689
)
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
from models_library.api_schemas_webserver.users import (
23
MyProfileRestGet,
34
)
@@ -7,7 +8,11 @@
78
from pydantic import TypeAdapter
89

910

10-
def test_adapter_from_model_to_schema():
11+
@pytest.mark.parametrize("with_support_group", [True, False])
12+
@pytest.mark.parametrize("with_standard_groups", [True, False])
13+
def test_adapter_from_model_to_schema(
14+
with_support_group: bool, with_standard_groups: bool
15+
):
1116
my_profile = MyProfile.model_validate(MyProfile.model_json_schema()["example"])
1217

1318
groups = TypeAdapter(list[Group]).validate_python(
@@ -17,13 +22,22 @@ def test_adapter_from_model_to_schema():
1722
ar = AccessRightsDict(read=False, write=False, delete=False)
1823

1924
my_groups_by_type = GroupsByTypeTuple(
20-
primary=(groups[1], ar), standard=[(groups[2], ar)], everyone=(groups[0], ar)
25+
primary=(groups[1], ar),
26+
standard=[(groups[2], ar)] if with_standard_groups else [],
27+
everyone=(groups[0], ar),
2128
)
22-
my_product_group = groups[-1], AccessRightsDict(
29+
my_product_group = groups[3], AccessRightsDict(
2330
read=False, write=False, delete=False
2431
)
32+
33+
my_support_group = groups[4]
34+
2535
my_preferences = {"foo": Preference(default_value=3, value=1)}
2636

2737
MyProfileRestGet.from_domain_model(
28-
my_profile, my_groups_by_type, my_product_group, my_preferences
38+
my_profile,
39+
my_groups_by_type,
40+
my_product_group,
41+
my_preferences,
42+
my_support_group if with_support_group else None,
2943
)

packages/pytest-simcore/src/pytest_simcore/faker_products_data.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
# pylint: disable=unused-argument
55
# pylint: disable=unused-variable
66
"""
7-
Fixtures to produce fake data for a product:
8-
- it is self-consistent
9-
- granular customization by overriding fixtures
7+
Fixtures to produce fake data for a product:
8+
- it is self-consistent
9+
- granular customization by overriding fixtures
1010
"""
1111

1212
from typing import Any
@@ -65,11 +65,25 @@ def bcc_email(request: pytest.FixtureRequest, product_name: ProductName) -> Emai
6565
)
6666

6767

68+
@pytest.fixture
69+
def support_standard_group_id(faker: Faker) -> int | None:
70+
# NOTE: override to change
71+
return None
72+
73+
6874
@pytest.fixture
6975
def product(
70-
faker: Faker, product_name: ProductName, support_email: EmailStr
76+
faker: Faker,
77+
product_name: ProductName,
78+
support_email: EmailStr,
79+
support_standard_group_id: int | None,
7180
) -> dict[str, Any]:
72-
return random_product(name=product_name, support_email=support_email, fake=faker)
81+
return random_product(
82+
name=product_name,
83+
support_email=support_email,
84+
support_standard_group_id=support_standard_group_id,
85+
fake=faker,
86+
)
7387

7488

7589
@pytest.fixture

packages/pytest-simcore/src/pytest_simcore/helpers/faker_factories.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def fake_task(**overrides) -> dict[str, Any]:
256256
def random_product(
257257
*,
258258
group_id: int | None = None,
259+
support_standard_group_id: int | None = None,
259260
registration_email_template: str | None = None,
260261
fake: Faker = DEFAULT_FAKER,
261262
**overrides,
@@ -302,6 +303,7 @@ def random_product(
302303
"priority": fake.pyint(0, 10),
303304
"max_open_studies_per_user": fake.pyint(1, 10),
304305
"group_id": group_id,
306+
"support_standard_group_id": support_standard_group_id,
305307
}
306308

307309
if ui := fake.random_element(

services/web/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.75.0
1+
0.76.0

services/web/server/setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.75.0
2+
current_version = 0.76.0
33
commit = True
44
message = services/webserver api version: {current_version} → {new_version}
55
tag = False

0 commit comments

Comments
 (0)