Skip to content

Commit aab6992

Browse files
Add product_name column to projects table 🗃️ (#8682)
Co-authored-by: Copilot <[email protected]>
1 parent 36e8752 commit aab6992

40 files changed

+391
-315
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,15 @@ class Project(BaseProjectModel):
263263
),
264264
] = DEFAULT_FACTORY
265265

266+
product_name: Annotated[
267+
ProductName,
268+
Field(
269+
description="Product to which the project belongs",
270+
alias="productName",
271+
examples=["osparc", "s4l"],
272+
),
273+
]
274+
266275
model_config = ConfigDict(
267276
# NOTE: this is a security measure until we get rid of the ProjectDict variants
268277
extra="forbid",

packages/models-library/tests/test_projects.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def minimal_project(faker: Faker) -> dict[str, Any]:
2727
"workbench": {},
2828
"type": "STANDARD",
2929
"templateType": None,
30+
"productName": "osparc",
3031
}
3132

3233

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Add product_name to projects
2+
3+
Revision ID: ce69cc44246a
4+
Revises: a85557c02d71
5+
Create Date: 2025-12-08 14:14:07.573764+00:00
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
from sqlalchemy.dialects import postgresql
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "ce69cc44246a"
15+
down_revision = "a85557c02d71"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
op.add_column("projects", sa.Column("product_name", sa.String(), nullable=True))
22+
23+
op.execute(
24+
"""
25+
UPDATE projects
26+
SET product_name = ptp.product_name
27+
FROM projects_to_products ptp
28+
WHERE projects.uuid = ptp.project_uuid
29+
"""
30+
)
31+
32+
op.alter_column("projects", "product_name", nullable=False)
33+
34+
op.create_foreign_key(
35+
"fk_projects_to_product_name",
36+
"projects",
37+
"products",
38+
["product_name"],
39+
["name"],
40+
onupdate="CASCADE",
41+
ondelete="CASCADE",
42+
)
43+
44+
op.drop_table("projects_to_products")
45+
46+
47+
def downgrade():
48+
op.create_table(
49+
"projects_to_products",
50+
sa.Column(
51+
"project_uuid",
52+
sa.String,
53+
sa.ForeignKey(
54+
"projects.uuid",
55+
onupdate="CASCADE",
56+
ondelete="CASCADE",
57+
name="fk_projects_to_products_product_uuid",
58+
),
59+
nullable=False,
60+
doc="Project unique ID",
61+
),
62+
sa.Column(
63+
"product_name",
64+
sa.String,
65+
sa.ForeignKey(
66+
"products.name",
67+
onupdate="CASCADE",
68+
ondelete="CASCADE",
69+
name="fk_projects_to_products_product_name",
70+
),
71+
nullable=False,
72+
doc="Products unique name",
73+
),
74+
# TIME STAMPS ----
75+
sa.Column(
76+
"created",
77+
postgresql.TIMESTAMP(timezone=True),
78+
server_default=sa.text("now()"),
79+
autoincrement=False,
80+
nullable=False,
81+
),
82+
sa.Column(
83+
"modified",
84+
postgresql.TIMESTAMP(timezone=True),
85+
server_default=sa.text("now()"),
86+
autoincrement=False,
87+
nullable=False,
88+
),
89+
sa.UniqueConstraint("project_uuid", "product_name"),
90+
sa.Index("idx_projects_to_products_product_name", "product_name"),
91+
)
92+
93+
op.execute(
94+
"""
95+
INSERT INTO projects_to_products (project_uuid, product_name, created, modified)
96+
SELECT uuid, product_name, NOW(), NOW()
97+
FROM projects
98+
WHERE product_name IS NOT NULL
99+
ON CONFLICT (project_uuid, product_name) DO NOTHING
100+
"""
101+
)
102+
103+
op.drop_constraint("fk_projects_to_product_name", "projects", type_="foreignkey")
104+
105+
op.drop_column("projects", "product_name")

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class ProjectTemplateType(str, enum.Enum):
123123
sa.Boolean,
124124
nullable=False,
125125
default=False,
126-
doc="If true, the project is publicaly accessible via the studies dispatcher (i.e. no registration required)",
126+
doc="If true, the project is publicly accessible via the studies dispatcher (i.e. no registration required)",
127127
),
128128
sa.Column(
129129
"hidden",
@@ -173,7 +173,7 @@ class ProjectTemplateType(str, enum.Enum):
173173
JSONB,
174174
nullable=False,
175175
server_default=sa.text("'{}'::jsonb"),
176-
doc="Free JSON with quality assesment based on TSR",
176+
doc="Free JSON with quality assessment based on TSR",
177177
),
178178
# DEPRECATED ----------------------------
179179
sa.Column(
@@ -183,6 +183,18 @@ class ProjectTemplateType(str, enum.Enum):
183183
server_default=sa.text("'{}'::jsonb"),
184184
doc="DEPRECATED: Read/write/delete access rights of each group (gid) on this project",
185185
),
186+
sa.Column(
187+
"product_name",
188+
sa.String,
189+
sa.ForeignKey(
190+
"products.name",
191+
onupdate=RefActions.CASCADE,
192+
ondelete=RefActions.CASCADE,
193+
name="fk_projects_to_product_name",
194+
),
195+
nullable=False,
196+
doc="Product to which this project belongs",
197+
),
186198
### INDEXES ----------------------------
187199
sa.Index(
188200
"idx_projects_last_change_date_desc",

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

Lines changed: 0 additions & 38 deletions
This file was deleted.

packages/postgres-database/tests/conftest.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,12 @@ async def create_fake_project(
304304
) -> AsyncIterator[Callable[..., Awaitable[RowProxy]]]:
305305
created_project_uuids = []
306306

307-
async def _creator(conn, user: RowProxy, **overrides) -> RowProxy:
308-
prj_to_insert = random_project(prj_owner=user.id, **overrides)
307+
async def _creator(
308+
conn, user: RowProxy, product: RowProxy, **overrides
309+
) -> RowProxy:
310+
prj_to_insert = random_project(
311+
prj_owner=user.id, product_name=product.name, **overrides
312+
)
309313
result = await conn.execute(
310314
projects.insert().values(**prj_to_insert).returning(projects)
311315
)

packages/postgres-database/tests/test_delete_projects_and_users.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99
from aiopg.sa.engine import Engine
1010
from aiopg.sa.result import ResultProxy, RowProxy
1111
from psycopg2.errors import ForeignKeyViolation
12-
from pytest_simcore.helpers.faker_factories import random_project, random_user
13-
from simcore_postgres_database.webserver_models import projects, users
12+
from pytest_simcore.helpers.faker_factories import (
13+
random_product,
14+
random_project,
15+
random_user,
16+
)
17+
from simcore_postgres_database.webserver_models import products, projects, users
1418
from sqlalchemy import func
1519

1620

@@ -21,12 +25,35 @@ async def engine(aiopg_engine: Engine):
2125
await conn.execute(users.insert().values(**random_user()))
2226
await conn.execute(users.insert().values(**random_user()))
2327

24-
await conn.execute(projects.insert().values(**random_project(prj_owner=1)))
25-
await conn.execute(projects.insert().values(**random_project(prj_owner=2)))
26-
await conn.execute(projects.insert().values(**random_project(prj_owner=3)))
28+
await conn.execute(
29+
products.insert().values(**random_product(name="test-product"))
30+
)
31+
32+
await conn.execute(
33+
projects.insert().values(
34+
**random_project(prj_owner=1, product_name="test-product")
35+
)
36+
)
37+
await conn.execute(
38+
projects.insert().values(
39+
**random_project(prj_owner=2, product_name="test-product")
40+
)
41+
)
42+
await conn.execute(
43+
projects.insert().values(
44+
**random_project(prj_owner=3, product_name="test-product")
45+
)
46+
)
2747
with pytest.raises(ForeignKeyViolation):
2848
await conn.execute(projects.insert().values(**random_project(prj_owner=4)))
2949

50+
with pytest.raises(ForeignKeyViolation):
51+
await conn.execute(
52+
projects.insert().values(
53+
**random_project(prj_owner=1, product_name="unknown-product")
54+
)
55+
)
56+
3057
return aiopg_engine
3158

3259

@@ -60,7 +87,7 @@ async def test_insert_user(engine):
6087
assert res.rowcount == 1
6188
assert len(res.keys()) > 1
6289

63-
# DIFFERENT betwen .first() and fetchone()
90+
# DIFFERENT between .first() and fetchone()
6491

6592
user2: RowProxy = await res.first()
6693
# Fetch the first row and then close the result set unconditionally.

0 commit comments

Comments
 (0)