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

View file

@ -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,

View file

@ -5,12 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Административная панель • Правовой трекер</title>
<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>
<body>
<div id="admin-root"></div>
<script src="/vendor/react.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>
</html>

View file

@ -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,

View file

@ -3999,6 +3999,8 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
<RequestWorkspace
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}

View file

@ -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 (
<li key={entry.key} className={itemClass}>

View file

@ -5,13 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Страница клиента • Правовой трекер</title>
<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">
</head>
<body>
<div id="client-root"></div>
<script src="/vendor/react.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>
</html>

View file

@ -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",
{

View file

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

View file

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