fixes and refactor
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
"""
|
||||
Document Management API endpoints - QDROs, Templates, and General Documents
|
||||
"""
|
||||
from typing import List, Optional, Dict, Any
|
||||
from __future__ import annotations
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File, Form, Request
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import or_, func, and_, desc, asc, text
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timezone
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
|
||||
from app.database.base import get_db
|
||||
from app.api.search_highlight import build_query_tokens
|
||||
from app.services.query_utils import tokenized_ilike_filter, apply_pagination, apply_sorting, paginate_with_total
|
||||
from app.models.qdro import QDRO
|
||||
from app.models.files import File as FileModel
|
||||
from app.models.rolodex import Rolodex
|
||||
@@ -20,18 +23,20 @@ from app.auth.security import get_current_user
|
||||
from app.models.additional import Document
|
||||
from app.core.logging import get_logger
|
||||
from app.services.audit import audit_service
|
||||
from app.services.cache import invalidate_search_cache
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Pydantic schemas
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class QDROBase(BaseModel):
|
||||
file_no: str
|
||||
version: str = "01"
|
||||
title: Optional[str] = None
|
||||
form_name: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
status: str = "DRAFT"
|
||||
created_date: Optional[date] = None
|
||||
@@ -51,6 +56,7 @@ class QDROCreate(QDROBase):
|
||||
class QDROUpdate(BaseModel):
|
||||
version: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
form_name: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
created_date: Optional[date] = None
|
||||
@@ -66,27 +72,61 @@ class QDROUpdate(BaseModel):
|
||||
class QDROResponse(QDROBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.get("/qdros/{file_no}", response_model=List[QDROResponse])
|
||||
class PaginatedQDROResponse(BaseModel):
|
||||
items: List[QDROResponse]
|
||||
total: int
|
||||
|
||||
|
||||
@router.get("/qdros/{file_no}", response_model=Union[List[QDROResponse], PaginatedQDROResponse])
|
||||
async def get_file_qdros(
|
||||
file_no: str,
|
||||
skip: int = Query(0, ge=0, description="Offset for pagination"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Page size"),
|
||||
sort_by: Optional[str] = Query("updated", description="Sort by: updated, created, version, status"),
|
||||
sort_dir: Optional[str] = Query("desc", description="Sort direction: asc or desc"),
|
||||
include_total: bool = Query(False, description="When true, returns {items, total} instead of a plain list"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get QDROs for specific file"""
|
||||
qdros = db.query(QDRO).filter(QDRO.file_no == file_no).order_by(QDRO.version).all()
|
||||
"""Get QDROs for a specific file with optional sorting/pagination"""
|
||||
query = db.query(QDRO).filter(QDRO.file_no == file_no)
|
||||
|
||||
# Sorting (whitelisted)
|
||||
query = apply_sorting(
|
||||
query,
|
||||
sort_by,
|
||||
sort_dir,
|
||||
allowed={
|
||||
"updated": [QDRO.updated_at, QDRO.id],
|
||||
"created": [QDRO.created_at, QDRO.id],
|
||||
"version": [QDRO.version],
|
||||
"status": [QDRO.status],
|
||||
},
|
||||
)
|
||||
|
||||
qdros, total = paginate_with_total(query, skip, limit, include_total)
|
||||
if include_total:
|
||||
return {"items": qdros, "total": total or 0}
|
||||
return qdros
|
||||
|
||||
|
||||
@router.get("/qdros/", response_model=List[QDROResponse])
|
||||
class PaginatedQDROResponse(BaseModel):
|
||||
items: List[QDROResponse]
|
||||
total: int
|
||||
|
||||
|
||||
@router.get("/qdros/", response_model=Union[List[QDROResponse], PaginatedQDROResponse])
|
||||
async def list_qdros(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
status_filter: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
sort_by: Optional[str] = Query(None, description="Sort by: file_no, version, status, created, updated"),
|
||||
sort_dir: Optional[str] = Query("asc", description="Sort direction: asc or desc"),
|
||||
include_total: bool = Query(False, description="When true, returns {items, total} instead of a plain list"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@@ -97,17 +137,37 @@ async def list_qdros(
|
||||
query = query.filter(QDRO.status == status_filter)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
QDRO.file_no.contains(search),
|
||||
QDRO.title.contains(search),
|
||||
QDRO.participant_name.contains(search),
|
||||
QDRO.spouse_name.contains(search),
|
||||
QDRO.plan_name.contains(search)
|
||||
)
|
||||
)
|
||||
|
||||
qdros = query.offset(skip).limit(limit).all()
|
||||
# DRY: tokenize and apply case-insensitive search across common QDRO fields
|
||||
tokens = build_query_tokens(search)
|
||||
filter_expr = tokenized_ilike_filter(tokens, [
|
||||
QDRO.file_no,
|
||||
QDRO.form_name,
|
||||
QDRO.pet,
|
||||
QDRO.res,
|
||||
QDRO.case_number,
|
||||
QDRO.notes,
|
||||
QDRO.status,
|
||||
])
|
||||
if filter_expr is not None:
|
||||
query = query.filter(filter_expr)
|
||||
|
||||
# Sorting (whitelisted)
|
||||
query = apply_sorting(
|
||||
query,
|
||||
sort_by,
|
||||
sort_dir,
|
||||
allowed={
|
||||
"file_no": [QDRO.file_no],
|
||||
"version": [QDRO.version],
|
||||
"status": [QDRO.status],
|
||||
"created": [QDRO.created_at],
|
||||
"updated": [QDRO.updated_at],
|
||||
},
|
||||
)
|
||||
|
||||
qdros, total = paginate_with_total(query, skip, limit, include_total)
|
||||
if include_total:
|
||||
return {"items": qdros, "total": total or 0}
|
||||
return qdros
|
||||
|
||||
|
||||
@@ -135,6 +195,10 @@ async def create_qdro(
|
||||
db.commit()
|
||||
db.refresh(qdro)
|
||||
|
||||
try:
|
||||
await invalidate_search_cache()
|
||||
except Exception:
|
||||
pass
|
||||
return qdro
|
||||
|
||||
|
||||
@@ -189,6 +253,10 @@ async def update_qdro(
|
||||
db.commit()
|
||||
db.refresh(qdro)
|
||||
|
||||
try:
|
||||
await invalidate_search_cache()
|
||||
except Exception:
|
||||
pass
|
||||
return qdro
|
||||
|
||||
|
||||
@@ -213,7 +281,10 @@ async def delete_qdro(
|
||||
|
||||
db.delete(qdro)
|
||||
db.commit()
|
||||
|
||||
try:
|
||||
await invalidate_search_cache()
|
||||
except Exception:
|
||||
pass
|
||||
return {"message": "QDRO deleted successfully"}
|
||||
|
||||
|
||||
@@ -241,8 +312,7 @@ class TemplateResponse(TemplateBase):
|
||||
active: bool = True
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# Document Generation Schema
|
||||
class DocumentGenerateRequest(BaseModel):
|
||||
@@ -269,13 +339,21 @@ class DocumentStats(BaseModel):
|
||||
recent_activity: List[Dict[str, Any]]
|
||||
|
||||
|
||||
@router.get("/templates/", response_model=List[TemplateResponse])
|
||||
class PaginatedTemplatesResponse(BaseModel):
|
||||
items: List[TemplateResponse]
|
||||
total: int
|
||||
|
||||
|
||||
@router.get("/templates/", response_model=Union[List[TemplateResponse], PaginatedTemplatesResponse])
|
||||
async def list_templates(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
category: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
active_only: bool = Query(True),
|
||||
sort_by: Optional[str] = Query(None, description="Sort by: form_id, form_name, category, created, updated"),
|
||||
sort_dir: Optional[str] = Query("asc", description="Sort direction: asc or desc"),
|
||||
include_total: bool = Query(False, description="When true, returns {items, total} instead of a plain list"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
@@ -289,14 +367,31 @@ async def list_templates(
|
||||
query = query.filter(FormIndex.category == category)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
FormIndex.form_name.contains(search),
|
||||
FormIndex.form_id.contains(search)
|
||||
)
|
||||
)
|
||||
|
||||
templates = query.offset(skip).limit(limit).all()
|
||||
# DRY: tokenize and apply case-insensitive search for templates
|
||||
tokens = build_query_tokens(search)
|
||||
filter_expr = tokenized_ilike_filter(tokens, [
|
||||
FormIndex.form_name,
|
||||
FormIndex.form_id,
|
||||
FormIndex.category,
|
||||
])
|
||||
if filter_expr is not None:
|
||||
query = query.filter(filter_expr)
|
||||
|
||||
# Sorting (whitelisted)
|
||||
query = apply_sorting(
|
||||
query,
|
||||
sort_by,
|
||||
sort_dir,
|
||||
allowed={
|
||||
"form_id": [FormIndex.form_id],
|
||||
"form_name": [FormIndex.form_name],
|
||||
"category": [FormIndex.category],
|
||||
"created": [FormIndex.created_at],
|
||||
"updated": [FormIndex.updated_at],
|
||||
},
|
||||
)
|
||||
|
||||
templates, total = paginate_with_total(query, skip, limit, include_total)
|
||||
|
||||
# Enhanced response with template content
|
||||
results = []
|
||||
@@ -317,6 +412,8 @@ async def list_templates(
|
||||
"variables": _extract_variables_from_content(content)
|
||||
})
|
||||
|
||||
if include_total:
|
||||
return {"items": results, "total": total or 0}
|
||||
return results
|
||||
|
||||
|
||||
@@ -356,6 +453,10 @@ async def create_template(
|
||||
|
||||
db.commit()
|
||||
db.refresh(form_index)
|
||||
try:
|
||||
await invalidate_search_cache()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"form_id": form_index.form_id,
|
||||
@@ -440,6 +541,10 @@ async def update_template(
|
||||
|
||||
db.commit()
|
||||
db.refresh(template)
|
||||
try:
|
||||
await invalidate_search_cache()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Get updated content
|
||||
template_lines = db.query(FormList).filter(
|
||||
@@ -480,6 +585,10 @@ async def delete_template(
|
||||
# Delete template
|
||||
db.delete(template)
|
||||
db.commit()
|
||||
try:
|
||||
await invalidate_search_cache()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"message": "Template deleted successfully"}
|
||||
|
||||
@@ -574,7 +683,7 @@ async def generate_document(
|
||||
"file_name": file_name,
|
||||
"file_path": file_path,
|
||||
"size": file_size,
|
||||
"created_at": datetime.now()
|
||||
"created_at": datetime.now(timezone.utc)
|
||||
}
|
||||
|
||||
|
||||
@@ -629,32 +738,49 @@ async def get_document_stats(
|
||||
@router.get("/file/{file_no}/documents")
|
||||
async def get_file_documents(
|
||||
file_no: str,
|
||||
sort_by: Optional[str] = Query("updated", description="Sort by: updated, created"),
|
||||
sort_dir: Optional[str] = Query("desc", description="Sort direction: asc or desc"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
include_total: bool = Query(False, description="When true, returns {items, total} instead of a plain list"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get all documents associated with a specific file"""
|
||||
# Get QDROs for this file
|
||||
qdros = db.query(QDRO).filter(QDRO.file_no == file_no).order_by(desc(QDRO.updated_at)).all()
|
||||
|
||||
# Format response
|
||||
documents = [
|
||||
"""Get all documents associated with a specific file, with optional sorting/pagination"""
|
||||
# Base query for QDROs tied to the file
|
||||
query = db.query(QDRO).filter(QDRO.file_no == file_no)
|
||||
|
||||
# Apply sorting using shared helper (map friendly names to columns)
|
||||
query = apply_sorting(
|
||||
query,
|
||||
sort_by,
|
||||
sort_dir,
|
||||
allowed={
|
||||
"updated": [QDRO.updated_at, QDRO.id],
|
||||
"created": [QDRO.created_at, QDRO.id],
|
||||
},
|
||||
)
|
||||
|
||||
qdros, total = paginate_with_total(query, skip, limit, include_total)
|
||||
|
||||
items = [
|
||||
{
|
||||
"id": qdro.id,
|
||||
"type": "QDRO",
|
||||
"title": f"QDRO v{qdro.version}",
|
||||
"status": qdro.status,
|
||||
"created_date": qdro.created_date.isoformat() if qdro.created_date else None,
|
||||
"updated_at": qdro.updated_at.isoformat() if qdro.updated_at else None,
|
||||
"file_no": qdro.file_no
|
||||
"created_date": qdro.created_date.isoformat() if getattr(qdro, "created_date", None) else None,
|
||||
"updated_at": qdro.updated_at.isoformat() if getattr(qdro, "updated_at", None) else None,
|
||||
"file_no": qdro.file_no,
|
||||
}
|
||||
for qdro in qdros
|
||||
]
|
||||
|
||||
return {
|
||||
"file_no": file_no,
|
||||
"documents": documents,
|
||||
"total_count": len(documents)
|
||||
}
|
||||
|
||||
payload = {"file_no": file_no, "documents": items, "total_count": (total if include_total else None)}
|
||||
# Maintain previous shape by omitting total_count when include_total is False? The prior code always returned total_count.
|
||||
# Keep total_count for backward compatibility but set to actual total when include_total else len(items)
|
||||
payload["total_count"] = (total if include_total else len(items))
|
||||
return payload
|
||||
|
||||
|
||||
def _extract_variables_from_content(content: str) -> Dict[str, str]:
|
||||
|
||||
Reference in New Issue
Block a user