Untitled

 avatar
unknown
plain_text
9 months ago
11 kB
18
Indexable
from __future__ import annotations

import os
from datetime import date as DateType
from pathlib import Path
from typing import List, Optional
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, ConfigDict
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from src.database import get_db
from src.models.case_model import Case
from src.models.patient_model import Patient
from src.models.image_model import Image
from src.config import settings

router = APIRouter(prefix="/cases", tags=["cases"])

SLIDES_ROOT = Path(settings.SLIDES_DIR)

# ======================= Yardımcılar =======================

def _remove_slide_assets(case_id: UUID, filename: Optional[str]) -> None:
    """Case'e ait tekil bir slide dosyası boş kaldıysa temizle."""
    if not filename:
        return
    filepath = SLIDES_ROOT / str(case_id) / filename
    try:
        filepath.unlink(missing_ok=True)
    except OSError:
        pass
    parent = filepath.parent
    try:
        if parent.exists() and not any(parent.iterdir()):
            parent.rmdir()
    except OSError:
        pass


def _patient_name_safe(p: Optional[Patient]) -> Optional[str]:
    if not p:
        return None
    return getattr(p, "full_name", None) or getattr(p, "name", None)


def _normalize_protocol_seed(seed: Optional[str]) -> str:
    s = (seed or "").strip()
    return s or "PRT"


def _next_unique_protocol(db: Session, seed: str) -> str:
    base = _normalize_protocol_seed(seed)
    candidate = base
    counter = 1
    while db.query(Case).filter(Case.protocol == candidate).first():
        candidate = f"{base}-{counter:03d}"
        counter += 1
    return candidate


# ======================= Schemas =======================

class CaseCreate(BaseModel):
    date: DateType
    patient_id: int
    doctor: str
    diagnosis: Optional[str] = None
    status: Optional[str] = "Bekliyor"
    stage: Optional[str] = "Pre-analitik"
    protocol: Optional[str] = None
    slide_filename: Optional[str] = None


class CaseUpdate(BaseModel):
    date: Optional[DateType] = None
    patient_id: Optional[int] = None
    doctor: Optional[str] = None
    diagnosis: Optional[str] = None
    status: Optional[str] = None
    stage: Optional[str] = None
    protocol: Optional[str] = None
    slide_filename: Optional[str] = None


class CaseOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: UUID
    date: DateType
    patient_id: int
    patient: Optional[str] = None
    protocol: str
    doctor: str
    diagnosis: Optional[str] = None
    status: str
    stage: str
    slide_filename: Optional[str] = None
    slide_id: Optional[str] = None


class ImageOut(BaseModel):
    """images tablosu kolon adlarıyla birebir eşleşir."""
    model_config = ConfigDict(from_attributes=True)
    id: int
    case_id: UUID
    filename: str
    tile_base_url: Optional[str] = None


# ======================= Dönüşüm =======================

def _to_out(c: Case) -> CaseOut:
    slide_id = os.path.splitext(c.slide_filename)[0] if c.slide_filename else None
    return CaseOut(
        id=c.id,
        date=c.date,
        patient_id=c.patient_id,
        patient=_patient_name_safe(c.patient),
        protocol=c.protocol,
        doctor=c.doctor,
        diagnosis=c.diagnosis,
        status=c.status,
        stage=c.stage,
        slide_filename=c.slide_filename,
        slide_id=slide_id,
    )


# ======================= CRUD: Cases =======================

@router.post("/", response_model=CaseOut, status_code=status.HTTP_201_CREATED)
def create_case(body: CaseCreate, db: Session = Depends(get_db)):
    patient = db.get(Patient, body.patient_id)
    if not patient:
        raise HTTPException(status_code=400, detail="Invalid patient_id")

    protocol = _next_unique_protocol(db, body.protocol)
    case = Case(
        date=body.date,
        patient_id=body.patient_id,
        protocol=protocol,
        doctor=(body.doctor or "").strip(),
        diagnosis=body.diagnosis,
        status=(body.status or "Bekliyor"),
        stage=(body.stage or "Pre-analitik"),
        slide_filename=(body.slide_filename or None),
    )

    try:
        db.add(case)
        db.commit()
        db.refresh(case)
    except IntegrityError:
        # Aynı protokol çakıştıysa benzersiz bir protokol üret ve tekrar dene
        db.rollback()
        case.protocol = _next_unique_protocol(db, protocol)
        db.add(case)
        db.commit()
        db.refresh(case)

    return _to_out(case)


@router.get("", response_model=List[CaseOut], name="list_cases")
@router.get("/", response_model=List[CaseOut], include_in_schema=False)
def list_cases(
    db: Session = Depends(get_db),
    patient_id: Optional[int] = Query(default=None),
    patient_name: Optional[str] = Query(default=None),
    start_date: Optional[DateType] = Query(default=None, description="Başlangıç tarihi"),
    end_date: Optional[DateType] = Query(default=None, description="Bitiş tarihi"),
    limit: Optional[int] = Query(default=None, ge=1, le=1000),
    offset: Optional[int] = Query(default=None, ge=0),
):
    q = db.query(Case)
    if patient_id is not None:
        q = q.filter(Case.patient_id == patient_id)
    if patient_name:
        like = f"%{patient_name}%"
        q = q.filter(Case.patient.has(Patient.full_name.ilike(like)))

    if start_date:
        q = q.filter(Case.date >= start_date)
    if end_date:
        q = q.filter(Case.date <= end_date)

    q = q.order_by(Case.date.desc(), Case.protocol.desc())

    if limit is not None:
        q = q.limit(limit)
    if offset is not None:
        q = q.offset(offset)

    return [_to_out(c) for c in q.all()]


@router.get("/{case_id}", response_model=CaseOut, name="get_case")
def get_case(case_id: UUID, db: Session = Depends(get_db)):
    c = db.get(Case, case_id)
    if not c:
        raise HTTPException(status_code=404, detail="Case not found")
    return _to_out(c)


@router.patch("/{case_id}", response_model=CaseOut, name="update_case")
@router.patch("/{case_id}/", response_model=CaseOut, include_in_schema=False)
def update_case(case_id: UUID, body: CaseUpdate, db: Session = Depends(get_db)):
    c = db.get(Case, case_id)
    if not c:
        raise HTTPException(status_code=404, detail="Case not found")

    data = body.model_dump(exclude_unset=True)

    # If protocol is provided, ensure uniqueness similar to create
    if "protocol" in data and data["protocol"]:
        # Avoid regenerating if unchanged
        if data["protocol"] != c.protocol:
            data["protocol"] = _next_unique_protocol(db, data["protocol"])

    for k, v in data.items():
        setattr(c, k, v)

    try:
        db.add(c)
        db.commit()
        db.refresh(c)
    except Exception:
        db.rollback()
        raise

    return _to_out(c)


@router.patch("/{case_id}/slide", response_model=CaseOut, name="update_case_slide")
def update_case_slide(case_id: UUID, body: CaseUpdate, db: Session = Depends(get_db)):
    """
    Update only the slide filename for a case. Body may include slide_filename.
    """
    c = db.get(Case, case_id)
    if not c:
        raise HTTPException(status_code=404, detail="Case not found")

    prev = c.slide_filename
    data = body.model_dump(exclude_unset=True)
    slide_filename = data.get("slide_filename", None)
    # Explicitly allow nulling out the slide filename
    c.slide_filename = slide_filename

    try:
        db.add(c)
        db.commit()
        db.refresh(c)
    except Exception:
        db.rollback()
        raise

    # If slide changed, try to remove old file (best-effort)
    if prev and prev != slide_filename:
        try:
            _remove_slide_assets(case_id, prev)
        except Exception:
            pass

    return _to_out(c)


@router.delete("/{case_id}", status_code=status.HTTP_204_NO_CONTENT, name="delete_case")
@router.delete("/{case_id}/", status_code=status.HTTP_204_NO_CONTENT, include_in_schema=False)
def delete_case(case_id: UUID, db: Session = Depends(get_db)):
    c = db.get(Case, case_id)
    if not c:
        raise HTTPException(status_code=404, detail="Case not found")

    # Best-effort: remove linked slide assets for this case
    try:
        _remove_slide_assets(case_id, c.slide_filename)
    except Exception:
        pass

    try:
        db.delete(c)
        db.commit()
    except Exception:
        db.rollback()
        raise
    return None


# ======================= Case -> Images =======================

@router.get("/{case_id}/images", response_model=List[ImageOut])
def list_case_images(
    case_id: UUID,
    raw: bool = Query(False, description="true ise Pydantic yerine sade dict döner (debug)"),
    limit: int = Query(200, ge=1, le=1000),
    offset: int = Query(0, ge=0),
    db: Session = Depends(get_db),
):
    """
    Bir vakanın görüntülerini döndürür.
    - Case yoksa 200 [] döner (UI akışı bozulmasın).
    - `raw=true` verilirse Pydantic by-pass edilerek sade dict döner (debug için).
    """
    try:
        case = db.get(Case, case_id)
        if not case:
            return []

        rows: List[Image] = (
            db.query(Image)
              .filter(Image.case_id == case_id)
              .order_by(Image.id.asc())
              .limit(limit)
              .offset(offset)
              .all()
        )

        if raw:
            # Şemayı by-pass edip sade dict döndür — hata ayıklama kolaylaşır
            return [
                {
                    "id": int(r.id),
                    "case_id": str(r.case_id) if getattr(r, "case_id", None) else None,
                    "filename": r.filename,
                    "tile_base_url": getattr(r, "tile_base_url", None),
                }
                for r in rows
            ]

        # Tip-güvenli dönüş — ImageOut ile birebir alan adları
        return rows

    except Exception as e:
        import traceback
        print("🔥 CASE_IMAGES_ERR:", e, traceback.format_exc())
        # Global CORS middleware 500'de de header ekleyecek
        raise HTTPException(status_code=500, detail="internal")
Editor is loading...
Leave a Comment