Spaces:
Sleeping
Sleeping
| """Merge and persist region review state (FR-08).""" | |
| from __future__ import annotations | |
| import json | |
| from datetime import datetime, timezone | |
| from typing import Any, Dict, List, Optional | |
| from sqlalchemy.orm import Session | |
| from ..models import DetectionRun | |
| from .models import RegionReview | |
| VALID_REVIEW_STATUSES = {"pending", "confirmed", "false_positive", "submitted"} | |
| def _utcnow() -> datetime: | |
| return datetime.now(timezone.utc) | |
| def load_regions(run: DetectionRun) -> List[dict]: | |
| try: | |
| return json.loads(run.regions_json or "[]") | |
| except json.JSONDecodeError: | |
| return [] | |
| def save_regions(db: Session, run: DetectionRun, regions: List[dict]) -> None: | |
| run.regions_json = json.dumps(regions) | |
| run.regions_count = len(regions) | |
| db.commit() | |
| def merge_reviews(db: Session, run_id: int, regions: List[dict]) -> List[dict]: | |
| """Attach reviewStatus / notes from RegionReview rows onto region dicts.""" | |
| reviews = { | |
| r.region_id: r | |
| for r in db.query(RegionReview).filter(RegionReview.run_id == run_id).all() | |
| } | |
| out = [] | |
| for region in regions: | |
| rid = int(region.get("id", 0)) | |
| rev = reviews.get(rid) | |
| merged = dict(region) | |
| if rev: | |
| merged["reviewStatus"] = rev.status | |
| merged["reviewNotes"] = rev.notes or "" | |
| merged["reviewedAt"] = rev.reviewed_at.isoformat() if rev.reviewed_at else None | |
| else: | |
| merged.setdefault("reviewStatus", region.get("reviewStatus", "pending")) | |
| out.append(merged) | |
| return out | |
| def get_or_create_review(db: Session, run_id: int, region_id: int) -> RegionReview: | |
| row = ( | |
| db.query(RegionReview) | |
| .filter(RegionReview.run_id == run_id, RegionReview.region_id == region_id) | |
| .first() | |
| ) | |
| if row: | |
| return row | |
| row = RegionReview(run_id=run_id, region_id=region_id, status="pending") | |
| db.add(row) | |
| db.commit() | |
| db.refresh(row) | |
| return row | |
| def set_region_review( | |
| db: Session, | |
| run: DetectionRun, | |
| region_id: int, | |
| review_status: str, | |
| reviewer_id: int, | |
| notes: str = "", | |
| ) -> dict: | |
| if review_status not in ("confirmed", "false_positive", "pending"): | |
| raise ValueError(f"Invalid reviewStatus: {review_status}") | |
| regions = load_regions(run) | |
| region = next((r for r in regions if int(r.get("id", -1)) == region_id), None) | |
| if not region: | |
| raise LookupError("Region not found in run") | |
| row = get_or_create_review(db, run.id, region_id) | |
| if row.status == "submitted": | |
| raise PermissionError("Region already submitted — cannot change review") | |
| row.status = review_status | |
| row.reviewer_id = reviewer_id | |
| row.notes = (notes or "").strip() | |
| row.reviewed_at = _utcnow() | |
| db.commit() | |
| region["reviewStatus"] = review_status | |
| if notes: | |
| region["reviewNotes"] = notes | |
| save_regions(db, run, regions) | |
| return merge_reviews(db, run.id, [region])[0] | |
| def filter_regions_by_review(regions: List[dict], status: str) -> List[dict]: | |
| return [r for r in regions if (r.get("reviewStatus") or "pending") == status] | |
| def mark_confirmed_submitted(db: Session, run_id: int, region_ids: List[int]) -> int: | |
| q = db.query(RegionReview).filter( | |
| RegionReview.run_id == run_id, | |
| RegionReview.region_id.in_(region_ids), | |
| RegionReview.status == "confirmed", | |
| ) | |
| now = _utcnow() | |
| count = 0 | |
| for row in q.all(): | |
| row.status = "submitted" | |
| row.submitted_at = now | |
| count += 1 | |
| db.commit() | |
| return count | |
| def review_summary(regions: List[dict]) -> Dict[str, int]: | |
| summary = {"pending": 0, "confirmed": 0, "false_positive": 0, "submitted": 0} | |
| for r in regions: | |
| st = r.get("reviewStatus") or "pending" | |
| summary[st] = summary.get(st, 0) + 1 | |
| return summary | |