import logging from datetime import datetime from flask_restx import Resource from pydantic import BaseModel, Field, TypeAdapter, computed_field, field_validator 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 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 from services.workflow_comment_service import WorkflowCommentService logger = logging.getLogger(__name__) class WorkflowCommentCreatePayload(BaseModel): content: str = Field(..., description="Comment content") position_x: float = Field(..., description="Comment X position") position_y: float = Field(..., description="Comment Y position") mentioned_user_ids: list[str] = Field(default_factory=list, description="Mentioned user IDs") class WorkflowCommentUpdatePayload(BaseModel): content: str = Field(..., description="Comment content") position_x: float | None = Field(default=None, description="Comment X position") position_y: float | None = Field(default=None, description="Comment Y position") mentioned_user_ids: list[str] | None = Field( default=None, description="Mentioned user IDs. Omit to keep existing mentions.", ) class WorkflowCommentReplyPayload(BaseModel): content: str = Field(..., description="Reply content") mentioned_user_ids: list[str] = Field(default_factory=list, description="Mentioned user IDs") 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, WorkflowCommentMentionUsersPayload, WorkflowCommentCreatePayload, WorkflowCommentUpdatePayload, WorkflowCommentReplyPayload, ) register_response_schema_models( console_ns, WorkflowCommentAccount, WorkflowCommentReply, WorkflowCommentMention, WorkflowCommentBasic, WorkflowCommentBasicList, WorkflowCommentDetail, WorkflowCommentCreate, WorkflowCommentUpdate, WorkflowCommentResolve, WorkflowCommentReplyCreate, WorkflowCommentReplyUpdate, ) @console_ns.route("/apps//workflow/comments") class WorkflowCommentListApi(Resource): """API for listing and creating workflow comments.""" @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", console_ns.models[WorkflowCommentBasicList.__name__]) @login_required @setup_required @account_initialization_required @get_app_model() 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 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", console_ns.models[WorkflowCommentCreate.__name__]) @login_required @setup_required @account_initialization_required @get_app_model() @edit_permission_required def post(self, app_model: App): """Create a new workflow comment.""" payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {}) result = WorkflowCommentService.create_comment( tenant_id=current_user.current_tenant_id, app_id=app_model.id, created_by=current_user.id, content=payload.content, position_x=payload.position_x, position_y=payload.position_y, mentioned_user_ids=payload.mentioned_user_ids, ) return dump_response(WorkflowCommentCreate, result), 201 @console_ns.route("/apps//workflow/comments/") class WorkflowCommentDetailApi(Resource): """API for managing individual workflow comments.""" @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", console_ns.models[WorkflowCommentDetail.__name__]) @login_required @setup_required @account_initialization_required @get_app_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 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", console_ns.models[WorkflowCommentUpdate.__name__]) @login_required @setup_required @account_initialization_required @get_app_model() @edit_permission_required def put(self, app_model: App, comment_id: str): """Update a workflow comment.""" payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {}) result = WorkflowCommentService.update_comment( tenant_id=current_user.current_tenant_id, app_id=app_model.id, comment_id=comment_id, user_id=current_user.id, content=payload.content, position_x=payload.position_x, position_y=payload.position_y, mentioned_user_ids=payload.mentioned_user_ids, ) return dump_response(WorkflowCommentUpdate, result) @console_ns.doc("delete_workflow_comment") @console_ns.doc(description="Delete a workflow comment") @console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID"}) @console_ns.response(204, "Comment deleted successfully") @login_required @setup_required @account_initialization_required @get_app_model() @edit_permission_required def delete(self, app_model: App, comment_id: str): """Delete a workflow comment.""" WorkflowCommentService.delete_comment( tenant_id=current_user.current_tenant_id, app_id=app_model.id, comment_id=comment_id, user_id=current_user.id, ) return {"result": "success"}, 204 @console_ns.route("/apps//workflow/comments//resolve") class WorkflowCommentResolveApi(Resource): """API for resolving and reopening workflow comments.""" @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", console_ns.models[WorkflowCommentResolve.__name__]) @login_required @setup_required @account_initialization_required @get_app_model() @edit_permission_required def post(self, app_model: App, comment_id: str): """Resolve a workflow comment.""" comment = WorkflowCommentService.resolve_comment( tenant_id=current_user.current_tenant_id, app_id=app_model.id, comment_id=comment_id, user_id=current_user.id, ) return dump_response(WorkflowCommentResolve, comment) @console_ns.route("/apps//workflow/comments//replies") class WorkflowCommentReplyApi(Resource): """API for managing comment replies.""" @console_ns.doc("create_workflow_comment_reply") @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", console_ns.models[WorkflowCommentReplyCreate.__name__]) @login_required @setup_required @account_initialization_required @get_app_model() @edit_permission_required def post(self, app_model: App, comment_id: str): """Add a reply to a workflow comment.""" # Validate comment access first WorkflowCommentService.validate_comment_access( comment_id=comment_id, tenant_id=current_user.current_tenant_id, app_id=app_model.id ) payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {}) result = WorkflowCommentService.create_reply( comment_id=comment_id, content=payload.content, created_by=current_user.id, mentioned_user_ids=payload.mentioned_user_ids, ) return dump_response(WorkflowCommentReplyCreate, result), 201 @console_ns.route("/apps//workflow/comments//replies/") class WorkflowCommentReplyDetailApi(Resource): """API for managing individual comment replies.""" @console_ns.doc("update_workflow_comment_reply") @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", console_ns.models[WorkflowCommentReplyUpdate.__name__]) @login_required @setup_required @account_initialization_required @get_app_model() @edit_permission_required def put(self, app_model: App, comment_id: str, reply_id: str): """Update a comment reply.""" # Validate comment access first WorkflowCommentService.validate_comment_access( comment_id=comment_id, tenant_id=current_user.current_tenant_id, app_id=app_model.id ) payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {}) reply = WorkflowCommentService.update_reply( tenant_id=current_user.current_tenant_id, app_id=app_model.id, comment_id=comment_id, reply_id=reply_id, user_id=current_user.id, content=payload.content, mentioned_user_ids=payload.mentioned_user_ids, ) return dump_response(WorkflowCommentReplyUpdate, reply) @console_ns.doc("delete_workflow_comment_reply") @console_ns.doc(description="Delete a comment reply") @console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID", "reply_id": "Reply ID"}) @console_ns.response(204, "Reply deleted successfully") @login_required @setup_required @account_initialization_required @get_app_model() @edit_permission_required def delete(self, app_model: App, comment_id: str, reply_id: str): """Delete a comment reply.""" # Validate comment access first WorkflowCommentService.validate_comment_access( comment_id=comment_id, tenant_id=current_user.current_tenant_id, app_id=app_model.id ) WorkflowCommentService.delete_reply( tenant_id=current_user.current_tenant_id, app_id=app_model.id, comment_id=comment_id, reply_id=reply_id, user_id=current_user.id, ) return {"result": "success"}, 204 @console_ns.route("/apps//workflow/comments/mention-users") class WorkflowCommentMentionUsersApi(Resource): """API for getting mentionable users for workflow comments.""" @console_ns.doc("workflow_comment_mention_users") @console_ns.doc(description="Get all users in current tenant for mentions") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.response( 200, "Mentionable users retrieved successfully", console_ns.models[WorkflowCommentMentionUsersPayload.__name__] ) @login_required @setup_required @account_initialization_required @get_app_model() def get(self, app_model: App): """Get all users in current tenant for mentions.""" members = TenantService.get_tenant_members(current_user.current_tenant) users = TypeAdapter(list[AccountWithRole]).validate_python(members, from_attributes=True) response = WorkflowCommentMentionUsersPayload(users=users) return response.model_dump(mode="json"), 200