feat: enhance processing status tracking and UI display for file details
This commit is contained in:
+70
-29
@@ -2,7 +2,7 @@
|
||||
Shared file query utilities for filtering files by processing status.
|
||||
|
||||
This module contains reusable query logic for filtering FileRecord objects
|
||||
based on their processing status (pending, processing, failed, completed).
|
||||
based on their processing status using the FileProcessingStep table.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
@@ -10,24 +10,33 @@ from typing import Optional
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Query, Session
|
||||
|
||||
from app.models import FileRecord, ProcessingLog
|
||||
from app.models import FileRecord, FileProcessingStep
|
||||
|
||||
|
||||
def apply_status_filter(query: Query, db: Session, status: Optional[str]) -> Query:
|
||||
"""
|
||||
Apply status filter to a FileRecord query.
|
||||
Apply status filter to a FileRecord query using FileProcessingStep table.
|
||||
|
||||
This function modifies a SQLAlchemy query to filter files based on their
|
||||
processing status by examining associated ProcessingLog entries.
|
||||
processing status by examining associated FileProcessingStep entries.
|
||||
|
||||
Only tracks "real" processing steps that represent user-facing status:
|
||||
- Main steps: create_file_record, check_text, extract_text, process_with_azure_document_intelligence,
|
||||
extract_metadata_with_gpt, embed_metadata_into_pdf, finalize_document_storage,
|
||||
send_to_all_destinations
|
||||
- Upload steps: queue_*, upload_to_*
|
||||
|
||||
Diagnostic/internal steps (poll_task, upload_file, set_custom_fields, etc.) are ignored
|
||||
as they may not complete properly and don't affect the actual status.
|
||||
|
||||
Args:
|
||||
query: The base SQLAlchemy query for FileRecord objects
|
||||
db: Database session for creating subqueries
|
||||
status: Status filter to apply. Valid values:
|
||||
- "pending": Files with no ProcessingLog entries
|
||||
- "processing": Files with in_progress logs
|
||||
- "failed": Files with failure logs
|
||||
- "completed": Files with success logs but no failures or in_progress
|
||||
- "pending": Files with no real FileProcessingStep entries
|
||||
- "processing": Files with in_progress real steps
|
||||
- "failed": Files with failure real steps
|
||||
- "completed": Files with all real steps success/skipped
|
||||
- None: No filter applied (returns query unchanged)
|
||||
|
||||
Returns:
|
||||
@@ -41,32 +50,64 @@ def apply_status_filter(query: Query, db: Session, status: Optional[str]) -> Que
|
||||
if not status:
|
||||
return query
|
||||
|
||||
# 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()
|
||||
# Define which steps are "real" status-determining steps
|
||||
# Only high-level logical steps and actual upload destinations (not queue_* steps)
|
||||
REAL_STEPS = {
|
||||
"create_file_record",
|
||||
"check_text",
|
||||
"extract_text",
|
||||
"process_with_azure_document_intelligence",
|
||||
"extract_metadata_with_gpt",
|
||||
"embed_metadata_into_pdf",
|
||||
"finalize_document_storage",
|
||||
"send_to_all_destinations",
|
||||
"upload_to_dropbox",
|
||||
"upload_to_paperless",
|
||||
"upload_to_google_drive",
|
||||
"upload_to_ftp",
|
||||
"upload_to_onedrive",
|
||||
"upload_to_webdav",
|
||||
"upload_to_sftp",
|
||||
"upload_to_nextcloud",
|
||||
"upload_to_paperless_ngx",
|
||||
"upload_to_email",
|
||||
"upload_to_s3",
|
||||
}
|
||||
|
||||
failed_files = (
|
||||
db.query(ProcessingLog.file_id)
|
||||
.filter(or_(ProcessingLog.status == "failure", ProcessingLog.status == "in_progress"))
|
||||
# Filter to only real steps
|
||||
real_steps_subq = (
|
||||
db.query(FileProcessingStep)
|
||||
.filter(FileProcessingStep.step_name.in_(REAL_STEPS))
|
||||
)
|
||||
|
||||
if status == "pending":
|
||||
# Files with no real steps (never started processing)
|
||||
subq = real_steps_subq.distinct().subquery()
|
||||
query = query.filter(~FileRecord.id.in_(db.query(subq.c.file_id)))
|
||||
elif status == "processing":
|
||||
# Files with in_progress real steps
|
||||
subq = real_steps_subq.filter(FileProcessingStep.status == "in_progress").distinct().subquery()
|
||||
query = query.filter(FileRecord.id.in_(db.query(subq.c.file_id)))
|
||||
elif status == "failed":
|
||||
# Files with failure real steps
|
||||
subq = real_steps_subq.filter(FileProcessingStep.status == "failure").distinct().subquery()
|
||||
query = query.filter(FileRecord.id.in_(db.query(subq.c.file_id)))
|
||||
elif status == "completed":
|
||||
# Files where all real steps are either success or skipped (no failures or in_progress)
|
||||
# Get files that have real steps
|
||||
files_with_real_steps = real_steps_subq.distinct().subquery()
|
||||
|
||||
# Get files with failures or in_progress on real steps
|
||||
files_with_issues = (
|
||||
real_steps_subq
|
||||
.filter(or_(FileProcessingStep.status == "failure", FileProcessingStep.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))
|
||||
# Select files with real steps that don't have issues
|
||||
query = query.filter(FileRecord.id.in_(db.query(files_with_real_steps.c.file_id))).filter(
|
||||
~FileRecord.id.in_(db.query(files_with_issues.c.file_id))
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
@@ -46,6 +46,14 @@ def get_files_processing_status(db: Session, file_ids: List[int]) -> Dict[int, D
|
||||
"""
|
||||
Get processing status for multiple files efficiently.
|
||||
|
||||
Only counts "real" processing steps that represent user-facing status:
|
||||
- Main steps: create_file_record, check_text, extract_text, process_with_azure_document_intelligence,
|
||||
extract_metadata_with_gpt, embed_metadata_into_pdf, finalize_document_storage,
|
||||
send_to_all_destinations
|
||||
- Upload steps: upload_to_*
|
||||
|
||||
Diagnostic/internal steps (poll_task, upload_file, set_custom_fields, etc.) are ignored.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
file_ids: List of file IDs
|
||||
@@ -53,8 +61,35 @@ def get_files_processing_status(db: Session, file_ids: List[int]) -> Dict[int, D
|
||||
Returns:
|
||||
dict mapping file_id to status dict
|
||||
"""
|
||||
# Get all steps for these files in one query
|
||||
steps = db.query(FileProcessingStep).filter(FileProcessingStep.file_id.in_(file_ids)).all()
|
||||
# Define which steps are "real" status-determining steps
|
||||
REAL_STEPS = {
|
||||
"create_file_record",
|
||||
"check_text",
|
||||
"extract_text",
|
||||
"process_with_azure_document_intelligence",
|
||||
"extract_metadata_with_gpt",
|
||||
"embed_metadata_into_pdf",
|
||||
"finalize_document_storage",
|
||||
"send_to_all_destinations",
|
||||
"upload_to_dropbox",
|
||||
"upload_to_paperless",
|
||||
"upload_to_google_drive",
|
||||
"upload_to_ftp",
|
||||
"upload_to_onedrive",
|
||||
"upload_to_webdav",
|
||||
"upload_to_sftp",
|
||||
"upload_to_nextcloud",
|
||||
"upload_to_paperless_ngx",
|
||||
"upload_to_email",
|
||||
"upload_to_s3",
|
||||
}
|
||||
|
||||
# Get all REAL steps for these files in one query
|
||||
steps = (
|
||||
db.query(FileProcessingStep)
|
||||
.filter(FileProcessingStep.file_id.in_(file_ids), FileProcessingStep.step_name.in_(REAL_STEPS))
|
||||
.all()
|
||||
)
|
||||
|
||||
# Group steps by file_id
|
||||
steps_by_file = {}
|
||||
|
||||
+38
-1
@@ -1,9 +1,10 @@
|
||||
import logging
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
from app.database import SessionLocal
|
||||
from app.models import ProcessingLog
|
||||
from app.models import FileProcessingStep, ProcessingLog
|
||||
|
||||
|
||||
class TaskLogCollector(logging.Handler):
|
||||
@@ -66,6 +67,8 @@ def log_task_progress(task_id, step_name, status, message=None, file_id=None, de
|
||||
If no explicit detail is provided, automatically drains any buffered
|
||||
worker log output for this task ID and stores it as the detail.
|
||||
|
||||
Also updates the FileProcessingStep table for definitive status tracking.
|
||||
|
||||
Args:
|
||||
task_id: The Celery task ID
|
||||
step_name: Name of the processing step
|
||||
@@ -83,6 +86,7 @@ def log_task_progress(task_id, step_name, status, message=None, file_id=None, de
|
||||
detail = collected
|
||||
|
||||
with SessionLocal() as db:
|
||||
# Log to ProcessingLog (for historical viewing)
|
||||
log_entry = ProcessingLog(
|
||||
task_id=task_id,
|
||||
step_name=step_name,
|
||||
@@ -92,4 +96,37 @@ def log_task_progress(task_id, step_name, status, message=None, file_id=None, de
|
||||
detail=detail,
|
||||
)
|
||||
db.add(log_entry)
|
||||
|
||||
# Update FileProcessingStep table (for status tracking) if file_id is provided
|
||||
if file_id and step_name:
|
||||
# Find or create the step record
|
||||
step_record = (
|
||||
db.query(FileProcessingStep)
|
||||
.filter(FileProcessingStep.file_id == file_id, FileProcessingStep.step_name == step_name)
|
||||
.first()
|
||||
)
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
if not step_record:
|
||||
# Create new step record
|
||||
step_record = FileProcessingStep(
|
||||
file_id=file_id,
|
||||
step_name=step_name,
|
||||
status=status,
|
||||
started_at=now if status == "in_progress" else None,
|
||||
completed_at=now if status in ("success", "failure", "skipped") else None,
|
||||
error_message=message if status == "failure" else None,
|
||||
)
|
||||
db.add(step_record)
|
||||
else:
|
||||
# Update existing step record
|
||||
step_record.status = status
|
||||
if status == "in_progress" and not step_record.started_at:
|
||||
step_record.started_at = now
|
||||
if status in ("success", "failure", "skipped"):
|
||||
step_record.completed_at = now
|
||||
if status == "failure":
|
||||
step_record.error_message = message or detail
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -156,6 +156,9 @@ def get_file_step_status(db: Session, file_id: int) -> Dict[str, Dict]:
|
||||
def get_file_overall_status(db: Session, file_id: int) -> Dict:
|
||||
"""
|
||||
Get the overall processing status for a file based on its steps.
|
||||
|
||||
Only considers "real" processing steps that represent user-facing status.
|
||||
Ignores diagnostic/internal steps like poll_task, upload_file, etc.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
@@ -172,7 +175,28 @@ def get_file_overall_status(db: Session, file_id: int) -> Dict:
|
||||
"in_progress_steps": 2
|
||||
}
|
||||
"""
|
||||
steps = db.query(FileProcessingStep).filter(FileProcessingStep.file_id == file_id).all()
|
||||
# Define which steps are "real" status-determining steps
|
||||
# Only high-level logical steps, not implementation sub-steps
|
||||
REAL_MAIN_STEPS = {
|
||||
"create_file_record",
|
||||
"check_text",
|
||||
"extract_text",
|
||||
"process_with_azure_document_intelligence",
|
||||
"extract_metadata_with_gpt",
|
||||
"embed_metadata_into_pdf",
|
||||
"finalize_document_storage",
|
||||
"send_to_all_destinations",
|
||||
}
|
||||
|
||||
all_steps = db.query(FileProcessingStep).filter(FileProcessingStep.file_id == file_id).all()
|
||||
|
||||
# Filter to only real steps
|
||||
steps = [
|
||||
s for s in all_steps
|
||||
if s.step_name in REAL_MAIN_STEPS
|
||||
or s.step_name.startswith("queue_")
|
||||
or s.step_name.startswith("upload_to_")
|
||||
]
|
||||
|
||||
if not steps:
|
||||
return {
|
||||
@@ -217,6 +241,9 @@ def get_step_summary(db: Session, file_id: int) -> Dict:
|
||||
"""
|
||||
Get a summary of main steps vs upload steps with status counts.
|
||||
|
||||
Only counts "real" processing steps that represent user-facing status.
|
||||
Ignores diagnostic/internal steps like poll_task, upload_file, etc.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
file_id: ID of the file
|
||||
@@ -230,13 +257,24 @@ def get_step_summary(db: Session, file_id: int) -> Dict:
|
||||
"total_upload_tasks": 6
|
||||
}
|
||||
"""
|
||||
# Define which steps are "real" status-determining steps
|
||||
# Only high-level logical steps, not implementation sub-steps
|
||||
REAL_MAIN_STEPS = {
|
||||
"create_file_record",
|
||||
"check_text",
|
||||
"extract_text",
|
||||
"process_with_azure_document_intelligence",
|
||||
"extract_metadata_with_gpt",
|
||||
"embed_metadata_into_pdf",
|
||||
"finalize_document_storage",
|
||||
"send_to_all_destinations",
|
||||
}
|
||||
|
||||
steps = db.query(FileProcessingStep).filter(FileProcessingStep.file_id == file_id).all()
|
||||
|
||||
main_counts = {"queued": 0, "in_progress": 0, "success": 0, "failure": 0, "skipped": 0}
|
||||
upload_counts = {"queued": 0, "in_progress": 0, "success": 0, "failure": 0, "skipped": 0}
|
||||
|
||||
upload_prefixes = ["upload_to_", "queue_"]
|
||||
|
||||
main_steps_count = 0
|
||||
upload_steps_count = 0
|
||||
|
||||
@@ -246,14 +284,21 @@ def get_step_summary(db: Session, file_id: int) -> Dict:
|
||||
if status == "pending":
|
||||
status = "queued"
|
||||
|
||||
# Check if it's an upload task
|
||||
is_upload = any(step.step_name.startswith(prefix) for prefix in upload_prefixes)
|
||||
# Check if it's an upload task (only count actual upload_to_* steps, not queue_* steps)
|
||||
is_upload = step.step_name.startswith("upload_to_")
|
||||
|
||||
# Only count "real" steps
|
||||
is_real_step = step.step_name in REAL_MAIN_STEPS or is_upload or step.step_name.startswith("queue_")
|
||||
|
||||
if not is_real_step:
|
||||
# Skip diagnostic/internal steps like poll_task, upload_file, set_custom_fields, etc.
|
||||
continue
|
||||
|
||||
if is_upload:
|
||||
if status in upload_counts:
|
||||
upload_counts[status] += 1
|
||||
upload_steps_count += 1
|
||||
elif step.step_name in MAIN_PROCESSING_STEPS:
|
||||
elif step.step_name in REAL_MAIN_STEPS:
|
||||
if status in main_counts:
|
||||
main_counts[status] += 1
|
||||
main_steps_count += 1
|
||||
@@ -264,3 +309,4 @@ def get_step_summary(db: Session, file_id: int) -> Dict:
|
||||
"total_main_steps": main_steps_count,
|
||||
"total_upload_tasks": upload_steps_count,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user