|
|
@@ -1,11 +1,15 @@
|
|
|
from typing import Any, List
|
|
|
-from fastapi import APIRouter, Depends, HTTPException
|
|
|
+from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
|
|
+from fastapi.responses import Response
|
|
|
from sqlalchemy.orm import Session
|
|
|
from backend.app.api import deps
|
|
|
from backend.app.core.database import get_db
|
|
|
from backend.app.models import sql_models
|
|
|
from backend.app.schemas import schemas
|
|
|
from backend.app.services.scheduler import update_job, run_analysis_pipeline
|
|
|
+import openpyxl
|
|
|
+import io
|
|
|
+import json
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@@ -18,6 +22,180 @@ def read_tasks(
|
|
|
) -> Any:
|
|
|
return db.query(sql_models.Task).offset(skip).limit(limit).all()
|
|
|
|
|
|
+@router.get("/export_template")
|
|
|
+def export_template(
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ current_user: sql_models.User = Depends(deps.get_current_user)
|
|
|
+) -> Any:
|
|
|
+ """
|
|
|
+ Export task configuration template (including current data)
|
|
|
+ """
|
|
|
+ wb = openpyxl.Workbook()
|
|
|
+ ws = wb.active
|
|
|
+ ws.title = "Tasks"
|
|
|
+
|
|
|
+ # Headers
|
|
|
+ headers = ["Task Name", "Model Config Name", "Camera Names", "Cron Expression", "Rules (JSON)"]
|
|
|
+ ws.append(headers)
|
|
|
+
|
|
|
+ # Data
|
|
|
+ tasks = db.query(sql_models.Task).all()
|
|
|
+
|
|
|
+ # Pre-fetch cameras for name lookup (optimization)
|
|
|
+ all_cameras = {c.id: c.name for c in db.query(sql_models.Camera).all()}
|
|
|
+
|
|
|
+ for task in tasks:
|
|
|
+ # Get Model Name
|
|
|
+ model_name = task.model_config.name if task.model_config else ""
|
|
|
+
|
|
|
+ # Get Camera Names
|
|
|
+ cam_names = []
|
|
|
+ if task.camera_ids:
|
|
|
+ for cid in task.camera_ids:
|
|
|
+ if cid in all_cameras:
|
|
|
+ cam_names.append(all_cameras[cid])
|
|
|
+ camera_names_str = ",".join(cam_names)
|
|
|
+
|
|
|
+ # Rules to JSON
|
|
|
+ rules_str = json.dumps(task.rules, ensure_ascii=False) if task.rules else "[]"
|
|
|
+
|
|
|
+ ws.append([task.name, model_name, camera_names_str, task.cron_expression, rules_str])
|
|
|
+
|
|
|
+ # Adjust column width
|
|
|
+ ws.column_dimensions['A'].width = 25
|
|
|
+ ws.column_dimensions['B'].width = 25
|
|
|
+ ws.column_dimensions['C'].width = 40
|
|
|
+ ws.column_dimensions['D'].width = 20
|
|
|
+ ws.column_dimensions['E'].width = 50
|
|
|
+
|
|
|
+ # Save to buffer
|
|
|
+ buffer = io.BytesIO()
|
|
|
+ wb.save(buffer)
|
|
|
+ buffer.seek(0)
|
|
|
+
|
|
|
+ return Response(
|
|
|
+ content=buffer.getvalue(),
|
|
|
+ media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
|
+ headers={"Content-Disposition": "attachment; filename=tasks_template.xlsx"}
|
|
|
+ )
|
|
|
+
|
|
|
+@router.post("/import_template")
|
|
|
+async def import_template(
|
|
|
+ file: UploadFile = File(...),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ current_user: sql_models.User = Depends(deps.get_current_user)
|
|
|
+) -> Any:
|
|
|
+ """
|
|
|
+ Import tasks from template
|
|
|
+ """
|
|
|
+ if not file.filename.endswith(('.xlsx', '.xls')):
|
|
|
+ raise HTTPException(status_code=400, detail="Invalid file format. Please upload an Excel file.")
|
|
|
+
|
|
|
+ try:
|
|
|
+ contents = await file.read()
|
|
|
+ wb = openpyxl.load_workbook(io.BytesIO(contents))
|
|
|
+ ws = wb.active
|
|
|
+
|
|
|
+ # Read headers
|
|
|
+ rows = list(ws.rows)
|
|
|
+ if not rows:
|
|
|
+ return {"message": "Empty file"}
|
|
|
+
|
|
|
+ header_row = rows[0]
|
|
|
+ headers = [cell.value for cell in header_row]
|
|
|
+
|
|
|
+ # Validate headers
|
|
|
+ required_headers = ["Task Name", "Model Config Name", "Camera Names", "Cron Expression", "Rules (JSON)"]
|
|
|
+ if not all(h in headers for h in required_headers):
|
|
|
+ raise HTTPException(status_code=400, detail=f"Invalid template format. Required columns: {', '.join(required_headers)}")
|
|
|
+
|
|
|
+ name_idx = headers.index("Task Name")
|
|
|
+ model_idx = headers.index("Model Config Name")
|
|
|
+ cams_idx = headers.index("Camera Names")
|
|
|
+ cron_idx = headers.index("Cron Expression")
|
|
|
+ rules_idx = headers.index("Rules (JSON)")
|
|
|
+
|
|
|
+ added_count = 0
|
|
|
+ updated_count = 0
|
|
|
+
|
|
|
+ # Pre-fetch lookup maps
|
|
|
+ # Model Configs: Name -> ID
|
|
|
+ model_map = {m.name: m.id for m in db.query(sql_models.ModelConfig).all()}
|
|
|
+ # Cameras: Name -> ID
|
|
|
+ camera_map = {c.name: c.id for c in db.query(sql_models.Camera).all()}
|
|
|
+
|
|
|
+ # Process data
|
|
|
+ for row in rows[1:]:
|
|
|
+ task_name = row[name_idx].value
|
|
|
+ model_config_name = row[model_idx].value
|
|
|
+ camera_names_str = row[cams_idx].value
|
|
|
+ cron_expr = row[cron_idx].value
|
|
|
+ rules_str = row[rules_idx].value
|
|
|
+
|
|
|
+ if not task_name:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # Resolve Model ID
|
|
|
+ model_config_id = model_map.get(model_config_name)
|
|
|
+ if not model_config_id:
|
|
|
+ # Optionally skip or log warning. Here we skip.
|
|
|
+ continue
|
|
|
+
|
|
|
+ # Resolve Camera IDs
|
|
|
+ camera_ids = []
|
|
|
+ if camera_names_str:
|
|
|
+ cam_names = [c.strip() for c in str(camera_names_str).split(",")]
|
|
|
+ for cname in cam_names:
|
|
|
+ if cname in camera_map:
|
|
|
+ camera_ids.append(camera_map[cname])
|
|
|
+
|
|
|
+ # Parse Rules
|
|
|
+ try:
|
|
|
+ rules = json.loads(rules_str) if rules_str else []
|
|
|
+ except:
|
|
|
+ rules = []
|
|
|
+
|
|
|
+ # Check if task exists
|
|
|
+ existing_task = db.query(sql_models.Task).filter(sql_models.Task.name == task_name).first()
|
|
|
+
|
|
|
+ if existing_task:
|
|
|
+ # Update
|
|
|
+ existing_task.model_config_id = model_config_id
|
|
|
+ existing_task.camera_ids = camera_ids
|
|
|
+ existing_task.rules = rules
|
|
|
+ existing_task.cron_expression = cron_expr
|
|
|
+
|
|
|
+ # If task is running, update scheduler
|
|
|
+ if existing_task.is_running:
|
|
|
+ update_job(existing_task.id, existing_task.cron_expression, True)
|
|
|
+
|
|
|
+ updated_count += 1
|
|
|
+ else:
|
|
|
+ # Create
|
|
|
+ new_task = sql_models.Task(
|
|
|
+ name=task_name,
|
|
|
+ model_config_id=model_config_id,
|
|
|
+ camera_ids=camera_ids,
|
|
|
+ rules=rules,
|
|
|
+ cron_expression=cron_expr,
|
|
|
+ is_running=False
|
|
|
+ )
|
|
|
+ db.add(new_task)
|
|
|
+ added_count += 1
|
|
|
+
|
|
|
+ db.commit()
|
|
|
+
|
|
|
+ return {
|
|
|
+ "message": "Import successful",
|
|
|
+ "added": added_count,
|
|
|
+ "updated": updated_count
|
|
|
+ }
|
|
|
+
|
|
|
+ except HTTPException as e:
|
|
|
+ raise e
|
|
|
+ except Exception as e:
|
|
|
+ raise HTTPException(status_code=400, detail=f"Import failed: {str(e)}")
|
|
|
+
|
|
|
@router.post("", response_model=schemas.Task)
|
|
|
def create_task(
|
|
|
*,
|