Skip to content

Commit 7f78097

Browse files
committed
model conversion ready
1 parent 7cadd12 commit 7f78097

File tree

4 files changed

+84
-23
lines changed

4 files changed

+84
-23
lines changed

services/web/server/src/simcore_service_webserver/users/_models.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from pydantic import BaseModel
1+
from typing import Annotated, Any, Self
2+
3+
from pydantic import BaseModel, ConfigDict, Field
24

35

46
#
@@ -12,3 +14,48 @@ class ProfilePrivacyGet(BaseModel):
1214
class ProfilePrivacyUpdate(BaseModel):
1315
hide_fullname: bool | None = None
1416
hide_email: bool | None = None
17+
18+
19+
#
20+
# DB models
21+
#
22+
23+
24+
def flatten_dict(d: dict, parent_key="", sep="_"):
25+
items = []
26+
for key, value in d.items():
27+
new_key = f"{parent_key}{sep}{key}" if parent_key else key
28+
if isinstance(value, dict):
29+
# Recursively process nested dictionaries
30+
items.extend(flatten_dict(value, new_key, sep=sep).items())
31+
else:
32+
items.append((new_key, value))
33+
return dict(items)
34+
35+
36+
class ToUserUpdateDB(BaseModel):
37+
"""
38+
Maps ProfileUpdate api model into UserDB db model
39+
"""
40+
41+
# NOTE: field names are UserDB columns
42+
# NOTE: aliases are ProfileUpdate field names
43+
44+
name: Annotated[str | None, Field(alias="user_name")] = None
45+
first_name: str | None = None
46+
last_name: str | None = None
47+
48+
privacy_hide_fullname: bool | None = None
49+
privacy_hide_email: bool | None = None
50+
51+
model_config = ConfigDict(extra="forbid")
52+
53+
@classmethod
54+
def from_api(cls, profile_update) -> Self:
55+
# The mapping of embed fields to flatten keys is done here
56+
return cls.model_validate(
57+
flatten_dict(profile_update.model_dump(exclude_unset=True, by_alias=False))
58+
)
59+
60+
def to_columns(self) -> dict[str, Any]:
61+
return self.model_dump(exclude_unset=True, by_alias=False)

services/web/server/src/simcore_service_webserver/users/api.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ..security.api import clean_auth_policy_cache
3131
from . import _db
3232
from ._api import get_user_credentials, get_user_invoice_address, set_user_as_deleted
33+
from ._models import ToUserUpdateDB
3334
from ._preferences_api import get_frontend_user_preferences_aggregation
3435
from .exceptions import MissingGroupExtraPropertiesForProductError, UserNotFoundError
3536
from .schemas import ProfileGet, ProfileUpdate
@@ -159,26 +160,12 @@ async def update_user_profile(
159160
UserNotFoundError
160161
"""
161162
user_id = _parse_as_user(user_id)
162-
async with get_database_engine(app).acquire() as conn:
163-
updated_values = update.model_dump(
164-
include={
165-
"first_name": True,
166-
"last_name": True,
167-
"user_name": True,
168-
"privacy": {
169-
"hide_email",
170-
"hide_fullname",
171-
},
172-
},
173-
exclude_unset=True,
174-
)
175-
# flatten dict
176-
if privacy := updated_values.pop("privacy", None):
177-
updated_values |= {f"privacy_{k}": v for k, v in privacy.items()}
178163

179-
query = users.update().where(users.c.id == user_id).values(**updated_values)
180-
resp = await conn.execute(query)
181-
assert resp.rowcount == 1 # nosec
164+
if updated_values := ToUserUpdateDB.from_api(update).to_columns():
165+
async with get_database_engine(app).acquire() as conn:
166+
query = users.update().where(users.c.id == user_id).values(**updated_values)
167+
resp = await conn.execute(query)
168+
assert resp.rowcount == 1 # nosec
182169

183170

184171
async def get_user_role(app: web.Application, user_id: UserID) -> UserRole:

services/web/server/src/simcore_service_webserver/users/schemas.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ def _to_upper_string(cls, v):
104104
class ProfileUpdate(BaseModel):
105105
first_name: FirstNameStr | None = None
106106
last_name: LastNameStr | None = None
107-
user_name: IDStr | None = None
107+
user_name: Annotated[IDStr | None, Field(alias="userName")] = None
108+
108109
privacy: ProfilePrivacyUpdate | None = None
109110

110111
model_config = ConfigDict(

services/web/server/tests/unit/isolated/test_users_models.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515
from pydantic import BaseModel
1616
from servicelib.rest_constants import RESPONSE_MODEL_POLICY
1717
from simcore_postgres_database.models.users import UserRole
18-
from simcore_service_webserver.users._models import ProfilePrivacyGet
19-
from simcore_service_webserver.users.schemas import ProfileGet, ThirdPartyToken
18+
from simcore_service_webserver.users._models import ProfilePrivacyGet, ToUserUpdateDB
19+
from simcore_service_webserver.users.schemas import (
20+
ProfileGet,
21+
ProfileUpdate,
22+
ThirdPartyToken,
23+
)
2024

2125

2226
@pytest.mark.parametrize(
@@ -152,3 +156,25 @@ def test_parsing_output_of_get_user_profile():
152156

153157
profile = ProfileGet.model_validate(result_from_db_query_and_composition)
154158
assert "password" not in profile.model_dump(exclude_unset=True)
159+
160+
161+
def test_mapping_update_models_from_rest_to_db():
162+
163+
profile_update = ProfileUpdate.model_validate(
164+
# input in rest
165+
{
166+
"first_name": "foo",
167+
"userName": "foo1234",
168+
"privacy": {"hide_fullname": False},
169+
}
170+
)
171+
172+
# to db
173+
profile_update_db = ToUserUpdateDB.from_api(profile_update)
174+
175+
# expected
176+
assert profile_update_db.to_columns() == {
177+
"first_name": "foo",
178+
"name": "foo1234",
179+
"privacy_hide_fullname": False,
180+
}

0 commit comments

Comments
 (0)