Import Vector lab project
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
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
|
||||
Reference in New Issue
Block a user