Skip to content

Commit 510d846

Browse files
authored
✨ product's ui config 🗃️ (#7217)
1 parent c53197d commit 510d846

File tree

15 files changed

+413
-169
lines changed

15 files changed

+413
-169
lines changed

api/specs/web-server/_products.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
from models_library.api_schemas_webserver.product import (
1212
GenerateInvitation,
1313
GetCreditPrice,
14-
GetProduct,
1514
InvitationGenerated,
15+
ProductGet,
16+
ProductUIGet,
1617
UpdateProductTemplate,
1718
)
1819
from models_library.generics import Envelope
@@ -40,7 +41,8 @@ async def get_current_product_price():
4041

4142
@router.get(
4243
"/products/{product_name}",
43-
response_model=Envelope[GetProduct],
44+
response_model=Envelope[ProductGet],
45+
description="NOTE: `/products/current` is used to define current project w/o naming it",
4446
tags=[
4547
"po",
4648
],
@@ -49,6 +51,14 @@ async def get_product(_params: Annotated[_ProductsRequestParams, Depends()]):
4951
...
5052

5153

54+
@router.get(
55+
"/products/current/ui",
56+
response_model=Envelope[ProductUIGet],
57+
)
58+
async def get_current_product_ui():
59+
...
60+
61+
5262
@router.put(
5363
"/products/{product_name}/templates/{template_id}",
5464
status_code=status.HTTP_204_NO_CONTENT,
Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
2-
from typing import Annotated, TypeAlias
2+
from typing import Annotated, Any, TypeAlias
33

4+
from common_library.basic_types import DEFAULT_FACTORY
45
from pydantic import (
56
ConfigDict,
67
Field,
@@ -10,6 +11,7 @@
1011
PlainSerializer,
1112
PositiveInt,
1213
)
14+
from pydantic.config import JsonDict
1315

1416
from ..basic_types import IDStr, NonNegativeDecimal
1517
from ..emails import LowerCaseEmailStr
@@ -27,64 +29,83 @@ class GetCreditPrice(OutputSchema):
2729
description="Price of a credit in USD. "
2830
"If None, then this product's price is UNDEFINED",
2931
)
30-
min_payment_amount_usd: NonNegativeInt | None = Field(
31-
...,
32-
description="Minimum amount (included) in USD that can be paid for this product"
33-
"Can be None if this product's price is UNDEFINED",
34-
)
32+
min_payment_amount_usd: Annotated[
33+
NonNegativeInt | None,
34+
Field(
35+
description="Minimum amount (included) in USD that can be paid for this product"
36+
"Can be None if this product's price is UNDEFINED",
37+
),
38+
]
39+
40+
@staticmethod
41+
def _update_json_schema_extra(schema: JsonDict) -> None:
42+
schema.update(
43+
{
44+
"examples": [
45+
{
46+
"productName": "osparc",
47+
"usdPerCredit": None,
48+
"minPaymentAmountUsd": None,
49+
},
50+
{
51+
"productName": "osparc",
52+
"usdPerCredit": "10",
53+
"minPaymentAmountUsd": "10",
54+
},
55+
]
56+
}
57+
)
3558

3659
model_config = ConfigDict(
37-
json_schema_extra={
38-
"examples": [
39-
{
40-
"productName": "osparc",
41-
"usdPerCredit": None,
42-
"minPaymentAmountUsd": None,
43-
},
44-
{
45-
"productName": "osparc",
46-
"usdPerCredit": "10",
47-
"minPaymentAmountUsd": "10",
48-
},
49-
]
50-
}
60+
json_schema_extra=_update_json_schema_extra,
5161
)
5262

5363

5464
class GetProductTemplate(OutputSchema):
55-
id_: IDStr = Field(..., alias="id")
65+
id_: Annotated[IDStr, Field(alias="id")]
5666
content: str
5767

5868

5969
class UpdateProductTemplate(InputSchema):
6070
content: str
6171

6272

63-
class GetProduct(OutputSchema):
73+
class ProductGet(OutputSchema):
6474
name: ProductName
6575
display_name: str
66-
short_name: str | None = Field(
67-
default=None, description="Short display name for SMS"
68-
)
69-
70-
vendor: dict | None = Field(default=None, description="vendor attributes")
71-
issues: list[dict] | None = Field(
72-
default=None, description="Reference to issues tracker"
73-
)
74-
manuals: list[dict] | None = Field(default=None, description="List of manuals")
75-
support: list[dict] | None = Field(
76-
default=None, description="List of support resources"
77-
)
76+
short_name: Annotated[
77+
str | None, Field(description="Short display name for SMS")
78+
] = None
79+
80+
vendor: Annotated[dict | None, Field(description="vendor attributes")] = None
81+
issues: Annotated[
82+
list[dict] | None, Field(description="Reference to issues tracker")
83+
] = None
84+
manuals: Annotated[list[dict] | None, Field(description="List of manuals")] = None
85+
support: Annotated[
86+
list[dict] | None, Field(description="List of support resources")
87+
] = None
7888

7989
login_settings: dict
8090
max_open_studies_per_user: PositiveInt | None
8191
is_payment_enabled: bool
8292
credits_per_usd: NonNegativeDecimal | None
8393

84-
templates: list[GetProductTemplate] = Field(
85-
default_factory=list,
86-
description="List of templates available to this product for communications (e.g. emails, sms, etc)",
87-
)
94+
templates: Annotated[
95+
list[GetProductTemplate],
96+
Field(
97+
description="List of templates available to this product for communications (e.g. emails, sms, etc)",
98+
default_factory=list,
99+
),
100+
] = DEFAULT_FACTORY
101+
102+
103+
class ProductUIGet(OutputSchema):
104+
product_name: ProductName
105+
ui: Annotated[
106+
dict[str, Any],
107+
Field(description="Front-end owned ui product configuration"),
108+
]
88109

89110

90111
ExtraCreditsUsdRangeInt: TypeAlias = Annotated[int, Field(ge=0, lt=500)]
@@ -105,26 +126,32 @@ class InvitationGenerated(OutputSchema):
105126
created: datetime
106127
invitation_link: HttpUrl
107128

129+
@staticmethod
130+
def _update_json_schema_extra(schema: JsonDict) -> None:
131+
schema.update(
132+
{
133+
"examples": [
134+
{
135+
"productName": "osparc",
136+
"issuer": "john.doe",
137+
"guest": "[email protected]",
138+
"trialAccountDays": 7,
139+
"extraCreditsInUsd": 30,
140+
"created": "2023-09-27T15:30:00",
141+
"invitationLink": "https://example.com/invitation#1234",
142+
},
143+
# w/o optional
144+
{
145+
"productName": "osparc",
146+
"issuer": "[email protected]",
147+
"guest": "[email protected]",
148+
"created": "2023-09-27T15:30:00",
149+
"invitationLink": "https://example.com/invitation#1234",
150+
},
151+
]
152+
}
153+
)
154+
108155
model_config = ConfigDict(
109-
json_schema_extra={
110-
"examples": [
111-
{
112-
"productName": "osparc",
113-
"issuer": "john.doe",
114-
"guest": "[email protected]",
115-
"trialAccountDays": 7,
116-
"extraCreditsInUsd": 30,
117-
"created": "2023-09-27T15:30:00",
118-
"invitationLink": "https://example.com/invitation#1234",
119-
},
120-
# w/o optional
121-
{
122-
"productName": "osparc",
123-
"issuer": "[email protected]",
124-
"guest": "[email protected]",
125-
"created": "2023-09-27T15:30:00",
126-
"invitationLink": "https://example.com/invitation#1234",
127-
},
128-
]
129-
}
156+
json_schema_extra=_update_json_schema_extra,
130157
)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""new products ui column
2+
3+
Revision ID: 78f24aaf3f78
4+
Revises: 68777fdf9539
5+
Create Date: 2025-02-12 16:06:09.815111+00:00
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "78f24aaf3f78"
14+
down_revision = "68777fdf9539"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.add_column(
22+
"products",
23+
sa.Column(
24+
"ui",
25+
postgresql.JSONB(astext_type=sa.Text()),
26+
server_default=sa.text("'{}'::jsonb"),
27+
nullable=False,
28+
),
29+
)
30+
# ### end Alembic commands ###
31+
32+
33+
def downgrade():
34+
# ### commands auto generated by Alembic - please adjust! ###
35+
op.drop_column("products", "ui")
36+
# ### end Alembic commands ###

packages/postgres-database/src/simcore_postgres_database/models/products.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ class ProductLoginSettingsDict(TypedDict, total=False):
146146
nullable=False,
147147
doc="Regular expression that matches product hostname from an url string",
148148
),
149+
# EMAILS --------------------
149150
sa.Column(
150151
"support_email",
151152
sa.String,
@@ -200,6 +201,13 @@ class ProductLoginSettingsDict(TypedDict, total=False):
200201
doc="Overrides simcore_service_webserver.login.settings.LoginSettings."
201202
"SEE LoginSettingsForProduct",
202203
),
204+
sa.Column(
205+
"ui",
206+
JSONB,
207+
nullable=False,
208+
server_default=sa.text("'{}'::jsonb"),
209+
doc="Front-end owned UI configuration",
210+
),
203211
sa.Column(
204212
"registration_email_template",
205213
sa.String,
@@ -212,6 +220,7 @@ class ProductLoginSettingsDict(TypedDict, total=False):
212220
nullable=True,
213221
doc="Custom jinja2 template for registration email",
214222
),
223+
# lifecycle
215224
sa.Column(
216225
"created",
217226
sa.DateTime(),

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,24 @@ def random_product(
265265
"group_id": group_id,
266266
}
267267

268+
if ui := fake.random_element(
269+
[
270+
None,
271+
# Examples from https://github.com/itisfoundation/osparc-simcore/blob/1dcd369717959348099cc6241822a1f0aff0382c/services/static-webserver/client/source/resource/osparc/new_studies.json
272+
{
273+
"categories": [
274+
{"id": "precomputed", "title": "Precomputed"},
275+
{
276+
"id": "personalized",
277+
"title": "Personalized",
278+
"description": fake.sentence(),
279+
},
280+
]
281+
},
282+
]
283+
):
284+
data.update(ui=ui)
285+
268286
assert set(data.keys()).issubset({c.name for c in products.columns})
269287
data.update(overrides)
270288
return data

services/web/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.56.0
1+
0.57.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.56.0
2+
current_version = 0.57.0
33
commit = True
44
message = services/webserver api version: {current_version} → {new_version}
55
tag = False

0 commit comments

Comments
 (0)