Public Access
R1 stabilization: pytest harness with transactional db fixture, smoke + alembic + auth + scrape + approval + swiftly tests, github actions ci yaml. Bearer-token admin auth + signed-cookie session for family ui mutations. Async POST /api/admin/scrape (BackgroundTasks, returns 202). Path canonicalization (no /list, /planned suffixes). DATABASE_URL fail-fast on empty. R2 deferred-risk spikes: live lucky california fetch (R2-A), full email+per-voter approval click round trip with single-use enforcement (R2-B, console email backend, sendgrid stub). R3-0 phase 3 redesign: replaced playwright html scraper with requests based swiftly json api client. 17 categories, ~10k products per scrape, upsert by (source, external_id). 401 surfaces actionable token-refresh message via ScrapeLog.error_message. Pre-existing defects fixed: shopping_list.py syntax error blocking app import, MealPlan.votes orphan relationship, JSONB(astext=True) invalid kwarg, missing requests dep, calorie_target schema drift, every SQLEnum needed values_callable, 0001 had empty downgrade(), seed had duplicate ingredient rows. Migrations added: 0003 grocery_item.description, 0004 family_profile. calorie_target, 0005 grocery_item.external_id + source + composite index. Verified: 31/31 pytest green, alembic upgrade->downgrade->upgrade clean, frontend npm run build clean, live scrape 9,960 grocery_item rows in 36s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
81 lines
2.9 KiB
Python
81 lines
2.9 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.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"} |