fix: inherit person data via request_number for individually uploaded pages

- Detect page 2+ from page_info field (e.g. صفحة ٢ من 5) with Arabic digit support
- Find sibling documents by matching request_number, not just pdf_group_id
- Works for both PDF splits AND individually photographed pages
- Inherit all person fields (name, father, family, registry, etc.) from confirmed sibling
- Update review banner to show for all subsequent pages
This commit is contained in:
Georges Haddad
2026-04-11 17:25:25 +03:00
parent 6ba5e7ee79
commit 123492ae90
2 changed files with 126 additions and 48 deletions
+123 -45
View File
@@ -1,5 +1,6 @@
import json import json
import asyncio import asyncio
import re
from fastapi import APIRouter, HTTPException, Request, status from fastapi import APIRouter, HTTPException, Request, status
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
@@ -15,6 +16,25 @@ templates = Jinja2Templates(directory="templates")
REVIEWABLE_DOCUMENT_STATUSES = {"extracted", "confirmed", "error"} REVIEWABLE_DOCUMENT_STATUSES = {"extracted", "confirmed", "error"}
# Map Arabic-Indic digits to Western
_ARABIC_DIGIT_MAP = str.maketrans("٠١٢٣٤٥٦٧٨٩", "0123456789")
def _is_subsequent_page(page_info: str) -> bool:
"""Return True if page_info indicates this is page 2 or later.
Handles formats like: 'صفحة ٢ من 5', '2 / 3', 'صفحة 2 من 3', '٢ من ٣'
"""
if not page_info:
return False
# Normalize Arabic-Indic digits to Western
normalized = page_info.translate(_ARABIC_DIGIT_MAP)
# Find the first number (the page number)
m = re.search(r"(\d+)", normalized)
if m:
page_num = int(m.group(1))
return page_num > 1
return False
def _get_document(doc_id: int) -> dict | None: def _get_document(doc_id: int) -> dict | None:
with get_db() as conn: with get_db() as conn:
@@ -42,46 +62,77 @@ def _get_document(doc_id: int) -> dict | None:
else: else:
doc["person"] = {} doc["person"] = {}
# For multi-page PDFs (page 2+), inherit person info & doc fields from page 1 # For multi-page documents (page 2+), inherit person info & doc fields from page 1.
# Works for both PDF splits (pdf_group_id) and individually uploaded images (via request_number + page_info).
doc["inherited_from_page1"] = False doc["inherited_from_page1"] = False
doc["page1_person_id"] = None
current_first = (doc["person"].get("first_name") or "").strip()
is_subsequent_page = False
sibling_doc = None
# Method 1: PDF group link
if doc.get("pdf_group_id") and (doc.get("page_number") or 0) > 1: if doc.get("pdf_group_id") and (doc.get("page_number") or 0) > 1:
page1 = conn.execute( is_subsequent_page = True
sibling_doc = conn.execute(
"""SELECT * FROM documents """SELECT * FROM documents
WHERE pdf_group_id=? AND page_number=1""", WHERE pdf_group_id=? AND page_number=1""",
(doc["pdf_group_id"],), (doc["pdf_group_id"],),
).fetchone() ).fetchone()
if page1:
page1 = dict(page1)
# Inherit person data if current page has no name
current_first = (doc["person"].get("first_name") or "").strip()
if not current_first:
if page1.get("person_id"):
person = conn.execute(
"SELECT * FROM persons WHERE id=?", (page1["person_id"],)
).fetchone()
if person:
doc["person"] = dict(person)
doc["inherited_from_page1"] = True
doc["page1_person_id"] = page1["person_id"]
elif page1.get("raw_extraction_json"):
try:
p1_extracted = json.loads(page1["raw_extraction_json"])
p1_person = p1_extracted.get("person", {})
if (p1_person.get("first_name") or "").strip():
doc["person"] = p1_person
doc["inherited_from_page1"] = True
except Exception:
pass
# Inherit document-level fields if missing # Method 2: Same request_number + page_info indicates page 2+
inherit_fields = [ if not sibling_doc and not current_first:
"request_number", "request_date", "search_scope", page_info_str = doc.get("page_info") or ""
"request_purpose", "data_valid_until", "registry_office", req_num = doc.get("request_number") or ""
"applicant_name_raw", if req_num and _is_subsequent_page(page_info_str):
] is_subsequent_page = True
for field in inherit_fields: # Find another document with same request_number that has person data
if not (doc.get(field) or "").strip() and (page1.get(field) or "").strip(): sibling_doc = conn.execute(
doc[field] = page1[field] """SELECT * FROM documents
WHERE request_number=? AND id != ? AND person_id IS NOT NULL
ORDER BY id LIMIT 1""",
(req_num, doc["id"]),
).fetchone()
if not sibling_doc:
# Page 1 may not be confirmed yet; look for one with raw extraction
sibling_doc = conn.execute(
"""SELECT * FROM documents
WHERE request_number=? AND id != ? AND raw_extraction_json IS NOT NULL
ORDER BY id LIMIT 1""",
(req_num, doc["id"]),
).fetchone()
if sibling_doc and not current_first:
sibling_doc = dict(sibling_doc)
if sibling_doc.get("person_id"):
person = conn.execute(
"SELECT * FROM persons WHERE id=?", (sibling_doc["person_id"],)
).fetchone()
if person:
doc["person"] = dict(person)
doc["inherited_from_page1"] = True
doc["page1_person_id"] = sibling_doc["person_id"]
elif sibling_doc.get("raw_extraction_json"):
try:
sib_extracted = json.loads(sibling_doc["raw_extraction_json"])
sib_person = sib_extracted.get("person", {})
if (sib_person.get("first_name") or "").strip():
doc["person"] = sib_person
doc["inherited_from_page1"] = True
except Exception:
pass
# Inherit document-level fields if missing
inherit_fields = [
"request_number", "request_date", "search_scope",
"request_purpose", "data_valid_until", "registry_office",
"applicant_name_raw",
]
for field in inherit_fields:
if not (doc.get(field) or "").strip() and (sibling_doc.get(field) or "").strip():
doc[field] = sibling_doc[field]
doc["is_subsequent_page"] = is_subsequent_page
return doc return doc
@@ -294,13 +345,16 @@ async def confirm_document(doc_id: int, request: Request):
current_doc = dict(current_doc) current_doc = dict(current_doc)
# For multi-page PDFs (page 2+), resolve page 1's person and inherit name if needed # For multi-page documents (page 2+), resolve sibling document's person
# Works via pdf_group_id OR request_number + page_info
page1_person_id = None page1_person_id = None
is_subsequent_page = ( is_subsequent = (
current_doc.get("pdf_group_id") current_doc.get("pdf_group_id")
and (current_doc.get("page_number") or 0) > 1 and (current_doc.get("page_number") or 0) > 1
) )
if is_subsequent_page:
# Method 1: PDF group link
if is_subsequent:
page1_doc = conn.execute( page1_doc = conn.execute(
"""SELECT d.person_id, p.first_name, p.father_name, p.family_name, """SELECT d.person_id, p.first_name, p.father_name, p.family_name,
p.registry_number p.registry_number
@@ -311,15 +365,39 @@ async def confirm_document(doc_id: int, request: Request):
).fetchone() ).fetchone()
if page1_doc and page1_doc["person_id"]: if page1_doc and page1_doc["person_id"]:
page1_person_id = page1_doc["person_id"] page1_person_id = page1_doc["person_id"]
# Inherit name from page 1's person if not provided
if not first_name and page1_doc["first_name"]: # Method 2: Same request_number + page_info indicates page 2+
first_name = page1_doc["first_name"] if not page1_person_id and not first_name:
person_data["first_name"] = first_name req_num = current_doc.get("request_number") or ""
# Also inherit other person fields if empty page_info_val = current_doc.get("page_info") or ""
for field in ("father_name", "family_name", "registry_number"): if req_num and _is_subsequent_page(page_info_val):
if not (person_data.get(field) or "").strip() and page1_doc[field]: is_subsequent = True
person_data[field] = page1_doc[field] sibling = conn.execute(
registry_number = (person_data.get("registry_number") or "").strip() or None """SELECT d.person_id, p.first_name, p.father_name, p.family_name,
p.registry_number
FROM documents d
LEFT JOIN persons p ON p.id = d.person_id
WHERE d.request_number=? AND d.id != ? AND d.person_id IS NOT NULL
ORDER BY d.id LIMIT 1""",
(req_num, doc_id),
).fetchone()
if sibling and sibling["person_id"]:
page1_person_id = sibling["person_id"]
# Inherit person fields from sibling if not provided
if page1_person_id and not first_name:
sibling_person = conn.execute(
"SELECT * FROM persons WHERE id=?", (page1_person_id,)
).fetchone()
if sibling_person:
first_name = sibling_person["first_name"] or ""
person_data["first_name"] = first_name
for field in ("father_name", "mother_name", "family_name",
"family_origin", "nationality", "birth_date",
"registry_number", "registry_place"):
if not (person_data.get(field) or "").strip() and sibling_person[field]:
person_data[field] = sibling_person[field]
registry_number = (person_data.get("registry_number") or "").strip() or None
if not first_name: if not first_name:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="First name is required") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="First name is required")
+3 -3
View File
@@ -54,14 +54,14 @@
<div class="form-panel"> <div class="form-panel">
<form id="reviewForm"> <form id="reviewForm">
{% if doc.get('pdf_group_id') and (doc.get('page_number') or 0) > 1 %} {% if doc.get('is_subsequent_page') %}
<div class="info-banner page-group-banner"> <div class="info-banner page-group-banner">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
هذه الصفحة {{ doc.page_number }} من مستند متعدد الصفحات. هذه صفحة تكميلية ({{ doc.get('page_info') or doc.get('page_number') or '' }}) من مستند متعدد الصفحات.
{% if doc.get('inherited_from_page1') %} {% if doc.get('inherited_from_page1') %}
بيانات الشخص مستوردة تلقائياً من الصفحة الأولى. بيانات الشخص مستوردة تلقائياً من الصفحة الأولى.
{% else %} {% else %}
يرجى التأكد من بيانات الشخص يدوياً. يرجى التأكد من بيانات الشخص يدوياً — لم يتم العثور على صفحة أولى مؤكدة بعد.
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}