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: tools } = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
const { store: foods } = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value); const { store: foods } = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
const { store: households } = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(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> </script>

View File

@@ -9,6 +9,7 @@ from typing import Any
from fastapi import HTTPException from fastapi import HTTPException
from pydantic import UUID4, BaseModel from pydantic import UUID4, BaseModel
from sqlalchemy import ColumnElement, Select, case, delete, func, nulls_first, nulls_last, select 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 import InstrumentedAttribute
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from sqlalchemy.sql import sqltypes 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): def _query(self, override_schema: type[MealieModel] | None = None, with_options=True):
q = select(self.model) 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: if with_options:
schema = override_schema or self.schema schema = override_schema or self.schema
return q.options(*schema.loader_options()) return q.options(*schema.loader_options())
@@ -87,6 +95,7 @@ class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]:
dct = {} dct = {}
if self.group_id: if self.group_id:
dct["group_id"] = self.group_id dct["group_id"] = self.group_id
if self.household_id: if self.household_id:
dct["household_id"] = 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.db.models.users.users import User
from mealie.schema.cookbook.cookbook import ReadCookBook from mealie.schema.cookbook.cookbook import ReadCookBook
from mealie.schema.recipe import Recipe 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_ingredient import IngredientFood
from mealie.schema.recipe.recipe_suggestion import RecipeSuggestionQuery, RecipeSuggestionResponseItem from mealie.schema.recipe.recipe_suggestion import RecipeSuggestionQuery, RecipeSuggestionResponseItem
from mealie.schema.recipe.recipe_tool import RecipeToolOut from mealie.schema.recipe.recipe_tool import RecipeToolOut
@@ -214,7 +214,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
) -> RecipePagination: ) -> RecipePagination:
# Copy this, because calling methods (e.g. tests) might rely on it not getting mutated # Copy this, because calling methods (e.g. tests) might rely on it not getting mutated
pagination_result = pagination.model_copy() 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() fltr = self._filter_builder()
q = q.filter_by(**fltr) q = q.filter_by(**fltr)
@@ -271,24 +271,6 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
items=items, 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( def _build_recipe_filter(
self, self,
categories: list[UUID4] | None = None, categories: list[UUID4] | None = None,
@@ -334,7 +316,9 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
return fltr return fltr
def get_random(self, limit=1) -> list[Recipe]: 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: if self.group_id:
stmt = stmt.filter(RecipeModel.group_id == self.group_id) stmt = stmt.filter(RecipeModel.group_id == self.group_id)
if self.household_id: if self.household_id:
@@ -405,7 +389,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
ingredients_alias = orm.aliased(RecipeIngredientModel) ingredients_alias = orm.aliased(RecipeIngredientModel)
tools_alias = orm.aliased(Tool) 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() fltr = self._filter_builder()
q = q.filter_by(**fltr) q = q.filter_by(**fltr)

View File

@@ -3,6 +3,7 @@ from functools import cached_property
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import UUID4, BaseModel, ConfigDict 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 import BaseCrudController, controller
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.schema import mapper from mealie.schema import mapper
@@ -123,9 +124,15 @@ class RecipeCategoryController(BaseCrudController):
def get_one_by_slug(self, category_slug: str): def get_one_by_slug(self, category_slug: str):
"""Returns a category object with the associated recieps relating to the category""" """Returns a category object with the associated recieps relating to the category"""
category: RecipeCategory = self.mixins.get_one(category_slug, "slug") 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( return RecipeCategoryResponse.model_construct(
id=category.id, id=category.id,
slug=category.slug, slug=category.slug,
name=category.name, 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 datetime import UTC, datetime, timedelta
from typing import cast
from uuid import UUID from uuid import UUID
import pytest import pytest
@@ -7,11 +6,10 @@ from sqlalchemy.orm import Session
from mealie.repos.all_repositories import get_repositories from mealie.repos.all_repositories import get_repositories
from mealie.repos.repository_factory import AllRepositories 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.household.household import HouseholdCreate, HouseholdRecipeCreate
from mealie.schema.recipe import RecipeIngredient, SaveIngredientFood from mealie.schema.recipe import RecipeIngredient, SaveIngredientFood
from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipeSummary from mealie.schema.recipe.recipe import Recipe
from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagSave from mealie.schema.recipe.recipe_category import CategorySave, TagSave
from mealie.schema.recipe.recipe_tool import RecipeToolSave from mealie.schema.recipe.recipe_tool import RecipeToolSave
from mealie.schema.response import OrderDirection, PaginationQuery from mealie.schema.response import OrderDirection, PaginationQuery
from mealie.schema.user.user import GroupBase, UserRatingCreate 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) 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): def test_recipe_repo_pagination_by_categories(unique_user: TestUser):
database = unique_user.repos database = unique_user.repos
slug1, slug2 = (random_string(10) for _ in range(2)) slug1, slug2 = (random_string(10) for _ in range(2))