213 lines
6.6 KiB
Python
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
|