Files
OCR/docengine/app/api/v1/templates.py

424 lines
13 KiB
Python

from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.dependencies import CurrentUser
from app.core.logging_config import get_logger
from app.repositories.document_repository import DocumentRepository, TemplateMatchRepository
from app.repositories.template_repository import (
TemplateRepository,
DocumentRegionRepository,
DocumentCellRepository,
TableFormatRepository,
TableColumnRepository
)
from app.schemas.common import PaginatedResponse, SuccessResponse
from app.schemas.document import TemplateMatchRequest, TemplateMatchResponse
from app.schemas.template import (
TemplateListResponse,
TemplateRenderRequest,
TemplateRenderResponse,
TemplateResponse,
TemplateCreateRequest,
TemplateMappingSaveRequest,
)
logger = get_logger(__name__)
router = APIRouter(prefix="/templates", tags=["Templates"])
@router.post(
"",
response_model=dict,
summary="Create Template",
description="Create a new template.",
)
def create_template(
payload: TemplateCreateRequest,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> dict:
"""Create a template."""
template_repo = TemplateRepository(db)
template = template_repo.create_template(
name=payload.template_name,
page_width=1000.0,
page_height=1000.0,
source_document_id=uuid.UUID(payload.source_document_id) if payload.source_document_id else None
)
cell_repo = DocumentCellRepository(db)
saved_fields = []
for index, field in enumerate(payload.fields):
cell = cell_repo.create_cell(
format_id=template.id,
page_number=1,
x=0.0,
y=0.0,
width=0.0,
height=0.0,
data_type=field.field_type,
field_name=field.field_label,
sequence=field.display_order or index,
is_dynamic=True
)
saved_fields.append({
"field_label": cell.field_name,
"field_type": cell.data_type
})
db.commit()
return {
"pk_template_id": str(template.id),
"template_name": template.name,
"fields": saved_fields
}
@router.put(
"/{template_id}",
response_model=dict,
summary="Update Template",
description="Update an existing template name and its fields.",
)
def update_template(
template_id: uuid.UUID,
payload: TemplateCreateRequest,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> dict:
"""Update a template."""
template_repo = TemplateRepository(db)
template = template_repo.get_by_id(template_id)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Template '{template_id}' not found",
)
if template.name != payload.template_name:
template.name = payload.template_name
from app.models.template import DocumentCell
db.query(DocumentCell).filter(DocumentCell.format_id == template_id).delete()
saved_fields = []
cell_repo = DocumentCellRepository(db)
for index, field in enumerate(payload.fields):
cell = cell_repo.create_cell(
format_id=template.id,
page_number=1,
row_no=0,
column_no=0,
field_name=field.field_label,
data_type=field.field_type,
x=0.0,
y=0.0,
width=0.0,
height=0.0,
is_dynamic=True
)
saved_fields.append({
"field_label": cell.field_name,
"field_type": cell.data_type
})
db.commit()
return {
"pk_template_id": str(template.id),
"template_name": template.name,
"fields": saved_fields
}
@router.post(
"/{template_id}/mappings/save",
response_model=SuccessResponse,
summary="Save Template Mappings",
description="Save the field mappings for a template.",
)
def save_mappings(
template_id: uuid.UUID,
payload: list[TemplateMappingSaveRequest],
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> SuccessResponse:
"""Save template mappings."""
template_repo = TemplateRepository(db)
template = template_repo.get_by_id(template_id)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Template '{template_id}' not found",
)
region_repo = DocumentRegionRepository(db)
cell_repo = DocumentCellRepository(db)
table_repo = TableFormatRepository(db)
table_col_repo = TableColumnRepository(db)
# Delete existing field mappings and table formats for this template
from app.models.template import DocumentRegion, TableFormat
db.query(DocumentRegion).filter(
DocumentRegion.format_id == template_id,
DocumentRegion.region_type == "field_mapping"
).delete()
db.query(TableFormat).filter(TableFormat.format_id == template_id).delete()
db.flush()
cells = cell_repo.get_template_cells(template_id)
cell_map = {c.field_name: c for c in cells}
table_format = None
# Save each mapped node
for mapping in payload:
cell = cell_map.get(mapping.field_name)
is_table_column = cell and cell.data_type == 'TABLE_COLUMN'
for node_idx, node in enumerate(mapping.mapped_nodes):
if is_table_column:
if not table_format:
table_format = table_repo.create_table_format(
format_id=template_id,
page_number=node.page_no or 1,
x=0.0, y=0.0, width=1000.0, height=1000.0,
rows=1, columns=10
)
table_col_repo.create_column(
table_format_id=table_format.id,
column_index=node_idx,
width=node.width,
header_text=mapping.field_name,
data_type="text"
)
region_repo.create_region(
format_id=template_id,
page_number=node.page_no or 1,
region_type="field_mapping",
x=node.x_coordinate,
y=node.y_coordinate,
width=node.width,
height=node.height,
content={
"field_name": mapping.field_name,
"text_value": node.text_value
}
)
db.commit()
# Generate fingerprint now that mappings are populated
from app.services.fingerprint_service import FingerprintService
FingerprintService(db).generate_fingerprint(template)
db.commit()
return SuccessResponse(message="Mappings saved successfully")
@router.get(
"",
response_model=PaginatedResponse[TemplateListResponse],
summary="List Templates",
description="List all active templates with pagination.",
)
def list_templates(
current_user: CurrentUser,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
db: Session = Depends(get_db),
) -> PaginatedResponse[TemplateListResponse]:
"""List all active templates."""
template_repo = TemplateRepository(db)
offset = (page - 1) * page_size
templates = template_repo.get_active_templates(offset=offset, limit=page_size)
total = template_repo.count_active()
items = [TemplateListResponse.model_validate(t) for t in templates]
return PaginatedResponse.create(
items=items,
total=total,
page=page,
page_size=page_size,
)
@router.get(
"/{template_id}",
response_model=TemplateResponse,
summary="Get Template",
description="Retrieve a template by ID with all its components.",
)
def get_template(
template_id: uuid.UUID,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> TemplateResponse:
"""Get a template by ID."""
template_repo = TemplateRepository(db)
template = template_repo.get_by_id(template_id)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Template '{template_id}' not found",
)
return TemplateResponse.model_validate(template)
@router.delete(
"/{template_id}",
response_model=SuccessResponse,
summary="Delete Template",
description="Soft-delete a template by deactivating it.",
)
def delete_template(
template_id: uuid.UUID,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> SuccessResponse:
"""Soft-delete a template."""
template_repo = TemplateRepository(db)
template = template_repo.get_by_id(template_id)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Template '{template_id}' not found",
)
template_repo.deactivate_template(template_id)
db.commit()
return SuccessResponse(message=f"Template '{template_id}' deactivated successfully")
@router.post(
"/match",
response_model=list[TemplateMatchResponse],
summary="Match Document to Templates",
description="Match a document against existing templates and return ranked results.",
)
def match_template(
payload: TemplateMatchRequest,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> list[TemplateMatchResponse]:
"""Match a document against existing templates."""
doc_repo = DocumentRepository(db)
document = doc_repo.get_by_id(payload.document_id)
if not document:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Document '{payload.document_id}' not found",
)
if document.status != "completed":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Document must be in 'completed' status. Current status: '{document.status}'",
)
# Perform template matching
from app.services.matching_service import MatchingService
matching_service = MatchingService(db)
matches = matching_service.match_document(
document_id=payload.document_id,
min_confidence=payload.min_confidence,
max_results=payload.max_results,
)
db.commit()
results = []
for match in matches:
resp = TemplateMatchResponse(
id=match.id,
document_id=match.document_id,
format_id=match.format_id,
confidence_score=match.confidence_score,
match_details=match.match_details,
selected=match.selected,
template_name=match.template.name if match.template else None,
created_at=match.created_at,
)
results.append(resp)
return results
@router.post(
"/render",
response_model=TemplateRenderResponse,
summary="Render Template to PDF",
description="Generate a PDF from a stored template with supplied data.",
)
def render_template(
payload: TemplateRenderRequest,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> TemplateRenderResponse:
"""Render a template to PDF."""
template_repo = TemplateRepository(db)
template = template_repo.get_by_id(payload.template_id)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Template '{payload.template_id}' not found",
)
if not template.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Template is deactivated",
)
from app.services.reconstruction_service import ReconstructionService
reconstruction_service = ReconstructionService(db)
result = reconstruction_service.render_template(
template=template,
data=payload.data,
output_filename=payload.output_filename,
images=payload.images,
)
return result
@router.get(
"/{template_id}/download",
summary="Download Rendered PDF",
description="Download a previously rendered PDF.",
)
def download_rendered_pdf(
template_id: uuid.UUID,
current_user: CurrentUser,
filename: str = Query(..., description="Filename of the rendered PDF"),
db: Session = Depends(get_db),
) -> FileResponse:
"""Download a rendered PDF."""
from app.storage.provider import get_storage_provider
storage = get_storage_provider()
storage_path = f"rendered/{filename}"
if not storage.file_exists(storage_path):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Rendered PDF '{filename}' not found",
)
absolute_path = storage.get_absolute_path(storage_path)
return FileResponse(
path=absolute_path,
media_type="application/pdf",
filename=filename,
)