fix speed up 05

This commit is contained in:
TronoSfera 2026-03-17 10:57:57 +03:00
parent 2b7043a89e
commit c686d304c3
11 changed files with 120 additions and 11 deletions

View file

@ -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")

View file

@ -16,6 +16,7 @@ class Message(Base, UUIDMixin, TimestampMixin):
) )
request_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True, nullable=False) 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_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) author_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
body: Mapped[str | None] = mapped_column(Text, nullable=True) body: Mapped[str | None] = mapped_column(Text, nullable=True)
immutable: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) immutable: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)

View file

@ -225,6 +225,7 @@ def serialize_message(row: Message, *, body: str | None = None, body_loaded: boo
"id": str(row.id), "id": str(row.id),
"request_id": str(row.request_id), "request_id": str(row.request_id),
"author_type": row.author_type, "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, "author_name": row.author_name,
"body": body, "body": body,
"body_loaded": bool(body_loaded), "body_loaded": bool(body_loaded),
@ -281,6 +282,16 @@ def _normalize_admin_uuid(value: str | None) -> str | None:
return 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: def _register_chat_participant(request: Request, admin_user_id: str | None) -> None:
normalized = _normalize_admin_uuid(admin_user_id) normalized = _normalize_admin_uuid(admin_user_id)
if not normalized: if not normalized:
@ -525,6 +536,7 @@ def create_admin_or_lawyer_message(
row = Message( row = Message(
request_id=request.id, request_id=request.id,
author_type=author_type, 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, author_name=str(actor_name or "").strip() or author_type,
body=message_body, body=message_body,
responsible=responsible, responsible=responsible,

View file

@ -5,12 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Административная панель • Правовой трекер</title> <title>Административная панель • Правовой трекер</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01"> <link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01">
<link rel="stylesheet" href="/admin.css?v=20260303-05"> <link rel="stylesheet" href="/admin.css?v=20260317-01">
</head> </head>
<body> <body>
<div id="admin-root"></div> <div id="admin-root"></div>
<script src="/vendor/react.production.min.js"></script> <script src="/vendor/react.production.min.js"></script>
<script src="/vendor/react-dom.production.min.js"></script> <script src="/vendor/react-dom.production.min.js"></script>
<script src="/admin.js?v=20260303-05"></script> <script src="/admin.js?v=20260317-01"></script>
</body> </body>
</html> </html>

View file

@ -3688,6 +3688,8 @@
function RequestWorkspace({ function RequestWorkspace({
viewerRole, viewerRole,
viewerUserId, viewerUserId,
viewerUserEmail,
viewerUserName,
loading, loading,
trackNumber, trackNumber,
requestData, requestData,
@ -4848,7 +4850,17 @@
const authorType = String(payload?.author_type || "").trim().toUpperCase(); const authorType = String(payload?.author_type || "").trim().toUpperCase();
if (!authorType) return false; if (!authorType) return false;
if (viewerRoleCode === "CLIENT") return authorType === "CLIENT"; 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 renderMessageMeta = (payload) => {
const timeLabel = fmtTimeOnly(payload?.created_at); const timeLabel = fmtTimeOnly(payload?.created_at);
@ -5108,7 +5120,8 @@
const serviceMessageContent = resolveServiceMessageContent(entry.payload); const serviceMessageContent = resolveServiceMessageContent(entry.payload);
const requestDataInteractive = isRequestDataMessage && (canRequestData || canFillRequestData); 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 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( 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", "div",
{ {
@ -10210,6 +10223,8 @@
{ {
viewerRole: role, viewerRole: role,
viewerUserId: userId, viewerUserId: userId,
viewerUserEmail: email,
viewerUserName: dictionaries.users?.find((item) => String(item?.id || "") === String(userId || ""))?.name || "",
loading: requestModal.loading, loading: requestModal.loading,
trackNumber: requestModal.trackNumber, trackNumber: requestModal.trackNumber,
requestData: requestModal.requestData, requestData: requestModal.requestData,

View file

@ -3999,6 +3999,8 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
<RequestWorkspace <RequestWorkspace
viewerRole={role} viewerRole={role}
viewerUserId={userId} viewerUserId={userId}
viewerUserEmail={email}
viewerUserName={dictionaries.users?.find((item) => String(item?.id || "") === String(userId || ""))?.name || ""}
loading={requestModal.loading} loading={requestModal.loading}
trackNumber={requestModal.trackNumber} trackNumber={requestModal.trackNumber}
requestData={requestModal.requestData} requestData={requestModal.requestData}

View file

@ -14,6 +14,8 @@ import {
export function RequestWorkspace({ export function RequestWorkspace({
viewerRole, viewerRole,
viewerUserId, viewerUserId,
viewerUserEmail,
viewerUserName,
loading, loading,
trackNumber, trackNumber,
requestData, requestData,
@ -1297,7 +1299,17 @@ export function RequestWorkspace({
const authorType = String(payload?.author_type || "").trim().toUpperCase(); const authorType = String(payload?.author_type || "").trim().toUpperCase();
if (!authorType) return false; if (!authorType) return false;
if (viewerRoleCode === "CLIENT") return authorType === "CLIENT"; 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 renderMessageMeta = (payload) => {
@ -1719,9 +1731,10 @@ export function RequestWorkspace({
(isRequestDataMessage ? " chat-request-data-bubble" : "") + (isRequestDataMessage ? " chat-request-data-bubble" : "") +
(entry.payload?.request_data_all_filled ? " all-filled" : "") + (entry.payload?.request_data_all_filled ? " all-filled" : "") +
(isRequestDataMessage && canFillRequestData ? " request-data-message-btn" : ""); (isRequestDataMessage && canFillRequestData ? " request-data-message-btn" : "");
const isOutgoing = isOutgoingForViewer(entry.payload);
const itemClass = const itemClass =
"chat-message " + "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" : "") : ""); (isRequestDataMessage && canFillRequestData ? " request-data-item" + (entry.payload?.request_data_all_filled ? " done" : "") : "");
return ( return (
<li key={entry.key} className={itemClass}> <li key={entry.key} className={itemClass}>

View file

@ -5,13 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Страница клиента • Правовой трекер</title> <title>Страница клиента • Правовой трекер</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01"> <link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01">
<link rel="stylesheet" href="/admin.css?v=20260303-05"> <link rel="stylesheet" href="/admin.css?v=20260317-01">
<link rel="stylesheet" href="/client.css"> <link rel="stylesheet" href="/client.css">
</head> </head>
<body> <body>
<div id="client-root"></div> <div id="client-root"></div>
<script src="/vendor/react.production.min.js"></script> <script src="/vendor/react.production.min.js"></script>
<script src="/vendor/react-dom.production.min.js"></script> <script src="/vendor/react-dom.production.min.js"></script>
<script src="/client.js?v=20260303-05"></script> <script src="/client.js?v=20260317-01"></script>
</body> </body>
</html> </html>

View file

@ -188,6 +188,8 @@
function RequestWorkspace({ function RequestWorkspace({
viewerRole, viewerRole,
viewerUserId, viewerUserId,
viewerUserEmail,
viewerUserName,
loading, loading,
trackNumber, trackNumber,
requestData, requestData,
@ -1348,7 +1350,14 @@
const authorType = String(payload?.author_type || "").trim().toUpperCase(); const authorType = String(payload?.author_type || "").trim().toUpperCase();
if (!authorType) return false; if (!authorType) return false;
if (viewerRoleCode === "CLIENT") return authorType === "CLIENT"; 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 renderMessageMeta = (payload) => {
const timeLabel = fmtTimeOnly(payload?.created_at); const timeLabel = fmtTimeOnly(payload?.created_at);
@ -1608,7 +1617,8 @@
const serviceMessageContent = resolveServiceMessageContent(entry.payload); const serviceMessageContent = resolveServiceMessageContent(entry.payload);
const requestDataInteractive = isRequestDataMessage && (canRequestData || canFillRequestData); 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 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( 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", "div",
{ {

View file

@ -552,6 +552,7 @@ class AdminLawyerChatTests(AdminUniversalCrudBase):
) )
self.assertEqual(own_create.status_code, 201) self.assertEqual(own_create.status_code, 201)
self.assertEqual(own_create.json()["author_type"], "LAWYER") 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( unassigned_create = self.chat_client.post(
f"/api/admin/chat/requests/{unassigned_id}/messages", f"/api/admin/chat/requests/{unassigned_id}/messages",

View file

@ -114,7 +114,7 @@ class MigrationTests(unittest.TestCase):
def test_alembic_version_is_set(self): def test_alembic_version_is_set(self):
with self.engine.connect() as conn: with self.engine.connect() as conn:
version = conn.execute(text("SELECT version_num FROM alembic_version")).scalar_one() 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): def test_responsible_column_exists_in_all_domain_tables(self):
tables = { tables = {
@ -214,9 +214,14 @@ class MigrationTests(unittest.TestCase):
attachment_indexes = {index["name"] for index in self.inspector.get_indexes("attachments")} attachment_indexes = {index["name"] for index in self.inspector.get_indexes("attachments")}
invoice_indexes = {index["name"] for index in self.inspector.get_indexes("invoices")} 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_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_attachments_request_created_id", attachment_indexes)
self.assertIn("ix_invoices_request_issued_id", invoice_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): def test_data_retention_policies_contains_core_columns(self):
columns = {column["name"] for column in self.inspector.get_columns("data_retention_policies")} columns = {column["name"] for column in self.inspector.get_columns("data_retention_policies")}
self.assertIn("id", columns) self.assertIn("id", columns)