fix: Only fetch recipes with a household id (#6773)

This commit is contained in:
Michael Genson
2025-12-23 16:48:27 -06:00
committed by GitHub
parent 64d8786d8f
commit d02023e12c
5 changed files with 35 additions and 141 deletions

View File

@@ -101,4 +101,14 @@ const { store: tags } = isOwnGroup.value ? useTagStore() : usePublicTagStore(gro
const { store: tools } = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
const { store: foods } = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
const { store: households } = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value);
watch(
households,
() => {
// if exactly one household exists, then we shouldn't be filtering by household
if (households.value.length == 1) {
selectedHouseholds.value = [];
}
},
);
</script>

View File

@@ -9,6 +9,7 @@ from typing import Any
from fastapi import HTTPException
from pydantic import UUID4, BaseModel
from sqlalchemy import ColumnElement, Select, case, delete, func, nulls_first, nulls_last, select
from sqlalchemy.ext.associationproxy import AssociationProxyInstance
from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy.orm.session import Session
from sqlalchemy.sql import sqltypes
@@ -77,6 +78,13 @@ class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]:
def _query(self, override_schema: type[MealieModel] | None = None, with_options=True):
q = select(self.model)
try:
if isinstance(self.model.household_id, AssociationProxyInstance):
q.filter(self.model.household_id.is_not(None))
except (AttributeError, NotImplementedError):
pass
if with_options:
schema = override_schema or self.schema
return q.options(*schema.loader_options())
@@ -87,6 +95,7 @@ class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]:
dct = {}
if self.group_id:
dct["group_id"] = self.group_id
if self.household_id:
dct["household_id"] = self.household_id

View File

@@ -21,7 +21,7 @@ from mealie.db.models.users.user_to_recipe import UserToRecipe
from mealie.db.models.users.users import User
from mealie.schema.cookbook.cookbook import ReadCookBook
from mealie.schema.recipe import Recipe
from mealie.schema.recipe.recipe import RecipeCategory, RecipePagination, RecipeSummary, create_recipe_slug
from mealie.schema.recipe.recipe import RecipePagination, RecipeSummary, create_recipe_slug
from mealie.schema.recipe.recipe_ingredient import IngredientFood
from mealie.schema.recipe.recipe_suggestion import RecipeSuggestionQuery, RecipeSuggestionResponseItem
from mealie.schema.recipe.recipe_tool import RecipeToolOut
@@ -214,7 +214,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
) -> RecipePagination:
# Copy this, because calling methods (e.g. tests) might rely on it not getting mutated
pagination_result = pagination.model_copy()
q = sa.select(self.model)
q = sa.select(self.model).filter(self.model.household_id.is_not(None))
fltr = self._filter_builder()
q = q.filter_by(**fltr)
@@ -271,24 +271,6 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
items=items,
)
def get_by_categories(self, categories: list[RecipeCategory]) -> list[RecipeSummary]:
"""
get_by_categories returns all the Recipes that contain every category provided in the list
"""
ids = [x.id for x in categories]
stmt = (
sa.select(RecipeModel)
.join(RecipeModel.recipe_category)
.filter(RecipeModel.recipe_category.any(Category.id.in_(ids)))
)
if self.group_id:
stmt = stmt.filter(RecipeModel.group_id == self.group_id)
if self.household_id:
stmt = stmt.filter(RecipeModel.household_id == self.household_id)
return [RecipeSummary.model_validate(x) for x in self.session.execute(stmt).unique().scalars().all()]
def _build_recipe_filter(
self,
categories: list[UUID4] | None = None,
@@ -334,7 +316,9 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
return fltr
def get_random(self, limit=1) -> list[Recipe]:
stmt = sa.select(RecipeModel).order_by(sa.func.random()).limit(limit) # Postgres and SQLite specific
stmt = (
sa.select(RecipeModel).filter(RecipeModel.household_id.is_not(None)).order_by(sa.func.random()).limit(limit)
) # Postgres and SQLite specific
if self.group_id:
stmt = stmt.filter(RecipeModel.group_id == self.group_id)
if self.household_id:
@@ -405,7 +389,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
ingredients_alias = orm.aliased(RecipeIngredientModel)
tools_alias = orm.aliased(Tool)
q = sa.select(self.model)
q = sa.select(self.model).filter(self.model.household_id.is_not(None))
fltr = self._filter_builder()
q = q.filter_by(**fltr)

View File

@@ -3,6 +3,7 @@ from functools import cached_property
from fastapi import APIRouter, Depends
from pydantic import UUID4, BaseModel, ConfigDict
from mealie.repos.all_repositories import get_repositories
from mealie.routes._base import BaseCrudController, controller
from mealie.routes._base.mixins import HttpRepo
from mealie.schema import mapper
@@ -123,9 +124,15 @@ class RecipeCategoryController(BaseCrudController):
def get_one_by_slug(self, category_slug: str):
"""Returns a category object with the associated recieps relating to the category"""
category: RecipeCategory = self.mixins.get_one(category_slug, "slug")
group_recipes = get_repositories(self.repos.session, group_id=self.group_id, household_id=None).recipes
recipe_data = group_recipes.page_all(
PaginationQuery(per_page=-1, query_filter=f'recipe_category.id IN ["{category.id}"]')
)
return RecipeCategoryResponse.model_construct(
id=category.id,
slug=category.slug,
name=category.name,
recipes=self.repos.recipes.get_by_categories([category]),
recipes=recipe_data.items,
)

View File

@@ -1,5 +1,4 @@
from datetime import UTC, datetime, timedelta
from typing import cast
from uuid import UUID
import pytest
@@ -7,11 +6,10 @@ from sqlalchemy.orm import Session
from mealie.repos.all_repositories import get_repositories
from mealie.repos.repository_factory import AllRepositories
from mealie.repos.repository_recipes import RepositoryRecipes
from mealie.schema.household.household import HouseholdCreate, HouseholdRecipeCreate
from mealie.schema.recipe import RecipeIngredient, SaveIngredientFood
from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipeSummary
from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagSave
from mealie.schema.recipe.recipe import Recipe
from mealie.schema.recipe.recipe_category import CategorySave, TagSave
from mealie.schema.recipe.recipe_tool import RecipeToolSave
from mealie.schema.response import OrderDirection, PaginationQuery
from mealie.schema.user.user import GroupBase, UserRatingCreate
@@ -137,120 +135,6 @@ def search_recipes(unique_db: AllRepositories, unique_ids: tuple[str, str, str])
return unique_db.recipes.create_many(recipes)
def test_recipe_repo_get_by_categories_basic(unique_user: TestUser):
database = unique_user.repos
# Bootstrap the database with categories
slug1, slug2, slug3 = (random_string(10) for _ in range(3))
categories: list[CategoryOut | CategorySave] = [
CategorySave(group_id=unique_user.group_id, name=slug1, slug=slug1),
CategorySave(group_id=unique_user.group_id, name=slug2, slug=slug2),
CategorySave(group_id=unique_user.group_id, name=slug3, slug=slug3),
]
created_categories: list[CategoryOut] = []
for category in categories:
model = database.categories.create(category)
created_categories.append(model)
# Bootstrap the database with recipes
recipes: list[Recipe | RecipeSummary] = []
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(cast(Recipe, recipe))
created_recipes.append(models)
# Get all recipes by category
for category in created_categories:
repo: RepositoryRecipes = database.recipes
recipes = repo.get_by_categories([cast(RecipeCategory, category)])
assert len(recipes) == 5
for recipe in recipes:
assert recipe.recipe_category is not None
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(unique_user: TestUser):
database = unique_user.repos
slug1, slug2 = (random_string(10) for _ in range(2))
categories = [
CategorySave(group_id=unique_user.group_id, name=slug1, slug=slug1),
CategorySave(group_id=unique_user.group_id, 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_category = repo.get_by_categories(cast(list[RecipeCategory], created_categories))
assert len(by_category) == 10
for recipe_summary in by_category:
assert recipe_summary.recipe_category is not None
for recipe_category in recipe_summary.recipe_category:
assert recipe_category.id in known_category_ids
def test_recipe_repo_pagination_by_categories(unique_user: TestUser):
database = unique_user.repos
slug1, slug2 = (random_string(10) for _ in range(2))