fix: Include unmade recipes when filtering by last made (#7130)

This commit is contained in:
Michael Genson
2026-02-23 18:34:16 -06:00
committed by GitHub
parent 02d32c8905
commit 216ae8571c
3 changed files with 50 additions and 4 deletions

View File

@@ -79,8 +79,8 @@ This filter will find all foods that are not named "carrot": <br>
##### Keyword Filters ##### 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`). 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: <br> Here is an example of a filter that returns all shopping list items without a food: <br>
`lastMade IS NOT NULL` `foodId IS NULL`
This filter will find all recipes that don't start with the word "Test": <br> This filter will find all recipes that don't start with the word "Test": <br>
`name NOT LIKE "Test%"` `name NOT LIKE "Test%"`

View File

@@ -1,5 +1,6 @@
import re as re import re as re
from collections.abc import Iterable, Sequence from collections.abc import Iterable, Sequence
from datetime import UTC, datetime
from random import randint from random import randint
from typing import Self, cast from typing import Self, cast
from uuid import UUID from uuid import UUID
@@ -51,10 +52,13 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
return self return self
def _get_last_made_col_alias(self) -> sa.ColumnElement | None: 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() 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) sa.select(HouseholdToRecipe.last_made)
.where( .where(
HouseholdToRecipe.recipe_id == self.model.id, HouseholdToRecipe.recipe_id == self.model.id,
@@ -63,6 +67,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
.correlate(self.model) .correlate(self.model)
.scalar_subquery() .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: 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""" """Computed rating which uses the user's rating if it exists, otherwise falling back to the recipe's rating"""

View File

@@ -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] 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]): def test_order_by_rating(user_tuple: tuple[TestUser, TestUser]):
user_1, user_2 = user_tuple user_1, user_2 = user_tuple
database = user_1.repos database = user_1.repos