d4c82768a8
- helpers.py: _extract_lat, _extract_lon, plan index utilities - dashboard.py: dashboard, run_list, run_detail, run_status_api, start_analysis - pipeline.py: _run_pipeline_worker, _background_process - clustering.py: cluster_overview, cluster_detail, _run_clustering_pipeline, _get_globe_flows - entity.py: entity_profile - upload.py: upload_page, upload_csv, upload_csv_batch, finalize_upload, delete_upload - auto.py: auto_page, run_llm_analysis_view - manual.py: manual_page, manual_run_analysis - globe.py: globe_view, _extract_flows_from_df - config.py: config_view, llm_test - log_viewer.py: log_viewer - tools.py: tool_lab, tool_lab_run, tool_plan, apply_filter, reload_run_data, retry_run All function logic preserved exactly. views/__init__.py re-exports all public functions for backward compatibility with urls.py and run_pipeline.py. Original views.py archived as views_backup.py.
134 lines
5.3 KiB
Python
134 lines
5.3 KiB
Python
"""Upload views: CSV upload, batch, finalize, delete."""
|
|
import threading
|
|
import logging
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from django.shortcuts import render, get_object_or_404
|
|
from django.http import JsonResponse
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
from analysis.appdata import ensure_appdata_dir
|
|
from analysis.models import AnalysisRun
|
|
from .pipeline import _background_process
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def upload_page(request):
|
|
"""CSV upload page."""
|
|
runs = AnalysisRun.objects.all().order_by('-created_at')[:20]
|
|
return render(request, 'tianxuan/upload.html', {'runs': runs})
|
|
|
|
|
|
@csrf_exempt
|
|
def upload_csv(request):
|
|
"""Handle CSV upload AJAX POST."""
|
|
if request.method != 'POST':
|
|
return JsonResponse({'error': 'POST required'}, status=405)
|
|
files = request.FILES.getlist('files')
|
|
if not files:
|
|
return JsonResponse({'error': 'No files uploaded'})
|
|
logger.info('[UPLOAD] Received %d files', len(files))
|
|
for f in files:
|
|
if f.size > 500 * 1024 * 1024:
|
|
return JsonResponse({'warning': f'文件 {f.name} 超过 500MB,建议分割后上传'})
|
|
total_size = sum(f.size for f in files)
|
|
if total_size > 50 * 1024 * 1024 * 1024:
|
|
return JsonResponse({'error': '总大小超过 50GB 限制'})
|
|
from django.utils import timezone
|
|
ts = timezone.now().strftime('%Y%m%d_%H%M%S')
|
|
from analysis.appdata import ensure_appdata_dir
|
|
base = ensure_appdata_dir() / 'data' / 'uploads'
|
|
upload_dir = base / ts
|
|
upload_dir.mkdir(parents=True, exist_ok=True)
|
|
saved = []
|
|
for f in files:
|
|
path = upload_dir / f.name
|
|
with open(path, 'wb+') as dst:
|
|
for chunk in f.chunks():
|
|
dst.write(chunk)
|
|
saved.append(str(path))
|
|
# Create run with status='pending' — background processing deferred to finalize endpoint
|
|
run = AnalysisRun.objects.create(
|
|
csv_glob=str(upload_dir / '*.csv'),
|
|
status='pending',
|
|
run_type='upload',
|
|
)
|
|
# Deferred: _background_process will be started by finalize_upload endpoint
|
|
return JsonResponse({'run_id': run.display_id, 'files': saved, 'status': 'pending'})
|
|
|
|
|
|
@csrf_exempt
|
|
def upload_csv_batch(request, display_id):
|
|
"""Append files to existing upload run. Does NOT re-trigger _background_process."""
|
|
run = get_object_or_404(AnalysisRun, display_id=display_id)
|
|
files = request.FILES.getlist('files')
|
|
if not files:
|
|
return JsonResponse({'error': 'No files uploaded'})
|
|
upload_dir = Path(run.csv_glob).parent if run.csv_glob else None
|
|
if upload_dir:
|
|
expected_base = ensure_appdata_dir() / 'data' / 'uploads'
|
|
if not str(upload_dir.resolve()).startswith(str(expected_base.resolve())):
|
|
return JsonResponse({'error': 'Invalid upload path'}, status=400)
|
|
if not upload_dir or not upload_dir.exists():
|
|
return JsonResponse({'error': 'Upload directory not found'})
|
|
saved = []
|
|
for f in files:
|
|
path = upload_dir / f.name
|
|
with open(path, 'wb+') as dst:
|
|
for chunk in f.chunks():
|
|
dst.write(chunk)
|
|
saved.append(str(path))
|
|
# NO background process re-trigger — first batch handles it
|
|
return JsonResponse({'run_id': run.display_id, 'files': len(saved), 'status': 'ok'})
|
|
|
|
|
|
@csrf_exempt
|
|
def finalize_upload(request, display_id):
|
|
"""Finalize batch upload: validate upload_dir, set status='loading', start background processing."""
|
|
if request.method != 'POST':
|
|
return JsonResponse({'error': 'POST required'}, status=405)
|
|
run = get_object_or_404(AnalysisRun, display_id=display_id)
|
|
if run.status != 'pending':
|
|
return JsonResponse({'error': f'Run status is {run.status}, expected pending'}, status=400)
|
|
upload_dir = Path(run.csv_glob).parent if run.csv_glob else None
|
|
if not upload_dir or not upload_dir.exists():
|
|
return JsonResponse({'error': 'Upload directory not found'}, status=400)
|
|
run.status = 'loading'
|
|
run.save(update_fields=['status'])
|
|
t = threading.Thread(target=_background_process, args=(run.id, upload_dir), daemon=True)
|
|
t.start()
|
|
return JsonResponse({'run_id': run.display_id, 'status': 'loading'})
|
|
|
|
|
|
@csrf_exempt
|
|
def delete_upload(request, display_id):
|
|
"""Delete uploaded files, SQLite data table, and AnalysisRun.
|
|
|
|
Handles all run statuses (loading, profiling, clustering, completed, failed).
|
|
Gracefully tolerates null csv_glob and missing/errored SQLite tables.
|
|
"""
|
|
run = get_object_or_404(AnalysisRun, display_id=display_id)
|
|
|
|
# 1. Drop the SQLite data table if one exists
|
|
if run.sqlite_table:
|
|
try:
|
|
from analysis.data_loader import drop_sqlite_table # fmt: skip
|
|
drop_sqlite_table(run.sqlite_table)
|
|
except Exception as exc:
|
|
logger.warning('[DELETE] failed to drop SQLite table %s: %s', run.sqlite_table, exc)
|
|
|
|
# 2. Remove the upload directory if csv_glob is set
|
|
if run.csv_glob:
|
|
p = Path(run.csv_glob).parent
|
|
expected_base = ensure_appdata_dir() / 'data' / 'uploads'
|
|
if not str(p.resolve()).startswith(str(expected_base.resolve())):
|
|
return JsonResponse({'error': 'Invalid upload path'}, status=400)
|
|
if p.exists():
|
|
shutil.rmtree(p, ignore_errors=True)
|
|
|
|
# 3. Delete the ORM record
|
|
run.delete()
|
|
return JsonResponse({'deleted': True})
|