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