192 lines
7.9 KiB
Python
192 lines
7.9 KiB
Python
"""
|
||
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, "内部服务器错误") |