fixes and refactor
This commit is contained in:
@@ -1,25 +1,29 @@
|
||||
"""
|
||||
File Management API endpoints
|
||||
"""
|
||||
from typing import List, Optional, Dict, Any
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import or_, func, and_, desc
|
||||
from datetime import date, datetime
|
||||
|
||||
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.files import File
|
||||
from app.models.rolodex import Rolodex
|
||||
from app.models.ledger import Ledger
|
||||
from app.models.lookups import Employee, FileType, FileStatus
|
||||
from app.models.user import User
|
||||
from app.auth.security import get_current_user
|
||||
from app.services.cache import invalidate_search_cache
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Pydantic schemas
|
||||
from pydantic import BaseModel
|
||||
from pydantic.config import ConfigDict
|
||||
|
||||
|
||||
class FileBase(BaseModel):
|
||||
@@ -67,17 +71,24 @@ class FileResponse(FileBase):
|
||||
amount_owing: float = 0.0
|
||||
transferable: float = 0.0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[FileResponse])
|
||||
class PaginatedFilesResponse(BaseModel):
|
||||
items: List[FileResponse]
|
||||
total: int
|
||||
|
||||
|
||||
@router.get("/", response_model=Union[List[FileResponse], PaginatedFilesResponse])
|
||||
async def list_files(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
search: Optional[str] = Query(None),
|
||||
status_filter: Optional[str] = Query(None),
|
||||
employee_filter: Optional[str] = Query(None),
|
||||
sort_by: Optional[str] = Query(None, description="Sort by: file_no, client, opened, closed, status, amount_owing, total_charges"),
|
||||
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)
|
||||
):
|
||||
@@ -85,14 +96,17 @@ async def list_files(
|
||||
query = db.query(File)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
File.file_no.contains(search),
|
||||
File.id.contains(search),
|
||||
File.regarding.contains(search),
|
||||
File.file_type.contains(search)
|
||||
)
|
||||
)
|
||||
# DRY: tokenize and apply case-insensitive search consistently with search endpoints
|
||||
tokens = build_query_tokens(search)
|
||||
filter_expr = tokenized_ilike_filter(tokens, [
|
||||
File.file_no,
|
||||
File.id,
|
||||
File.regarding,
|
||||
File.file_type,
|
||||
File.memo,
|
||||
])
|
||||
if filter_expr is not None:
|
||||
query = query.filter(filter_expr)
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(File.status == status_filter)
|
||||
@@ -100,7 +114,25 @@ async def list_files(
|
||||
if employee_filter:
|
||||
query = query.filter(File.empl_num == employee_filter)
|
||||
|
||||
files = query.offset(skip).limit(limit).all()
|
||||
# Sorting (whitelisted)
|
||||
query = apply_sorting(
|
||||
query,
|
||||
sort_by,
|
||||
sort_dir,
|
||||
allowed={
|
||||
"file_no": [File.file_no],
|
||||
"client": [File.id],
|
||||
"opened": [File.opened],
|
||||
"closed": [File.closed],
|
||||
"status": [File.status],
|
||||
"amount_owing": [File.amount_owing],
|
||||
"total_charges": [File.total_charges],
|
||||
},
|
||||
)
|
||||
|
||||
files, total = paginate_with_total(query, skip, limit, include_total)
|
||||
if include_total:
|
||||
return {"items": files, "total": total or 0}
|
||||
return files
|
||||
|
||||
|
||||
@@ -142,6 +174,10 @@ async def create_file(
|
||||
db.commit()
|
||||
db.refresh(file_obj)
|
||||
|
||||
try:
|
||||
await invalidate_search_cache()
|
||||
except Exception:
|
||||
pass
|
||||
return file_obj
|
||||
|
||||
|
||||
@@ -167,7 +203,10 @@ async def update_file(
|
||||
|
||||
db.commit()
|
||||
db.refresh(file_obj)
|
||||
|
||||
try:
|
||||
await invalidate_search_cache()
|
||||
except Exception:
|
||||
pass
|
||||
return file_obj
|
||||
|
||||
|
||||
@@ -188,7 +227,10 @@ async def delete_file(
|
||||
|
||||
db.delete(file_obj)
|
||||
db.commit()
|
||||
|
||||
try:
|
||||
await invalidate_search_cache()
|
||||
except Exception:
|
||||
pass
|
||||
return {"message": "File deleted successfully"}
|
||||
|
||||
|
||||
@@ -433,11 +475,13 @@ async def advanced_file_search(
|
||||
query = query.filter(File.file_no.contains(file_no))
|
||||
|
||||
if client_name:
|
||||
# SQLite-safe concatenation for first + last name
|
||||
full_name_expr = (func.coalesce(Rolodex.first, '') + ' ' + func.coalesce(Rolodex.last, ''))
|
||||
query = query.join(Rolodex).filter(
|
||||
or_(
|
||||
Rolodex.first.contains(client_name),
|
||||
Rolodex.last.contains(client_name),
|
||||
func.concat(Rolodex.first, ' ', Rolodex.last).contains(client_name)
|
||||
full_name_expr.contains(client_name)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user