Untitled
unknown
plain_text
9 months ago
11 kB
16
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