finishing QDRO section

This commit is contained in:
HotSwapp
2025-08-15 17:19:51 -05:00
parent 006ef3d7b1
commit abc7f289d1
22 changed files with 2753 additions and 46 deletions

View 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()