Skip to content

Commit 425e498

Browse files
first part
1 parent 64d6099 commit 425e498

File tree

9 files changed

+187
-8
lines changed

9 files changed

+187
-8
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from datetime import datetime
22
from enum import auto
3-
from typing import Annotated, TypeAlias
3+
from typing import Annotated, Any, TypeAlias
44
from uuid import UUID
55

66
from models_library.groups import GroupID
@@ -23,6 +23,7 @@ class ConversationType(StrAutoEnum):
2323
PROJECT_ANNOTATION = (
2424
auto() # Something like sticky note, can be located anywhere in the pipeline UI
2525
)
26+
SUPPORT = auto() # Support conversation
2627

2728

2829
class ConversationMessageType(StrAutoEnum):
@@ -44,6 +45,7 @@ class ConversationGetDB(BaseModel):
4445
project_uuid: ProjectID | None
4546
user_group_id: GroupID
4647
type: ConversationType
48+
extra_context: dict[str, Any]
4749

4850
# states
4951
created: datetime

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import enum
22

33
import sqlalchemy as sa
4-
from sqlalchemy.dialects.postgresql import UUID
4+
from sqlalchemy.dialects.postgresql import JSONB, UUID
55

66
from ._common import RefActions, column_created_datetime, column_modified_datetime
77
from .base import metadata
@@ -12,6 +12,7 @@
1212
class ConversationType(enum.Enum):
1313
PROJECT_STATIC = "PROJECT_STATIC" # Static conversation for the project
1414
PROJECT_ANNOTATION = "PROJECT_ANNOTATION" # Something like sticky note, can be located anywhere in the pipeline UI
15+
SUPPORT = "SUPPORT" # Support conversation
1516

1617

1718
conversations = sa.Table(
@@ -70,6 +71,13 @@ class ConversationType(enum.Enum):
7071
nullable=False,
7172
doc="Product name identifier. If None, then the item is not exposed",
7273
),
74+
sa.Column(
75+
"extra_context",
76+
JSONB,
77+
nullable=False,
78+
server_default=sa.text("'{}'::jsonb"),
79+
doc="Free JSON to store extra context",
80+
),
7381
column_created_datetime(timezone=True),
7482
column_modified_datetime(timezone=True),
7583
)

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,5 +269,18 @@ class ProductLoginSettingsDict(TypedDict, total=False):
269269
nullable=True,
270270
doc="Group associated to this product",
271271
),
272+
sa.Column(
273+
"support_standard_group_id",
274+
sa.BigInteger,
275+
sa.ForeignKey(
276+
groups.c.gid,
277+
name="fk_products_support_standard_group_id",
278+
ondelete=RefActions.SET_NULL,
279+
onupdate=RefActions.CASCADE,
280+
),
281+
unique=False,
282+
nullable=True,
283+
doc="Group associated to this product support",
284+
),
272285
sa.PrimaryKeyConstraint("name", name="products_pk"),
273286
)

services/web/server/src/simcore_service_webserver/conversations/_conversation_repository.py

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import cast
2+
from typing import Any, cast
33

44
from aiohttp import web
55
from models_library.conversations import (
@@ -42,6 +42,7 @@ async def create(
4242
user_group_id: GroupID,
4343
type_: ConversationType,
4444
product_name: ProductName,
45+
extra_context: dict[str, Any],
4546
) -> ConversationGetDB:
4647
async with transaction_context(get_asyncpg_engine(app), connection) as conn:
4748
result = await conn.execute(
@@ -54,6 +55,7 @@ async def create(
5455
created=func.now(),
5556
modified=func.now(),
5657
product_name=product_name,
58+
extra_context=extra_context,
5759
)
5860
.returning(*_SELECTION_ARGS)
5961
)
@@ -76,7 +78,110 @@ async def list_project_conversations(
7678
base_query = (
7779
select(*_SELECTION_ARGS)
7880
.select_from(conversations)
79-
.where(conversations.c.project_uuid == f"{project_uuid}")
81+
.where(
82+
(conversations.c.project_uuid == f"{project_uuid}")
83+
& (
84+
conversations.c.type
85+
in [
86+
ConversationType.PROJECT_STATIC,
87+
ConversationType.PROJECT_ANNOTATION,
88+
]
89+
)
90+
)
91+
)
92+
93+
# Select total count from base_query
94+
subquery = base_query.subquery()
95+
count_query = select(func.count()).select_from(subquery)
96+
97+
# Ordering and pagination
98+
if order_by.direction == OrderDirection.ASC:
99+
list_query = base_query.order_by(
100+
asc(getattr(conversations.c, order_by.field)),
101+
conversations.c.conversation_id,
102+
)
103+
else:
104+
list_query = base_query.order_by(
105+
desc(getattr(conversations.c, order_by.field)),
106+
conversations.c.conversation_id,
107+
)
108+
list_query = list_query.offset(offset).limit(limit)
109+
110+
async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn:
111+
total_count = await conn.scalar(count_query)
112+
113+
result = await conn.stream(list_query)
114+
items: list[ConversationGetDB] = [
115+
ConversationGetDB.model_validate(row) async for row in result
116+
]
117+
118+
return cast(int, total_count), items
119+
120+
121+
async def list_support_conversations_for_user(
122+
app: web.Application,
123+
connection: AsyncConnection | None = None,
124+
*,
125+
user_group_id: GroupID,
126+
# pagination
127+
offset: NonNegativeInt,
128+
limit: NonNegativeInt,
129+
# ordering
130+
order_by: OrderBy,
131+
) -> tuple[PageTotalCount, list[ConversationGetDB]]:
132+
133+
base_query = (
134+
select(*_SELECTION_ARGS)
135+
.select_from(conversations)
136+
.where(
137+
(conversations.c.user_group_id == user_group_id)
138+
& (conversations.c.type == ConversationType.SUPPORT)
139+
)
140+
)
141+
142+
# Select total count from base_query
143+
subquery = base_query.subquery()
144+
count_query = select(func.count()).select_from(subquery)
145+
146+
# Ordering and pagination
147+
if order_by.direction == OrderDirection.ASC:
148+
list_query = base_query.order_by(
149+
asc(getattr(conversations.c, order_by.field)),
150+
conversations.c.conversation_id,
151+
)
152+
else:
153+
list_query = base_query.order_by(
154+
desc(getattr(conversations.c, order_by.field)),
155+
conversations.c.conversation_id,
156+
)
157+
list_query = list_query.offset(offset).limit(limit)
158+
159+
async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn:
160+
total_count = await conn.scalar(count_query)
161+
162+
result = await conn.stream(list_query)
163+
items: list[ConversationGetDB] = [
164+
ConversationGetDB.model_validate(row) async for row in result
165+
]
166+
167+
return cast(int, total_count), items
168+
169+
170+
async def list_all_support_conversations_for_support_user(
171+
app: web.Application,
172+
connection: AsyncConnection | None = None,
173+
*,
174+
# pagination
175+
offset: NonNegativeInt,
176+
limit: NonNegativeInt,
177+
# ordering
178+
order_by: OrderBy,
179+
) -> tuple[PageTotalCount, list[ConversationGetDB]]:
180+
181+
base_query = (
182+
select(*_SELECTION_ARGS)
183+
.select_from(conversations)
184+
.where(conversations.c.type == ConversationType.SUPPORT)
80185
)
81186

82187
# Select total count from base_query

services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# pylint: disable=unused-argument
22

33
import logging
4+
from typing import Any
45

56
from aiohttp import web
67
from models_library.basic_types import IDStr
@@ -21,6 +22,8 @@
2122
notify_conversation_deleted,
2223
notify_conversation_updated,
2324
)
25+
from ..groups.api import list_user_groups_ids_with_read_access
26+
from ..products import products_service
2427
from ..projects._groups_repository import list_project_groups
2528
from ..users import users_service
2629
from ..users._users_service import get_users_in_group
@@ -48,6 +51,7 @@ async def create_conversation(
4851
# Creation attributes
4952
name: str,
5053
type_: ConversationType,
54+
extra_context: dict[str, Any],
5155
) -> ConversationGetDB:
5256
if project_uuid is None:
5357
raise NotImplementedError
@@ -61,6 +65,7 @@ async def create_conversation(
6165
user_group_id=_user_group_id,
6266
type_=type_,
6367
product_name=product_name,
68+
extra_context=extra_context,
6469
)
6570

6671
await notify_conversation_created(
@@ -133,7 +138,7 @@ async def delete_conversation(
133138
)
134139

135140

136-
async def list_conversations_for_project(
141+
async def list_project_conversations(
137142
app: web.Application,
138143
*,
139144
project_uuid: ProjectID,
@@ -148,3 +153,41 @@ async def list_conversations_for_project(
148153
limit=limit,
149154
order_by=OrderBy(field=IDStr("conversation_id"), direction=OrderDirection.DESC),
150155
)
156+
157+
158+
async def list_support_conversations_for_user(
159+
app: web.Application,
160+
*,
161+
user_id: UserID,
162+
product_name: ProductName,
163+
# pagination
164+
offset: int = 0,
165+
limit: int = 20,
166+
) -> tuple[PageTotalCount, list[ConversationGetDB]]:
167+
168+
# Check if user is part of support group (in that case list all support conversations)
169+
product = products_service.get_product(app, product_name=product_name)
170+
_support_standard_group_id = product.support_standard_group_id
171+
if _support_standard_group_id is not None:
172+
_user_group_ids = await list_user_groups_ids_with_read_access(
173+
app, user_id=user_id
174+
)
175+
if _support_standard_group_id in _user_group_ids:
176+
# I am a support user
177+
return await _conversation_repository.list_all_support_conversations_for_support_user(
178+
app,
179+
offset=offset,
180+
limit=limit,
181+
order_by=OrderBy(
182+
field=IDStr("conversation_id"), direction=OrderDirection.DESC
183+
),
184+
)
185+
186+
_user_group_id = await users_service.get_user_primary_group_id(app, user_id=user_id)
187+
return await _conversation_repository.list_support_conversations_for_user(
188+
app,
189+
user_group_id=_user_group_id,
190+
offset=offset,
191+
limit=limit,
192+
order_by=OrderBy(field=IDStr("conversation_id"), direction=OrderDirection.DESC),
193+
)

services/web/server/src/simcore_service_webserver/conversations/conversations_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
create_conversation,
1111
delete_conversation,
1212
get_conversation,
13-
list_conversations_for_project,
13+
list_project_conversations,
1414
update_conversation,
1515
)
1616

@@ -21,7 +21,7 @@
2121
"delete_message",
2222
"get_conversation",
2323
"get_message",
24-
"list_conversations_for_project",
24+
"list_project_conversations",
2525
"list_messages_for_conversation",
2626
"update_conversation",
2727
"update_message",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ class Product(BaseModel):
140140
group_id: Annotated[
141141
int | None, Field(description="Groups associated to this product")
142142
] = None
143+
support_standard_group_id: Annotated[
144+
int | None, Field(description="Support standard group ID")
145+
] = None
143146

144147
is_payment_enabled: Annotated[
145148
bool,

services/web/server/src/simcore_service_webserver/projects/_controller/conversations_rest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ async def create_project_conversation(request: web.Request):
7474
body_params = await parse_request_body_as(
7575
_ProjectConversationsCreateBodyParams, request
7676
)
77+
_extra_context = (
78+
body_params.extra_context if body_params.extra_context is not None else {}
79+
)
7780

7881
conversation = await _conversations_service.create_project_conversation(
7982
app=request.app,
@@ -82,6 +85,7 @@ async def create_project_conversation(request: web.Request):
8285
project_uuid=path_params.project_id,
8386
name=body_params.name,
8487
conversation_type=body_params.type,
88+
extra_context=_extra_context,
8589
)
8690
data = ConversationRestGet.from_domain_model(conversation)
8791

services/web/server/src/simcore_service_webserver/projects/_conversations_service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ async def create_project_conversation(
5252
project_uuid=project_uuid,
5353
name=name,
5454
type_=conversation_type,
55+
extra_context={},
5556
)
5657

5758

@@ -72,7 +73,7 @@ async def list_project_conversations(
7273
project_id=project_uuid,
7374
permission="read",
7475
)
75-
return await conversations_service.list_conversations_for_project(
76+
return await conversations_service.list_project_conversations(
7677
app,
7778
project_uuid=project_uuid,
7879
offset=offset,

0 commit comments

Comments
 (0)