Stabilize Cloudron SQLite and refine planner suggestions
This commit is contained in:
+94
-35
@@ -4,6 +4,7 @@ from collections import defaultdict
|
||||
from datetime import date, datetime, timedelta
|
||||
from itertools import product
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
@@ -79,8 +80,10 @@ def refresh_due_context():
|
||||
if getattr(g, "user", None) is None:
|
||||
return None
|
||||
if request.method == "GET" and endpoint.startswith("main."):
|
||||
ensure_user_settings_row()
|
||||
activate_due_shopping_needs()
|
||||
try:
|
||||
activate_due_shopping_needs()
|
||||
except sqlite3.OperationalError:
|
||||
current_app.logger.warning("Due shopping needs could not be activated during this request.")
|
||||
return None
|
||||
|
||||
|
||||
@@ -172,19 +175,54 @@ def get_household_settings() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def ensure_user_settings_row() -> None:
|
||||
def default_user_settings() -> dict:
|
||||
suggestion_style = "balanced"
|
||||
return {
|
||||
"user_id": int(g.user["id"]),
|
||||
"reminders_enabled": True,
|
||||
"push_enabled": False,
|
||||
"notification_channel": "in_app",
|
||||
"suggestion_style": suggestion_style,
|
||||
"energy_preference": suggestion_style_energy_preference(suggestion_style),
|
||||
"remind_before_shopping": True,
|
||||
"remind_on_shopping_day": True,
|
||||
"show_missing_for_upcoming_week": True,
|
||||
"show_planned_not_shopped": True,
|
||||
"remind_tomorrow_if_sparse": True,
|
||||
"remind_week_if_sparse": True,
|
||||
"push_missing_breakfast": False,
|
||||
"push_missing_lunch": False,
|
||||
"push_missing_dinner": False,
|
||||
"suggest_home_for_today": True,
|
||||
"remind_small_snack": False,
|
||||
"remind_nuts": False,
|
||||
"show_meal_balancing": True,
|
||||
"suggest_templates": True,
|
||||
"suggest_patterns": True,
|
||||
}
|
||||
|
||||
|
||||
def ensure_user_settings_row(*, commit: bool = False) -> None:
|
||||
existing = get_db().execute(
|
||||
"SELECT 1 FROM user_settings WHERE user_id = ? LIMIT 1",
|
||||
(g.user["id"],),
|
||||
).fetchone()
|
||||
if existing is not None:
|
||||
return
|
||||
get_db().execute(
|
||||
"INSERT OR IGNORE INTO user_settings (user_id) VALUES (?)",
|
||||
"INSERT INTO user_settings (user_id) VALUES (?)",
|
||||
(g.user["id"],),
|
||||
)
|
||||
if commit:
|
||||
get_db().commit()
|
||||
|
||||
|
||||
def get_user_settings() -> dict:
|
||||
ensure_user_settings_row()
|
||||
settings = default_user_settings()
|
||||
row = get_db().execute("SELECT * FROM user_settings WHERE user_id = ?", (g.user["id"],)).fetchone()
|
||||
if row is None:
|
||||
return {}
|
||||
settings = dict(row)
|
||||
return settings
|
||||
settings.update(dict(row))
|
||||
boolean_fields = {
|
||||
"reminders_enabled",
|
||||
"push_enabled",
|
||||
@@ -1239,57 +1277,76 @@ def build_generated_meal_name(combo: list[dict], daypart_slug: str) -> str:
|
||||
|
||||
|
||||
def build_dynamic_meal_suggestions(home_foods: list[dict], daypart_slug: str, limit: int) -> list[dict]:
|
||||
builder_groups: dict[str, list[dict]] = defaultdict(list)
|
||||
for food in home_foods:
|
||||
for builder_key in food.get("builder_keys", ["neutral"]):
|
||||
builder_groups[builder_key].append(food)
|
||||
|
||||
if daypart_slug in {"breakfast", "morning-snack", "afternoon-snack", "late-snack"}:
|
||||
target_patterns = [
|
||||
("carb", "dairy", "fruit"),
|
||||
("carb", "dairy", "nuts"),
|
||||
("carb", "dairy", "seeds"),
|
||||
("carb", "fruit", "dairy"),
|
||||
{
|
||||
"slots": ({"carb"}, {"dairy", "protein"}, {"fruit", "nuts", "seeds"}),
|
||||
"reason": "Passt gut zu Frühstück oder Snack",
|
||||
},
|
||||
{
|
||||
"slots": ({"carb"}, {"dairy", "protein"}),
|
||||
"reason": "Zuhause schnell kombinierbar",
|
||||
},
|
||||
{
|
||||
"slots": ({"dairy", "protein"}, {"fruit", "nuts", "seeds"}),
|
||||
"reason": "Lässt sich gut als kleiner Snack vormerken",
|
||||
},
|
||||
]
|
||||
reasons = {
|
||||
("carb", "dairy", "fruit"): "Passt gut zu Frühstück oder Snack",
|
||||
("carb", "dairy", "nuts"): "Lässt sich gut für einen Snack vormerken",
|
||||
("carb", "dairy", "seeds"): "Lässt sich gut für einen Snack vormerken",
|
||||
("carb", "fruit", "dairy"): "Zuhause gut kombinierbar",
|
||||
}
|
||||
else:
|
||||
target_patterns = [
|
||||
("protein", "carb", "veg"),
|
||||
("protein", "carb"),
|
||||
{
|
||||
"slots": ({"protein"}, {"carb"}, {"veg"}),
|
||||
"reason": "Zuhause als vollständige Mahlzeit möglich",
|
||||
},
|
||||
{
|
||||
"slots": ({"protein"}, {"carb"}),
|
||||
"reason": "Lässt sich leicht ergänzen",
|
||||
},
|
||||
{
|
||||
"slots": ({"protein"}, {"veg"}),
|
||||
"reason": "Zuhause schon gut kombinierbar",
|
||||
},
|
||||
{
|
||||
"slots": ({"carb"}, {"veg"}),
|
||||
"reason": "Daraus kann schnell etwas Einfaches werden",
|
||||
},
|
||||
]
|
||||
reasons = {
|
||||
("protein", "carb", "veg"): "Zuhause als vollständige Mahlzeit möglich",
|
||||
("protein", "carb"): "Lässt sich leicht ergänzen",
|
||||
}
|
||||
|
||||
suggestions: list[dict] = []
|
||||
seen_signatures: set[tuple[int, ...]] = set()
|
||||
|
||||
def slot_matches(food: dict, slot_keys: set[str]) -> bool:
|
||||
return bool(slot_keys & set(food.get("builder_keys", ["neutral"])))
|
||||
|
||||
for pattern in target_patterns:
|
||||
groups = [builder_groups.get(builder_key, []) for builder_key in pattern]
|
||||
if any(not group for group in groups):
|
||||
slot_candidates = []
|
||||
for slot_keys in pattern["slots"]:
|
||||
matches = [food for food in home_foods if slot_matches(food, slot_keys)]
|
||||
if not matches:
|
||||
slot_candidates = []
|
||||
break
|
||||
slot_candidates.append(matches)
|
||||
if not slot_candidates:
|
||||
continue
|
||||
for combo in product(*groups):
|
||||
|
||||
for combo in product(*slot_candidates):
|
||||
signature = normalized_component_signature([item["id"] for item in combo])
|
||||
if len(signature) != len(pattern) or signature in seen_signatures:
|
||||
if len(signature) != len(combo) or signature in seen_signatures:
|
||||
continue
|
||||
seen_signatures.add(signature)
|
||||
combo_items = list(combo)
|
||||
suggestions.append(
|
||||
{
|
||||
"title": build_generated_meal_name(combo_items, daypart_slug),
|
||||
"reason": reasons.get(pattern, "Zuhause gut kombinierbar"),
|
||||
"reason": pattern["reason"],
|
||||
"component_ids": [item["id"] for item in combo_items],
|
||||
"existing_item_id": None,
|
||||
}
|
||||
)
|
||||
if len(suggestions) >= limit:
|
||||
return suggestions
|
||||
if len(suggestions) >= limit * 3:
|
||||
break
|
||||
if len(suggestions) >= limit * 3:
|
||||
break
|
||||
return suggestions
|
||||
|
||||
|
||||
@@ -1719,6 +1776,7 @@ def build_day_planner_sections(
|
||||
+ [item for item in candidates if item["kind"] == "food"],
|
||||
limit=20,
|
||||
)
|
||||
search_candidates = dedupe_items(meal_candidates + food_candidates, limit=24)
|
||||
entry_item_ids = [int(entry["item_id"]) for entry in entries]
|
||||
sections.append(
|
||||
{
|
||||
@@ -1727,6 +1785,7 @@ def build_day_planner_sections(
|
||||
"candidates": candidates,
|
||||
"meal_candidates": meal_candidates,
|
||||
"food_candidates": food_candidates,
|
||||
"search_candidates": search_candidates,
|
||||
"recipe_suggestions": build_home_recipe_suggestions(int(daypart["id"]), limit=3),
|
||||
"suggestions": build_daypart_suggestions(daypart["id"]),
|
||||
"balance_suggestion": build_balance_suggestion(int(daypart["id"]), entry_item_ids),
|
||||
|
||||
Reference in New Issue
Block a user