diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 705b184..5c7543b 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -135,10 +135,27 @@ def get_all_meal_plans( @router.post("/test-email") def test_email(email: str, db: Session = Depends(get_db)): + from app.services.email import get_email_backend + + html = "

MealPlanner Test Email

This is a test email from MealPlanner.

" + text = "MealPlanner Test Email\n\nThis is a test email from MealPlanner." + + try: + backend = get_email_backend() + backend.send( + to=email, + subject="MealPlanner Test Email", + html=html, + text=text, + ) + status = "sent" + except Exception as e: + status = f"failed: {e}" + email_log = EmailLog( recipient_email=email, template="test", - status="sent", + status=status, created_at=datetime.now() ) db.add(email_log) @@ -146,9 +163,10 @@ def test_email(email: str, db: Session = Depends(get_db)): db.refresh(email_log) return { - "message": "Test email logged", + "message": f"Test email {status}", "email_log_id": str(email_log.id), - "recipient": email + "recipient": email, + "status": status, } diff --git a/backend/app/api/meals.py b/backend/app/api/meals.py index b913488..8a5cb60 100644 --- a/backend/app/api/meals.py +++ b/backend/app/api/meals.py @@ -156,6 +156,38 @@ def get_vote_page( safe_meal = _html_escape(meal_type) action_url = f"/api/meals/vote/{item_id}?token={_html_escape(token, quote=True)}" + existing_vote = db.query(MealPlanVote).filter( + MealPlanVote.meal_plan_item_id == item_id, + MealPlanVote.family_member_id == voter.id, + ).first() + + if existing_vote: + already_voted_html = f""" + + + +Already Voted + + + + +

Hi {safe_voter}, you already voted on this meal

+
+
{safe_recipe}
+
{safe_day} · {safe_meal}
+
+
Your vote has already been recorded. Thank you!
+ + +""" + return HTMLResponse(content=already_voted_html, status_code=200) + html = f""" @@ -251,11 +283,11 @@ def submit_vote( ).all() if any(v.vote is False for v in votes): - item.approval_status = MealPlanItemStatus.DENIED + item.approval_status = MealPlanItemStatus.denied elif electorate_ids and {v.family_member_id for v in votes} >= electorate_ids: - item.approval_status = MealPlanItemStatus.APPROVED + item.approval_status = MealPlanItemStatus.approved else: - item.approval_status = MealPlanItemStatus.PENDING + item.approval_status = MealPlanItemStatus.pending db.commit() return {"status": "recorded", "item_status": item.approval_status.value} @@ -282,7 +314,7 @@ def swap_meal_item(item_id: UUID, new_recipe_id: UUID, db: Session = Depends(get raise HTTPException(status_code=404, detail="New recipe not found") item.recipe_id = new_recipe_id - item.approval_status = MealPlanItemStatus.PENDING + item.approval_status = MealPlanItemStatus.pending item.denial_reason = None item.denial_details = None diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 683edf5..0543636 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -33,11 +33,11 @@ class MealPlanStatus(enum.Enum): LOCKED = "locked" -class MealPlanItemStatus(enum.Enum): - PENDING = "pending" - APPROVED = "approved" - DENIED = "denied" - SWAPPED = "swapped" +class MealPlanItemStatus(str, enum.Enum): + pending = "pending" + approved = "approved" + denied = "denied" + swapped = "swapped" class ApprovalTokenStatus(enum.Enum): @@ -221,7 +221,7 @@ class MealPlanItem(Base): recipe_id = Column(UUID(as_uuid=True), ForeignKey("recipe.id")) day_of_week = Column(Integer, nullable=False) meal_type = Column(SQLEnum(MealType, name="meal_type_enum", create_type=False, values_callable=lambda obj: [e.value for e in obj]), nullable=False) - approval_status = Column(SQLEnum(MealPlanItemStatus, name="meal_plan_item_status_enum", create_type=False, values_callable=lambda obj: [e.value for e in obj]), default=MealPlanItemStatus.PENDING) + approval_status = Column(SQLEnum(MealPlanItemStatus, name="meal_plan_item_status_enum", create_type=False, values_callable=lambda obj: [e.value for e in obj]), default=MealPlanItemStatus.pending) denial_reason = Column(SQLEnum(DenialReason, name="denial_reason_enum", create_type=False, values_callable=lambda obj: [e.value for e in obj])) denial_details = Column(Text) estimated_cost = Column(Numeric(10, 2)) diff --git a/backend/app/services/orchestrator/steps.py b/backend/app/services/orchestrator/steps.py index b64bc37..5488445 100644 --- a/backend/app/services/orchestrator/steps.py +++ b/backend/app/services/orchestrator/steps.py @@ -126,7 +126,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None: # Preload ingredient names for all pending items ing_ids: set = set() for _item in plan.items: - if _item.approval_status == MealPlanItemStatus.PENDING and _item.recipe: + if _item.approval_status == MealPlanItemStatus.pending and _item.recipe: for _ing in (_item.recipe.ingredients or []): if "ingredient_id" in _ing: ing_ids.add(_ing["ingredient_id"]) @@ -141,7 +141,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None: # Build shopping list preview once (shared across all member emails) all_ingredients: dict[str, tuple[str, str, str, str]] = {} for item in plan.items: - if item.approval_status != MealPlanItemStatus.PENDING: + if item.approval_status != MealPlanItemStatus.pending: continue for ing in (item.recipe.ingredients or [] if item.recipe else []): ing_name = ingredient_names.get(str(ing.get("ingredient_id", "")), ing.get("name", "unknown")).strip() @@ -193,7 +193,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None: for member in members: item_html_parts = [] for item in plan.items: - if item.approval_status != MealPlanItemStatus.PENDING: + if item.approval_status != MealPlanItemStatus.pending: continue token = issue_token(item.id, member.id) vote_url = ( @@ -317,11 +317,11 @@ def step_deadline(run: "WeeklyRun", db: "Session") -> None: resolved = 0 for item in plan.items: - if item.approval_status == MealPlanItemStatus.PENDING: + if item.approval_status == MealPlanItemStatus.pending: item.approval_status = ( - MealPlanItemStatus.APPROVED +MealPlanItemStatus.approved if policy == "approve" - else MealPlanItemStatus.DENIED + else MealPlanItemStatus.denied ) resolved += 1 @@ -351,7 +351,7 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None: approved_items = [ item for item in (plan.items if plan else []) - if item.approval_status == MealPlanItemStatus.APPROVED + if item.approval_status == MealPlanItemStatus.approved ] members = ( @@ -482,7 +482,7 @@ def step_reminder(run: "WeeklyRun", db: "Session") -> None: pending_item_ids = [ item.id for item in plan.items - if item.approval_status == MealPlanItemStatus.PENDING + if item.approval_status == MealPlanItemStatus.pending ] if not pending_item_ids: run.reminded_at = datetime.now(timezone.utc) @@ -512,7 +512,7 @@ def step_reminder(run: "WeeklyRun", db: "Session") -> None: unvoted = [ item for item in plan.items - if item.approval_status == MealPlanItemStatus.PENDING + if item.approval_status == MealPlanItemStatus.pending and item.id not in voted_ids ] if not unvoted: diff --git a/backend/app/services/planner/generate.py b/backend/app/services/planner/generate.py index 78fdb41..28eb00f 100644 --- a/backend/app/services/planner/generate.py +++ b/backend/app/services/planner/generate.py @@ -182,7 +182,7 @@ def generate_meal_plan( recipe_id=scored_recipe.recipe_id, day_of_week=index + 1, # Mon=1, Tue=2, Wed=3 by default meal_type=MealType.DINNER, - approval_status=MealPlanItemStatus.PENDING, + approval_status=MealPlanItemStatus.pending, estimated_cost=scored_recipe.cost.total_cost, ) db.add(item) diff --git a/backend/tests/test_orchestrator.py b/backend/tests/test_orchestrator.py index badc139..01d8dc4 100644 --- a/backend/tests/test_orchestrator.py +++ b/backend/tests/test_orchestrator.py @@ -112,7 +112,7 @@ def pending_item(db, meal_plan): recipe_id=recipe.id, day_of_week=5, meal_type=MealType.DINNER, - approval_status=MealPlanItemStatus.PENDING, + approval_status=MealPlanItemStatus.pending, ) db.add(item) db.flush() @@ -467,7 +467,7 @@ def test_step_deadline_resolves_pending_to_approved(db, weekly_run_emailed, meal db.flush() step_deadline(weekly_run_emailed, db) db.refresh(pending_item) - assert pending_item.approval_status == MealPlanItemStatus.APPROVED + assert pending_item.approval_status == MealPlanItemStatus.approved assert weekly_run_emailed.deadline_passed_at is not None @@ -478,7 +478,7 @@ def test_step_deadline_resolves_pending_to_denied(db, weekly_run_emailed, meal_p db.flush() step_deadline(weekly_run_emailed, db) db.refresh(pending_item) - assert pending_item.approval_status == MealPlanItemStatus.DENIED + assert pending_item.approval_status == MealPlanItemStatus.denied # --------------------------------------------------------------------------- @@ -520,7 +520,7 @@ def approved_item(db, meal_plan): recipe_id=recipe.id, day_of_week=6, meal_type=MealType.DINNER, - approval_status=MealPlanItemStatus.APPROVED, + approval_status=MealPlanItemStatus.approved, ) db.add(item) db.flush()