Public Access
- Add planner_config JSONB to family_profile model + migration - Add PlannerConfig.merge(overrides) + to_dict() for family-level override merging - generate_meal_plan merges family.planner_config into DEFAULT before filtering/scoring/selection - New endpoints on /api/profile: - GET /planner-config — returns merged effective config - PUT /planner-config — partial override validation + merge - DELETE /planner-config — reset to system defaults - Schemas: PlannerConfigOverride, PlannerConfigResponse, PlannerConfigUpdateRequest with weight-sum validation (0.999–1.001) - Export RecipeBase/Create/Read/Update from schemas/__init__ to resolve forward refs
149 lines
5.4 KiB
Python
149 lines
5.4 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from app.database import get_db
|
|
from app.models import FamilyProfile, FamilyMember, DayOfWeek, MealType, MealPlanStatus, MealPlanItemStatus, ApprovalTokenStatus, FamilyMemberRole, DenialReason, NeverSuggestReason, ScrapeStatus, EmailStatus
|
|
from app.schemas import (
|
|
FamilyProfileResponse, FamilyProfileUpdate, FamilyProfileCreate,
|
|
FamilyMemberResponse, FamilyMemberCreate
|
|
)
|
|
from app.schemas.planner_config import PlannerConfigOverride, PlannerConfigResponse, PlannerConfigUpdateRequest
|
|
from app.services.planner.config import DEFAULT
|
|
from app.security import require_session
|
|
from uuid import UUID
|
|
from typing import List
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("", response_model=FamilyProfileResponse)
|
|
def get_profile(db: Session = Depends(get_db)):
|
|
profile = db.query(FamilyProfile).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail="Family profile not found")
|
|
return profile
|
|
|
|
|
|
@router.put("", response_model=FamilyProfileResponse, dependencies=[Depends(require_session)])
|
|
def update_profile(update: FamilyProfileUpdate, db: Session = Depends(get_db)):
|
|
profile = db.query(FamilyProfile).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail="Family profile not found")
|
|
|
|
update_data = update.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(profile, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(profile)
|
|
return profile
|
|
|
|
|
|
@router.get("/members", response_model=List[FamilyMemberResponse])
|
|
def get_members(db: Session = Depends(get_db)):
|
|
profile = db.query(FamilyProfile).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail="Family profile not found")
|
|
return profile.members
|
|
|
|
|
|
@router.post("/members", response_model=FamilyMemberResponse, dependencies=[Depends(require_session)])
|
|
def add_member(member: FamilyMemberCreate, db: Session = Depends(get_db)):
|
|
profile = db.query(FamilyProfile).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail="Family profile not found")
|
|
|
|
existing = db.query(FamilyMember).filter(
|
|
FamilyMember.family_profile_id == profile.id,
|
|
FamilyMember.email == member.email
|
|
).first()
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="Member with this email already exists")
|
|
|
|
db_member = FamilyMember(
|
|
family_profile_id=profile.id,
|
|
name=member.name,
|
|
email=member.email,
|
|
role=FamilyMemberRole[member.role.value.upper()],
|
|
likes_mushrooms=member.likes_mushrooms
|
|
)
|
|
db.add(db_member)
|
|
db.commit()
|
|
db.refresh(db_member)
|
|
return db_member
|
|
|
|
|
|
@router.delete("/members/{member_id}", dependencies=[Depends(require_session)])
|
|
def delete_member(member_id: UUID, db: Session = Depends(get_db)):
|
|
member = db.query(FamilyMember).filter(FamilyMember.id == member_id).first()
|
|
if not member:
|
|
raise HTTPException(status_code=404, detail="Family member not found")
|
|
|
|
db.delete(member)
|
|
db.commit()
|
|
return {"message": "Member deleted"}
|
|
|
|
|
|
@router.get("/planner-config", response_model=PlannerConfigResponse)
|
|
def get_planner_config(db: Session = Depends(get_db)):
|
|
"""Return the effective planner configuration for the family (merged defaults + overrides)."""
|
|
profile = db.query(FamilyProfile).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail="Family profile not found")
|
|
|
|
effective = DEFAULT
|
|
if profile.planner_config:
|
|
try:
|
|
effective = DEFAULT.merge(profile.planner_config)
|
|
except ValueError:
|
|
pass # fall back to default
|
|
|
|
data = effective.to_dict()
|
|
data["source"] = "family_override" if profile.planner_config else "default"
|
|
return data
|
|
|
|
|
|
@router.put("/planner-config", response_model=PlannerConfigResponse, dependencies=[Depends(require_session)])
|
|
def update_planner_config(payload: PlannerConfigUpdateRequest, db: Session = Depends(get_db)):
|
|
"""Update the family planner_config. Partial overrides are merged into defaults."""
|
|
profile = db.query(FamilyProfile).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail="Family profile not found")
|
|
|
|
# Build a clean dict of overrides
|
|
overrides = payload.planner_config.model_dump(exclude_unset=True, exclude_none=True)
|
|
if not overrides:
|
|
raise HTTPException(status_code=400, detail="No overrides provided")
|
|
|
|
# Merge with existing family config if present
|
|
existing = dict(profile.planner_config) if profile.planner_config else {}
|
|
merged = {**existing, **overrides}
|
|
|
|
# Validate via PlannerConfig
|
|
try:
|
|
DEFAULT.merge(merged)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=400, detail=f"Invalid planner config: {exc}")
|
|
|
|
profile.planner_config = merged
|
|
db.commit()
|
|
db.refresh(profile)
|
|
|
|
effective = DEFAULT.merge(merged)
|
|
data = effective.to_dict()
|
|
data["source"] = "family_override"
|
|
return data
|
|
|
|
|
|
@router.delete("/planner-config", dependencies=[Depends(require_session)])
|
|
def reset_planner_config(db: Session = Depends(get_db)):
|
|
"""Clear family planner_config and restore system defaults."""
|
|
profile = db.query(FamilyProfile).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail="Family profile not found")
|
|
|
|
profile.planner_config = None
|
|
db.commit()
|
|
data = DEFAULT.to_dict()
|
|
data["source"] = "default"
|
|
return data
|