Files
UAV-CO2/src/gasflux/blueprints/reports.py
2026-02-05 15:13:54 +08:00

192 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Reports Blueprint
Provides report listing and management endpoints.
"""
import time
from pathlib import Path
from flask import Blueprint, request, current_app
from ..shared import _get_file_type, _format_response, log_performance, logger, task_status
from ..app import Config
# Create blueprint
reports_bp = Blueprint('reports', __name__, url_prefix='/reports')
@reports_bp.route('', methods=['GET'])
@log_performance
def list_reports():
"""List all generated reports with pagination and filtering."""
logger.debug("Reports list requested")
try:
# Parse query parameters
try:
page = int(request.args.get('page', 1))
if page < 1:
return _format_response(400, "Invalid parameter: page must be >= 1")
except (ValueError, TypeError):
return _format_response(400, "Invalid parameter: page must be a valid integer")
try:
per_page = int(request.args.get('per_page', 20))
if per_page < 1 or per_page > 100:
return _format_response(400, "Invalid parameter: per_page must be between 1 and 100")
except (ValueError, TypeError):
return _format_response(400, "Invalid parameter: per_page must be a valid integer")
sort_by = request.args.get('sort_by', 'created_at')
sort_order = request.args.get('sort_order', 'desc')
status_filter = request.args.get('status', None) # 'completed', 'failed', or None for all
# Validate sort parameters
valid_sort_fields = ['created_at', 'task_id', 'file_size', 'processing_time']
if sort_by not in valid_sort_fields:
return _format_response(400, f"Invalid parameter: sort_by must be one of {valid_sort_fields}")
if sort_order not in ['asc', 'desc']:
return _format_response(400, "Invalid parameter: sort_order must be 'asc' or 'desc'")
# Validate status filter
valid_statuses = ['completed', 'failed', None]
if status_filter is not None and status_filter not in ['completed', 'failed']:
return _format_response(400, "Invalid parameter: status must be 'completed', 'failed', or not specified")
# 兼容缺省:优先 app.config,其次 Config.OUTPUT_FOLDER
output_root = current_app.config.get('OUTPUT_FOLDER') or getattr(Config, 'OUTPUT_FOLDER', None)
if not output_root:
return _format_response(200, "报告列表获取成功", {
'reports': [],
'pagination': {'page': page, 'per_page': per_page, 'total_reports': 0, 'total_pages': 0, 'has_next': False, 'has_prev': False},
'filters': {'sort_by': sort_by, 'sort_order': sort_order, 'status': status_filter}
})
output_folder = Path(output_root)
reports = []
# Scan all task directories
if output_folder.exists():
for task_dir in output_folder.iterdir():
if not task_dir.is_dir():
continue
task_id = task_dir.name
# Get task information from global task_status
task_info = task_status.get(task_id, {})
task_status_value = task_info.get('status')
# Log task status for debugging
logger.debug(f"Task {task_id}: status from memory={task_status_value}, info={task_info}")
# 直接扫描平铺文件
files = [p for p in task_dir.iterdir() if p.is_file()]
if not files:
# 按需应用状态过滤
if status_filter:
continue
reports.append({
'task_id': task_id,
'report_name': "N/A",
'status': 'failed',
'created_at': task_dir.stat().st_mtime,
'file_count': 0,
'total_size': 0,
'processing_time_seconds': None,
'main_report': None,
'all_files': [],
'run_directory': f'{task_id}'
})
continue
# 识别主报告与统计
total_size = sum(f.stat().st_size for f in files)
created_at = max(f.stat().st_mtime for f in files) if files else task_dir.stat().st_mtime
def file_entry(p):
return {
'name': p.name,
'size': p.stat().st_size,
'type': _get_file_type(p.name),
# 使用相对路径下载,清晰且安全
'download_url': f"/download/{task_id}/{p.name}"
}
all_files = [file_entry(f) for f in files]
# 优先 CO2_report,其次任意 *_report_*.html
report_html = None
for f in files:
if f.name.endswith('_report_') and f.suffix == '.html':
report_html = file_entry(f)
break
if not report_html:
for f in files:
if f.name.endswith('.html'):
report_html = file_entry(f)
break
# 任务状态:若有报告或关键产物则视为 completed
has_outputs = any(f.name.startswith(('config_', 'output_vars_', 'processed_data_', 'CO2_report_')) for f in files)
task_status_value = 'completed' if has_outputs else 'unknown'
if status_filter and task_status_value != status_filter:
continue
# Create report entry
report_entry = {
'task_id': task_id,
'report_name': task_id,
'status': task_status_value,
'created_at': created_at,
'file_count': len(files),
'total_size': total_size,
'processing_time_seconds': None,
'main_report': report_html,
'all_files': all_files,
'run_directory': f'{task_id}'
}
reports.append(report_entry)
# Sort reports
reverse_order = sort_order == 'desc'
if sort_by == 'created_at':
reports.sort(key=lambda x: x['created_at'], reverse=reverse_order)
elif sort_by == 'task_id':
reports.sort(key=lambda x: x['task_id'], reverse=reverse_order)
elif sort_by == 'file_size':
reports.sort(key=lambda x: x['total_size'], reverse=reverse_order)
elif sort_by == 'processing_time':
reports.sort(key=lambda x: x['processing_time_seconds'] or 0, reverse=reverse_order)
# Paginate results
total_reports = len(reports)
start_idx = (page - 1) * per_page
end_idx = start_idx + per_page
paginated_reports = reports[start_idx:end_idx]
# Calculate pagination metadata
total_pages = (total_reports + per_page - 1) // per_page
response_data = {
'reports': paginated_reports,
'pagination': {
'page': page,
'per_page': per_page,
'total_reports': total_reports,
'total_pages': total_pages,
'has_next': page < total_pages,
'has_prev': page > 1
},
'filters': {
'sort_by': sort_by,
'sort_order': sort_order,
'status': status_filter
}
}
logger.info(f"Returning {len(paginated_reports)} reports (page {page}/{total_pages})")
return _format_response(200, "报告列表获取成功", response_data)
except Exception as e:
logger.error(f"Error listing reports: {str(e)}", exc_info=True)
return _format_response(500, "内部服务器错误")