feat: improve audit exports and admin analytics
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""add human attention time filter index
|
||||
|
||||
Revision ID: 0040_attention_filters
|
||||
Revises: 0039_attention_config
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision = "0040_attention_filters"
|
||||
down_revision = "0039_attention_config"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_index(
|
||||
"ix_human_attention_created",
|
||||
"sys_human_attention_record",
|
||||
["created_at", "id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_human_attention_created", table_name="sys_human_attention_record")
|
||||
@@ -0,0 +1,87 @@
|
||||
"""normalize legacy server timestamps to UTC
|
||||
|
||||
Revision ID: 0041_utc_time_contract
|
||||
Revises: 0040_attention_filters
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
revision = "0041_utc_time_contract"
|
||||
down_revision = "0040_attention_filters"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
# Columns populated by MySQL CURRENT_TIMESTAMP. Fields that application
|
||||
# services already wrote explicitly in UTC are intentionally excluded.
|
||||
SERVER_TIMESTAMP_COLUMNS: dict[str, tuple[str, ...]] = {
|
||||
"sys_admin": ("created_at", "updated_at"),
|
||||
"sys_agent_batch_test": ("created_at", "updated_at"),
|
||||
"sys_agent_batch_test_item": ("created_at", "updated_at"),
|
||||
"sys_ai_request_log": ("created_at",),
|
||||
"sys_chat_session": ("created_at", "updated_at"),
|
||||
"sys_content_generation_config": ("updated_at",),
|
||||
"sys_entitlement_plan": ("created_at", "updated_at"),
|
||||
"sys_growth_profile_revision": ("created_at",),
|
||||
"sys_human_attention_history": ("created_at",),
|
||||
"sys_human_attention_job": ("created_at",),
|
||||
"sys_human_attention_record": ("created_at", "updated_at"),
|
||||
"sys_knowledge": ("created_at", "updated_at"),
|
||||
"sys_knowledge_publish_log": ("created_at",),
|
||||
"sys_knowledge_retrieval_log": ("created_at",),
|
||||
"sys_knowledge_review_record": ("created_at",),
|
||||
"sys_knowledge_sync_job": ("created_at",),
|
||||
"sys_knowledge_version": ("created_at",),
|
||||
"sys_log_retention_policy": ("updated_at",),
|
||||
"sys_message_feedback": ("created_at",),
|
||||
"sys_operation_log": ("created_at",),
|
||||
"sys_periodic_report": ("created_at", "updated_at"),
|
||||
"sys_prompt": ("updated_at",),
|
||||
"sys_question_insight_cleaned_question": ("created_at", "updated_at"),
|
||||
"sys_role": ("created_at", "updated_at"),
|
||||
"sys_share_draft": ("created_at",),
|
||||
"sys_sso_client": ("created_at", "updated_at"),
|
||||
"sys_sso_login_audit": ("created_at",),
|
||||
"sys_storage_snapshot": ("created_at",),
|
||||
"sys_system_config": ("updated_at",),
|
||||
"sys_teacher_help_card": ("created_at",),
|
||||
"sys_topic_session": ("created_at", "updated_at"),
|
||||
"sys_topic_summary": ("created_at", "updated_at"),
|
||||
"sys_user": ("created_at", "updated_at"),
|
||||
"sys_user_behavior_event": ("created_at",),
|
||||
"sys_user_entitlement": ("created_at", "updated_at"),
|
||||
"sys_user_external_identity": ("created_at", "updated_at"),
|
||||
"sys_user_growth_profile": ("created_at", "updated_at"),
|
||||
"sys_user_kb": ("created_at", "updated_at"),
|
||||
}
|
||||
|
||||
|
||||
def _server_offset_seconds() -> int:
|
||||
connection = op.get_bind()
|
||||
if connection.dialect.name != "mysql":
|
||||
return 0
|
||||
return int(connection.scalar(text("SELECT TIMESTAMPDIFF(SECOND, UTC_TIMESTAMP(), NOW())")) or 0)
|
||||
|
||||
|
||||
def _shift_server_timestamps(offset_seconds: int) -> None:
|
||||
if not offset_seconds:
|
||||
return
|
||||
connection = op.get_bind()
|
||||
operation = "SUB" if offset_seconds > 0 else "ADD"
|
||||
seconds = abs(offset_seconds)
|
||||
for table_name, columns in SERVER_TIMESTAMP_COLUMNS.items():
|
||||
assignments = ", ".join(
|
||||
f"`{column}` = DATE_{operation}(`{column}`, INTERVAL {seconds} SECOND)"
|
||||
for column in columns
|
||||
)
|
||||
connection.execute(text(f"UPDATE `{table_name}` SET {assignments}"))
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
_shift_server_timestamps(_server_offset_seconds())
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
_shift_server_timestamps(-_server_offset_seconds())
|
||||
@@ -0,0 +1,48 @@
|
||||
"""add granular record export permission
|
||||
|
||||
Revision ID: 0042_record_export_permission
|
||||
Revises: 0041_utc_time_contract
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
revision = "0042_record_export_permission"
|
||||
down_revision = "0041_utc_time_contract"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _rewrite_permissions(*, add_export: bool) -> None:
|
||||
connection = op.get_bind()
|
||||
rows = connection.execute(text("SELECT id, permissions FROM sys_role")).mappings().all()
|
||||
for row in rows:
|
||||
try:
|
||||
permissions = list(json.loads(row["permissions"] or "[]"))
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
continue
|
||||
changed = False
|
||||
if add_export and "records.view" in permissions and "records.export" not in permissions:
|
||||
permissions.append("records.export")
|
||||
changed = True
|
||||
if not add_export and "records.export" in permissions:
|
||||
permissions = [item for item in permissions if item != "records.export"]
|
||||
changed = True
|
||||
if changed:
|
||||
connection.execute(
|
||||
text("UPDATE sys_role SET permissions = :permissions WHERE id = :role_id"),
|
||||
{"permissions": json.dumps(sorted(set(permissions))), "role_id": row["id"]},
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
_rewrite_permissions(add_export=True)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
_rewrite_permissions(add_export=False)
|
||||
Reference in New Issue
Block a user