Use exact CT links for PACS STL view
This commit is contained in:
@@ -75,7 +75,7 @@ def sql_literal(value: Any) -> str:
|
||||
|
||||
|
||||
def normalize_ct(value: str) -> str:
|
||||
return re.sub(r"^D", "", re.sub(r"\s+", "", str(value or "").upper()))
|
||||
return re.sub(r"\s+", "", str(value or "").upper())
|
||||
|
||||
|
||||
def db_available() -> tuple[bool, str]:
|
||||
@@ -92,7 +92,7 @@ def relation_cte() -> str:
|
||||
p AS (
|
||||
SELECT
|
||||
p.*,
|
||||
regexp_replace(upper(p.ct_number), '^D', '') AS norm_ct
|
||||
upper(p.ct_number) AS ct_key
|
||||
FROM public.{PACS_TABLE_SQL} p
|
||||
),
|
||||
ps AS (
|
||||
@@ -102,49 +102,51 @@ def relation_cte() -> str:
|
||||
u_raw AS (
|
||||
SELECT
|
||||
u.*,
|
||||
regexp_replace(upper(u.ct_number), '^D', '') AS norm_ct
|
||||
upper(u.ct_number) AS ct_key
|
||||
FROM public.{UPP_ASSET_TABLE_SQL} u
|
||||
),
|
||||
u AS (
|
||||
SELECT DISTINCT ON (norm_ct) *
|
||||
SELECT DISTINCT ON (ct_key) *
|
||||
FROM u_raw
|
||||
ORDER BY norm_ct, stl_present DESC, list_present DESC, updated_at DESC NULLS LAST
|
||||
ORDER BY ct_key, stl_present DESC, list_present DESC, updated_at DESC NULLS LAST
|
||||
),
|
||||
s_raw AS (
|
||||
SELECT
|
||||
s.*,
|
||||
regexp_replace(upper(s.ct_number), '^D', '') AS norm_ct
|
||||
upper(s.ct_number) AS ct_key
|
||||
FROM public.{UPP_STL_TABLE_SQL} s
|
||||
),
|
||||
s AS (
|
||||
SELECT DISTINCT ON (norm_ct) *
|
||||
SELECT DISTINCT ON (ct_key) *
|
||||
FROM s_raw
|
||||
ORDER BY norm_ct, file_count DESC, updated_at DESC NULLS LAST
|
||||
ORDER BY ct_key, file_count DESC, updated_at DESC NULLS LAST
|
||||
),
|
||||
keys AS (
|
||||
SELECT norm_ct FROM p
|
||||
SELECT ct_key FROM p
|
||||
UNION
|
||||
SELECT norm_ct FROM u
|
||||
SELECT ct_key FROM u
|
||||
),
|
||||
relation AS (
|
||||
SELECT
|
||||
k.norm_ct,
|
||||
k.ct_key,
|
||||
p.ct_number AS pacs_ct_number,
|
||||
u.ct_number AS stl_ct_number,
|
||||
u.ct_number AS upp_ct_number,
|
||||
s.ct_number AS stl_ct_number,
|
||||
s.ct_number AS stl_file_ct_number,
|
||||
p.ct_number IS NOT NULL AS pacs_present,
|
||||
u.ct_number IS NOT NULL AS stl_asset_present,
|
||||
u.ct_number IS NOT NULL AS upp_present,
|
||||
COALESCE(u.stl_present, false) OR s.ct_number IS NOT NULL AS stl_present,
|
||||
CASE
|
||||
WHEN p.ct_number IS NOT NULL AND u.ct_number IS NOT NULL AND (COALESCE(u.stl_present, false) OR s.ct_number IS NOT NULL) THEN 'complete'
|
||||
WHEN p.ct_number IS NOT NULL AND (COALESCE(u.stl_present, false) OR s.ct_number IS NOT NULL) THEN 'complete'
|
||||
WHEN p.ct_number IS NOT NULL AND u.ct_number IS NOT NULL THEN 'no_stl'
|
||||
WHEN p.ct_number IS NOT NULL THEN 'pacs_only'
|
||||
WHEN u.ct_number IS NOT NULL THEN 'upp_only'
|
||||
WHEN (COALESCE(u.stl_present, false) OR s.ct_number IS NOT NULL) THEN 'stl_only'
|
||||
WHEN u.ct_number IS NOT NULL THEN 'list_only'
|
||||
ELSE 'unknown'
|
||||
END AS relation_status,
|
||||
CASE
|
||||
WHEN p.ct_number IS NOT NULL AND u.ct_number IS NOT NULL AND p.ct_number = u.ct_number THEN 'exact'
|
||||
WHEN p.ct_number IS NOT NULL AND u.ct_number IS NOT NULL THEN 'normalized'
|
||||
ELSE ''
|
||||
END AS match_type,
|
||||
p.batch_name,
|
||||
@@ -165,6 +167,13 @@ def relation_cte() -> str:
|
||||
ps.undetermined_series,
|
||||
ps.completed,
|
||||
ps.body_parts,
|
||||
to_jsonb(array_remove(ARRAY[
|
||||
CASE WHEN ps.body_parts ? 'upper_abdomen' THEN '肝胆模型' END,
|
||||
CASE WHEN ps.body_parts ? 'chest' THEN '胸外模型' END,
|
||||
CASE WHEN ps.body_parts ? 'upper_abdomen'
|
||||
OR ps.body_parts ? 'lower_abdomen'
|
||||
OR ps.body_parts ? 'pelvis' THEN '泌尿模型' END
|
||||
], NULL)) AS dicom_models,
|
||||
u.patient_name AS upp_patient_name,
|
||||
u.patient_sex AS upp_patient_sex,
|
||||
u.patient_age,
|
||||
@@ -200,15 +209,15 @@ def relation_cte() -> str:
|
||||
ELSE 'different'
|
||||
END AS patient_name_match
|
||||
FROM keys k
|
||||
LEFT JOIN p ON p.norm_ct = k.norm_ct
|
||||
LEFT JOIN p ON p.ct_key = k.ct_key
|
||||
LEFT JOIN ps ON ps.ct_number = p.ct_number
|
||||
LEFT JOIN u ON u.norm_ct = k.norm_ct
|
||||
LEFT JOIN s ON s.norm_ct = k.norm_ct
|
||||
LEFT JOIN u ON u.ct_key = k.ct_key
|
||||
LEFT JOIN s ON s.ct_key = k.ct_key
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def relation_where(q: str, status: str) -> str:
|
||||
def relation_where(q: str, status: str, algorithm_model: str, dicom_model: str) -> str:
|
||||
clauses: list[str] = []
|
||||
query = q.strip()
|
||||
if query:
|
||||
@@ -217,12 +226,14 @@ def relation_where(q: str, status: str) -> str:
|
||||
clauses.append(
|
||||
" OR ".join(
|
||||
[
|
||||
f"norm_ct ILIKE {literal}",
|
||||
f"ct_key ILIKE {literal}",
|
||||
f"pacs_ct_number ILIKE {literal}",
|
||||
f"upp_ct_number ILIKE {literal}",
|
||||
f"stl_ct_number ILIKE {literal}",
|
||||
f"pacs_patient_name ILIKE {literal}",
|
||||
f"upp_patient_name ILIKE {literal}",
|
||||
f"patient_id ILIKE {literal}",
|
||||
f"algorithm_model ILIKE {literal}",
|
||||
f"upp_status ILIKE {literal}",
|
||||
f"exam_description ILIKE {literal}",
|
||||
f"study_description ILIKE {literal}",
|
||||
]
|
||||
@@ -234,14 +245,18 @@ def relation_where(q: str, status: str) -> str:
|
||||
clauses.append("relation_status = 'no_stl'")
|
||||
elif status == "pacs_only":
|
||||
clauses.append("relation_status = 'pacs_only'")
|
||||
elif status == "upp_only":
|
||||
clauses.append("relation_status = 'upp_only'")
|
||||
elif status == "normalized":
|
||||
clauses.append("match_type = 'normalized'")
|
||||
elif status == "stl_only":
|
||||
clauses.append("relation_status = 'stl_only'")
|
||||
elif status == "list_only":
|
||||
clauses.append("relation_status = 'list_only'")
|
||||
elif status == "undetermined":
|
||||
clauses.append("COALESCE(undetermined_series, 0) > 0")
|
||||
elif status != "all":
|
||||
raise HTTPException(status_code=400, detail="invalid status filter")
|
||||
if algorithm_model:
|
||||
clauses.append(f"algorithm_model = {sql_literal(algorithm_model)}")
|
||||
if dicom_model:
|
||||
clauses.append(f"dicom_models ? {sql_literal(dicom_model)}")
|
||||
return "WHERE " + " AND ".join(clauses) if clauses else ""
|
||||
|
||||
|
||||
@@ -266,13 +281,14 @@ def status() -> dict[str, Any]:
|
||||
SELECT
|
||||
count(*)::int AS total_ct,
|
||||
count(*) FILTER (WHERE pacs_present)::int AS pacs_count,
|
||||
count(*) FILTER (WHERE upp_present)::int AS upp_count,
|
||||
count(*) FILTER (WHERE stl_asset_present)::int AS stl_asset_count,
|
||||
count(*) FILTER (WHERE list_present)::int AS list_count,
|
||||
count(*) FILTER (WHERE stl_present)::int AS stl_count,
|
||||
count(*) FILTER (WHERE relation_status = 'complete')::int AS complete_count,
|
||||
count(*) FILTER (WHERE relation_status = 'no_stl')::int AS no_stl_count,
|
||||
count(*) FILTER (WHERE relation_status = 'pacs_only')::int AS pacs_only_count,
|
||||
count(*) FILTER (WHERE relation_status = 'upp_only')::int AS upp_only_count,
|
||||
count(*) FILTER (WHERE match_type = 'normalized')::int AS normalized_match_count,
|
||||
count(*) FILTER (WHERE relation_status = 'stl_only')::int AS stl_only_count,
|
||||
count(*) FILTER (WHERE relation_status = 'list_only')::int AS list_only_count,
|
||||
COALESCE(sum(COALESCE(undetermined_series, 0)), 0)::int AS undetermined_series
|
||||
FROM relation
|
||||
""",
|
||||
@@ -292,20 +308,26 @@ def status() -> dict[str, Any]:
|
||||
|
||||
|
||||
@app.get("/api/relations")
|
||||
def relations(q: str = "", status: str = "all", limit: int = Query(default=300, ge=1, le=1000)) -> list[dict[str, Any]]:
|
||||
where = relation_where(q, status)
|
||||
def relations(
|
||||
q: str = "",
|
||||
status: str = "all",
|
||||
algorithm_model: str = "",
|
||||
dicom_model: str = "",
|
||||
limit: int = Query(default=300, ge=1, le=1000),
|
||||
) -> list[dict[str, Any]]:
|
||||
where = relation_where(q, status, algorithm_model, dicom_model)
|
||||
return pg_json_rows(
|
||||
f"""
|
||||
{relation_cte()}
|
||||
SELECT
|
||||
norm_ct, pacs_ct_number, upp_ct_number, stl_ct_number,
|
||||
pacs_present, upp_present, stl_present, relation_status, match_type,
|
||||
ct_key, pacs_ct_number, stl_ct_number,
|
||||
pacs_present, stl_asset_present, list_present, stl_present, relation_status, match_type,
|
||||
pacs_patient_name, upp_patient_name, patient_name_match, patient_id, patient_id_masked,
|
||||
study_date, study_time, exam_date, task_created_at,
|
||||
study_description, exam_description, algorithm_model, upp_status,
|
||||
pacs_series_count, dicom_file_count, annotated_series, undetermined_series, completed,
|
||||
list_present, list_record_count, stl_file_count, stl_file_count_agg,
|
||||
body_parts, segment_categories, segment_families,
|
||||
body_parts, dicom_models, segment_categories, segment_families,
|
||||
pacs_updated_at, upp_updated_at, stl_updated_at
|
||||
FROM relation
|
||||
{where}
|
||||
@@ -314,7 +336,7 @@ def relations(q: str = "", status: str = "all", limit: int = Query(default=300,
|
||||
relation_status,
|
||||
COALESCE(study_date, '') DESC,
|
||||
COALESCE(study_time, '') DESC,
|
||||
norm_ct
|
||||
ct_key
|
||||
LIMIT {int(limit)}
|
||||
""",
|
||||
timeout=24,
|
||||
@@ -323,15 +345,15 @@ def relations(q: str = "", status: str = "all", limit: int = Query(default=300,
|
||||
|
||||
@app.get("/api/relations/{ct_number}")
|
||||
def relation_detail(ct_number: str) -> dict[str, Any]:
|
||||
norm_ct = normalize_ct(ct_number)
|
||||
if not norm_ct:
|
||||
ct_key = normalize_ct(ct_number)
|
||||
if not ct_key:
|
||||
raise HTTPException(status_code=400, detail="invalid CT number")
|
||||
rows = pg_json_rows(
|
||||
f"""
|
||||
{relation_cte()}
|
||||
SELECT *
|
||||
FROM relation
|
||||
WHERE norm_ct = {sql_literal(norm_ct)}
|
||||
WHERE ct_key = {sql_literal(ct_key)}
|
||||
LIMIT 1
|
||||
""",
|
||||
timeout=24,
|
||||
|
||||
Reference in New Issue
Block a user