Download backend/app/api/admin.py from Duke410/PawTrace: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/spaces/Duke410/PawTrace/resolve/main/backend/app/api/admin.py
- Command line
-
hf download hf://spaces/Duke410/PawTrace/backend/app/api/admin.py
-
curl -L -o admin.py https://huggingface.co/spaces/Duke410/PawTrace/resolve/main/backend/app/api/admin.py
10.3 kB
| """Thin admin slice (spec §2, §11): view pending matches, flag spam, close cases.""" | |
| from __future__ import annotations | |
| from fastapi import APIRouter, Depends, HTTPException, status | |
| from sqlalchemy import select | |
| from sqlalchemy.orm import Session | |
| from ..db import get_db | |
| from ..models import Case, Match, User | |
| from ..models.base import CaseStatus, CaseType, MatchStatus, SubjectType | |
| from ..schemas.case import CaseOut, MatchOut | |
| from ..security import require_admin | |
| from ..services import datasets as ds | |
| from .helpers import pictures_for | |
| from .hydrate import build_match_out | |
| router = APIRouter(prefix="/admin", tags=["admin"]) | |
| def pending_matches( | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> list[MatchOut]: | |
| matches = db.execute( | |
| select(Match).where(Match.status == MatchStatus.pending).order_by(Match.created_at.desc()) | |
| ).scalars().all() | |
| return [build_match_out(db, m) for m in matches] | |
| def all_cases( | |
| kind: str | None = None, | |
| status: str | None = None, | |
| limit: int = 30, | |
| offset: int = 0, | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> dict: | |
| """All cases (admin), hydrated with dog + owner/finder + match count; paginated + filterable.""" | |
| return ds.list_all_cases( | |
| db, case_type=kind, status=status, limit=max(1, min(limit, 100)), offset=max(0, offset) | |
| ) | |
| def all_owners( | |
| q: str | None = None, | |
| limit: int = 30, | |
| offset: int = 0, | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> dict: | |
| """Owners (newest first) and the dogs they own; optional name/email search (admin).""" | |
| return ds.list_owners(db, q=q, limit=max(1, min(limit, 100)), offset=max(0, offset)) | |
| def owner_detail( | |
| user_id: int, | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> dict: | |
| """One owner with all their dogs (photos-ready) and the cases they reported.""" | |
| data = ds.owner_detail(db, user_id) | |
| if data is None: | |
| raise HTTPException(status.HTTP_404_NOT_FOUND, "Owner not found") | |
| return data | |
| def case_detail( | |
| case_id: int, | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> dict: | |
| """A case with its subject dog (all photos) and its persisted matches (each candidate + photos).""" | |
| case = db.get(Case, case_id) | |
| if not case: | |
| raise HTTPException(status.HTTP_404_NOT_FOUND, "Case not found") | |
| dog = None | |
| if case.known_dog_id is not None: | |
| st, did = SubjectType.known, case.known_dog_id | |
| elif case.unknown_dog_id is not None: | |
| st, did = SubjectType.unknown, case.unknown_dog_id | |
| else: | |
| st = did = None | |
| if st is not None: | |
| dog = { | |
| "profile": ds.dog_profile(db, st, did), | |
| "photos": [p.model_dump() for p in pictures_for(db, st, did)], | |
| } | |
| matches = db.execute( | |
| select(Match).where(Match.case_id == case_id).order_by(Match.rank) | |
| ).scalars().all() | |
| return { | |
| "case": ds._case_row(db, case), | |
| "dog": dog, | |
| "matches": [build_match_out(db, m) for m in matches], | |
| } | |
| def run_case_match( | |
| case_id: int, | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> list[MatchOut]: | |
| """Run PRODUCTION matching for a case (persists ranked Match rows) and return them.""" | |
| case = db.get(Case, case_id) | |
| if not case: | |
| raise HTTPException(status.HTTP_404_NOT_FOUND, "Case not found") | |
| from ..services.matching import run_matching_for_case | |
| matches = run_matching_for_case(db, case) | |
| db.commit() | |
| return [build_match_out(db, m) for m in matches] | |
| def delete_case( | |
| case_id: int, | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> dict: | |
| """Delete a single case + its matches (temporary admin convenience for re-demoing).""" | |
| if not db.get(Case, case_id): | |
| raise HTTPException(status.HTTP_404_NOT_FOUND, "Case not found") | |
| return {"deleted": ds.delete_case(db, case_id)} | |
| def delete_person( | |
| user_id: int, | |
| admin: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> dict: | |
| """Delete a person + their dogs/cases/matches (temporary admin convenience). Admins are refused.""" | |
| from ..models.base import UserRole | |
| person = db.get(User, user_id) | |
| if not person: | |
| raise HTTPException(status.HTTP_404_NOT_FOUND, "User not found") | |
| if person.role == UserRole.admin: | |
| raise HTTPException(status.HTTP_400_BAD_REQUEST, "Refusing to delete an admin user") | |
| return {"deleted": ds.delete_person(db, user_id)} | |
| def delete_dog( | |
| kind: str, | |
| dog_id: int, | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> dict: | |
| """Delete a single dog + its pictures/embeddings/cases/matches (temporary admin convenience).""" | |
| from ..models import KnownDog, UnknownDog | |
| if kind not in ("known", "unknown"): | |
| raise HTTPException(status.HTTP_400_BAD_REQUEST, "kind must be 'known' or 'unknown'") | |
| st = SubjectType.known if kind == "known" else SubjectType.unknown | |
| exists = db.get(KnownDog if kind == "known" else UnknownDog, dog_id) | |
| if not exists: | |
| raise HTTPException(status.HTTP_404_NOT_FOUND, "Dog not found") | |
| return {"deleted": ds.delete_dog(db, st, dog_id)} | |
| def set_dog_status( | |
| kind: str, | |
| dog_id: int, | |
| payload: dict, | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> dict: | |
| """Admin: set a dog's status (e.g. a found dog 'at_shelter' vs 'reunited').""" | |
| from ..models import KnownDog, UnknownDog | |
| from ..models.base import KnownDogStatus, UnknownDogStatus | |
| if kind not in ("known", "unknown"): | |
| raise HTTPException(status.HTTP_400_BAD_REQUEST, "kind must be 'known' or 'unknown'") | |
| new_status = (payload or {}).get("status") | |
| enum_cls = KnownDogStatus if kind == "known" else UnknownDogStatus | |
| valid = [s.value for s in enum_cls] | |
| if new_status not in valid: | |
| raise HTTPException( | |
| status.HTTP_400_BAD_REQUEST, f"status must be one of {valid}" | |
| ) | |
| dog = db.get(KnownDog if kind == "known" else UnknownDog, dog_id) | |
| if not dog: | |
| raise HTTPException(status.HTTP_404_NOT_FOUND, "Dog not found") | |
| dog.status = enum_cls(new_status) | |
| db.commit() | |
| st = SubjectType.known if kind == "known" else SubjectType.unknown | |
| return {"profile": ds.dog_profile(db, st, dog_id)} | |
| def close_case( | |
| case_id: int, | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> CaseOut: | |
| case = db.get(Case, case_id) | |
| if not case: | |
| raise HTTPException(status.HTTP_404_NOT_FOUND, "Case not found") | |
| case.status = CaseStatus.closed | |
| db.commit() | |
| db.refresh(case) | |
| return CaseOut.model_validate(case) | |
| def reset_demo( | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> dict: | |
| """Reset the demo to its 'nothing solved yet' state: revert every match/case resolution and put | |
| matched dogs back to lost / found-not-matched. Dogs that were never lost (the intentionally-'home' | |
| ones, which have no lost case) are left home. Does not delete any dogs added during the demo.""" | |
| from ..models import KnownDog, UnknownDog | |
| from ..models.base import KnownDogStatus, UnknownDogStatus | |
| n_matches = 0 | |
| for m in db.execute(select(Match)).scalars(): | |
| if m.status != MatchStatus.pending or m.reviewed or m.reviewed_by is not None: | |
| m.status = MatchStatus.pending | |
| m.reviewed = False | |
| m.reviewed_by = None | |
| n_matches += 1 | |
| # Only undo confirm's effect (open/matched -> resolved). 'matched' is a normal pipeline state | |
| # (set whenever matching finds candidates, regardless of review) and 'closed' is a separate manual | |
| # admin action -- neither should be touched here. A resolved case goes back to 'matched' if it | |
| # still owns pending matches (it will, since they were just reset above), else 'open'. | |
| n_cases = 0 | |
| for c in db.execute(select(Case).where(Case.status == CaseStatus.resolved)).scalars(): | |
| has_matches = db.execute( | |
| select(Match.id).where(Match.case_id == c.id).limit(1) | |
| ).first() is not None | |
| c.status = CaseStatus.matched if has_matches else CaseStatus.open | |
| n_cases += 1 | |
| # Known dogs that have a lost case -> back to 'lost'. (Never-lost 'home' dogs have no lost case, | |
| # so they're skipped and stay home.) | |
| lost_known_ids = set(db.execute( | |
| select(Case.known_dog_id).where(Case.type == CaseType.lost, Case.known_dog_id.isnot(None)) | |
| ).scalars()) | |
| n_known = 0 | |
| if lost_known_ids: | |
| for d in db.execute(select(KnownDog).where(KnownDog.id.in_(lost_known_ids))).scalars(): | |
| if d.status != KnownDogStatus.lost: | |
| d.status = KnownDogStatus.lost | |
| n_known += 1 | |
| # Found dogs -> not-matched: 'at_shelter' if a shelter is recorded, else 'pending'. | |
| n_found = 0 | |
| for d in db.execute(select(UnknownDog)).scalars(): | |
| want = UnknownDogStatus.at_shelter if d.current_location_detail else UnknownDogStatus.pending | |
| if d.status != want: | |
| d.status = want | |
| n_found += 1 | |
| db.commit() | |
| return {"matches_reset": n_matches, "cases_reopened": n_cases, | |
| "known_relost": n_known, "found_reset": n_found} | |
| def flag_spam( | |
| match_id: int, | |
| _: User = Depends(require_admin), | |
| db: Session = Depends(get_db), | |
| ) -> MatchOut: | |
| match = db.get(Match, match_id) | |
| if not match: | |
| raise HTTPException(status.HTTP_404_NOT_FOUND, "Match not found") | |
| match.status = MatchStatus.rejected | |
| match.reviewed = True | |
| db.commit() | |
| db.refresh(match) | |
| return build_match_out(db, match) | |