Public Access
feat: add cooking instructions to vote email; shopping list grouped by meal; fix current_price field
- Vote email: recipe cards now include a collapsible <details> block with numbered cooking steps (recipe.instructions ARRAY) - Shopping list email: ingredients now grouped under each meal heading instead of a flat deduplicated list - step_finalize: fix grocery price lookup (.price -> .current_price) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -223,6 +223,21 @@ def step_email(run: "WeeklyRun", db: "Session") -> None:
|
|||||||
if ing_rows else ""
|
if ing_rows else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Cooking instructions (collapsed by default)
|
||||||
|
instructions = item.recipe.instructions or [] if item.recipe else []
|
||||||
|
instr_rows = "".join(
|
||||||
|
f"<li style='margin-bottom:4px'>{html.escape(str(step))}</li>"
|
||||||
|
for step in instructions
|
||||||
|
)
|
||||||
|
instructions_block = (
|
||||||
|
f"<details style='margin-top:8px'>"
|
||||||
|
f"<summary style='font-size:13px;color:#374151;cursor:pointer'>"
|
||||||
|
f"Cooking steps ({len(instructions)})</summary>"
|
||||||
|
f"<ol style='margin:8px 0;padding-left:20px;font-size:13px;color:#555'>{instr_rows}</ol>"
|
||||||
|
f"</details>"
|
||||||
|
if instr_rows else ""
|
||||||
|
)
|
||||||
|
|
||||||
# Estimated cost: sum top-confidence grocery match prices
|
# Estimated cost: sum top-confidence grocery match prices
|
||||||
est_cost = 0.0
|
est_cost = 0.0
|
||||||
for ing in ingredients:
|
for ing in ingredients:
|
||||||
@@ -247,6 +262,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None:
|
|||||||
f'{img_block}'
|
f'{img_block}'
|
||||||
f'<h3 style="margin:8px 0 4px">{recipe_name}</h3>'
|
f'<h3 style="margin:8px 0 4px">{recipe_name}</h3>'
|
||||||
f'{ing_block}'
|
f'{ing_block}'
|
||||||
|
f'{instructions_block}'
|
||||||
f'{cost_block}'
|
f'{cost_block}'
|
||||||
f'<a href="{vote_url}" style="display:inline-block;margin-top:8px;padding:8px 16px;'
|
f'<a href="{vote_url}" style="display:inline-block;margin-top:8px;padding:8px 16px;'
|
||||||
f'background:#2563eb;color:white;text-decoration:none;border-radius:6px;font-size:14px">'
|
f'background:#2563eb;color:white;text-decoration:none;border-radius:6px;font-size:14px">'
|
||||||
@@ -363,21 +379,20 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None:
|
|||||||
|
|
||||||
if approved_items:
|
if approved_items:
|
||||||
total_cost = 0.0
|
total_cost = 0.0
|
||||||
rows_html = ""
|
sections_html = ""
|
||||||
seen_ingredients: set = set()
|
|
||||||
for item in approved_items:
|
for item in approved_items:
|
||||||
if not item.recipe:
|
if not item.recipe:
|
||||||
continue
|
continue
|
||||||
|
recipe_name = html.escape(item.recipe.name)
|
||||||
|
rows_html = ""
|
||||||
for ing in (item.recipe.ingredients or []):
|
for ing in (item.recipe.ingredients or []):
|
||||||
ing_id = str(ing.get("ingredient_id", ""))
|
ing_id = str(ing.get("ingredient_id", ""))
|
||||||
ing_name = _fin_names.get(ing_id, "")
|
ing_name = _fin_names.get(ing_id, "")
|
||||||
if not ing_name or ing_name in seen_ingredients:
|
if not ing_name:
|
||||||
continue
|
continue
|
||||||
seen_ingredients.add(ing_name)
|
|
||||||
qty = ing.get("qty", "")
|
qty = ing.get("qty", "")
|
||||||
unit = ing.get("unit", "")
|
unit = ing.get("unit", "")
|
||||||
|
|
||||||
# Find Lucky CA match
|
|
||||||
match = (
|
match = (
|
||||||
db.query(IngredientGroceryMatch)
|
db.query(IngredientGroceryMatch)
|
||||||
.join(Ingredient, IngredientGroceryMatch.ingredient_id == Ingredient.id)
|
.join(Ingredient, IngredientGroceryMatch.ingredient_id == Ingredient.id)
|
||||||
@@ -387,7 +402,7 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None:
|
|||||||
)
|
)
|
||||||
if match and match.grocery_item:
|
if match and match.grocery_item:
|
||||||
lucky_name = html.escape(match.grocery_item.name or "")
|
lucky_name = html.escape(match.grocery_item.name or "")
|
||||||
price = float(match.grocery_item.price or 0)
|
price = float(match.grocery_item.current_price or 0)
|
||||||
total_cost += price
|
total_cost += price
|
||||||
price_str = f"${price:.2f}"
|
price_str = f"${price:.2f}"
|
||||||
else:
|
else:
|
||||||
@@ -403,19 +418,24 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None:
|
|||||||
f"</tr>"
|
f"</tr>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sections_html += (
|
||||||
|
f"<h3 style='margin:20px 0 4px;font-size:15px'>{recipe_name}</h3>"
|
||||||
|
f"<table style='border-collapse:collapse;width:100%;font-size:13px;margin-bottom:8px'>"
|
||||||
|
f"<thead><tr style='background:#f3f4f6'>"
|
||||||
|
f"<th style='text-align:left;padding:6px 8px;border-bottom:1px solid #e5e7eb'>Ingredient</th>"
|
||||||
|
f"<th style='text-align:left;padding:6px 8px;border-bottom:1px solid #e5e7eb'>Qty</th>"
|
||||||
|
f"<th style='text-align:left;padding:6px 8px;border-bottom:1px solid #e5e7eb'>At Lucky</th>"
|
||||||
|
f"<th style='text-align:left;padding:6px 8px;border-bottom:1px solid #e5e7eb'>Price</th>"
|
||||||
|
f"</tr></thead>"
|
||||||
|
f"<tbody>{rows_html}</tbody>"
|
||||||
|
f"</table>"
|
||||||
|
)
|
||||||
|
|
||||||
email_html = (
|
email_html = (
|
||||||
f"<div style='font-family:sans-serif;max-width:600px;margin:0 auto'>"
|
f"<div style='font-family:sans-serif;max-width:600px;margin:0 auto'>"
|
||||||
f"<h2>Shopping list — week of {run.week_start_date}</h2>"
|
f"<h2>Shopping list — week of {run.week_start_date}</h2>"
|
||||||
f"<p>{len(approved_items)} meal(s) approved.</p>"
|
f"<p>{len(approved_items)} meal(s) approved.</p>"
|
||||||
f"<table style='border-collapse:collapse;width:100%;font-size:14px'>"
|
f"{sections_html}"
|
||||||
f"<thead><tr style='background:#f3f4f6'>"
|
|
||||||
f"<th style='text-align:left;padding:6px 8px;border-bottom:2px solid #e5e7eb'>Ingredient</th>"
|
|
||||||
f"<th style='text-align:left;padding:6px 8px;border-bottom:2px solid #e5e7eb'>Qty</th>"
|
|
||||||
f"<th style='text-align:left;padding:6px 8px;border-bottom:2px solid #e5e7eb'>At Lucky</th>"
|
|
||||||
f"<th style='text-align:left;padding:6px 8px;border-bottom:2px solid #e5e7eb'>Price</th>"
|
|
||||||
f"</tr></thead>"
|
|
||||||
f"<tbody>{rows_html}</tbody>"
|
|
||||||
f"</table>"
|
|
||||||
f"<p style='margin-top:16px;font-weight:bold'>Estimated total: ${total_cost:.2f}</p>"
|
f"<p style='margin-top:16px;font-weight:bold'>Estimated total: ${total_cost:.2f}</p>"
|
||||||
f"</div>"
|
f"</div>"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user