mirror of
https://github.com/TronoSfera/Law.git
synced 2026-05-18 10:03:45 +03:00
fix speed up 05
This commit is contained in:
parent
2b7043a89e
commit
c686d304c3
11 changed files with 120 additions and 11 deletions
50
alembic/versions/0036_add_message_author_admin_user_id.py
Normal file
50
alembic/versions/0036_add_message_author_admin_user_id.py
Normal 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")
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue