174 lines
6.3 KiB
Python
174 lines
6.3 KiB
Python
import asyncio
|
|
import json
|
|
import uuid
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, File, Form, Request, UploadFile
|
|
from fastapi.responses import RedirectResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
from config import UPLOAD_DIR
|
|
from database.connection import get_db
|
|
from services.extractor import extract_document, get_available_providers, get_default_provider
|
|
from services.pdf_handler import pdf_to_images
|
|
|
|
router = APIRouter()
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
ALLOWED_IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp"}
|
|
ALLOWED_PDF_EXTS = {".pdf"}
|
|
ALLOWED_EXTENSIONS = ALLOWED_IMAGE_EXTS | ALLOWED_PDF_EXTS
|
|
|
|
|
|
def _save_image(file_bytes: bytes, original_name: str) -> str:
|
|
"""Save image to uploads/{date}/{uuid}_{name} and return relative path."""
|
|
today = date.today().isoformat()
|
|
dest_dir = Path(UPLOAD_DIR) / today
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
suffix = Path(original_name).suffix.lower() or ".jpg"
|
|
filename = f"{uuid.uuid4().hex}{suffix}"
|
|
dest = dest_dir / filename
|
|
dest.write_bytes(file_bytes)
|
|
return str(Path(today) / filename)
|
|
|
|
|
|
async def _extract_and_save(doc_id: int, image_path: str, provider: str = ""):
|
|
"""Background task: call extractor, parse result, update DB."""
|
|
try:
|
|
data = await extract_document(image_path, provider=provider)
|
|
raw_json = json.dumps(data, ensure_ascii=False)
|
|
|
|
with get_db() as conn:
|
|
conn.execute(
|
|
"""UPDATE documents SET status='extracted',
|
|
raw_extraction_json=?,
|
|
request_number=?,
|
|
request_date=?,
|
|
applicant_name_raw=?,
|
|
request_purpose=?,
|
|
data_valid_until=?,
|
|
registry_office=?,
|
|
owns_properties=?,
|
|
declared_property_count=?,
|
|
page_info=?,
|
|
search_scope=?,
|
|
updated_at=CURRENT_TIMESTAMP
|
|
WHERE id=?""",
|
|
(
|
|
raw_json,
|
|
data.get("request_number"),
|
|
data.get("request_date"),
|
|
data.get("applicant_name_raw"),
|
|
data.get("request_purpose"),
|
|
data.get("data_valid_until"),
|
|
data.get("registry_office"),
|
|
data.get("owns_properties"),
|
|
data.get("declared_property_count"),
|
|
data.get("page_info"),
|
|
data.get("search_scope"),
|
|
doc_id,
|
|
),
|
|
)
|
|
|
|
for i, prop in enumerate(data.get("properties", [])):
|
|
conn.execute(
|
|
"""INSERT INTO properties
|
|
(document_id, row_order, party_name, property_number,
|
|
section, block, real_estate_district, qaza, num_shares, ownership_type)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?)""",
|
|
(
|
|
doc_id, i,
|
|
prop.get("party_name"),
|
|
prop.get("property_number"),
|
|
prop.get("section"),
|
|
prop.get("block"),
|
|
prop.get("real_estate_district"),
|
|
prop.get("qaza"),
|
|
prop.get("num_shares"),
|
|
prop.get("ownership_type"),
|
|
),
|
|
)
|
|
|
|
except Exception as e:
|
|
with get_db() as conn:
|
|
conn.execute(
|
|
"""UPDATE documents SET status='error', extraction_error=?,
|
|
updated_at=CURRENT_TIMESTAMP WHERE id=?""",
|
|
(str(e), doc_id),
|
|
)
|
|
|
|
|
|
@router.get("/")
|
|
async def upload_page(request: Request):
|
|
with get_db() as conn:
|
|
stats = conn.execute(
|
|
"""SELECT
|
|
COUNT(*) AS total,
|
|
SUM(CASE WHEN status='confirmed' THEN 1 ELSE 0 END) AS confirmed,
|
|
SUM(CASE WHEN status='extracted' THEN 1 ELSE 0 END) AS pending_review,
|
|
SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors
|
|
FROM documents"""
|
|
).fetchone()
|
|
return templates.TemplateResponse(
|
|
request, "index.html", {
|
|
"stats": dict(stats) if stats else {},
|
|
"providers": get_available_providers(),
|
|
"default_provider": get_default_provider(),
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/upload")
|
|
async def upload_files(
|
|
request: Request,
|
|
files: list[UploadFile] = File(...),
|
|
provider: str = Form(""),
|
|
):
|
|
if not provider:
|
|
provider = get_default_provider()
|
|
|
|
doc_ids = []
|
|
|
|
for upload in files:
|
|
suffix = Path(upload.filename).suffix.lower()
|
|
if suffix not in ALLOWED_EXTENSIONS:
|
|
continue
|
|
|
|
file_bytes = await upload.read()
|
|
|
|
if suffix in ALLOWED_PDF_EXTS:
|
|
# PDF: split into per-page images
|
|
pages = pdf_to_images(file_bytes, upload.filename)
|
|
for page_info in pages:
|
|
with get_db() as conn:
|
|
cursor = conn.execute(
|
|
"""INSERT INTO documents
|
|
(image_path, status, provider, pdf_group_id, page_number)
|
|
VALUES (?, 'pending', ?, ?, ?)""",
|
|
(
|
|
page_info["image_path"],
|
|
provider,
|
|
page_info["pdf_group_id"],
|
|
page_info["page_number"],
|
|
),
|
|
)
|
|
doc_ids.append((cursor.lastrowid, page_info["image_path"]))
|
|
else:
|
|
# Image file
|
|
rel_path = _save_image(file_bytes, upload.filename)
|
|
with get_db() as conn:
|
|
cursor = conn.execute(
|
|
"INSERT INTO documents (image_path, status, provider) VALUES (?, 'pending', ?)",
|
|
(rel_path, provider),
|
|
)
|
|
doc_ids.append((cursor.lastrowid, rel_path))
|
|
|
|
# Fire background extractions
|
|
for doc_id, rel_path in doc_ids:
|
|
asyncio.create_task(_extract_and_save(doc_id, rel_path, provider))
|
|
|
|
if len(doc_ids) == 1:
|
|
return RedirectResponse(f"/review/{doc_ids[0][0]}?wait=1", status_code=303)
|
|
return RedirectResponse("/documents?uploaded=1", status_code=303)
|