mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-03 04:25:24 +02:00
Feature/automated meal planner (#939)
* cleanup oversized buttons * add get all by category function to reciep repos * fix shopping-list can_merge logic * use randomized data for testing * add random getter to repository for meal-planner * add stub route for random meals * cleanup global namespace * add rules database type * fix type * add plan rules schema * test plan rules methods * add mealplan rules controller * add new repository * update frontend types * formatting * fix regression * update autogenerated types * add api class for mealplan rules * add tests and fix bugs * fix data returns * proof of concept rules editor * add tag support * remove old group categories * add tag support * implement random by rules api * change snack to sides * remove incorrect typing * split repo for custom methods * fix query and use and_ clause * use repo function * remove old test * update changelog
This commit is contained in:
parent
40d1f586cd
commit
d1024e272d
43 changed files with 1153 additions and 175 deletions
|
@ -0,0 +1,127 @@
|
|||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.repos.all_repositories import AllRepositories
|
||||
from mealie.schema.meal_plan.plan_rules import PlanRulesOut, PlanRulesSave
|
||||
from mealie.schema.recipe.recipe import RecipeCategory
|
||||
from tests import utils
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
class Routes:
|
||||
base = "/api/groups/mealplans/rules"
|
||||
|
||||
@staticmethod
|
||||
def item(item_id: UUID4) -> str:
|
||||
return f"{Routes.base}/{item_id}"
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def category(database: AllRepositories):
|
||||
slug = utils.random_string(length=10)
|
||||
model = database.categories.create(RecipeCategory(slug=slug, name=slug))
|
||||
|
||||
yield model
|
||||
|
||||
try:
|
||||
database.categories.delete(model.slug)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def plan_rule(database: AllRepositories, unique_user: TestUser):
|
||||
schema = PlanRulesSave(
|
||||
group_id=unique_user.group_id,
|
||||
day="monday",
|
||||
entry_type="breakfast",
|
||||
categories=[],
|
||||
)
|
||||
|
||||
model = database.group_meal_plan_rules.create(schema)
|
||||
|
||||
yield model
|
||||
|
||||
try:
|
||||
database.group_meal_plan_rules.delete(model.id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_group_mealplan_rules_create(
|
||||
api_client: TestClient, unique_user: TestUser, category: RecipeCategory, database: AllRepositories
|
||||
):
|
||||
payload = {
|
||||
"groupId": unique_user.group_id,
|
||||
"day": "monday",
|
||||
"entryType": "breakfast",
|
||||
"categories": [category.dict()],
|
||||
}
|
||||
|
||||
response = api_client.post(Routes.base, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Validate the response data
|
||||
response_data = response.json()
|
||||
assert response_data["groupId"] == str(unique_user.group_id)
|
||||
assert response_data["day"] == "monday"
|
||||
assert response_data["entryType"] == "breakfast"
|
||||
assert len(response_data["categories"]) == 1
|
||||
assert response_data["categories"][0]["slug"] == category.slug
|
||||
|
||||
# Validate database entry
|
||||
rule = database.group_meal_plan_rules.get_one(UUID(response_data["id"]))
|
||||
|
||||
assert str(rule.group_id) == unique_user.group_id
|
||||
assert rule.day == "monday"
|
||||
assert rule.entry_type == "breakfast"
|
||||
assert len(rule.categories) == 1
|
||||
assert rule.categories[0].slug == category.slug
|
||||
|
||||
# Cleanup
|
||||
database.group_meal_plan_rules.delete(rule.id)
|
||||
|
||||
|
||||
def test_group_mealplan_rules_read(api_client: TestClient, unique_user: TestUser, plan_rule: PlanRulesOut):
|
||||
response = api_client.get(Routes.item(plan_rule.id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate the response data
|
||||
response_data = response.json()
|
||||
assert response_data["id"] == str(plan_rule.id)
|
||||
assert response_data["groupId"] == str(unique_user.group_id)
|
||||
assert response_data["day"] == "monday"
|
||||
assert response_data["entryType"] == "breakfast"
|
||||
assert len(response_data["categories"]) == 0
|
||||
|
||||
|
||||
def test_group_mealplan_rules_update(api_client: TestClient, unique_user: TestUser, plan_rule: PlanRulesOut):
|
||||
payload = {
|
||||
"groupId": unique_user.group_id,
|
||||
"day": "tuesday",
|
||||
"entryType": "lunch",
|
||||
}
|
||||
|
||||
response = api_client.put(Routes.item(plan_rule.id), json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate the response data
|
||||
response_data = response.json()
|
||||
assert response_data["id"] == str(plan_rule.id)
|
||||
assert response_data["groupId"] == str(unique_user.group_id)
|
||||
assert response_data["day"] == "tuesday"
|
||||
assert response_data["entryType"] == "lunch"
|
||||
assert len(response_data["categories"]) == 0
|
||||
|
||||
|
||||
def test_group_mealplan_rules_delete(
|
||||
api_client: TestClient, unique_user: TestUser, plan_rule: PlanRulesOut, database: AllRepositories
|
||||
):
|
||||
response = api_client.delete(Routes.item(plan_rule.id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate no entry in database
|
||||
assert database.group_meal_plan_rules.get_one(plan_rule.id) is None
|
115
tests/unit_tests/repository_tests/test_recipe_repository.py
Normal file
115
tests/unit_tests/repository_tests/test_recipe_repository.py
Normal file
|
@ -0,0 +1,115 @@
|
|||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.repos.repository_recipes import RepositoryRecipes
|
||||
from mealie.schema.recipe.recipe import Recipe, RecipeCategory
|
||||
from tests.utils.factories import random_string
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
def test_recipe_repo_get_by_categories_basic(database: AllRepositories, unique_user: TestUser):
|
||||
# Bootstrap the database with categories
|
||||
slug1, slug2, slug3 = [random_string(10) for _ in range(3)]
|
||||
|
||||
categories = [
|
||||
RecipeCategory(name=slug1, slug=slug1),
|
||||
RecipeCategory(name=slug2, slug=slug2),
|
||||
RecipeCategory(name=slug3, slug=slug3),
|
||||
]
|
||||
|
||||
created_categories = []
|
||||
|
||||
for category in categories:
|
||||
model = database.categories.create(category)
|
||||
created_categories.append(model)
|
||||
|
||||
# Bootstrap the database with recipes
|
||||
recipes = []
|
||||
|
||||
for idx in range(15):
|
||||
if idx % 3 == 0:
|
||||
category = created_categories[0]
|
||||
elif idx % 3 == 1:
|
||||
category = created_categories[1]
|
||||
else:
|
||||
category = created_categories[2]
|
||||
|
||||
recipes.append(
|
||||
Recipe(
|
||||
user_id=unique_user.user_id,
|
||||
group_id=unique_user.group_id,
|
||||
name=random_string(),
|
||||
recipe_category=[category],
|
||||
),
|
||||
)
|
||||
|
||||
created_recipes = []
|
||||
|
||||
for recipe in recipes:
|
||||
models = database.recipes.create(recipe)
|
||||
created_recipes.append(models)
|
||||
|
||||
# Get all recipes by category
|
||||
|
||||
for category in created_categories:
|
||||
repo: RepositoryRecipes = database.recipes.by_group(unique_user.group_id)
|
||||
recipes = repo.get_by_categories([category])
|
||||
|
||||
assert len(recipes) == 5
|
||||
|
||||
for recipe in recipes:
|
||||
found_cat = recipe.recipe_category[0]
|
||||
|
||||
assert found_cat.name == category.name
|
||||
assert found_cat.slug == category.slug
|
||||
assert found_cat.id == category.id
|
||||
|
||||
|
||||
def test_recipe_repo_get_by_categories_multi(database: AllRepositories, unique_user: TestUser):
|
||||
slug1, slug2 = [random_string(10) for _ in range(2)]
|
||||
|
||||
categories = [
|
||||
RecipeCategory(name=slug1, slug=slug1),
|
||||
RecipeCategory(name=slug2, slug=slug2),
|
||||
]
|
||||
|
||||
created_categories = []
|
||||
known_category_ids = []
|
||||
|
||||
for category in categories:
|
||||
model = database.categories.create(category)
|
||||
created_categories.append(model)
|
||||
known_category_ids.append(model.id)
|
||||
|
||||
# Bootstrap the database with recipes
|
||||
recipes = []
|
||||
|
||||
for _ in range(10):
|
||||
recipes.append(
|
||||
Recipe(
|
||||
user_id=unique_user.user_id,
|
||||
group_id=unique_user.group_id,
|
||||
name=random_string(),
|
||||
recipe_category=created_categories,
|
||||
),
|
||||
)
|
||||
|
||||
# Insert Non-Category Recipes
|
||||
recipes.append(
|
||||
Recipe(
|
||||
user_id=unique_user.user_id,
|
||||
group_id=unique_user.group_id,
|
||||
name=random_string(),
|
||||
)
|
||||
)
|
||||
|
||||
for recipe in recipes:
|
||||
database.recipes.create(recipe)
|
||||
|
||||
# Get all recipes by both categories
|
||||
repo: RepositoryRecipes = database.recipes.by_group(unique_user.group_id)
|
||||
by_category = repo.get_by_categories(created_categories)
|
||||
|
||||
assert len(by_category) == 10
|
||||
|
||||
for recipe in by_category:
|
||||
for category in recipe.recipe_category:
|
||||
assert category.id in known_category_ids
|
20
tests/unit_tests/schema_tests/test_meal_plan.py
Normal file
20
tests/unit_tests/schema_tests/test_meal_plan.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from mealie.schema.meal_plan.plan_rules import PlanRulesDay
|
||||
|
||||
test_cases = [
|
||||
(datetime(2022, 2, 7), PlanRulesDay.monday),
|
||||
(datetime(2022, 2, 8), PlanRulesDay.tuesday),
|
||||
(datetime(2022, 2, 9), PlanRulesDay.wednesday),
|
||||
(datetime(2022, 2, 10), PlanRulesDay.thursday),
|
||||
(datetime(2022, 2, 11), PlanRulesDay.friday),
|
||||
(datetime(2022, 2, 12), PlanRulesDay.saturday),
|
||||
(datetime(2022, 2, 13), PlanRulesDay.sunday),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("date, expected", test_cases)
|
||||
def test_date_obj_to_enum(date: datetime, expected: PlanRulesDay):
|
||||
assert PlanRulesDay.from_date(date) == expected
|
Loading…
Add table
Add a link
Reference in a new issue