feat(api): add advanced filtering and saved searches
- Add date range (date_from/date_to), storage provider, and tags filters to GET /api/files - Add SavedSearch model and migration (005_add_saved_searches) - Add CRUD API endpoints for saved searches at /api/saved-searches - Update files.html template with new filter controls and saved searches UI - Update files view to pass new filter parameters to template - Add comprehensive tests for all new functionality (26 tests) Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+50
-6
@@ -2,9 +2,10 @@
|
||||
File management views for displaying and managing files.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, Query, Request
|
||||
from fastapi import Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.utils.file_queries import apply_status_filter
|
||||
@@ -29,6 +30,10 @@ def files_page(
|
||||
search: Optional[str] = Query(None),
|
||||
mime_type: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
date_from: Optional[str] = Query(None),
|
||||
date_to: Optional[str] = Query(None),
|
||||
storage_provider: Optional[str] = Query(None),
|
||||
tags: Optional[str] = Query(None),
|
||||
):
|
||||
"""
|
||||
Return the 'files.html' template with server-side pagination, sorting, and filtering
|
||||
@@ -39,7 +44,7 @@ def files_page(
|
||||
# Import the model here to avoid circular imports
|
||||
from sqlalchemy import asc, desc
|
||||
|
||||
from app.models import FileRecord
|
||||
from app.models import FileProcessingStep, FileRecord
|
||||
|
||||
# Start with base query
|
||||
query = db.query(FileRecord)
|
||||
@@ -52,6 +57,41 @@ def files_page(
|
||||
if mime_type:
|
||||
query = query.filter(FileRecord.mime_type == mime_type)
|
||||
|
||||
# Apply date range filters
|
||||
if date_from:
|
||||
try:
|
||||
dt_from = datetime.fromisoformat(date_from).replace(tzinfo=timezone.utc)
|
||||
query = query.filter(FileRecord.created_at >= dt_from)
|
||||
except ValueError:
|
||||
pass # Silently ignore invalid dates in view
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
dt_to = datetime.fromisoformat(date_to).replace(tzinfo=timezone.utc)
|
||||
query = query.filter(FileRecord.created_at <= dt_to)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Apply storage provider filter
|
||||
if storage_provider:
|
||||
step_name = f"upload_to_{storage_provider}"
|
||||
uploaded_file_ids = (
|
||||
db.query(FileProcessingStep.file_id)
|
||||
.filter(
|
||||
FileProcessingStep.step_name == step_name,
|
||||
FileProcessingStep.status == "success",
|
||||
)
|
||||
.distinct()
|
||||
.subquery()
|
||||
)
|
||||
query = query.filter(FileRecord.id.in_(db.query(uploaded_file_ids.c.file_id)))
|
||||
|
||||
# Apply tags filter (AND logic)
|
||||
if tags:
|
||||
tag_list = [t.strip().lower() for t in tags.split(",") if t.strip()]
|
||||
for tag in tag_list:
|
||||
query = query.filter(FileRecord.ai_metadata.ilike(f"%{tag}%"))
|
||||
|
||||
# Apply status filter (before pagination for correct counts)
|
||||
query = apply_status_filter(query, db, status)
|
||||
|
||||
@@ -112,6 +152,10 @@ def files_page(
|
||||
"search": search or "",
|
||||
"mime_type": mime_type or "",
|
||||
"status": status or "",
|
||||
"date_from": date_from or "",
|
||||
"date_to": date_to or "",
|
||||
"storage_provider": storage_provider or "",
|
||||
"tags": tags or "",
|
||||
"mime_types": mime_types,
|
||||
"upload_concurrency": settings.upload_concurrency,
|
||||
"upload_queue_delay_ms": settings.upload_queue_delay_ms,
|
||||
@@ -524,7 +568,7 @@ def preview_original_file(request: Request, file_id: int, db: Session = Depends(
|
||||
"""
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi import status
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from app.models import FileRecord
|
||||
@@ -551,7 +595,7 @@ def preview_processed_file(request: Request, file_id: int, db: Session = Depends
|
||||
"""
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi import status
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from app.models import FileRecord
|
||||
@@ -578,7 +622,7 @@ def get_original_text(request: Request, file_id: int, db: Session = Depends(get_
|
||||
"""
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi import status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.models import FileRecord
|
||||
@@ -618,7 +662,7 @@ def get_processed_text(request: Request, file_id: int, db: Session = Depends(get
|
||||
"""
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi import status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.models import FileRecord
|
||||
|
||||
Reference in New Issue
Block a user