From 08775459aa8ee9619566f56c4f801aaf6c893cea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:52:38 +0000 Subject: [PATCH] Fix /files view issues and add bulk operations - Fixed status filter in /files view endpoint - Added bulk delete and reprocess API endpoints - Added bulk selection UI with checkboxes - Added bulk actions bar with reprocess and delete buttons - Updated JavaScript to handle bulk operations Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/api/files.py | 121 ++++++++++++++++++++++++++++ app/views/files.py | 37 ++++++++- frontend/templates/files.html | 146 +++++++++++++++++++++++++++++++++- 3 files changed, 302 insertions(+), 2 deletions(-) diff --git a/app/api/files.py b/app/api/files.py index 865250a0..c2df2a41 100644 --- a/app/api/files.py +++ b/app/api/files.py @@ -275,6 +275,127 @@ def delete_file_record(request: Request, file_id: int, db: Session = Depends(get detail=f"Error deleting file record: {str(e)}" ) +@router.post("/files/bulk-delete") +@require_login +def bulk_delete_files(request: Request, file_ids: List[int], db: Session = Depends(get_db)): + """ + Delete multiple file records from the database. + This only removes the database entries, not the actual files. + """ + # Check if file deletion is allowed + if not settings.allow_file_delete: + raise HTTPException( + status_code=403, + detail="File deletion is disabled in the configuration" + ) + + try: + # Find all file records + file_records = db.query(FileRecord).filter(FileRecord.id.in_(file_ids)).all() + + if not file_records: + raise HTTPException( + status_code=404, + detail="No files found with the provided IDs" + ) + + deleted_count = len(file_records) + deleted_ids = [f.id for f in file_records] + + # Log the deletion + logger.info(f"Bulk deleting {deleted_count} file records: IDs={deleted_ids}") + + # Delete all records + for file_record in file_records: + db.delete(file_record) + + db.commit() + + return { + "status": "success", + "message": f"Successfully deleted {deleted_count} file records", + "deleted_ids": deleted_ids + } + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.exception(f"Error bulk deleting file records: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error bulk deleting file records: {str(e)}" + ) + + +@router.post("/files/bulk-reprocess") +@require_login +def bulk_reprocess_files(request: Request, file_ids: List[int], db: Session = Depends(get_db)): + """ + Reprocess multiple files by queuing them for processing. + """ + try: + # Find all file records + file_records = db.query(FileRecord).filter(FileRecord.id.in_(file_ids)).all() + + if not file_records: + raise HTTPException( + status_code=404, + detail="No files found with the provided IDs" + ) + + task_ids = [] + processed_files = [] + errors = [] + + for file_record in file_records: + try: + # Check if local file exists + if not file_record.local_filename or not os.path.exists(file_record.local_filename): + errors.append({ + "file_id": file_record.id, + "filename": file_record.original_filename, + "error": "Local file not found" + }) + continue + + # Queue the file for processing + task = process_document.delay(file_record.local_filename) + task_ids.append(task.id) + processed_files.append({ + "file_id": file_record.id, + "filename": file_record.original_filename, + "task_id": task.id + }) + + logger.info(f"Reprocessing file: ID={file_record.id}, Filename={file_record.original_filename}, TaskID={task.id}") + + except Exception as e: + logger.exception(f"Error reprocessing file {file_record.id}: {str(e)}") + errors.append({ + "file_id": file_record.id, + "filename": file_record.original_filename, + "error": str(e) + }) + + return { + "status": "success" if processed_files else "error", + "message": f"Successfully queued {len(processed_files)} files for reprocessing", + "processed_files": processed_files, + "errors": errors if errors else None, + "task_ids": task_ids + } + + except HTTPException: + raise + except Exception as e: + logger.exception(f"Error bulk reprocessing files: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error bulk reprocessing files: {str(e)}" + ) + + @router.post("/ui-upload") @require_login async def ui_upload(request: Request, file: UploadFile = File(...)): diff --git a/app/views/files.py b/app/views/files.py index 8da5c545..ea793f33 100644 --- a/app/views/files.py +++ b/app/views/files.py @@ -29,7 +29,7 @@ def files_page( try: # Import the model here to avoid circular imports from app.models import FileRecord, ProcessingLog - from sqlalchemy import desc, asc + from sqlalchemy import desc, asc, or_ # Start with base query query = db.query(FileRecord) @@ -42,6 +42,41 @@ def files_page( if mime_type: query = query.filter(FileRecord.mime_type == mime_type) + # Apply status filter (before pagination for correct counts) + if status: + # Subquery to get file IDs matching the status + if status == "pending": + # Files with no logs + subq = db.query(ProcessingLog.file_id).distinct() + query = query.filter(~FileRecord.id.in_(subq)) + elif status == "processing": + # Files with in_progress logs + subq = db.query(ProcessingLog.file_id).filter( + ProcessingLog.status == "in_progress" + ).distinct() + query = query.filter(FileRecord.id.in_(subq)) + elif status == "failed": + # Files with failure logs + subq = db.query(ProcessingLog.file_id).filter( + ProcessingLog.status == "failure" + ).distinct() + query = query.filter(FileRecord.id.in_(subq)) + elif status == "completed": + # Files with success logs but no failures or in_progress + success_files = db.query(ProcessingLog.file_id).filter( + ProcessingLog.status == "success" + ).distinct().subquery() + + failed_files = db.query(ProcessingLog.file_id).filter( + or_(ProcessingLog.status == "failure", ProcessingLog.status == "in_progress") + ).distinct().subquery() + + query = query.filter( + FileRecord.id.in_(db.query(success_files.c.file_id)) + ).filter( + ~FileRecord.id.in_(db.query(failed_files.c.file_id)) + ) + # Get total count before pagination total_items = query.count() diff --git a/frontend/templates/files.html b/frontend/templates/files.html index 5fcfcc22..09c3795f 100644 --- a/frontend/templates/files.html +++ b/frontend/templates/files.html @@ -297,11 +297,34 @@ + +
+| + + |
ID
@@ -359,6 +382,9 @@
{% for file in files %}
|
+
+ |
{{ file.id }} |
{{ file.original_filename }} |
{{ (file.file_size / 1024) | round(2) }} KB |
@@ -382,7 +408,7 @@
No files found |
+ No files found |
| |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|