fix: restore govdoc version linkage and detail sidebar

This commit is contained in:
wren
2026-05-18 16:30:22 +08:00
parent cb5d199128
commit 3fb72d94ad
@@ -6,6 +6,7 @@ import hashlib
import json
import mimetypes
import time
import uuid
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
@@ -26,6 +27,7 @@ from fastapi_modules.fastapi_leaudit.govdoc_engine.parser.docx_parser import par
from fastapi_modules.fastapi_leaudit.govdoc_engine.reporter.html_paragraph import paragraphs_to_html
from fastapi_modules.fastapi_leaudit.models import LeauditDocument, LeauditDocumentFile
from fastapi_modules.fastapi_leaudit.services import IGovdocService, IOssService
from fastapi_modules.fastapi_leaudit.services.impl.documentServiceImpl import _find_latest_version_candidate
from fastapi_modules.fastapi_leaudit.services.impl.ossServiceImpl import OssServiceImpl
@@ -35,6 +37,11 @@ class _GovdocDocumentRow:
region: str
processingStatus: str
currentRunId: int | None
versionGroupKey: str | None
versionNo: int
rootVersionId: int | None
previousVersionId: int | None
isLatestVersion: bool
createdAt: Any
updatedAt: Any
fileId: int
@@ -49,6 +56,7 @@ class _GovdocDocumentRow:
passedCount: int | None
failedCount: int | None
skippedCount: int | None
resultSummaryJson: Any
hasHtmlReport: bool
hasDocxReport: bool
@@ -98,6 +106,29 @@ class GovdocServiceImpl(IGovdocService):
await self._ensureGovdocSchema(session)
currentUser = await self._getCurrentUserContext(createdBy)
resolvedRegion = self._resolve_upload_region(currentUser, normalizedRegion)
previousVersionId: int | None = None
rootVersionId: int | None = None
versionGroupKey: str | None = None
versionNo = 1
latestCandidate = await self._find_govdoc_latest_version_candidate(
session,
typeId=typeId,
region=resolvedRegion,
normalizedName=normalizedName,
fileExt=fileExt,
)
if latestCandidate:
previousVersionId = int(latestCandidate["document_id"])
rootVersionId = int(latestCandidate["root_version_id"] or latestCandidate["document_id"])
versionGroupKey = str(latestCandidate["version_group_key"] or "")
versionNo = int(latestCandidate["version_no"] or 1) + 1
previousDocument = await session.get(LeauditDocument, previousVersionId)
if previousDocument is not None:
previousDocument.isLatestVersion = False
else:
versionGroupKey = uuid.uuid4().hex
document = await LeauditDocument.create_new(
session,
@@ -107,14 +138,15 @@ class GovdocServiceImpl(IGovdocService):
region=resolvedRegion,
processingStatus="waiting",
currentRunId=None,
versionGroupKey=None,
versionNo=1,
previousVersionId=None,
rootVersionId=None,
versionGroupKey=versionGroupKey,
versionNo=versionNo,
previousVersionId=previousVersionId,
rootVersionId=rootVersionId,
isLatestVersion=True,
normalizedName=normalizedName,
reviewScope="govdoc",
)
if document.rootVersionId is None:
document.rootVersionId = document.Id
await session.flush()
@@ -122,7 +154,7 @@ class GovdocServiceImpl(IGovdocService):
Region=resolvedRegion,
TypeCode="govdoc",
DocumentId=document.Id,
Version="v1",
Version=f"v{document.versionNo or 1}",
FileRole="original",
FileName=fileName,
Year=uploadedAt.year,
@@ -224,6 +256,7 @@ class GovdocServiceImpl(IGovdocService):
"f.is_active = true",
"f.file_role = 'original'",
"COALESCE(d.engine_type, 'leaudit') = 'govdoc'",
"COALESCE(d.is_latest_version, true) = true",
]
filters.extend(
self._buildDocumentScopeFilters(
@@ -269,6 +302,11 @@ class GovdocServiceImpl(IGovdocService):
COALESCE(d.region, 'default') AS region,
COALESCE(d.processing_status, 'waiting') AS processing_status,
d.current_run_id,
d.version_group_key,
COALESCE(d.version_no, 1) AS version_no,
d.root_version_id,
d.previous_version_id,
COALESCE(d.is_latest_version, true) AS is_latest_version,
d.created_at,
d.updated_at,
f.id AS file_id,
@@ -283,6 +321,8 @@ class GovdocServiceImpl(IGovdocService):
gr.passed_count,
gr.failed_count,
gr.skipped_count,
gr.result_summary_json,
COALESCE(vc.total_versions, 1) AS total_versions,
EXISTS(
SELECT 1
FROM govdoc_report_artifacts gra
@@ -305,6 +345,15 @@ class GovdocServiceImpl(IGovdocService):
AND f.deleted_at IS NULL
LEFT JOIN govdoc_runs gr
ON gr.id = d.current_run_id
LEFT JOIN (
SELECT version_group_key, COUNT(*) AS total_versions
FROM leaudit_documents
WHERE deleted_at IS NULL
AND COALESCE(engine_type, 'leaudit') = 'govdoc'
AND COALESCE(version_group_key, '') <> ''
GROUP BY version_group_key
) vc
ON vc.version_group_key = d.version_group_key
WHERE {whereClause}
ORDER BY d.created_at DESC
LIMIT :limit OFFSET :offset
@@ -336,43 +385,97 @@ class GovdocServiceImpl(IGovdocService):
).scalar_one()
)
history_by_group: dict[str, list[dict[str, Any]]] = {}
total_versions_by_group = {
str(row["version_group_key"]): int(row.get("total_versions") or 1)
for row in rows
if row.get("version_group_key")
}
group_keys = [str(row["version_group_key"]) for row in rows if row.get("version_group_key")]
if group_keys:
history_rows = (
await session.execute(
text(
"""
SELECT
d.id AS document_id,
COALESCE(d.region, 'default') AS region,
COALESCE(d.processing_status, 'waiting') AS processing_status,
d.current_run_id,
d.version_group_key,
COALESCE(d.version_no, 1) AS version_no,
d.root_version_id,
d.previous_version_id,
COALESCE(d.is_latest_version, false) AS is_latest_version,
d.created_at,
d.updated_at,
f.id AS file_id,
f.file_name,
f.file_ext,
f.mime_type,
f.file_size,
f.oss_url,
f.created_by,
gr.result_status,
gr.total_score,
gr.passed_count,
gr.failed_count,
gr.skipped_count,
gr.result_summary_json,
EXISTS(
SELECT 1
FROM govdoc_report_artifacts gra
WHERE gra.run_id = d.current_run_id
AND gra.artifact_type = 'html_report'
AND gra.deleted_at IS NULL
) AS has_html_report,
EXISTS(
SELECT 1
FROM govdoc_report_artifacts gra
WHERE gra.run_id = d.current_run_id
AND gra.artifact_type = 'annotated_docx'
AND gra.deleted_at IS NULL
) AS has_docx_report
FROM leaudit_documents d
JOIN leaudit_document_files f
ON f.document_id = d.id
AND f.is_active = true
AND f.file_role = 'original'
AND f.deleted_at IS NULL
LEFT JOIN govdoc_runs gr
ON gr.id = d.current_run_id
WHERE d.deleted_at IS NULL
AND COALESCE(d.engine_type, 'leaudit') = 'govdoc'
AND d.version_group_key = ANY(:group_keys)
AND COALESCE(d.is_latest_version, false) = false
ORDER BY d.version_group_key, COALESCE(d.version_no, 1) DESC, d.id DESC
"""
),
{"group_keys": group_keys},
)
).mappings().all()
for history_row in history_rows:
group_key = str(history_row["version_group_key"] or "")
history_by_group.setdefault(group_key, []).append(
self._serialize_list_item_row(
self._map_document_row(history_row),
totalVersions=total_versions_by_group.get(group_key, 1),
historyVersions=[],
)
)
items = []
for row in rows:
mapped = self._map_document_row(row)
summary = self._build_summary_payload(
mapped.totalScore,
mapped.passedCount,
mapped.failedCount,
mapped.skippedCount,
)
group_key = str(mapped.versionGroupKey or "")
total_versions = int(row.get("total_versions") or 1)
history_versions = history_by_group.get(group_key, []) if group_key else []
items.append(
{
"documentId": mapped.documentId,
"fileId": mapped.fileId,
"fileName": mapped.fileName,
"fileExt": mapped.fileExt,
"mimeType": mapped.mimeType,
"fileSize": mapped.fileSize,
"region": mapped.region,
"processingStatus": mapped.processingStatus,
"currentRunId": mapped.currentRunId,
"latestRunId": mapped.currentRunId,
"resultStatus": mapped.resultStatus,
"score": float(mapped.totalScore) if mapped.totalScore is not None else None,
"passedCount": mapped.passedCount or 0,
"failedCount": mapped.failedCount or 0,
"skippedCount": mapped.skippedCount or 0,
"latestRun": {
"runId": mapped.currentRunId,
"summary": summary,
} if mapped.currentRunId else None,
"reports": {
"hasHtmlReport": mapped.hasHtmlReport,
"hasDocxReport": mapped.hasDocxReport,
},
"createdAt": self._iso(mapped.createdAt),
"updatedAt": self._iso(mapped.updatedAt),
}
self._serialize_list_item_row(
mapped,
totalVersions=total_versions,
historyVersions=history_versions,
)
)
return {"items": items, "total": total, "page": page, "pageSize": pageSize}
@@ -1406,6 +1509,67 @@ class GovdocServiceImpl(IGovdocService):
"skipped_count": int(skippedCount or 0),
}
async def _find_govdoc_latest_version_candidate(
self,
session,
*,
typeId: int | None,
region: str,
normalizedName: str,
fileExt: str | None,
) -> dict[str, Any] | None:
if typeId is not None:
candidate = await _find_latest_version_candidate(
session,
type_id=int(typeId),
root_group_id=None,
region=region,
normalized_name=normalizedName,
file_ext=fileExt,
)
if candidate:
return candidate
ext_clause = ""
params: dict[str, Any] = {
"region": region,
"normalized_name": normalizedName,
}
if fileExt:
ext_clause = " AND LOWER(COALESCE(f.file_ext, '')) = :file_ext"
params["file_ext"] = fileExt.lower()
row = (
await session.execute(
text(
f"""
SELECT
d.id AS document_id,
d.version_group_key,
d.version_no,
d.root_version_id,
f.id AS file_id,
f.sha256
FROM leaudit_documents d
JOIN leaudit_document_files f
ON f.document_id = d.id
AND f.is_active = true
AND f.file_role = 'original'
AND f.deleted_at IS NULL
WHERE d.region = :region
AND d.normalized_name = :normalized_name
AND COALESCE(d.engine_type, 'leaudit') = 'govdoc'
AND COALESCE(d.is_latest_version, true) = true
AND d.deleted_at IS NULL{ext_clause}
ORDER BY COALESCE(d.version_no, 1) DESC, d.id DESC
LIMIT 1
"""
),
params,
)
).mappings().first()
return dict(row) if row else None
def _group_artifacts_by_run(self, rows: list[Any]) -> dict[int, dict[str, Any]]:
grouped: dict[int, dict[str, Any]] = {}
artifactTypeMap = {
@@ -1445,12 +1609,76 @@ class GovdocServiceImpl(IGovdocService):
)
return grouped
def _build_list_summary_payload(self, row: _GovdocDocumentRow) -> dict[str, Any]:
summary = self._parse_json(row.resultSummaryJson) or {}
if not isinstance(summary, dict):
summary = {}
bySeverity = summary.get("by_severity")
byCategory = summary.get("by_category")
return {
"score": float(summary.get("score") or row.totalScore or 0),
"total_findings": int(summary.get("total_findings") or row.failedCount or 0),
"by_severity": bySeverity if isinstance(bySeverity, dict) else {},
"by_category": byCategory if isinstance(byCategory, dict) else {},
"passed_count": int(summary.get("passed_count") or row.passedCount or 0),
"failed_count": int(summary.get("failed_count") or row.failedCount or 0),
"skipped_count": int(summary.get("skipped_count") or row.skippedCount or 0),
}
def _serialize_list_item_row(
self,
row: _GovdocDocumentRow,
*,
totalVersions: int | None,
historyVersions: list[dict[str, Any]],
) -> dict[str, Any]:
summary = self._build_list_summary_payload(row)
return {
"documentId": row.documentId,
"fileId": row.fileId,
"fileName": row.fileName,
"fileExt": row.fileExt,
"mimeType": row.mimeType,
"fileSize": row.fileSize,
"region": row.region,
"processingStatus": row.processingStatus,
"currentRunId": row.currentRunId,
"latestRunId": row.currentRunId,
"resultStatus": row.resultStatus,
"score": float(row.totalScore) if row.totalScore is not None else None,
"passedCount": row.passedCount or 0,
"failedCount": row.failedCount or 0,
"skippedCount": row.skippedCount or 0,
"versionGroupKey": row.versionGroupKey or "",
"versionNo": int(row.versionNo or 1),
"rootVersionId": int(row.rootVersionId or row.documentId),
"previousVersionId": int(row.previousVersionId) if row.previousVersionId is not None else None,
"totalVersions": int(totalVersions or max(1, len(historyVersions) + 1)),
"historyCount": len(historyVersions),
"historyVersions": historyVersions,
"latestRun": {
"runId": row.currentRunId,
"summary": summary,
} if row.currentRunId else None,
"reports": {
"hasHtmlReport": row.hasHtmlReport,
"hasDocxReport": row.hasDocxReport,
},
"createdAt": self._iso(row.createdAt),
"updatedAt": self._iso(row.updatedAt),
}
def _map_document_row(self, row: Any) -> _GovdocDocumentRow:
return _GovdocDocumentRow(
documentId=int(row["document_id"]),
region=str(row["region"] or "default"),
processingStatus=str(row["processing_status"] or "waiting"),
currentRunId=int(row["current_run_id"]) if row.get("current_run_id") is not None else None,
versionGroupKey=str(row["version_group_key"]) if row.get("version_group_key") else None,
versionNo=int(row.get("version_no") or 1),
rootVersionId=int(row["root_version_id"]) if row.get("root_version_id") is not None else None,
previousVersionId=int(row["previous_version_id"]) if row.get("previous_version_id") is not None else None,
isLatestVersion=bool(row.get("is_latest_version", True)),
createdAt=row.get("created_at"),
updatedAt=row.get("updated_at"),
fileId=int(row["file_id"]),
@@ -1465,6 +1693,7 @@ class GovdocServiceImpl(IGovdocService):
passedCount=int(row["passed_count"]) if row.get("passed_count") is not None else None,
failedCount=int(row["failed_count"]) if row.get("failed_count") is not None else None,
skippedCount=int(row["skipped_count"]) if row.get("skipped_count") is not None else None,
resultSummaryJson=row.get("result_summary_json"),
hasHtmlReport=bool(row.get("has_html_report")),
hasDocxReport=bool(row.get("has_docx_report")),
)