1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-06 05:55:23 +02:00

feat: Query Filter Builder for Cookbooks and Meal Plans (#4346)

This commit is contained in:
Michael Genson 2024-10-17 10:35:39 -05:00 committed by GitHub
parent 2a9a6fa5e6
commit b8e62ab8dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 2043 additions and 440 deletions

View file

@ -21,7 +21,7 @@ def get_page_data(group_id: UUID | str, household_id: UUID4 | str):
"slug": name_and_slug,
"description": "",
"position": 0,
"categories": [],
"query_filter_string": "",
"group_id": str(group_id),
"household_id": str(household_id),
}
@ -143,3 +143,42 @@ def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbook
response = api_client.get(api_routes.households_cookbooks_item_id(sample.slug), headers=unique_user.token)
assert response.status_code == 404
@pytest.mark.parametrize(
"qf_string, expected_code",
[
('tags.name CONTAINS ALL ["tag1","tag2"]', 200),
('badfield = "badvalue"', 422),
('recipe_category.id IN ["1"]', 422),
('created_at >= "not-a-date"', 422),
],
ids=[
"valid qf",
"invalid field",
"invalid UUID",
"invalid date",
],
)
def test_cookbook_validate_query_filter_string(
api_client: TestClient, unique_user: TestUser, qf_string: str, expected_code: int
):
# Create
cb_data = {"name": random_string(10), "slug": random_string(10), "query_filter_string": qf_string}
response = api_client.post(api_routes.households_cookbooks, json=cb_data, headers=unique_user.token)
assert response.status_code == expected_code if expected_code != 200 else 201
# Update
cb_data = {"name": random_string(10), "slug": random_string(10), "query_filter_string": ""}
response = api_client.post(api_routes.households_cookbooks, json=cb_data, headers=unique_user.token)
assert response.status_code == 201
cb_data = response.json()
cb_data["queryFilterString"] = qf_string
response = api_client.put(
api_routes.households_cookbooks_item_id(cb_data["id"]), json=cb_data, headers=unique_user.token
)
assert response.status_code == expected_code if expected_code != 201 else 200
# Out; should skip validation, so this should never error out
ReadCookBook(**cb_data)

View file

@ -40,15 +40,22 @@ def create_rule(
categories: list[CategoryOut] | None = None,
households: list[HouseholdSummary] | None = None,
):
qf_parts: list[str] = []
if tags:
qf_parts.append(f'tags.id CONTAINS ALL [{",".join([str(tag.id) for tag in tags])}]')
if categories:
qf_parts.append(f'recipe_category.id CONTAINS ALL [{",".join([str(cat.id) for cat in categories])}]')
if households:
qf_parts.append(f'household_id IN [{",".join([str(household.id) for household in households])}]')
query_filter_string = " AND ".join(qf_parts)
return unique_user.repos.group_meal_plan_rules.create(
PlanRulesSave(
group_id=UUID(unique_user.group_id),
household_id=UUID(unique_user.household_id),
day=day,
entry_type=entry_type,
tags=tags or [],
categories=categories or [],
households=households or [],
query_filter_string=query_filter_string,
)
)

View file

@ -8,6 +8,7 @@ from mealie.schema.recipe.recipe import RecipeCategory
from mealie.schema.recipe.recipe_category import CategorySave
from tests import utils
from tests.utils import api_routes
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
@ -32,7 +33,7 @@ def plan_rule(api_client: TestClient, unique_user: TestUser):
"householdId": unique_user.household_id,
"day": "monday",
"entryType": "breakfast",
"categories": [],
"queryFilterString": "",
}
response = api_client.post(
@ -48,12 +49,13 @@ def plan_rule(api_client: TestClient, unique_user: TestUser):
def test_group_mealplan_rules_create(api_client: TestClient, unique_user: TestUser, category: RecipeCategory):
database = unique_user.repos
query_filter_string = f'recipe_category.id IN ["{category.id}"]'
payload = {
"groupId": unique_user.group_id,
"householdId": unique_user.household_id,
"day": "monday",
"entryType": "breakfast",
"categories": [category.model_dump()],
"queryFilterString": query_filter_string,
}
response = api_client.post(
@ -67,8 +69,8 @@ def test_group_mealplan_rules_create(api_client: TestClient, unique_user: TestUs
assert response_data["householdId"] == str(unique_user.household_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
assert len(response_data["queryFilter"]["parts"]) == 1
assert response_data["queryFilter"]["parts"][0]["value"] == [str(category.id)]
# Validate database entry
rule = database.group_meal_plan_rules.get_one(UUID(response_data["id"]))
@ -78,8 +80,7 @@ def test_group_mealplan_rules_create(api_client: TestClient, unique_user: TestUs
assert str(rule.household_id) == unique_user.household_id
assert rule.day == "monday"
assert rule.entry_type == "breakfast"
assert len(rule.categories) == 1
assert rule.categories[0].slug == category.slug
assert rule.query_filter_string == query_filter_string
# Cleanup
database.group_meal_plan_rules.delete(rule.id)
@ -96,7 +97,8 @@ def test_group_mealplan_rules_read(api_client: TestClient, unique_user: TestUser
assert response_data["householdId"] == str(unique_user.household_id)
assert response_data["day"] == "monday"
assert response_data["entryType"] == "breakfast"
assert len(response_data["categories"]) == 0
assert response_data["queryFilterString"] == ""
assert len(response_data["queryFilter"]["parts"]) == 0
def test_group_mealplan_rules_update(api_client: TestClient, unique_user: TestUser, plan_rule: PlanRulesOut):
@ -119,7 +121,8 @@ def test_group_mealplan_rules_update(api_client: TestClient, unique_user: TestUs
assert response_data["householdId"] == str(unique_user.household_id)
assert response_data["day"] == "tuesday"
assert response_data["entryType"] == "lunch"
assert len(response_data["categories"]) == 0
assert response_data["queryFilterString"] == ""
assert len(response_data["queryFilter"]["parts"]) == 0
def test_group_mealplan_rules_delete(api_client: TestClient, unique_user: TestUser, plan_rule: PlanRulesOut):
@ -131,3 +134,42 @@ def test_group_mealplan_rules_delete(api_client: TestClient, unique_user: TestUs
response = api_client.get(api_routes.households_mealplans_rules_item_id(plan_rule.id), headers=unique_user.token)
assert response.status_code == 404
@pytest.mark.parametrize(
"qf_string, expected_code",
[
('tags.name CONTAINS ALL ["tag1","tag2"]', 200),
('badfield = "badvalue"', 422),
('recipe_category.id IN ["1"]', 422),
('created_at >= "not-a-date"', 422),
],
ids=[
"valid qf",
"invalid field",
"invalid UUID",
"invalid date",
],
)
def test_group_mealplan_rules_validate_query_filter_string(
api_client: TestClient, unique_user: TestUser, qf_string: str, expected_code: int
):
# Create
rule_data = {"name": random_string(10), "slug": random_string(10), "query_filter_string": qf_string}
response = api_client.post(api_routes.households_mealplans_rules, json=rule_data, headers=unique_user.token)
assert response.status_code == expected_code if expected_code != 200 else 201
# Update
rule_data = {"name": random_string(10), "slug": random_string(10), "query_filter_string": ""}
response = api_client.post(api_routes.households_mealplans_rules, json=rule_data, headers=unique_user.token)
assert response.status_code == 201
rule_data = response.json()
rule_data["queryFilterString"] = qf_string
response = api_client.put(
api_routes.households_mealplans_rules_item_id(rule_data["id"]), json=rule_data, headers=unique_user.token
)
assert response.status_code == expected_code if expected_code != 201 else 200
# Out; should skip validation, so this should never error out
PlanRulesOut(**rule_data)