Public Access
feat(meals): enforce three lower-calorie dinners with media
This commit is contained in:
@@ -17,7 +17,7 @@ from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from app.config import settings
|
||||
from app.models import FamilyMember, FamilyProfile, Ingredient, IngredientGroceryMatch, MealPlan, MealPlanItemStatus, MealPlanVote
|
||||
from app.models import FamilyMember, FamilyProfile, Ingredient, IngredientGroceryMatch, MealPlan, MealPlanItemStatus, MealPlanVote, RecipeCatalogStatus
|
||||
from app.services.approval import issue_token
|
||||
from app.services.email import get_email_backend
|
||||
from app.services.orchestrator.alerts import send_admin_alert
|
||||
@@ -26,8 +26,7 @@ from app.services.scraper_service import ScraperService
|
||||
from app.utils.units import convert_qty
|
||||
|
||||
from app.services.feedback_analyzer import FeedbackAnalyzer
|
||||
from app.services.recipe_discovery import RecipeDiscoveryService
|
||||
from app.services.recipe_ingestion import RecipeIngestionService
|
||||
from app.services.wikibooks_import import WikibooksImportService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -56,6 +55,15 @@ def _suggested_sides_html(components: dict | None) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _active_recipe_items(items: list) -> list:
|
||||
return [
|
||||
item
|
||||
for item in items
|
||||
if item.recipe is not None
|
||||
and item.recipe.catalog_status == RecipeCatalogStatus.ACTIVE
|
||||
]
|
||||
|
||||
|
||||
def step_scrape(run: "WeeklyRun", db: "Session") -> None:
|
||||
if run.scraped_at is not None:
|
||||
logger.info("step_scrape: already done for %s, skipping", run.week_start_date)
|
||||
@@ -132,6 +140,12 @@ def step_email(run: "WeeklyRun", db: "Session") -> None:
|
||||
f"No MealPlan for family {run.family_id} week {run.week_start_date}"
|
||||
)
|
||||
|
||||
pending_items = [
|
||||
item
|
||||
for item in _active_recipe_items(plan.items)
|
||||
if item.approval_status == MealPlanItemStatus.pending
|
||||
]
|
||||
|
||||
members = (
|
||||
db.query(FamilyMember)
|
||||
.filter(
|
||||
@@ -150,11 +164,10 @@ 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:
|
||||
for _ing in (_item.recipe.ingredients or []):
|
||||
if "ingredient_id" in _ing:
|
||||
ing_ids.add(_ing["ingredient_id"])
|
||||
for _item in pending_items:
|
||||
for _ing in (_item.recipe.ingredients or []):
|
||||
if "ingredient_id" in _ing:
|
||||
ing_ids.add(_ing["ingredient_id"])
|
||||
|
||||
ingredient_names: dict = {}
|
||||
if ing_ids:
|
||||
@@ -165,10 +178,8 @@ 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:
|
||||
continue
|
||||
for ing in (item.recipe.ingredients or [] if item.recipe else []):
|
||||
for item in pending_items:
|
||||
for ing in (item.recipe.ingredients or []):
|
||||
ing_name = ingredient_names.get(str(ing.get("ingredient_id", "")), ing.get("name", "unknown")).strip()
|
||||
if not ing_name or ing_name in all_ingredients:
|
||||
continue
|
||||
@@ -217,9 +228,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None:
|
||||
backend = get_email_backend()
|
||||
for member in members:
|
||||
item_html_parts = []
|
||||
for item in plan.items:
|
||||
if item.approval_status != MealPlanItemStatus.pending:
|
||||
continue
|
||||
for item in pending_items:
|
||||
token = issue_token(item.id, member.id)
|
||||
vote_url = (
|
||||
f"{settings.APP_BASE_URL}/api/meals/vote/{item.id}?token={token}"
|
||||
@@ -400,7 +409,7 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None:
|
||||
|
||||
approved_items = [
|
||||
item
|
||||
for item in (plan.items if plan else [])
|
||||
for item in _active_recipe_items(plan.items if plan else [])
|
||||
if item.approval_status == MealPlanItemStatus.approved
|
||||
]
|
||||
|
||||
@@ -524,18 +533,19 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None:
|
||||
analysis.discovery_queries and
|
||||
analysis.confidence >= 0.5
|
||||
):
|
||||
discovery = RecipeDiscoveryService()
|
||||
candidates = discovery.discover(analysis.discovery_queries)
|
||||
if candidates:
|
||||
ingestion = RecipeIngestionService()
|
||||
added = ingestion.ingest(db, run.family_id, candidates, analysis.to_dict())
|
||||
logger.info(
|
||||
"step_finalize: recipe discovery added %d new recipes for family %s",
|
||||
added, run.family_id,
|
||||
# Roll back a partial discovery batch without losing finalization.
|
||||
with db.begin_nested():
|
||||
staged = WikibooksImportService(
|
||||
settings.WIKIBOOKS_USER_AGENT
|
||||
).stage_discovered_pages(
|
||||
db,
|
||||
limit=min(5, len(analysis.discovery_queries)),
|
||||
queries=analysis.discovery_queries,
|
||||
)
|
||||
# ingestion deliberately does not commit so orchestrator
|
||||
# can keep everything in one transaction
|
||||
db.commit()
|
||||
logger.info(
|
||||
"step_finalize: staged %d Wikibooks candidates for review",
|
||||
len(staged),
|
||||
)
|
||||
except Exception as exc:
|
||||
# Discovery is best-effort; never block finalization
|
||||
logger.warning("step_finalize: recipe discovery failed: %s", exc)
|
||||
@@ -565,11 +575,12 @@ def step_reminder(run: "WeeklyRun", db: "Session") -> None:
|
||||
db.commit()
|
||||
return
|
||||
|
||||
pending_item_ids = [
|
||||
item.id
|
||||
for item in plan.items
|
||||
pending_items = [
|
||||
item
|
||||
for item in _active_recipe_items(plan.items)
|
||||
if item.approval_status == MealPlanItemStatus.pending
|
||||
]
|
||||
pending_item_ids = [item.id for item in pending_items]
|
||||
if not pending_item_ids:
|
||||
run.reminded_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
@@ -597,9 +608,8 @@ def step_reminder(run: "WeeklyRun", db: "Session") -> None:
|
||||
}
|
||||
unvoted = [
|
||||
item
|
||||
for item in plan.items
|
||||
if item.approval_status == MealPlanItemStatus.pending
|
||||
and item.id not in voted_ids
|
||||
for item in pending_items
|
||||
if item.id not in voted_ids
|
||||
]
|
||||
if not unvoted:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user