finishing QDRO section
This commit is contained in:
112
app/services/template_merge.py
Normal file
112
app/services/template_merge.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Template variable resolution and DOCX preview using docxtpl.
|
||||
|
||||
MVP features:
|
||||
- Resolve variables from explicit context, FormVariable, ReportVariable
|
||||
- Built-in variables (dates)
|
||||
- Render DOCX using docxtpl when mime_type is docx; otherwise return bytes as-is
|
||||
- Return unresolved tokens list
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.additional import FormVariable, ReportVariable
|
||||
|
||||
try:
|
||||
from docxtpl import DocxTemplate
|
||||
DOCXTPL_AVAILABLE = True
|
||||
except Exception:
|
||||
DOCXTPL_AVAILABLE = False
|
||||
|
||||
|
||||
TOKEN_PATTERN = re.compile(r"\{\{\s*([a-zA-Z0-9_\.]+)\s*\}\}")
|
||||
|
||||
|
||||
def extract_tokens_from_bytes(content: bytes) -> List[str]:
|
||||
# Prefer docxtpl-based extraction for DOCX if available
|
||||
if DOCXTPL_AVAILABLE:
|
||||
try:
|
||||
buf = io.BytesIO(content)
|
||||
tpl = DocxTemplate(buf)
|
||||
# jinja2 analysis for undeclared template variables
|
||||
vars_set = tpl.get_undeclared_template_variables({})
|
||||
return sorted({str(v) for v in vars_set})
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback: naive regex over decoded text
|
||||
try:
|
||||
text = content.decode("utf-8", errors="ignore")
|
||||
except Exception:
|
||||
text = ""
|
||||
return sorted({m.group(1) for m in TOKEN_PATTERN.finditer(text)})
|
||||
|
||||
|
||||
def build_context(payload_context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# Built-ins
|
||||
today = date.today()
|
||||
builtins = {
|
||||
"TODAY": today.strftime("%B %d, %Y"),
|
||||
"TODAY_ISO": today.isoformat(),
|
||||
"NOW": datetime.utcnow().isoformat() + "Z",
|
||||
}
|
||||
merged = {**builtins}
|
||||
# Normalize keys to support both FOO and foo
|
||||
for k, v in payload_context.items():
|
||||
merged[k] = v
|
||||
if isinstance(k, str):
|
||||
merged.setdefault(k.upper(), v)
|
||||
return merged
|
||||
|
||||
|
||||
def _safe_lookup_variable(db: Session, identifier: str) -> Any:
|
||||
# 1) FormVariable
|
||||
fv = db.query(FormVariable).filter(FormVariable.identifier == identifier, FormVariable.active == 1).first()
|
||||
if fv:
|
||||
# MVP: use static response if present; otherwise treat as unresolved
|
||||
if fv.response is not None:
|
||||
return fv.response
|
||||
return None
|
||||
# 2) ReportVariable
|
||||
rv = db.query(ReportVariable).filter(ReportVariable.identifier == identifier, ReportVariable.active == 1).first()
|
||||
if rv:
|
||||
# MVP: no evaluation yet; unresolved
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def resolve_tokens(db: Session, tokens: List[str], context: Dict[str, Any]) -> Tuple[Dict[str, Any], List[str]]:
|
||||
resolved: Dict[str, Any] = {}
|
||||
unresolved: List[str] = []
|
||||
for tok in tokens:
|
||||
# Order: payload context (case-insensitive via upper) -> FormVariable -> ReportVariable
|
||||
value = context.get(tok)
|
||||
if value is None:
|
||||
value = context.get(tok.upper())
|
||||
if value is None:
|
||||
value = _safe_lookup_variable(db, tok)
|
||||
if value is None:
|
||||
unresolved.append(tok)
|
||||
else:
|
||||
resolved[tok] = value
|
||||
return resolved, unresolved
|
||||
|
||||
|
||||
def render_docx(docx_bytes: bytes, context: Dict[str, Any]) -> bytes:
|
||||
if not DOCXTPL_AVAILABLE:
|
||||
# Return original bytes if docxtpl is not installed
|
||||
return docx_bytes
|
||||
# Write to BytesIO for docxtpl
|
||||
in_buffer = io.BytesIO(docx_bytes)
|
||||
tpl = DocxTemplate(in_buffer)
|
||||
tpl.render(context)
|
||||
out_buffer = io.BytesIO()
|
||||
tpl.save(out_buffer)
|
||||
return out_buffer.getvalue()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user