refactor(api): migrate console.app.workflow_comment to BaseModel (#36180)

This commit is contained in:
chariri 2026-05-14 21:13:47 +09:00 committed by GitHub
parent a35b28dbef
commit 5798610f27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 554 additions and 231 deletions

View File

@ -1,22 +1,16 @@
import logging
from datetime import datetime
from flask_restx import Resource, marshal_with
from pydantic import BaseModel, Field, TypeAdapter
from flask_restx import Resource
from pydantic import BaseModel, Field, TypeAdapter, computed_field, field_validator
from controllers.common.schema import register_schema_models
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required
from fields.base import ResponseModel
from fields.member_fields import AccountWithRole
from fields.workflow_comment_fields import (
workflow_comment_basic_fields,
workflow_comment_create_fields,
workflow_comment_detail_fields,
workflow_comment_reply_create_fields,
workflow_comment_reply_update_fields,
workflow_comment_resolve_fields,
workflow_comment_update_fields,
)
from libs.helper import build_avatar_url, dump_response, to_timestamp
from libs.login import current_user, login_required
from models import App
from services.account_service import TenantService
@ -51,6 +45,138 @@ class WorkflowCommentMentionUsersPayload(BaseModel):
users: list[AccountWithRole]
class WorkflowCommentAccount(ResponseModel):
id: str
name: str
email: str
avatar: str | None = Field(default=None, exclude=True)
@computed_field(return_type=str | None) # type: ignore[prop-decorator]
@property
def avatar_url(self) -> str | None:
return build_avatar_url(self.avatar)
class WorkflowCommentReply(ResponseModel):
id: str
content: str
created_by: str
created_by_account: WorkflowCommentAccount | None = None
created_at: int | None = None
@field_validator("created_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class WorkflowCommentMention(ResponseModel):
mentioned_user_id: str
mentioned_user_account: WorkflowCommentAccount | None = None
reply_id: str | None = None
class WorkflowCommentBasic(ResponseModel):
id: str
position_x: float
position_y: float
content: str
created_by: str
created_by_account: WorkflowCommentAccount | None = None
created_at: int | None = None
updated_at: int | None = None
resolved: bool
resolved_at: int | None = None
resolved_by: str | None = None
resolved_by_account: WorkflowCommentAccount | None = None
reply_count: int
mention_count: int
participants: list[WorkflowCommentAccount]
@field_validator("created_at", "updated_at", "resolved_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class WorkflowCommentBasicList(ResponseModel):
data: list[WorkflowCommentBasic]
class WorkflowCommentDetail(ResponseModel):
id: str
position_x: float
position_y: float
content: str
created_by: str
created_by_account: WorkflowCommentAccount | None = None
created_at: int | None = None
updated_at: int | None = None
resolved: bool
resolved_at: int | None = None
resolved_by: str | None = None
resolved_by_account: WorkflowCommentAccount | None = None
replies: list[WorkflowCommentReply]
mentions: list[WorkflowCommentMention]
@field_validator("created_at", "updated_at", "resolved_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class WorkflowCommentCreate(ResponseModel):
id: str
created_at: int | None = None
@field_validator("created_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class WorkflowCommentUpdate(ResponseModel):
id: str
updated_at: int | None = None
@field_validator("updated_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class WorkflowCommentResolve(ResponseModel):
id: str
resolved: bool
resolved_at: int | None = None
resolved_by: str | None = None
@field_validator("resolved_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class WorkflowCommentReplyCreate(ResponseModel):
id: str
created_at: int | None = None
@field_validator("created_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class WorkflowCommentReplyUpdate(ResponseModel):
id: str
updated_at: int | None = None
@field_validator("updated_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
register_schema_models(
console_ns,
AccountWithRole,
@ -59,17 +185,19 @@ register_schema_models(
WorkflowCommentUpdatePayload,
WorkflowCommentReplyPayload,
)
workflow_comment_basic_model = console_ns.model("WorkflowCommentBasic", workflow_comment_basic_fields)
workflow_comment_detail_model = console_ns.model("WorkflowCommentDetail", workflow_comment_detail_fields)
workflow_comment_create_model = console_ns.model("WorkflowCommentCreate", workflow_comment_create_fields)
workflow_comment_update_model = console_ns.model("WorkflowCommentUpdate", workflow_comment_update_fields)
workflow_comment_resolve_model = console_ns.model("WorkflowCommentResolve", workflow_comment_resolve_fields)
workflow_comment_reply_create_model = console_ns.model(
"WorkflowCommentReplyCreate", workflow_comment_reply_create_fields
)
workflow_comment_reply_update_model = console_ns.model(
"WorkflowCommentReplyUpdate", workflow_comment_reply_update_fields
register_response_schema_models(
console_ns,
WorkflowCommentAccount,
WorkflowCommentReply,
WorkflowCommentMention,
WorkflowCommentBasic,
WorkflowCommentBasicList,
WorkflowCommentDetail,
WorkflowCommentCreate,
WorkflowCommentUpdate,
WorkflowCommentResolve,
WorkflowCommentReplyCreate,
WorkflowCommentReplyUpdate,
)
@ -80,28 +208,26 @@ class WorkflowCommentListApi(Resource):
@console_ns.doc("list_workflow_comments")
@console_ns.doc(description="Get all comments for a workflow")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.response(200, "Comments retrieved successfully", workflow_comment_basic_model)
@console_ns.response(200, "Comments retrieved successfully", console_ns.models[WorkflowCommentBasicList.__name__])
@login_required
@setup_required
@account_initialization_required
@get_app_model()
@marshal_with(workflow_comment_basic_model, envelope="data")
def get(self, app_model: App):
"""Get all comments for a workflow."""
comments = WorkflowCommentService.get_comments(tenant_id=current_user.current_tenant_id, app_id=app_model.id)
return comments
return WorkflowCommentBasicList.model_validate({"data": comments}).model_dump(mode="json")
@console_ns.doc("create_workflow_comment")
@console_ns.doc(description="Create a new workflow comment")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[WorkflowCommentCreatePayload.__name__])
@console_ns.response(201, "Comment created successfully", workflow_comment_create_model)
@console_ns.response(201, "Comment created successfully", console_ns.models[WorkflowCommentCreate.__name__])
@login_required
@setup_required
@account_initialization_required
@get_app_model()
@marshal_with(workflow_comment_create_model)
@edit_permission_required
def post(self, app_model: App):
"""Create a new workflow comment."""
@ -117,7 +243,7 @@ class WorkflowCommentListApi(Resource):
mentioned_user_ids=payload.mentioned_user_ids,
)
return result, 201
return dump_response(WorkflowCommentCreate, result), 201
@console_ns.route("/apps/<uuid:app_id>/workflow/comments/<string:comment_id>")
@ -127,30 +253,28 @@ class WorkflowCommentDetailApi(Resource):
@console_ns.doc("get_workflow_comment")
@console_ns.doc(description="Get a specific workflow comment")
@console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID"})
@console_ns.response(200, "Comment retrieved successfully", workflow_comment_detail_model)
@console_ns.response(200, "Comment retrieved successfully", console_ns.models[WorkflowCommentDetail.__name__])
@login_required
@setup_required
@account_initialization_required
@get_app_model()
@marshal_with(workflow_comment_detail_model)
def get(self, app_model: App, comment_id: str):
"""Get a specific workflow comment."""
comment = WorkflowCommentService.get_comment(
tenant_id=current_user.current_tenant_id, app_id=app_model.id, comment_id=comment_id
)
return comment
return dump_response(WorkflowCommentDetail, comment)
@console_ns.doc("update_workflow_comment")
@console_ns.doc(description="Update a workflow comment")
@console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID"})
@console_ns.expect(console_ns.models[WorkflowCommentUpdatePayload.__name__])
@console_ns.response(200, "Comment updated successfully", workflow_comment_update_model)
@console_ns.response(200, "Comment updated successfully", console_ns.models[WorkflowCommentUpdate.__name__])
@login_required
@setup_required
@account_initialization_required
@get_app_model()
@marshal_with(workflow_comment_update_model)
@edit_permission_required
def put(self, app_model: App, comment_id: str):
"""Update a workflow comment."""
@ -167,7 +291,7 @@ class WorkflowCommentDetailApi(Resource):
mentioned_user_ids=payload.mentioned_user_ids,
)
return result
return dump_response(WorkflowCommentUpdate, result)
@console_ns.doc("delete_workflow_comment")
@console_ns.doc(description="Delete a workflow comment")
@ -197,12 +321,11 @@ class WorkflowCommentResolveApi(Resource):
@console_ns.doc("resolve_workflow_comment")
@console_ns.doc(description="Resolve a workflow comment")
@console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID"})
@console_ns.response(200, "Comment resolved successfully", workflow_comment_resolve_model)
@console_ns.response(200, "Comment resolved successfully", console_ns.models[WorkflowCommentResolve.__name__])
@login_required
@setup_required
@account_initialization_required
@get_app_model()
@marshal_with(workflow_comment_resolve_model)
@edit_permission_required
def post(self, app_model: App, comment_id: str):
"""Resolve a workflow comment."""
@ -213,7 +336,7 @@ class WorkflowCommentResolveApi(Resource):
user_id=current_user.id,
)
return comment
return dump_response(WorkflowCommentResolve, comment)
@console_ns.route("/apps/<uuid:app_id>/workflow/comments/<string:comment_id>/replies")
@ -224,12 +347,11 @@ class WorkflowCommentReplyApi(Resource):
@console_ns.doc(description="Add a reply to a workflow comment")
@console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID"})
@console_ns.expect(console_ns.models[WorkflowCommentReplyPayload.__name__])
@console_ns.response(201, "Reply created successfully", workflow_comment_reply_create_model)
@console_ns.response(201, "Reply created successfully", console_ns.models[WorkflowCommentReplyCreate.__name__])
@login_required
@setup_required
@account_initialization_required
@get_app_model()
@marshal_with(workflow_comment_reply_create_model)
@edit_permission_required
def post(self, app_model: App, comment_id: str):
"""Add a reply to a workflow comment."""
@ -247,7 +369,7 @@ class WorkflowCommentReplyApi(Resource):
mentioned_user_ids=payload.mentioned_user_ids,
)
return result, 201
return dump_response(WorkflowCommentReplyCreate, result), 201
@console_ns.route("/apps/<uuid:app_id>/workflow/comments/<string:comment_id>/replies/<string:reply_id>")
@ -258,12 +380,11 @@ class WorkflowCommentReplyDetailApi(Resource):
@console_ns.doc(description="Update a comment reply")
@console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID", "reply_id": "Reply ID"})
@console_ns.expect(console_ns.models[WorkflowCommentReplyPayload.__name__])
@console_ns.response(200, "Reply updated successfully", workflow_comment_reply_update_model)
@console_ns.response(200, "Reply updated successfully", console_ns.models[WorkflowCommentReplyUpdate.__name__])
@login_required
@setup_required
@account_initialization_required
@get_app_model()
@marshal_with(workflow_comment_reply_update_model)
@edit_permission_required
def put(self, app_model: App, comment_id: str, reply_id: str):
"""Update a comment reply."""
@ -284,7 +405,7 @@ class WorkflowCommentReplyDetailApi(Resource):
mentioned_user_ids=payload.mentioned_user_ids,
)
return reply
return dump_response(WorkflowCommentReplyUpdate, reply)
@console_ns.doc("delete_workflow_comment_reply")
@console_ns.doc(description="Delete a comment reply")

View File

@ -6,8 +6,7 @@ from flask_restx import fields
from pydantic import computed_field, field_validator
from fields.base import ResponseModel
from graphon.file import helpers as file_helpers
from libs.helper import to_timestamp
from libs.helper import build_avatar_url, to_timestamp
simple_account_fields = {
"id": fields.String,
@ -16,14 +15,6 @@ simple_account_fields = {
}
def _build_avatar_url(avatar: str | None) -> str | None:
if avatar is None:
return None
if avatar.startswith(("http://", "https://")):
return avatar
return file_helpers.get_signed_file_url(avatar)
class SimpleAccount(ResponseModel):
id: str
name: str
@ -36,7 +27,7 @@ class _AccountAvatar(ResponseModel):
@computed_field(return_type=str | None) # type: ignore[prop-decorator]
@property
def avatar_url(self) -> str | None:
return _build_avatar_url(self.avatar)
return build_avatar_url(self.avatar)
class Account(_AccountAvatar):

View File

@ -1,96 +0,0 @@
from flask_restx import fields
from libs.helper import AvatarUrlField, TimestampField
# basic account fields for comments
account_fields = {
"id": fields.String,
"name": fields.String,
"email": fields.String,
"avatar_url": AvatarUrlField,
}
# Comment mention fields
workflow_comment_mention_fields = {
"mentioned_user_id": fields.String,
"mentioned_user_account": fields.Nested(account_fields, allow_null=True),
"reply_id": fields.String,
}
# Comment reply fields
workflow_comment_reply_fields = {
"id": fields.String,
"content": fields.String,
"created_by": fields.String,
"created_by_account": fields.Nested(account_fields, allow_null=True),
"created_at": TimestampField,
}
# Basic comment fields (for list views)
workflow_comment_basic_fields = {
"id": fields.String,
"position_x": fields.Float,
"position_y": fields.Float,
"content": fields.String,
"created_by": fields.String,
"created_by_account": fields.Nested(account_fields, allow_null=True),
"created_at": TimestampField,
"updated_at": TimestampField,
"resolved": fields.Boolean,
"resolved_at": TimestampField,
"resolved_by": fields.String,
"resolved_by_account": fields.Nested(account_fields, allow_null=True),
"reply_count": fields.Integer,
"mention_count": fields.Integer,
"participants": fields.List(fields.Nested(account_fields)),
}
# Detailed comment fields (for single comment view)
workflow_comment_detail_fields = {
"id": fields.String,
"position_x": fields.Float,
"position_y": fields.Float,
"content": fields.String,
"created_by": fields.String,
"created_by_account": fields.Nested(account_fields, allow_null=True),
"created_at": TimestampField,
"updated_at": TimestampField,
"resolved": fields.Boolean,
"resolved_at": TimestampField,
"resolved_by": fields.String,
"resolved_by_account": fields.Nested(account_fields, allow_null=True),
"replies": fields.List(fields.Nested(workflow_comment_reply_fields)),
"mentions": fields.List(fields.Nested(workflow_comment_mention_fields)),
}
# Comment creation response fields (simplified)
workflow_comment_create_fields = {
"id": fields.String,
"created_at": TimestampField,
}
# Comment update response fields (simplified)
workflow_comment_update_fields = {
"id": fields.String,
"updated_at": TimestampField,
}
# Comment resolve response fields
workflow_comment_resolve_fields = {
"id": fields.String,
"resolved": fields.Boolean,
"resolved_at": TimestampField,
"resolved_by": fields.String,
}
# Reply creation response fields (simplified)
workflow_comment_reply_create_fields = {
"id": fields.String,
"created_at": TimestampField,
}
# Reply update response fields
workflow_comment_reply_update_fields = {
"id": fields.String,
"updated_at": TimestampField,
}

View File

@ -136,6 +136,14 @@ def build_icon_url(icon_type: Any, icon: str | None) -> str | None:
return file_helpers.get_signed_file_url(icon)
def build_avatar_url(avatar: str | None) -> str | None:
if avatar is None:
return None
if avatar.startswith(("http://", "https://")):
return avatar
return file_helpers.get_signed_file_url(avatar)
class AvatarUrlField(fields.Raw):
def output(self, key, obj, **kwargs):
if obj is None:
@ -144,9 +152,7 @@ class AvatarUrlField(fields.Raw):
from models import Account
if isinstance(obj, Account) and obj.avatar is not None:
if obj.avatar.startswith(("http://", "https://")):
return obj.avatar
return file_helpers.get_signed_file_url(obj.avatar)
return build_avatar_url(obj.avatar)
return None
@ -181,6 +187,11 @@ def to_timestamp(value: datetime | int | None) -> int | None:
return value
def dump_response(model: type[BaseModel], data: Any) -> dict[str, Any]:
"""Serialize a Pydantic response model to JSON-compatible dict output."""
return model.model_validate(data, from_attributes=True).model_dump(mode="json")
def current_timestamp() -> int:
"""Return the current Unix timestamp in seconds."""
return int(time.time())

View File

@ -2404,7 +2404,7 @@ Get all comments for a workflow
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Comments retrieved successfully | [WorkflowCommentBasic](#workflowcommentbasic) |
| 200 | Comments retrieved successfully | [WorkflowCommentBasicList](#workflowcommentbasiclist) |
#### POST
##### Summary
@ -13927,32 +13927,47 @@ User action configuration.
| trigger_metadata | | | No |
| workflow_run | | | No |
#### WorkflowCommentAccount
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| avatar_url | | | Yes |
| email | string | | Yes |
| id | string | | Yes |
| name | string | | Yes |
#### WorkflowCommentBasic
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | No |
| created_at | object | | No |
| created_by | string | | No |
| created_by_account | [_AnonymousInlineModel_6fec07cd0d85](#_anonymousinlinemodel_6fec07cd0d85) | | No |
| id | string | | No |
| mention_count | integer | | No |
| participants | [ [_AnonymousInlineModel_6fec07cd0d85](#_anonymousinlinemodel_6fec07cd0d85) ] | | No |
| position_x | number | | No |
| position_y | number | | No |
| reply_count | integer | | No |
| resolved | boolean | | No |
| resolved_at | object | | No |
| resolved_by | string | | No |
| resolved_by_account | [_AnonymousInlineModel_6fec07cd0d85](#_anonymousinlinemodel_6fec07cd0d85) | | No |
| updated_at | object | | No |
| content | string | | Yes |
| created_at | | | No |
| created_by | string | | Yes |
| created_by_account | | | No |
| id | string | | Yes |
| mention_count | integer | | Yes |
| participants | [ [WorkflowCommentAccount](#workflowcommentaccount) ] | | Yes |
| position_x | number | | Yes |
| position_y | number | | Yes |
| reply_count | integer | | Yes |
| resolved | boolean | | Yes |
| resolved_at | | | No |
| resolved_by | | | No |
| resolved_by_account | | | No |
| updated_at | | | No |
#### WorkflowCommentBasicList
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | [ [WorkflowCommentBasic](#workflowcommentbasic) ] | | Yes |
#### WorkflowCommentCreate
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| created_at | object | | No |
| id | string | | No |
| created_at | | | No |
| id | string | | Yes |
#### WorkflowCommentCreatePayload
@ -13967,20 +13982,28 @@ User action configuration.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | No |
| created_at | object | | No |
| created_by | string | | No |
| created_by_account | [_AnonymousInlineModel_6fec07cd0d85](#_anonymousinlinemodel_6fec07cd0d85) | | No |
| id | string | | No |
| mentions | [ [_AnonymousInlineModel_f7ff64cce858](#_anonymousinlinemodel_f7ff64cce858) ] | | No |
| position_x | number | | No |
| position_y | number | | No |
| replies | [ [_AnonymousInlineModel_55c39c6a4b9e](#_anonymousinlinemodel_55c39c6a4b9e) ] | | No |
| resolved | boolean | | No |
| resolved_at | object | | No |
| resolved_by | string | | No |
| resolved_by_account | [_AnonymousInlineModel_6fec07cd0d85](#_anonymousinlinemodel_6fec07cd0d85) | | No |
| updated_at | object | | No |
| content | string | | Yes |
| created_at | | | No |
| created_by | string | | Yes |
| created_by_account | | | No |
| id | string | | Yes |
| mentions | [ [WorkflowCommentMention](#workflowcommentmention) ] | | Yes |
| position_x | number | | Yes |
| position_y | number | | Yes |
| replies | [ [WorkflowCommentReply](#workflowcommentreply) ] | | Yes |
| resolved | boolean | | Yes |
| resolved_at | | | No |
| resolved_by | | | No |
| resolved_by_account | | | No |
| updated_at | | | No |
#### WorkflowCommentMention
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| mentioned_user_account | | | No |
| mentioned_user_id | string | | Yes |
| reply_id | | | No |
#### WorkflowCommentMentionUsersPayload
@ -13988,12 +14011,22 @@ User action configuration.
| ---- | ---- | ----------- | -------- |
| users | [ [AccountWithRole](#accountwithrole) ] | | Yes |
#### WorkflowCommentReply
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | Yes |
| created_at | | | No |
| created_by | string | | Yes |
| created_by_account | | | No |
| id | string | | Yes |
#### WorkflowCommentReplyCreate
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| created_at | object | | No |
| id | string | | No |
| created_at | | | No |
| id | string | | Yes |
#### WorkflowCommentReplyPayload
@ -14006,24 +14039,24 @@ User action configuration.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| id | string | | No |
| updated_at | object | | No |
| id | string | | Yes |
| updated_at | | | No |
#### WorkflowCommentResolve
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| id | string | | No |
| resolved | boolean | | No |
| resolved_at | object | | No |
| resolved_by | string | | No |
| id | string | | Yes |
| resolved | boolean | | Yes |
| resolved_at | | | No |
| resolved_by | | | No |
#### WorkflowCommentUpdate
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| id | string | | No |
| updated_at | object | | No |
| id | string | | Yes |
| updated_at | | | No |
#### WorkflowCommentUpdatePayload
@ -14427,25 +14460,6 @@ Workflow tool configuration
| limit | integer | | No |
| page | integer | | No |
#### _AnonymousInlineModel_55c39c6a4b9e
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | No |
| created_at | object | | No |
| created_by | string | | No |
| created_by_account | [_AnonymousInlineModel_6fec07cd0d85](#_anonymousinlinemodel_6fec07cd0d85) | | No |
| id | string | | No |
#### _AnonymousInlineModel_6fec07cd0d85
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| avatar_url | object | | No |
| email | string | | No |
| id | string | | No |
| name | string | | No |
#### _AnonymousInlineModel_b1954337d565
| Name | Type | Description | Required |
@ -14455,14 +14469,6 @@ Workflow tool configuration
| model_provider_name | string | | No |
| summary_prompt | string | | No |
#### _AnonymousInlineModel_f7ff64cce858
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| mentioned_user_account | [_AnonymousInlineModel_6fec07cd0d85](#_anonymousinlinemodel_6fec07cd0d85) | | No |
| mentioned_user_id | string | | No |
| reply_id | string | | No |
## FastOpenAPI Preview (OpenAPI 3.0)
### Dify API (FastOpenAPI PoC)

View File

@ -17,6 +17,15 @@ from controllers.console.app import wraps as app_wraps
from libs import login as login_lib
from models.account import Account, AccountStatus, TenantAccountRole
JAN_1_2024_NOON = datetime(2024, 1, 1, 12, 0, 0)
JAN_1_2024_NOON_TS = int(JAN_1_2024_NOON.timestamp())
JAN_1_2024_1201 = datetime(2024, 1, 1, 12, 1, 0)
JAN_1_2024_1201_TS = int(JAN_1_2024_1201.timestamp())
JAN_1_2024_1202 = datetime(2024, 1, 1, 12, 2, 0)
JAN_1_2024_1202_TS = int(JAN_1_2024_1202.timestamp())
JAN_1_2024_1203 = datetime(2024, 1, 1, 12, 3, 0)
JAN_1_2024_1203_TS = int(JAN_1_2024_1203.timestamp())
def _make_account(role: TenantAccountRole) -> Account:
account = Account(name="tester", email="tester@example.com")
@ -78,6 +87,30 @@ class WriteCase:
payload: dict[str, object] | None = None
@dataclass(frozen=True)
class MutationResponseCase:
resource_cls: type
method_name: str
path: str
kwargs: dict[str, str]
service_method_name: str
service_return: object
expected_response: dict[str, object]
payload: dict[str, object] | None = None
expected_status: int | None = None
def _unwrap_response(result: object) -> tuple[dict[str, object], int | None]:
if isinstance(result, tuple):
response, status = result
assert isinstance(response, dict)
assert isinstance(status, int)
return response, status
assert isinstance(result, dict)
return result, None
@pytest.mark.parametrize(
"case",
[
@ -151,17 +184,20 @@ def test_create_comment_allows_editor(app: Flask, monkeypatch: pytest.MonkeyPatc
create_comment_mock = MagicMock(return_value={"id": "comment-1"})
monkeypatch.setattr(workflow_comment_module.WorkflowCommentService, "create_comment", create_comment_mock)
payload = {"content": "hello", "position_x": 1.0, "position_y": 2.0, "mentioned_user_ids": []}
payload: dict[str, object] = {
"content": "hello",
"position_x": 1.0,
"position_y": 2.0,
"mentioned_user_ids": [],
}
with app.test_request_context("/console/api/apps/app-123/workflow/comments", method="POST", json=payload):
with _patch_payload(payload):
result = workflow_comment_module.WorkflowCommentListApi().post(app_id="app-123")
if isinstance(result, tuple):
response = result[0]
else:
response = result
response, status = _unwrap_response(result)
assert response["id"] == "comment-1"
assert status == 201
create_comment_mock.assert_called_once_with(
tenant_id="tenant-123",
app_id="app-123",
@ -181,14 +217,17 @@ def test_update_comment_omits_mentions_when_payload_does_not_include_them(
app_model = _make_app()
_patch_console_guards(monkeypatch, account, app_model)
update_comment_mock = MagicMock(return_value={"id": "comment-1", "updated_at": datetime(2024, 1, 1, 12, 0, 0)})
update_comment_mock = MagicMock(return_value={"id": "comment-1", "updated_at": JAN_1_2024_NOON})
monkeypatch.setattr(workflow_comment_module.WorkflowCommentService, "update_comment", update_comment_mock)
payload = {"content": "hello", "position_x": 10.0, "position_y": 20.0}
payload: dict[str, object] = {"content": "hello", "position_x": 10.0, "position_y": 20.0}
with app.test_request_context("/console/api/apps/app-123/workflow/comments/comment-1", method="PUT", json=payload):
with _patch_payload(payload):
workflow_comment_module.WorkflowCommentDetailApi().put(app_id="app-123", comment_id="comment-1")
result = workflow_comment_module.WorkflowCommentDetailApi().put(app_id="app-123", comment_id="comment-1")
response, status = _unwrap_response(result)
assert response == {"id": "comment-1", "updated_at": JAN_1_2024_NOON_TS}
assert status is None
update_comment_mock.assert_called_once_with(
tenant_id="tenant-123",
app_id="app-123",
@ -199,3 +238,254 @@ def test_update_comment_omits_mentions_when_payload_does_not_include_them(
position_y=20.0,
mentioned_user_ids=None,
)
def test_list_comments_serializes_response_model(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
app.config.setdefault("RESTX_MASK_HEADER", "X-Fields")
account = _make_account(TenantAccountRole.NORMAL)
app_model = _make_app()
_patch_console_guards(monkeypatch, account, app_model)
comment_author = SimpleNamespace(
id="account-123",
name="tester",
email="tester@example.com",
avatar="https://example.com/avatar.png",
)
comment = SimpleNamespace(
id="comment-1",
position_x=1.5,
position_y=2.5,
content="hello",
created_by="account-123",
created_by_account=comment_author,
created_at=1_700_000_000,
updated_at=1_700_000_001,
resolved=False,
resolved_at=None,
resolved_by=None,
resolved_by_account=None,
reply_count=0,
mention_count=0,
participants=[comment_author],
)
get_comments_mock = MagicMock(return_value=[comment])
monkeypatch.setattr(workflow_comment_module.WorkflowCommentService, "get_comments", get_comments_mock)
with app.test_request_context("/console/api/apps/app-123/workflow/comments", method="GET"):
response = workflow_comment_module.WorkflowCommentListApi().get(app_id="app-123")
assert response == {
"data": [
{
"id": "comment-1",
"position_x": 1.5,
"position_y": 2.5,
"content": "hello",
"created_by": "account-123",
"created_by_account": {
"id": "account-123",
"name": "tester",
"email": "tester@example.com",
"avatar_url": "https://example.com/avatar.png",
},
"created_at": 1_700_000_000,
"updated_at": 1_700_000_001,
"resolved": False,
"resolved_at": None,
"resolved_by": None,
"resolved_by_account": None,
"reply_count": 0,
"mention_count": 0,
"participants": [
{
"id": "account-123",
"name": "tester",
"email": "tester@example.com",
"avatar_url": "https://example.com/avatar.png",
}
],
}
]
}
get_comments_mock.assert_called_once_with(tenant_id="tenant-123", app_id="app-123")
def test_get_comment_serializes_detail_response_model(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
app.config.setdefault("RESTX_MASK_HEADER", "X-Fields")
account = _make_account(TenantAccountRole.NORMAL)
app_model = _make_app()
_patch_console_guards(monkeypatch, account, app_model)
comment_author = SimpleNamespace(
id="account-123",
name="tester",
email="tester@example.com",
avatar="https://example.com/avatar.png",
)
mentioned_user = SimpleNamespace(
id="account-456",
name="mentioned",
email="mentioned@example.com",
avatar=None,
)
comment = SimpleNamespace(
id="comment-1",
position_x=1.5,
position_y=2.5,
content="hello",
created_by="account-123",
created_by_account=comment_author,
created_at=JAN_1_2024_NOON,
updated_at=JAN_1_2024_1201,
resolved=True,
resolved_at=JAN_1_2024_1202,
resolved_by="account-123",
resolved_by_account=comment_author,
replies=[
SimpleNamespace(
id="reply-1",
content="reply",
created_by="account-456",
created_by_account=mentioned_user,
created_at=JAN_1_2024_1203,
)
],
mentions=[
SimpleNamespace(
mentioned_user_id="account-456",
mentioned_user_account=mentioned_user,
reply_id="reply-1",
)
],
)
get_comment_mock = MagicMock(return_value=comment)
monkeypatch.setattr(workflow_comment_module.WorkflowCommentService, "get_comment", get_comment_mock)
with app.test_request_context("/console/api/apps/app-123/workflow/comments/comment-1", method="GET"):
response = workflow_comment_module.WorkflowCommentDetailApi().get(app_id="app-123", comment_id="comment-1")
assert response == {
"id": "comment-1",
"position_x": 1.5,
"position_y": 2.5,
"content": "hello",
"created_by": "account-123",
"created_by_account": {
"id": "account-123",
"name": "tester",
"email": "tester@example.com",
"avatar_url": "https://example.com/avatar.png",
},
"created_at": JAN_1_2024_NOON_TS,
"updated_at": JAN_1_2024_1201_TS,
"resolved": True,
"resolved_at": JAN_1_2024_1202_TS,
"resolved_by": "account-123",
"resolved_by_account": {
"id": "account-123",
"name": "tester",
"email": "tester@example.com",
"avatar_url": "https://example.com/avatar.png",
},
"replies": [
{
"id": "reply-1",
"content": "reply",
"created_by": "account-456",
"created_by_account": {
"id": "account-456",
"name": "mentioned",
"email": "mentioned@example.com",
"avatar_url": None,
},
"created_at": JAN_1_2024_1203_TS,
}
],
"mentions": [
{
"mentioned_user_id": "account-456",
"mentioned_user_account": {
"id": "account-456",
"name": "mentioned",
"email": "mentioned@example.com",
"avatar_url": None,
},
"reply_id": "reply-1",
}
],
}
get_comment_mock.assert_called_once_with(tenant_id="tenant-123", app_id="app-123", comment_id="comment-1")
@pytest.mark.parametrize(
"case",
[
MutationResponseCase(
resource_cls=workflow_comment_module.WorkflowCommentResolveApi,
method_name="post",
path="/console/api/apps/app-123/workflow/comments/comment-1/resolve",
kwargs={"app_id": "app-123", "comment_id": "comment-1"},
service_method_name="resolve_comment",
service_return={
"id": "comment-1",
"resolved": True,
"resolved_at": JAN_1_2024_NOON,
"resolved_by": "account-123",
},
expected_response={
"id": "comment-1",
"resolved": True,
"resolved_at": JAN_1_2024_NOON_TS,
"resolved_by": "account-123",
},
),
MutationResponseCase(
resource_cls=workflow_comment_module.WorkflowCommentReplyApi,
method_name="post",
path="/console/api/apps/app-123/workflow/comments/comment-1/replies",
kwargs={"app_id": "app-123", "comment_id": "comment-1"},
payload={"content": "reply", "mentioned_user_ids": []},
service_method_name="create_reply",
service_return={"id": "reply-1", "created_at": JAN_1_2024_NOON},
expected_response={"id": "reply-1", "created_at": JAN_1_2024_NOON_TS},
expected_status=201,
),
MutationResponseCase(
resource_cls=workflow_comment_module.WorkflowCommentReplyDetailApi,
method_name="put",
path="/console/api/apps/app-123/workflow/comments/comment-1/replies/reply-1",
kwargs={"app_id": "app-123", "comment_id": "comment-1", "reply_id": "reply-1"},
payload={"content": "reply", "mentioned_user_ids": []},
service_method_name="update_reply",
service_return={"id": "reply-1", "updated_at": JAN_1_2024_NOON},
expected_response={"id": "reply-1", "updated_at": JAN_1_2024_NOON_TS},
),
],
)
def test_mutation_endpoints_serialize_response_models(
app: Flask, monkeypatch: pytest.MonkeyPatch, case: MutationResponseCase
) -> None:
app.config.setdefault("RESTX_MASK_HEADER", "X-Fields")
account = _make_account(TenantAccountRole.EDITOR)
app_model = _make_app()
_patch_console_guards(monkeypatch, account, app_model)
_patch_write_services(monkeypatch)
monkeypatch.setattr(
workflow_comment_module.WorkflowCommentService,
case.service_method_name,
MagicMock(return_value=case.service_return),
)
with app.test_request_context(case.path, method=case.method_name.upper(), json=case.payload):
with _patch_payload(case.payload):
result = getattr(case.resource_cls(), case.method_name)(**case.kwargs)
response, status = _unwrap_response(result)
assert response == case.expected_response
assert status == case.expected_status
def test_workflow_comment_response_schemas_are_registered() -> None:
assert workflow_comment_module.WorkflowCommentBasicList.__name__ in workflow_comment_module.console_ns.models
assert workflow_comment_module.WorkflowCommentDetail.__name__ in workflow_comment_module.console_ns.models