From d02023e12c57dda53309d873b4db008c42c5dab6 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:48:27 -0600 Subject: [PATCH] fix: Only fetch recipes with a household id (#6773) --- .../RecipeExplorerPageSearchFilters.vue | 10 ++ mealie/repos/repository_generic.py | 9 ++ mealie/repos/repository_recipes.py | 28 +--- .../organizers/controller_categories.py | 9 +- .../test_recipe_repository.py | 120 +----------------- 5 files changed, 35 insertions(+), 141 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeExplorerPage/RecipeExplorerPageParts/RecipeExplorerPageSearchFilters.vue b/frontend/components/Domain/Recipe/RecipeExplorerPage/RecipeExplorerPageParts/RecipeExplorerPageSearchFilters.vue index 29b09948f..a7636679d 100644 --- a/frontend/components/Domain/Recipe/RecipeExplorerPage/RecipeExplorerPageParts/RecipeExplorerPageSearchFilters.vue +++ b/frontend/components/Domain/Recipe/RecipeExplorerPage/RecipeExplorerPageParts/RecipeExplorerPageSearchFilters.vue @@ -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 = []; + } + }, +); diff --git a/mealie/repos/repository_generic.py b/mealie/repos/repository_generic.py index 76a5b10a4..3565a1b8f 100644 --- a/mealie/repos/repository_generic.py +++ b/mealie/repos/repository_generic.py @@ -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 diff --git a/mealie/repos/repository_recipes.py b/mealie/repos/repository_recipes.py index 4caadfdb9..c76489a5f 100644 --- a/mealie/repos/repository_recipes.py +++ b/mealie/repos/repository_recipes.py @@ -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) diff --git a/mealie/routes/organizers/controller_categories.py b/mealie/routes/organizers/controller_categories.py index 59aae5e1e..61338f448 100644 --- a/mealie/routes/organizers/controller_categories.py +++ b/mealie/routes/organizers/controller_categories.py @@ -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, ) diff --git a/tests/unit_tests/repository_tests/test_recipe_repository.py b/tests/unit_tests/repository_tests/test_recipe_repository.py index 16ca63608..2c43f8235 100644 --- a/tests/unit_tests/repository_tests/test_recipe_repository.py +++ b/tests/unit_tests/repository_tests/test_recipe_repository.py @@ -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))