552 lines
17 KiB
Python
552 lines
17 KiB
Python
"""
|
|
Advanced Template Variables API
|
|
|
|
This API provides comprehensive variable management for document templates including:
|
|
- Variable definition and configuration
|
|
- Context-specific value management
|
|
- Advanced processing with conditional logic and calculations
|
|
- Variable testing and validation
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import List, Optional, Dict, Any, Union
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
|
|
from sqlalchemy.orm import Session, joinedload
|
|
from sqlalchemy import func, or_, and_
|
|
from pydantic import BaseModel, Field
|
|
from datetime import datetime
|
|
|
|
from app.database.base import get_db
|
|
from app.auth.security import get_current_user
|
|
from app.models.user import User
|
|
from app.models.template_variables import (
|
|
TemplateVariable, VariableContext, VariableAuditLog,
|
|
VariableType, VariableTemplate, VariableGroup
|
|
)
|
|
from app.services.advanced_variables import VariableProcessor
|
|
from app.services.query_utils import paginate_with_total
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# Pydantic schemas for API
|
|
class VariableCreate(BaseModel):
|
|
name: str = Field(..., max_length=100, description="Unique variable name")
|
|
display_name: Optional[str] = Field(None, max_length=200)
|
|
description: Optional[str] = None
|
|
variable_type: VariableType = VariableType.STRING
|
|
required: bool = False
|
|
default_value: Optional[str] = None
|
|
formula: Optional[str] = None
|
|
conditional_logic: Optional[Dict[str, Any]] = None
|
|
data_source_query: Optional[str] = None
|
|
lookup_table: Optional[str] = None
|
|
lookup_key_field: Optional[str] = None
|
|
lookup_value_field: Optional[str] = None
|
|
validation_rules: Optional[Dict[str, Any]] = None
|
|
format_pattern: Optional[str] = None
|
|
depends_on: Optional[List[str]] = None
|
|
scope: str = "global"
|
|
category: Optional[str] = None
|
|
tags: Optional[List[str]] = None
|
|
cache_duration_minutes: int = 0
|
|
|
|
|
|
class VariableUpdate(BaseModel):
|
|
display_name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
required: Optional[bool] = None
|
|
active: Optional[bool] = None
|
|
default_value: Optional[str] = None
|
|
formula: Optional[str] = None
|
|
conditional_logic: Optional[Dict[str, Any]] = None
|
|
data_source_query: Optional[str] = None
|
|
lookup_table: Optional[str] = None
|
|
lookup_key_field: Optional[str] = None
|
|
lookup_value_field: Optional[str] = None
|
|
validation_rules: Optional[Dict[str, Any]] = None
|
|
format_pattern: Optional[str] = None
|
|
depends_on: Optional[List[str]] = None
|
|
category: Optional[str] = None
|
|
tags: Optional[List[str]] = None
|
|
cache_duration_minutes: Optional[int] = None
|
|
|
|
|
|
class VariableResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
display_name: Optional[str]
|
|
description: Optional[str]
|
|
variable_type: VariableType
|
|
required: bool
|
|
active: bool
|
|
default_value: Optional[str]
|
|
scope: str
|
|
category: Optional[str]
|
|
tags: Optional[List[str]]
|
|
created_at: datetime
|
|
updated_at: Optional[datetime]
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class VariableContextSet(BaseModel):
|
|
variable_name: str
|
|
value: Any
|
|
context_type: str = "global"
|
|
context_id: str = "default"
|
|
|
|
|
|
class VariableTestRequest(BaseModel):
|
|
variables: List[str]
|
|
context_type: str = "global"
|
|
context_id: str = "default"
|
|
test_context: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
class VariableTestResponse(BaseModel):
|
|
resolved: Dict[str, Any]
|
|
unresolved: List[str]
|
|
processing_time_ms: float
|
|
errors: List[str]
|
|
|
|
|
|
class VariableAuditResponse(BaseModel):
|
|
id: int
|
|
variable_name: str
|
|
context_type: Optional[str]
|
|
context_id: Optional[str]
|
|
old_value: Optional[str]
|
|
new_value: Optional[str]
|
|
change_type: str
|
|
change_reason: Optional[str]
|
|
changed_by: Optional[str]
|
|
changed_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
@router.post("/variables/", response_model=VariableResponse)
|
|
async def create_variable(
|
|
variable_data: VariableCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Create a new template variable with advanced features"""
|
|
|
|
# Check if variable name already exists
|
|
existing = db.query(TemplateVariable).filter(
|
|
TemplateVariable.name == variable_data.name
|
|
).first()
|
|
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Variable with name '{variable_data.name}' already exists"
|
|
)
|
|
|
|
# Validate dependencies
|
|
if variable_data.depends_on:
|
|
for dep_name in variable_data.depends_on:
|
|
dep_var = db.query(TemplateVariable).filter(
|
|
TemplateVariable.name == dep_name,
|
|
TemplateVariable.active == True
|
|
).first()
|
|
if not dep_var:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Dependency variable '{dep_name}' not found"
|
|
)
|
|
|
|
# Create variable
|
|
variable = TemplateVariable(
|
|
name=variable_data.name,
|
|
display_name=variable_data.display_name,
|
|
description=variable_data.description,
|
|
variable_type=variable_data.variable_type,
|
|
required=variable_data.required,
|
|
default_value=variable_data.default_value,
|
|
formula=variable_data.formula,
|
|
conditional_logic=variable_data.conditional_logic,
|
|
data_source_query=variable_data.data_source_query,
|
|
lookup_table=variable_data.lookup_table,
|
|
lookup_key_field=variable_data.lookup_key_field,
|
|
lookup_value_field=variable_data.lookup_value_field,
|
|
validation_rules=variable_data.validation_rules,
|
|
format_pattern=variable_data.format_pattern,
|
|
depends_on=variable_data.depends_on,
|
|
scope=variable_data.scope,
|
|
category=variable_data.category,
|
|
tags=variable_data.tags,
|
|
cache_duration_minutes=variable_data.cache_duration_minutes,
|
|
created_by=current_user.username,
|
|
active=True
|
|
)
|
|
|
|
db.add(variable)
|
|
db.commit()
|
|
db.refresh(variable)
|
|
|
|
return variable
|
|
|
|
|
|
@router.get("/variables/", response_model=List[VariableResponse])
|
|
async def list_variables(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
category: Optional[str] = Query(None),
|
|
variable_type: Optional[VariableType] = Query(None),
|
|
active_only: bool = Query(True),
|
|
search: Optional[str] = Query(None),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""List template variables with filtering options"""
|
|
|
|
query = db.query(TemplateVariable)
|
|
|
|
if active_only:
|
|
query = query.filter(TemplateVariable.active == True)
|
|
|
|
if category:
|
|
query = query.filter(TemplateVariable.category == category)
|
|
|
|
if variable_type:
|
|
query = query.filter(TemplateVariable.variable_type == variable_type)
|
|
|
|
if search:
|
|
search_filter = f"%{search}%"
|
|
query = query.filter(
|
|
or_(
|
|
TemplateVariable.name.ilike(search_filter),
|
|
TemplateVariable.display_name.ilike(search_filter),
|
|
TemplateVariable.description.ilike(search_filter)
|
|
)
|
|
)
|
|
|
|
query = query.order_by(TemplateVariable.category, TemplateVariable.name)
|
|
variables, _ = paginate_with_total(query, skip, limit, False)
|
|
|
|
return variables
|
|
|
|
|
|
@router.get("/variables/{variable_id}", response_model=VariableResponse)
|
|
async def get_variable(
|
|
variable_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get a specific variable by ID"""
|
|
|
|
variable = db.query(TemplateVariable).filter(
|
|
TemplateVariable.id == variable_id
|
|
).first()
|
|
|
|
if not variable:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Variable not found"
|
|
)
|
|
|
|
return variable
|
|
|
|
|
|
@router.put("/variables/{variable_id}", response_model=VariableResponse)
|
|
async def update_variable(
|
|
variable_id: int,
|
|
variable_data: VariableUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Update a template variable"""
|
|
|
|
variable = db.query(TemplateVariable).filter(
|
|
TemplateVariable.id == variable_id
|
|
).first()
|
|
|
|
if not variable:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Variable not found"
|
|
)
|
|
|
|
# Update fields that are provided
|
|
update_data = variable_data.dict(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(variable, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(variable)
|
|
|
|
return variable
|
|
|
|
|
|
@router.delete("/variables/{variable_id}")
|
|
async def delete_variable(
|
|
variable_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Delete a template variable (soft delete by setting active=False)"""
|
|
|
|
variable = db.query(TemplateVariable).filter(
|
|
TemplateVariable.id == variable_id
|
|
).first()
|
|
|
|
if not variable:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Variable not found"
|
|
)
|
|
|
|
# Soft delete
|
|
variable.active = False
|
|
db.commit()
|
|
|
|
return {"message": "Variable deleted successfully"}
|
|
|
|
|
|
@router.post("/variables/test", response_model=VariableTestResponse)
|
|
async def test_variables(
|
|
test_request: VariableTestRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Test variable resolution with given context"""
|
|
|
|
import time
|
|
start_time = time.time()
|
|
errors = []
|
|
|
|
try:
|
|
processor = VariableProcessor(db)
|
|
resolved, unresolved = processor.resolve_variables(
|
|
variables=test_request.variables,
|
|
context_type=test_request.context_type,
|
|
context_id=test_request.context_id,
|
|
base_context=test_request.test_context or {}
|
|
)
|
|
|
|
processing_time = (time.time() - start_time) * 1000
|
|
|
|
return VariableTestResponse(
|
|
resolved=resolved,
|
|
unresolved=unresolved,
|
|
processing_time_ms=processing_time,
|
|
errors=errors
|
|
)
|
|
|
|
except Exception as e:
|
|
processing_time = (time.time() - start_time) * 1000
|
|
errors.append(str(e))
|
|
|
|
return VariableTestResponse(
|
|
resolved={},
|
|
unresolved=test_request.variables,
|
|
processing_time_ms=processing_time,
|
|
errors=errors
|
|
)
|
|
|
|
|
|
@router.post("/variables/set-value")
|
|
async def set_variable_value(
|
|
context_data: VariableContextSet,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Set a variable value in a specific context"""
|
|
|
|
processor = VariableProcessor(db)
|
|
success = processor.set_variable_value(
|
|
variable_name=context_data.variable_name,
|
|
value=context_data.value,
|
|
context_type=context_data.context_type,
|
|
context_id=context_data.context_id,
|
|
user_name=current_user.username
|
|
)
|
|
|
|
if not success:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Failed to set variable value"
|
|
)
|
|
|
|
return {"message": "Variable value set successfully"}
|
|
|
|
|
|
@router.get("/variables/{variable_id}/contexts")
|
|
async def get_variable_contexts(
|
|
variable_id: int,
|
|
context_type: Optional[str] = Query(None),
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get all contexts where this variable has values"""
|
|
|
|
query = db.query(VariableContext).filter(
|
|
VariableContext.variable_id == variable_id
|
|
)
|
|
|
|
if context_type:
|
|
query = query.filter(VariableContext.context_type == context_type)
|
|
|
|
query = query.order_by(VariableContext.context_type, VariableContext.context_id)
|
|
contexts, total = paginate_with_total(query, skip, limit, True)
|
|
|
|
return {
|
|
"items": [
|
|
{
|
|
"context_type": ctx.context_type,
|
|
"context_id": ctx.context_id,
|
|
"value": ctx.value,
|
|
"computed_value": ctx.computed_value,
|
|
"is_valid": ctx.is_valid,
|
|
"validation_errors": ctx.validation_errors,
|
|
"last_computed_at": ctx.last_computed_at
|
|
}
|
|
for ctx in contexts
|
|
],
|
|
"total": total
|
|
}
|
|
|
|
|
|
@router.get("/variables/{variable_id}/audit", response_model=List[VariableAuditResponse])
|
|
async def get_variable_audit_log(
|
|
variable_id: int,
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(20, ge=1, le=100),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get audit log for a variable"""
|
|
|
|
query = db.query(VariableAuditLog, TemplateVariable.name).join(
|
|
TemplateVariable, VariableAuditLog.variable_id == TemplateVariable.id
|
|
).filter(
|
|
VariableAuditLog.variable_id == variable_id
|
|
).order_by(VariableAuditLog.changed_at.desc())
|
|
|
|
audit_logs, _ = paginate_with_total(query, skip, limit, False)
|
|
|
|
return [
|
|
VariableAuditResponse(
|
|
id=log.id,
|
|
variable_name=var_name,
|
|
context_type=log.context_type,
|
|
context_id=log.context_id,
|
|
old_value=log.old_value,
|
|
new_value=log.new_value,
|
|
change_type=log.change_type,
|
|
change_reason=log.change_reason,
|
|
changed_by=log.changed_by,
|
|
changed_at=log.changed_at
|
|
)
|
|
for log, var_name in audit_logs
|
|
]
|
|
|
|
|
|
@router.get("/templates/{template_id}/variables")
|
|
async def get_template_variables(
|
|
template_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get all variables associated with a template"""
|
|
|
|
processor = VariableProcessor(db)
|
|
variables = processor.get_variables_for_template(template_id)
|
|
|
|
return {"variables": variables}
|
|
|
|
|
|
@router.post("/templates/{template_id}/variables/{variable_id}")
|
|
async def associate_variable_with_template(
|
|
template_id: int,
|
|
variable_id: int,
|
|
override_default: Optional[str] = Body(None),
|
|
override_required: Optional[bool] = Body(None),
|
|
display_order: int = Body(0),
|
|
group_name: Optional[str] = Body(None),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Associate a variable with a template"""
|
|
|
|
# Check if association already exists
|
|
existing = db.query(VariableTemplate).filter(
|
|
VariableTemplate.template_id == template_id,
|
|
VariableTemplate.variable_id == variable_id
|
|
).first()
|
|
|
|
if existing:
|
|
# Update existing association
|
|
existing.override_default = override_default
|
|
existing.override_required = override_required
|
|
existing.display_order = display_order
|
|
existing.group_name = group_name
|
|
else:
|
|
# Create new association
|
|
association = VariableTemplate(
|
|
template_id=template_id,
|
|
variable_id=variable_id,
|
|
override_default=override_default,
|
|
override_required=override_required,
|
|
display_order=display_order,
|
|
group_name=group_name
|
|
)
|
|
db.add(association)
|
|
|
|
db.commit()
|
|
return {"message": "Variable associated with template successfully"}
|
|
|
|
|
|
@router.delete("/templates/{template_id}/variables/{variable_id}")
|
|
async def remove_variable_from_template(
|
|
template_id: int,
|
|
variable_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Remove variable association from template"""
|
|
|
|
association = db.query(VariableTemplate).filter(
|
|
VariableTemplate.template_id == template_id,
|
|
VariableTemplate.variable_id == variable_id
|
|
).first()
|
|
|
|
if not association:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Variable association not found"
|
|
)
|
|
|
|
db.delete(association)
|
|
db.commit()
|
|
|
|
return {"message": "Variable removed from template successfully"}
|
|
|
|
|
|
@router.get("/categories")
|
|
async def get_variable_categories(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get list of variable categories"""
|
|
|
|
categories = db.query(
|
|
TemplateVariable.category,
|
|
func.count(TemplateVariable.id).label('count')
|
|
).filter(
|
|
TemplateVariable.active == True,
|
|
TemplateVariable.category.isnot(None)
|
|
).group_by(TemplateVariable.category).order_by(TemplateVariable.category).all()
|
|
|
|
return [
|
|
{"category": cat, "count": count}
|
|
for cat, count in categories
|
|
]
|