Files
vector/backend/api/v1/cases.py
T
2026-06-06 18:31:55 +00:00

213 lines
6.6 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import desc
from typing import List, Optional
from uuid import UUID
from database import get_db
from models import Case, User
from schemas import CaseCreate, CaseOut
from pydantic import BaseModel
# Импортируем auth dependencies
import sys
sys.path.append('/app/api/v1')
from auth import get_current_user, require_role
router = APIRouter()
class CaseUpdate(BaseModel):
"""Schema for updating case fields"""
# Ребёнок
child_name: Optional[str] = None
age_years: Optional[int] = None
gender: Optional[str] = None
height_build: Optional[str] = None
clothes_upper: Optional[str] = None
clothes_lower: Optional[str] = None
shoes: Optional[str] = None
clothes_description: Optional[str] = None
special_marks: Optional[str] = None
phone_status: Optional[str] = None
# Здоровье
has_diagnosis: Optional[bool] = None
diagnosis_type: Optional[List[str]] = None
fitness_level: Optional[str] = None
has_transport: Optional[str] = None
cant_swim: Optional[bool] = None
# Психотип
psychotype: Optional[str] = None
psychotype_answers: Optional[dict] = None
# Обстоятельства
loss_reason: Optional[str] = None
loss_time: Optional[str] = None
elapsed_hours: Optional[float] = None
last_seen_direction: Optional[str] = None
last_seen_reliability: Optional[str] = None
last_seen_description: Optional[str] = None
behavior_description: Optional[str] = None
familiar_places: Optional[str] = None
lost_before: Optional[str] = None
# Среда
season: Optional[str] = None
temperature_c: Optional[float] = None
precipitation: Optional[str] = None
visibility: Optional[str] = None
wind: Optional[str] = None
terrain: Optional[List[str]] = None
# GPS
tnp_lat: Optional[float] = None
tnp_lon: Optional[float] = None
tnp_address: Optional[str] = None
# Ресурсы
teams_count: Optional[int] = None
team_size: Optional[int] = None
has_dog: Optional[bool] = None
extra_resources: Optional[List[str]] = None
# Исход
found_alive: Optional[bool] = None
found_distance_km: Optional[float] = None
found_direction: Optional[str] = None
found_location_type: Optional[str] = None
found_lat: Optional[float] = None
found_lon: Optional[float] = None
search_duration_hours: Optional[float] = None
who_found: Optional[str] = None
# Статус
status: Optional[str] = None
class CaseListResponse(BaseModel):
"""Response for list endpoint with pagination"""
total: int
skip: int
limit: int
cases: List[CaseOut]
@router.post("/cases", response_model=CaseOut, status_code=201)
async def create_case(
case_data: CaseCreate,
db: Session = Depends(get_db)
):
"""
Создать новый случай поиска.
Принимает все поля из формы опроса (5 шагов).
Публичный эндпоинт - не требует аутентификации.
"""
db_case = Case(**case_data.model_dump(exclude_unset=True))
db.add(db_case)
db.commit()
db.refresh(db_case)
return db_case
@router.get("/cases", response_model=CaseListResponse)
async def list_cases(
skip: int = Query(0, ge=0, description="Количество пропускаемых записей"),
limit: int = Query(50, ge=1, le=100, description="Максимум записей на страницу"),
status: Optional[str] = Query(None, description="Фильтр по статусу: active/closed/archived"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Получить список случаев с пагинацией и фильтрацией.
- **skip**: смещение (для пагинации)
- **limit**: количество записей (макс 100)
- **status**: фильтр по статусу (active/closed/archived)
Требуется аутентификация (operator, field, admin).
"""
query = db.query(Case)
if status:
query = query.filter(Case.status == status)
total = query.count()
cases = query.order_by(desc(Case.created_at)).offset(skip).limit(limit).all()
return {
"total": total,
"skip": skip,
"limit": limit,
"cases": cases
}
@router.get("/cases/{case_id}", response_model=CaseOut)
async def get_case(
case_id: UUID,
db: Session = Depends(get_db)
):
"""
Получить случай по ID.
Возвращает все поля случая включая исход (если заполнен).
Публичный эндпоинт - не требует аутентификации.
"""
case = db.query(Case).filter(Case.id == case_id).first()
if not case:
raise HTTPException(status_code=404, detail=f"Case {case_id} not found")
return case
@router.patch("/cases/{case_id}", response_model=CaseOut)
async def update_case(
case_id: UUID,
case_update: CaseUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Обновить случай (частичное обновление).
Используется для:
- Корректировки данных опроса
- Внесения исхода поиска (found_alive, found_distance_km и т.д.)
- Изменения статуса (active → closed)
Требуется аутентификация (operator, field, admin).
"""
case = db.query(Case).filter(Case.id == case_id).first()
if not case:
raise HTTPException(status_code=404, detail=f"Case {case_id} not found")
update_data = case_update.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(case, field, value)
db.commit()
db.refresh(case)
return case
@router.delete("/cases/{case_id}", status_code=204)
async def delete_case(
case_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(require_role(["admin"]))
):
"""
Удалить случай (только для admin).
В продакшене рекомендуется использовать архивацию вместо удаления.
"""
case = db.query(Case).filter(Case.id == case_id).first()
if not case:
raise HTTPException(status_code=404, detail=f"Case {case_id} not found")
db.delete(case)
db.commit()
return None