Filter meal suggestions by simple flavor profiles
This commit is contained in:
+72
-26
@@ -33,6 +33,9 @@ from .constants import (
|
||||
DEFAULT_CATEGORIES,
|
||||
ENERGY_DENSITY_LABELS,
|
||||
ENERGY_DENSITY_OPTIONS,
|
||||
FOOD_FLAVOR_DESCRIPTIONS,
|
||||
FOOD_FLAVOR_LABELS,
|
||||
FOOD_FLAVOR_OPTIONS,
|
||||
FOOD_ROLE_DESCRIPTIONS,
|
||||
FOOD_ROLE_LABELS,
|
||||
FOOD_ROLE_OPTIONS,
|
||||
@@ -311,6 +314,11 @@ def normalize_base_type(raw: str | None, default: str = "neutral") -> str:
|
||||
return raw if raw in allowed else default
|
||||
|
||||
|
||||
def normalize_food_flavor(raw: str | None, default: str = "neutral") -> str:
|
||||
allowed = {value for value, _label in FOOD_FLAVOR_OPTIONS}
|
||||
return raw if raw in allowed else default
|
||||
|
||||
|
||||
def normalize_food_role(raw: str | None, default: str = "base") -> str:
|
||||
allowed = {value for value, _label in FOOD_ROLE_OPTIONS}
|
||||
return raw if raw in allowed else default
|
||||
@@ -428,6 +436,8 @@ def describe_record(entry: dict) -> dict:
|
||||
entry["energy_density_label"] = ENERGY_DENSITY_LABELS.get(entry["energy_density"], ENERGY_DENSITY_LABELS["neutral"])
|
||||
entry["base_type"] = normalize_base_type(entry.get("base_type"), "neutral")
|
||||
entry["base_type_label"] = BUILDER_LABELS.get(entry["base_type"], BUILDER_LABELS["neutral"])
|
||||
entry["flavor_profile"] = normalize_food_flavor(entry.get("flavor_profile"), "neutral")
|
||||
entry["flavor_profile_label"] = FOOD_FLAVOR_LABELS.get(entry["flavor_profile"], FOOD_FLAVOR_LABELS["neutral"])
|
||||
entry["suggestion_role"] = normalize_food_role(entry.get("suggestion_role"), "base")
|
||||
entry["suggestion_role_label"] = FOOD_ROLE_LABELS.get(entry["suggestion_role"], FOOD_ROLE_LABELS["base"])
|
||||
entry["suggestion_priority"] = normalize_suggestion_priority(entry.get("suggestion_priority"), "normal")
|
||||
@@ -714,12 +724,29 @@ def food_supports_slot(food: dict, slot: dict) -> bool:
|
||||
|
||||
base_type = normalize_base_type(food.get("base_type"), "neutral")
|
||||
accepted = set(slot.get("base_types", set()))
|
||||
if "fiber" in accepted and base_type in {"veg", "fruit"}:
|
||||
matches_base_type = ("fiber" in accepted and base_type in {"veg", "fruit"}) or base_type in accepted
|
||||
if not matches_base_type:
|
||||
return False
|
||||
|
||||
accepted_flavors = set(slot.get("flavors", set()))
|
||||
if not accepted_flavors:
|
||||
return True
|
||||
return base_type in accepted
|
||||
return normalize_food_flavor(food.get("flavor_profile"), "neutral") in accepted_flavors
|
||||
|
||||
|
||||
def components_are_flavor_compatible(component_items: list[dict]) -> bool:
|
||||
flavors = {
|
||||
normalize_food_flavor(item.get("flavor_profile"), "neutral")
|
||||
for item in component_items
|
||||
if meaningful_component(item)
|
||||
}
|
||||
return not ({"sweet", "savory"} <= flavors)
|
||||
|
||||
|
||||
def score_suggestion_components(component_items: list[dict], daypart_slug: str, settings: dict) -> int:
|
||||
if not components_are_flavor_compatible(component_items):
|
||||
return -999
|
||||
|
||||
meaningful_items = [item for item in component_items if meaningful_component(item)]
|
||||
builder_keys = {key for item in meaningful_items for key in item.get("builder_keys", ["neutral"])}
|
||||
energy_values = [normalize_energy_density(item.get("energy_density"), "neutral") for item in component_items]
|
||||
@@ -857,6 +884,7 @@ def extract_item_form_data(existing: dict | None = None) -> dict:
|
||||
"name": request.form.get("name", "").strip(),
|
||||
"category": request.form.get("category", "").strip(),
|
||||
"base_type": normalize_base_type(request.form.get("base_type"), form_data.get("base_type", "neutral")),
|
||||
"flavor_profile": normalize_food_flavor(request.form.get("flavor_profile"), form_data.get("flavor_profile", "neutral")),
|
||||
"suggestion_role": normalize_food_role(request.form.get("suggestion_role"), form_data.get("suggestion_role", "base")),
|
||||
"suggestion_priority": normalize_suggestion_priority(
|
||||
request.form.get("suggestion_priority"),
|
||||
@@ -879,6 +907,10 @@ def extract_item_form_data(existing: dict | None = None) -> dict:
|
||||
request.form.get("quick_food_base_type"),
|
||||
form_data.get("quick_food_base_type", "neutral"),
|
||||
),
|
||||
"quick_food_flavor_profile": normalize_food_flavor(
|
||||
request.form.get("quick_food_flavor_profile"),
|
||||
form_data.get("quick_food_flavor_profile", "neutral"),
|
||||
),
|
||||
"quick_food_role": normalize_food_role(
|
||||
request.form.get("quick_food_role"),
|
||||
form_data.get("quick_food_role", "base"),
|
||||
@@ -899,9 +931,9 @@ def create_quick_food_from_form(form_data: dict) -> int:
|
||||
cursor = get_db().execute(
|
||||
"""
|
||||
INSERT INTO items (
|
||||
household_id, owner_user_id, target_user_id, visibility, kind, name, category, base_type, suggestion_role, suggestion_priority, can_be_meal_core, energy_density, note, created_by, updated_by
|
||||
household_id, owner_user_id, target_user_id, visibility, kind, name, category, base_type, flavor_profile, suggestion_role, suggestion_priority, can_be_meal_core, energy_density, note, created_by, updated_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, 'food', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, 'food', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
current_household_id(),
|
||||
@@ -911,6 +943,7 @@ def create_quick_food_from_form(form_data: dict) -> int:
|
||||
form_data["quick_food_name"],
|
||||
form_data["quick_food_category"],
|
||||
form_data["quick_food_base_type"],
|
||||
form_data["quick_food_flavor_profile"],
|
||||
form_data["quick_food_role"],
|
||||
form_data["quick_food_priority"],
|
||||
1 if form_data["quick_food_can_be_meal_core"] else 0,
|
||||
@@ -1522,17 +1555,17 @@ def meal_pattern_definitions(daypart_slug: str) -> list[dict]:
|
||||
{
|
||||
"reason": "Passt gut zu Frühstück",
|
||||
"slots": [
|
||||
{"base_types": {"carb"}, "roles": {"base", "main", "solo"}, "core_only": True},
|
||||
{"base_types": {"dairy", "protein"}, "roles": {"base", "main", "complement", "solo", "snack"}, "core_only": False},
|
||||
{"base_types": {"fruit"}, "roles": {"complement", "topping", "snack", "base"}, "core_only": False},
|
||||
{"base_types": {"carb"}, "roles": {"base", "main", "solo"}, "core_only": True, "flavors": {"sweet", "neutral"}},
|
||||
{"base_types": {"dairy", "protein"}, "roles": {"base", "main", "complement", "solo", "snack"}, "core_only": False, "flavors": {"sweet", "neutral"}},
|
||||
{"base_types": {"fruit"}, "roles": {"complement", "topping", "snack", "base"}, "core_only": False, "flavors": {"sweet", "neutral"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"reason": "Passt gut für Frühstück",
|
||||
"slots": [
|
||||
{"base_types": {"dairy"}, "roles": {"base", "main", "solo", "snack"}, "core_only": True},
|
||||
{"base_types": {"carb"}, "roles": {"base", "main", "complement", "solo"}, "core_only": False},
|
||||
{"base_types": {"nuts", "seeds", "fruit"}, "roles": {"topping", "complement", "snack"}, "core_only": False},
|
||||
{"base_types": {"dairy"}, "roles": {"base", "main", "solo", "snack"}, "core_only": True, "flavors": {"sweet", "neutral"}},
|
||||
{"base_types": {"carb"}, "roles": {"base", "main", "complement", "solo"}, "core_only": False, "flavors": {"sweet", "neutral"}},
|
||||
{"base_types": {"nuts", "seeds", "fruit"}, "roles": {"topping", "complement", "snack"}, "core_only": False, "flavors": {"sweet", "neutral"}},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1542,22 +1575,22 @@ def meal_pattern_definitions(daypart_slug: str) -> list[dict]:
|
||||
{
|
||||
"reason": "Passt gut zu einem kleinen Snack",
|
||||
"slots": [
|
||||
{"base_types": {"dairy"}, "roles": {"base", "solo", "snack"}, "core_only": True},
|
||||
{"base_types": {"fruit"}, "roles": {"complement", "snack", "topping"}, "core_only": False},
|
||||
{"base_types": {"dairy"}, "roles": {"base", "solo", "snack"}, "core_only": True, "flavors": {"sweet", "neutral"}},
|
||||
{"base_types": {"fruit"}, "roles": {"complement", "snack", "topping"}, "core_only": False, "flavors": {"sweet", "neutral"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"reason": "Zuhause schnell kombinierbar",
|
||||
"slots": [
|
||||
{"base_types": {"fruit"}, "roles": {"base", "snack", "complement"}, "core_only": True},
|
||||
{"base_types": {"nuts", "seeds"}, "roles": {"topping", "snack", "complement"}, "core_only": False},
|
||||
{"base_types": {"fruit"}, "roles": {"base", "snack", "complement"}, "core_only": True, "flavors": {"sweet", "neutral"}},
|
||||
{"base_types": {"nuts", "seeds"}, "roles": {"topping", "snack", "complement"}, "core_only": False, "flavors": {"sweet", "neutral"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"reason": "Passt gut zu einem kleinen Snack",
|
||||
"slots": [
|
||||
{"base_types": {"carb"}, "roles": {"solo", "base", "snack"}, "core_only": True},
|
||||
{"base_types": {"protein", "dairy"}, "roles": {"complement", "snack", "base"}, "core_only": False},
|
||||
{"base_types": {"carb"}, "roles": {"solo", "base", "snack"}, "core_only": True, "flavors": {"sweet", "neutral"}},
|
||||
{"base_types": {"protein", "dairy"}, "roles": {"complement", "snack", "base"}, "core_only": False, "flavors": {"sweet", "neutral"}},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1566,24 +1599,24 @@ def meal_pattern_definitions(daypart_slug: str) -> list[dict]:
|
||||
{
|
||||
"reason": "Zuhause als vollständige Mahlzeit möglich",
|
||||
"slots": [
|
||||
{"base_types": {"protein"}, "roles": {"main", "base", "solo"}, "core_only": True},
|
||||
{"base_types": {"carb"}, "roles": {"base", "main", "solo"}, "core_only": True},
|
||||
{"base_types": {"fiber"}, "roles": {"complement", "base", "main"}, "core_only": False},
|
||||
{"base_types": {"protein"}, "roles": {"main", "base", "solo"}, "core_only": True, "flavors": {"savory", "neutral"}},
|
||||
{"base_types": {"carb"}, "roles": {"base", "main", "solo"}, "core_only": True, "flavors": {"savory", "neutral"}},
|
||||
{"base_types": {"fiber"}, "roles": {"complement", "base", "main"}, "core_only": False, "flavors": {"savory", "neutral"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"reason": "Lässt sich gut ergänzen",
|
||||
"slots": [
|
||||
{"base_types": {"protein"}, "roles": {"main", "base", "solo"}, "core_only": True},
|
||||
{"base_types": {"fiber"}, "roles": {"complement", "base", "main"}, "core_only": False},
|
||||
{"base_types": {"carb"}, "roles": {"base", "complement", "solo"}, "core_only": False},
|
||||
{"base_types": {"protein"}, "roles": {"main", "base", "solo"}, "core_only": True, "flavors": {"savory", "neutral"}},
|
||||
{"base_types": {"fiber"}, "roles": {"complement", "base", "main"}, "core_only": False, "flavors": {"savory", "neutral"}},
|
||||
{"base_types": {"carb"}, "roles": {"base", "complement", "solo"}, "core_only": False, "flavors": {"savory", "neutral"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"reason": "Schnell und alltagstauglich",
|
||||
"slots": [
|
||||
{"base_types": {"carb", "protein"}, "roles": {"solo"}, "core_only": True},
|
||||
{"base_types": {"fiber"}, "roles": {"complement", "base", "main"}, "core_only": False},
|
||||
{"base_types": {"carb", "protein"}, "roles": {"solo"}, "core_only": True, "flavors": {"savory", "neutral"}},
|
||||
{"base_types": {"fiber"}, "roles": {"complement", "base", "main"}, "core_only": False, "flavors": {"savory", "neutral"}},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1629,6 +1662,8 @@ def build_dynamic_meal_suggestions(home_foods: list[dict], daypart_slug: str, li
|
||||
continue
|
||||
seen_signatures.add(signature)
|
||||
combo_items = list(combo)
|
||||
if not components_are_flavor_compatible(combo_items):
|
||||
continue
|
||||
suggestions.append(
|
||||
{
|
||||
"title": build_generated_meal_name(combo_items, daypart_slug),
|
||||
@@ -1685,6 +1720,8 @@ def build_home_recipe_suggestions(daypart_id: int | None = None, limit: int = 4)
|
||||
continue
|
||||
|
||||
component_items = [visible_food_map[component_id] for component_id in component_ids]
|
||||
if not components_are_flavor_compatible(component_items):
|
||||
continue
|
||||
available_items = [home_food_map[component_id] for component_id in component_ids if component_id in home_food_map]
|
||||
missing_items = [visible_food_map[component_id] for component_id in component_ids if component_id not in home_food_ids]
|
||||
|
||||
@@ -2851,6 +2888,8 @@ def render_item_form(kind: str, *, item: dict | None, form_data: dict):
|
||||
),
|
||||
form_data=form_data,
|
||||
builder_options=[(key, label) for key, label in BUILDER_LABELS.items()],
|
||||
food_flavor_options=FOOD_FLAVOR_OPTIONS,
|
||||
food_flavor_descriptions=FOOD_FLAVOR_DESCRIPTIONS,
|
||||
food_role_options=FOOD_ROLE_OPTIONS,
|
||||
food_role_descriptions=FOOD_ROLE_DESCRIPTIONS,
|
||||
suggestion_priority_options=SUGGESTION_PRIORITY_OPTIONS,
|
||||
@@ -3684,6 +3723,7 @@ def item_create(kind: str):
|
||||
"name": request.args.get("name", "").strip(),
|
||||
"category": "",
|
||||
"base_type": "neutral",
|
||||
"flavor_profile": "neutral",
|
||||
"suggestion_role": "base",
|
||||
"suggestion_priority": "normal",
|
||||
"can_be_meal_core": False,
|
||||
@@ -3700,6 +3740,7 @@ def item_create(kind: str):
|
||||
"quick_food_name": "",
|
||||
"quick_food_category": "",
|
||||
"quick_food_base_type": "neutral",
|
||||
"quick_food_flavor_profile": "neutral",
|
||||
"quick_food_role": "base",
|
||||
"quick_food_priority": "normal",
|
||||
"quick_food_can_be_meal_core": False,
|
||||
@@ -3742,9 +3783,9 @@ def item_create(kind: str):
|
||||
cursor = get_db().execute(
|
||||
"""
|
||||
INSERT INTO items (
|
||||
household_id, owner_user_id, target_user_id, visibility, kind, name, category, base_type, suggestion_role, suggestion_priority, can_be_meal_core, meal_type, meal_tags, energy_density, note, photo_filename, created_by, updated_by
|
||||
household_id, owner_user_id, target_user_id, visibility, kind, name, category, base_type, flavor_profile, suggestion_role, suggestion_priority, can_be_meal_core, meal_type, meal_tags, energy_density, note, photo_filename, created_by, updated_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
current_household_id(),
|
||||
@@ -3755,6 +3796,7 @@ def item_create(kind: str):
|
||||
form_data["name"],
|
||||
form_data["category"] if kind == "food" else None,
|
||||
form_data["base_type"] if kind == "food" else "neutral",
|
||||
form_data["flavor_profile"] if kind == "food" else "neutral",
|
||||
form_data["suggestion_role"] if kind == "food" else "base",
|
||||
form_data["suggestion_priority"] if kind == "food" else "normal",
|
||||
1 if (form_data["can_be_meal_core"] if kind == "food" else False) else 0,
|
||||
@@ -3793,6 +3835,7 @@ def item_edit(item_id: int):
|
||||
"name": item["name"],
|
||||
"category": item["category"] or "",
|
||||
"base_type": item.get("base_type") or "neutral",
|
||||
"flavor_profile": item.get("flavor_profile") or "neutral",
|
||||
"suggestion_role": item.get("suggestion_role") or "base",
|
||||
"suggestion_priority": item.get("suggestion_priority") or "normal",
|
||||
"can_be_meal_core": bool(item.get("can_be_meal_core")),
|
||||
@@ -3809,6 +3852,7 @@ def item_edit(item_id: int):
|
||||
"quick_food_name": "",
|
||||
"quick_food_category": "",
|
||||
"quick_food_base_type": "neutral",
|
||||
"quick_food_flavor_profile": "neutral",
|
||||
"quick_food_role": "base",
|
||||
"quick_food_priority": "normal",
|
||||
"quick_food_can_be_meal_core": False,
|
||||
@@ -3854,6 +3898,7 @@ def item_edit(item_id: int):
|
||||
SET name = ?,
|
||||
category = ?,
|
||||
base_type = ?,
|
||||
flavor_profile = ?,
|
||||
suggestion_role = ?,
|
||||
suggestion_priority = ?,
|
||||
can_be_meal_core = ?,
|
||||
@@ -3872,6 +3917,7 @@ def item_edit(item_id: int):
|
||||
form_data["name"],
|
||||
form_data["category"] if item["kind"] == "food" else None,
|
||||
form_data["base_type"] if item["kind"] == "food" else "neutral",
|
||||
form_data["flavor_profile"] if item["kind"] == "food" else "neutral",
|
||||
form_data["suggestion_role"] if item["kind"] == "food" else "base",
|
||||
form_data["suggestion_priority"] if item["kind"] == "food" else "normal",
|
||||
1 if (form_data["can_be_meal_core"] if item["kind"] == "food" else False) else 0,
|
||||
|
||||
Reference in New Issue
Block a user