From 216ae8571ce5a51602078235262f0c967c5f30fe Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:34:16 -0600 Subject: [PATCH] fix: Include unmade recipes when filtering by last made (#7130) --- .../getting-started/api-usage.md | 4 +- mealie/repos/repository_recipes.py | 9 +++- .../test_recipe_repository.py | 41 +++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/docs/documentation/getting-started/api-usage.md b/docs/docs/documentation/getting-started/api-usage.md index 4eaa97ee7..a85eb2df6 100644 --- a/docs/docs/documentation/getting-started/api-usage.md +++ b/docs/docs/documentation/getting-started/api-usage.md @@ -79,8 +79,8 @@ This filter will find all foods that are not named "carrot":
##### Keyword Filters The API supports many SQL keywords, such as `IS NULL` and `IN`, as well as their negations (e.g. `IS NOT NULL` and `NOT IN`). -Here is an example of a filter that returns all recipes where the "last made" value is not null:
-`lastMade IS NOT NULL` +Here is an example of a filter that returns all shopping list items without a food:
+`foodId IS NULL` This filter will find all recipes that don't start with the word "Test":
`name NOT LIKE "Test%"` diff --git a/mealie/repos/repository_recipes.py b/mealie/repos/repository_recipes.py index fe2c6d588..23972d024 100644 --- a/mealie/repos/repository_recipes.py +++ b/mealie/repos/repository_recipes.py @@ -1,5 +1,6 @@ import re as re from collections.abc import Iterable, Sequence +from datetime import UTC, datetime from random import randint from typing import Self, cast from uuid import UUID @@ -51,10 +52,13 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]): return self def _get_last_made_col_alias(self) -> sa.ColumnElement | None: - """Computed last_made which uses `HouseholdToRecipe.last_made` for the user's household, otherwise None""" + """ + Computed last_made which uses `HouseholdToRecipe.last_made` for the user's household, + otherwise an arbitrarily low date + """ user_household_subquery = sa.select(User.household_id).where(User.id == self.user_id).scalar_subquery() - return ( + last_made_subquery = ( sa.select(HouseholdToRecipe.last_made) .where( HouseholdToRecipe.recipe_id == self.model.id, @@ -63,6 +67,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]): .correlate(self.model) .scalar_subquery() ) + return sa.func.coalesce(last_made_subquery, datetime(year=1900, month=1, day=1, tzinfo=UTC)) def _get_rating_col_alias(self) -> sa.ColumnElement | None: """Computed rating which uses the user's rating if it exists, otherwise falling back to the recipe's rating""" diff --git a/tests/unit_tests/repository_tests/test_recipe_repository.py b/tests/unit_tests/repository_tests/test_recipe_repository.py index 2c43f8235..a7cc28910 100644 --- a/tests/unit_tests/repository_tests/test_recipe_repository.py +++ b/tests/unit_tests/repository_tests/test_recipe_repository.py @@ -647,6 +647,47 @@ def test_order_by_last_made(unique_user: TestUser, h2_user: TestUser): assert [item.id for item in h2_query.items] == [recipe_2.id, recipe_1.id] +def test_coalesce_last_made(unique_user: TestUser): + dt = datetime.now(UTC) + + made_recipe, unmade_recipe = ( + unique_user.repos.recipes.create( + Recipe(user_id=unique_user.user_id, group_id=unique_user.group_id, name=random_string()) + ) + for _ in range(2) + ) + unique_user.repos.household_recipes.create( + HouseholdRecipeCreate(recipe_id=made_recipe.id, household_id=unique_user.household_id, last_made=dt) + ) + + repos = get_repositories( + unique_user.repos.session, group_id=unique_user.group_id, household_id=None + ).recipes.by_user(unique_user.user_id) + r = repos.page_all( + PaginationQuery( + page=1, + per_page=-1, + order_by="last_made", + order_direction=OrderDirection.asc, + query_filter=f"id IN [{made_recipe.id}, {unmade_recipe.id}] AND lastMade <= {dt.isoformat()}", + ) + ) + assert len(r.items) == 2 + assert [item.id for item in r.items] == [unmade_recipe.id, made_recipe.id] + + r = repos.page_all( + PaginationQuery( + page=1, + per_page=-1, + order_by="last_made", + order_direction=OrderDirection.desc, + query_filter=f"id IN [{made_recipe.id}, {unmade_recipe.id}]", + ) + ) + assert len(r.items) == 2 + assert [item.id for item in r.items] == [made_recipe.id, unmade_recipe.id] + + def test_order_by_rating(user_tuple: tuple[TestUser, TestUser]): user_1, user_2 = user_tuple database = user_1.repos