From c686d304c3575be0add64f3d6cc2c5de81106dee Mon Sep 17 00:00:00 2001 From: TronoSfera <119615520+TronoSfera@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:57:57 +0300 Subject: [PATCH] fix speed up 05 --- .../0036_add_message_author_admin_user_id.py | 50 +++++++++++++++++++ app/models/message.py | 1 + app/services/chat_secure_service.py | 12 +++++ app/web/admin.html | 4 +- app/web/admin.js | 19 ++++++- app/web/admin.jsx | 2 + .../features/requests/RequestWorkspace.jsx | 17 ++++++- app/web/client.html | 4 +- app/web/client.js | 14 +++++- tests/admin/test_lawyer_chat.py | 1 + tests/test_migrations.py | 7 ++- 11 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 alembic/versions/0036_add_message_author_admin_user_id.py diff --git a/alembic/versions/0036_add_message_author_admin_user_id.py b/alembic/versions/0036_add_message_author_admin_user_id.py new file mode 100644 index 0000000..13af732 --- /dev/null +++ b/alembic/versions/0036_add_message_author_admin_user_id.py @@ -0,0 +1,50 @@ +"""add author admin user id to messages + +Revision ID: 0036_message_author_admin_id +Revises: 0035_workspace_perf_indexes +Create Date: 2026-03-17 +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +revision = "0036_message_author_admin_id" +down_revision = "0035_workspace_perf_indexes" +branch_labels = None +depends_on = None + + +def _has_column(inspector: sa.Inspector, table: str, column_name: str) -> bool: + return any(str(column.get("name")) == column_name for column in inspector.get_columns(table)) + + +def _has_index(inspector: sa.Inspector, table: str, index_name: str) -> bool: + return any(str(index.get("name")) == index_name for index in inspector.get_indexes(table)) + + +def upgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + + if not _has_column(inspector, "messages", "author_admin_user_id"): + op.add_column("messages", sa.Column("author_admin_user_id", postgresql.UUID(as_uuid=True), nullable=True)) + + inspector = sa.inspect(bind) + if not _has_index(inspector, "messages", "ix_messages_author_admin_user_id"): + op.create_index("ix_messages_author_admin_user_id", "messages", ["author_admin_user_id"], unique=False) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + + if _has_index(inspector, "messages", "ix_messages_author_admin_user_id"): + op.drop_index("ix_messages_author_admin_user_id", table_name="messages") + + inspector = sa.inspect(bind) + if _has_column(inspector, "messages", "author_admin_user_id"): + op.drop_column("messages", "author_admin_user_id") diff --git a/app/models/message.py b/app/models/message.py index 99d646d..10d17e3 100644 --- a/app/models/message.py +++ b/app/models/message.py @@ -16,6 +16,7 @@ class Message(Base, UUIDMixin, TimestampMixin): ) request_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True, nullable=False) author_type: Mapped[str] = mapped_column(String(20), nullable=False) # CLIENT|LAWYER|SYSTEM + author_admin_user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True, index=True) author_name: Mapped[str | None] = mapped_column(String(200), nullable=True) body: Mapped[str | None] = mapped_column(Text, nullable=True) immutable: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) diff --git a/app/services/chat_secure_service.py b/app/services/chat_secure_service.py index b7ffdba..e7d9967 100644 --- a/app/services/chat_secure_service.py +++ b/app/services/chat_secure_service.py @@ -225,6 +225,7 @@ def serialize_message(row: Message, *, body: str | None = None, body_loaded: boo "id": str(row.id), "request_id": str(row.request_id), "author_type": row.author_type, + "author_admin_user_id": str(row.author_admin_user_id) if row.author_admin_user_id else None, "author_name": row.author_name, "body": body, "body_loaded": bool(body_loaded), @@ -281,6 +282,16 @@ def _normalize_admin_uuid(value: str | None) -> str | None: return None +def _normalize_admin_uuid_value(value: str | None) -> uuid.UUID | None: + normalized = _normalize_admin_uuid(value) + if not normalized: + return None + try: + return uuid.UUID(normalized) + except (TypeError, ValueError): + return None + + def _register_chat_participant(request: Request, admin_user_id: str | None) -> None: normalized = _normalize_admin_uuid(admin_user_id) if not normalized: @@ -525,6 +536,7 @@ def create_admin_or_lawyer_message( row = Message( request_id=request.id, author_type=author_type, + author_admin_user_id=_normalize_admin_uuid_value(actor_admin_user_id), author_name=str(actor_name or "").strip() or author_type, body=message_body, responsible=responsible, diff --git a/app/web/admin.html b/app/web/admin.html index 7993b5a..acc99c0 100644 --- a/app/web/admin.html +++ b/app/web/admin.html @@ -5,12 +5,12 @@ Административная панель • Правовой трекер - +
- + diff --git a/app/web/admin.js b/app/web/admin.js index 5879d53..ceeadff 100644 --- a/app/web/admin.js +++ b/app/web/admin.js @@ -3688,6 +3688,8 @@ function RequestWorkspace({ viewerRole, viewerUserId, + viewerUserEmail, + viewerUserName, loading, trackNumber, requestData, @@ -4848,7 +4850,17 @@ const authorType = String(payload?.author_type || "").trim().toUpperCase(); if (!authorType) return false; if (viewerRoleCode === "CLIENT") return authorType === "CLIENT"; - return authorType !== "CLIENT"; + if (authorType === "CLIENT") return false; + const authorAdminUserId = String(payload?.author_admin_user_id || "").trim(); + const currentViewerUserId = String(viewerUserId || "").trim(); + if (authorAdminUserId && currentViewerUserId) return authorAdminUserId === currentViewerUserId; + const authorName = String(payload?.author_name || "").trim().toLowerCase(); + const viewerName = String(viewerUserName || "").trim().toLowerCase(); + const viewerEmail = String(viewerUserEmail || "").trim().toLowerCase(); + if (authorName && (viewerName && authorName === viewerName || viewerEmail && authorName === viewerEmail)) { + return true; + } + return !viewerName && !viewerEmail ? authorType !== "CLIENT" : false; }; const renderMessageMeta = (payload) => { const timeLabel = fmtTimeOnly(payload?.created_at); @@ -5108,7 +5120,8 @@ const serviceMessageContent = resolveServiceMessageContent(entry.payload); const requestDataInteractive = isRequestDataMessage && (canRequestData || canFillRequestData); const bubbleClass = "chat-message-bubble" + (isRequestDataMessage ? " chat-request-data-bubble" : "") + (entry.payload?.request_data_all_filled ? " all-filled" : "") + (isRequestDataMessage && canFillRequestData ? " request-data-message-btn" : ""); - const itemClass = "chat-message " + (String(entry.payload?.author_type || "").toUpperCase() === "CLIENT" ? "incoming" : "outgoing") + (isRequestDataMessage && canFillRequestData ? " request-data-item" + (entry.payload?.request_data_all_filled ? " done" : "") : ""); + const isOutgoing = isOutgoingForViewer(entry.payload); + const itemClass = "chat-message " + (isOutgoing ? "outgoing" : "incoming") + (isRequestDataMessage && canFillRequestData ? " request-data-item" + (entry.payload?.request_data_all_filled ? " done" : "") : ""); return /* @__PURE__ */ React.createElement("li", { key: entry.key, className: itemClass }, /* @__PURE__ */ React.createElement("div", { className: "chat-message-author" }, String(entry.payload?.author_name || entry.payload?.author_type || "\u0421\u0438\u0441\u0442\u0435\u043C\u0430")), /* @__PURE__ */ React.createElement( "div", { @@ -10210,6 +10223,8 @@ { viewerRole: role, viewerUserId: userId, + viewerUserEmail: email, + viewerUserName: dictionaries.users?.find((item) => String(item?.id || "") === String(userId || ""))?.name || "", loading: requestModal.loading, trackNumber: requestModal.trackNumber, requestData: requestModal.requestData, diff --git a/app/web/admin.jsx b/app/web/admin.jsx index 1c7b2bd..a6ba20b 100644 --- a/app/web/admin.jsx +++ b/app/web/admin.jsx @@ -3999,6 +3999,8 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__"; String(item?.id || "") === String(userId || ""))?.name || ""} loading={requestModal.loading} trackNumber={requestModal.trackNumber} requestData={requestModal.requestData} diff --git a/app/web/admin/features/requests/RequestWorkspace.jsx b/app/web/admin/features/requests/RequestWorkspace.jsx index 9f883e2..c47d4bd 100644 --- a/app/web/admin/features/requests/RequestWorkspace.jsx +++ b/app/web/admin/features/requests/RequestWorkspace.jsx @@ -14,6 +14,8 @@ import { export function RequestWorkspace({ viewerRole, viewerUserId, + viewerUserEmail, + viewerUserName, loading, trackNumber, requestData, @@ -1297,7 +1299,17 @@ export function RequestWorkspace({ const authorType = String(payload?.author_type || "").trim().toUpperCase(); if (!authorType) return false; if (viewerRoleCode === "CLIENT") return authorType === "CLIENT"; - return authorType !== "CLIENT"; + if (authorType === "CLIENT") return false; + const authorAdminUserId = String(payload?.author_admin_user_id || "").trim(); + const currentViewerUserId = String(viewerUserId || "").trim(); + if (authorAdminUserId && currentViewerUserId) return authorAdminUserId === currentViewerUserId; + const authorName = String(payload?.author_name || "").trim().toLowerCase(); + const viewerName = String(viewerUserName || "").trim().toLowerCase(); + const viewerEmail = String(viewerUserEmail || "").trim().toLowerCase(); + if (authorName && ((viewerName && authorName === viewerName) || (viewerEmail && authorName === viewerEmail))) { + return true; + } + return !viewerName && !viewerEmail ? authorType !== "CLIENT" : false; }; const renderMessageMeta = (payload) => { @@ -1719,9 +1731,10 @@ export function RequestWorkspace({ (isRequestDataMessage ? " chat-request-data-bubble" : "") + (entry.payload?.request_data_all_filled ? " all-filled" : "") + (isRequestDataMessage && canFillRequestData ? " request-data-message-btn" : ""); + const isOutgoing = isOutgoingForViewer(entry.payload); const itemClass = "chat-message " + - (String(entry.payload?.author_type || "").toUpperCase() === "CLIENT" ? "incoming" : "outgoing") + + (isOutgoing ? "outgoing" : "incoming") + (isRequestDataMessage && canFillRequestData ? " request-data-item" + (entry.payload?.request_data_all_filled ? " done" : "") : ""); return (
  • diff --git a/app/web/client.html b/app/web/client.html index 7a1a279..9d96314 100644 --- a/app/web/client.html +++ b/app/web/client.html @@ -5,13 +5,13 @@ Страница клиента • Правовой трекер - +
    - + diff --git a/app/web/client.js b/app/web/client.js index 740f319..333343a 100644 --- a/app/web/client.js +++ b/app/web/client.js @@ -188,6 +188,8 @@ function RequestWorkspace({ viewerRole, viewerUserId, + viewerUserEmail, + viewerUserName, loading, trackNumber, requestData, @@ -1348,7 +1350,14 @@ const authorType = String(payload?.author_type || "").trim().toUpperCase(); if (!authorType) return false; if (viewerRoleCode === "CLIENT") return authorType === "CLIENT"; - return authorType !== "CLIENT"; + if (authorType === "CLIENT") return false; + const authorName = String(payload?.author_name || "").trim().toLowerCase(); + const viewerName = String(viewerUserName || "").trim().toLowerCase(); + const viewerEmail = String(viewerUserEmail || "").trim().toLowerCase(); + if (authorName && (viewerName && authorName === viewerName || viewerEmail && authorName === viewerEmail)) { + return true; + } + return !viewerName && !viewerEmail ? authorType !== "CLIENT" : false; }; const renderMessageMeta = (payload) => { const timeLabel = fmtTimeOnly(payload?.created_at); @@ -1608,7 +1617,8 @@ const serviceMessageContent = resolveServiceMessageContent(entry.payload); const requestDataInteractive = isRequestDataMessage && (canRequestData || canFillRequestData); const bubbleClass = "chat-message-bubble" + (isRequestDataMessage ? " chat-request-data-bubble" : "") + (entry.payload?.request_data_all_filled ? " all-filled" : "") + (isRequestDataMessage && canFillRequestData ? " request-data-message-btn" : ""); - const itemClass = "chat-message " + (String(entry.payload?.author_type || "").toUpperCase() === "CLIENT" ? "incoming" : "outgoing") + (isRequestDataMessage && canFillRequestData ? " request-data-item" + (entry.payload?.request_data_all_filled ? " done" : "") : ""); + const isOutgoing = isOutgoingForViewer(entry.payload); + const itemClass = "chat-message " + (isOutgoing ? "outgoing" : "incoming") + (isRequestDataMessage && canFillRequestData ? " request-data-item" + (entry.payload?.request_data_all_filled ? " done" : "") : ""); return /* @__PURE__ */ React.createElement("li", { key: entry.key, className: itemClass }, /* @__PURE__ */ React.createElement("div", { className: "chat-message-author" }, String(entry.payload?.author_name || entry.payload?.author_type || "\u0421\u0438\u0441\u0442\u0435\u043C\u0430")), /* @__PURE__ */ React.createElement( "div", { diff --git a/tests/admin/test_lawyer_chat.py b/tests/admin/test_lawyer_chat.py index a83a7e9..6210100 100644 --- a/tests/admin/test_lawyer_chat.py +++ b/tests/admin/test_lawyer_chat.py @@ -552,6 +552,7 @@ class AdminLawyerChatTests(AdminUniversalCrudBase): ) self.assertEqual(own_create.status_code, 201) self.assertEqual(own_create.json()["author_type"], "LAWYER") + self.assertEqual(own_create.json().get("author_admin_user_id"), self_id) unassigned_create = self.chat_client.post( f"/api/admin/chat/requests/{unassigned_id}/messages", diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 68a8f31..b621cbb 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -114,7 +114,7 @@ class MigrationTests(unittest.TestCase): def test_alembic_version_is_set(self): with self.engine.connect() as conn: version = conn.execute(text("SELECT version_num FROM alembic_version")).scalar_one() - self.assertEqual(version, "0035_workspace_perf_indexes") + self.assertEqual(version, "0036_message_author_admin_id") def test_responsible_column_exists_in_all_domain_tables(self): tables = { @@ -214,9 +214,14 @@ class MigrationTests(unittest.TestCase): attachment_indexes = {index["name"] for index in self.inspector.get_indexes("attachments")} invoice_indexes = {index["name"] for index in self.inspector.get_indexes("invoices")} self.assertIn("ix_messages_request_created_id", message_indexes) + self.assertIn("ix_messages_author_admin_user_id", message_indexes) self.assertIn("ix_attachments_request_created_id", attachment_indexes) self.assertIn("ix_invoices_request_issued_id", invoice_indexes) + def test_messages_contains_author_admin_user_id_column(self): + columns = {column["name"] for column in self.inspector.get_columns("messages")} + self.assertIn("author_admin_user_id", columns) + def test_data_retention_policies_contains_core_columns(self): columns = {column["name"] for column in self.inspector.get_columns("data_retention_policies")} self.assertIn("id", columns)