mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
feat: category and tag filters to recipe pagination route (#1508)
* fixed incorrect response model * added category and tag filters * moved categories and tags params to route and changed to query array param * type fixes * added category and tag tests
This commit is contained in:
parent
e82e7d0fb3
commit
f45e2587a0
@ -129,7 +129,14 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.all()
|
||||
)
|
||||
|
||||
def page_all(self, pagination: PaginationQuery, override=None, load_food=False) -> RecipePagination:
|
||||
def page_all(
|
||||
self,
|
||||
pagination: PaginationQuery,
|
||||
override=None,
|
||||
load_food=False,
|
||||
categories: Optional[list[UUID4 | str]] = None,
|
||||
tags: Optional[list[UUID4 | str]] = None,
|
||||
) -> RecipePagination:
|
||||
q = self.session.query(self.model)
|
||||
|
||||
args = [
|
||||
@ -145,6 +152,23 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
|
||||
fltr = self._filter_builder()
|
||||
q = q.filter_by(**fltr)
|
||||
|
||||
if categories:
|
||||
for category in categories:
|
||||
if isinstance(category, UUID):
|
||||
q = q.filter(RecipeModel.recipe_category.any(Category.id == category))
|
||||
|
||||
else:
|
||||
q = q.filter(RecipeModel.recipe_category.any(Category.slug == category))
|
||||
|
||||
if tags:
|
||||
for tag in tags:
|
||||
if isinstance(tag, UUID):
|
||||
q = q.filter(RecipeModel.tags.any(Tag.id == tag))
|
||||
|
||||
else:
|
||||
q = q.filter(RecipeModel.tags.any(Tag.slug == tag))
|
||||
|
||||
q, count, total_pages = self.add_pagination_to_query(q, pagination)
|
||||
|
||||
try:
|
||||
|
@ -1,13 +1,14 @@
|
||||
from functools import cached_property
|
||||
from shutil import copyfileobj
|
||||
from typing import Optional
|
||||
from zipfile import ZipFile
|
||||
|
||||
import sqlalchemy
|
||||
from fastapi import BackgroundTasks, Depends, File, Form, HTTPException, status
|
||||
from fastapi import BackgroundTasks, Depends, File, Form, HTTPException, Query, status
|
||||
from fastapi.datastructures import UploadFile
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import UUID4, BaseModel, Field
|
||||
from slugify import slugify
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
@ -21,7 +22,13 @@ from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
|
||||
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipePaginationQuery, RecipeSummary
|
||||
from mealie.schema.recipe.recipe import (
|
||||
CreateRecipe,
|
||||
CreateRecipeByUrlBulk,
|
||||
RecipePagination,
|
||||
RecipePaginationQuery,
|
||||
RecipeSummary,
|
||||
)
|
||||
from mealie.schema.recipe.recipe_asset import RecipeAsset
|
||||
from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest
|
||||
from mealie.schema.recipe.request_helpers import RecipeZipTokenResponse, UpdateImageResponse
|
||||
@ -200,11 +207,18 @@ class RecipeController(BaseRecipeController):
|
||||
# ==================================================================================================================
|
||||
# CRUD Operations
|
||||
|
||||
@router.get("", response_model=list[RecipeSummary])
|
||||
def get_all(self, q: RecipePaginationQuery = Depends(RecipePaginationQuery)):
|
||||
@router.get("", response_model=RecipePagination)
|
||||
def get_all(
|
||||
self,
|
||||
q: RecipePaginationQuery = Depends(RecipePaginationQuery),
|
||||
categories: Optional[list[UUID4 | str]] = Query(None),
|
||||
tags: Optional[list[UUID4 | str]] = Query(None),
|
||||
):
|
||||
response = self.repo.page_all(
|
||||
pagination=q,
|
||||
load_food=q.load_food,
|
||||
categories=categories,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
|
@ -1,7 +1,9 @@
|
||||
from typing import cast
|
||||
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.repos.repository_recipes import RepositoryRecipes
|
||||
from mealie.schema.recipe.recipe import Recipe
|
||||
from mealie.schema.recipe.recipe_category import CategorySave
|
||||
from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipePaginationQuery, RecipeSummary
|
||||
from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagSave
|
||||
from tests.utils.factories import random_string
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
@ -10,20 +12,20 @@ def test_recipe_repo_get_by_categories_basic(database: AllRepositories, unique_u
|
||||
# Bootstrap the database with categories
|
||||
slug1, slug2, slug3 = [random_string(10) for _ in range(3)]
|
||||
|
||||
categories = [
|
||||
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 = []
|
||||
created_categories: list[CategoryOut] = []
|
||||
|
||||
for category in categories:
|
||||
model = database.categories.create(category)
|
||||
created_categories.append(model)
|
||||
|
||||
# Bootstrap the database with recipes
|
||||
recipes = []
|
||||
recipes: list[Recipe | RecipeSummary] = []
|
||||
|
||||
for idx in range(15):
|
||||
if idx % 3 == 0:
|
||||
@ -45,14 +47,14 @@ def test_recipe_repo_get_by_categories_basic(database: AllRepositories, unique_u
|
||||
created_recipes = []
|
||||
|
||||
for recipe in recipes:
|
||||
models = database.recipes.create(recipe)
|
||||
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.by_group(unique_user.group_id)
|
||||
recipes = repo.get_by_categories([category])
|
||||
repo: RepositoryRecipes = database.recipes.by_group(unique_user.group_id) # type: ignore
|
||||
recipes = repo.get_by_categories([cast(RecipeCategory, category)])
|
||||
|
||||
assert len(recipes) == 5
|
||||
|
||||
@ -106,11 +108,171 @@ def test_recipe_repo_get_by_categories_multi(database: AllRepositories, unique_u
|
||||
database.recipes.create(recipe)
|
||||
|
||||
# Get all recipes by both categories
|
||||
repo: RepositoryRecipes = database.recipes.by_group(unique_user.group_id)
|
||||
by_category = repo.get_by_categories(created_categories)
|
||||
repo: RepositoryRecipes = database.recipes.by_group(unique_user.group_id) # type: ignore
|
||||
by_category = repo.get_by_categories(cast(list[RecipeCategory], created_categories))
|
||||
|
||||
assert len(by_category) == 10
|
||||
|
||||
for recipe in by_category:
|
||||
for category in recipe.recipe_category:
|
||||
assert category.id in known_category_ids
|
||||
for recipe_summary in by_category:
|
||||
for recipe_category in recipe_summary.recipe_category:
|
||||
assert recipe_category.id in known_category_ids
|
||||
|
||||
|
||||
def test_recipe_repo_pagination_by_categories(database: AllRepositories, unique_user: TestUser):
|
||||
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 = [database.categories.create(category) for category in categories]
|
||||
|
||||
# Bootstrap the database with recipes
|
||||
recipes = []
|
||||
|
||||
for i in range(10):
|
||||
# None of the categories
|
||||
recipes.append(
|
||||
Recipe(
|
||||
user_id=unique_user.user_id,
|
||||
group_id=unique_user.group_id,
|
||||
name=random_string(),
|
||||
)
|
||||
)
|
||||
|
||||
# Only one of the categories
|
||||
recipes.append(
|
||||
Recipe(
|
||||
user_id=unique_user.user_id,
|
||||
group_id=unique_user.group_id,
|
||||
name=random_string(),
|
||||
recipe_category=[created_categories[i % 2]],
|
||||
),
|
||||
)
|
||||
|
||||
# Both of the categories
|
||||
recipes.append(
|
||||
Recipe(
|
||||
user_id=unique_user.user_id,
|
||||
group_id=unique_user.group_id,
|
||||
name=random_string(),
|
||||
recipe_category=created_categories,
|
||||
)
|
||||
)
|
||||
|
||||
for recipe in recipes:
|
||||
database.recipes.create(recipe)
|
||||
|
||||
pagination_query = RecipePaginationQuery(
|
||||
page=1,
|
||||
per_page=-1,
|
||||
)
|
||||
|
||||
# Get all recipes with only one category by UUID
|
||||
category_id = created_categories[0].id
|
||||
recipes_with_one_category = database.recipes.page_all(pagination_query, categories=[category_id]).items
|
||||
assert len(recipes_with_one_category) == 15
|
||||
|
||||
for recipe_summary in recipes_with_one_category:
|
||||
category_ids = [category.id for category in recipe_summary.recipe_category]
|
||||
assert category_id in category_ids
|
||||
|
||||
# Get all recipes with only one category by slug
|
||||
category_slug = created_categories[1].slug
|
||||
recipes_with_one_category = database.recipes.page_all(pagination_query, categories=[category_slug]).items
|
||||
assert len(recipes_with_one_category) == 15
|
||||
|
||||
for recipe_summary in recipes_with_one_category:
|
||||
category_slugs = [category.slug for category in recipe_summary.recipe_category]
|
||||
assert category_slug in category_slugs
|
||||
|
||||
# Get all recipes with both categories
|
||||
recipes_with_both_categories = database.recipes.page_all(
|
||||
pagination_query, categories=[category.id for category in created_categories]
|
||||
).items
|
||||
assert len(recipes_with_both_categories) == 10
|
||||
|
||||
for recipe_summary in recipes_with_both_categories:
|
||||
category_ids = [category.id for category in recipe_summary.recipe_category]
|
||||
for category in created_categories:
|
||||
assert category.id in category_ids
|
||||
|
||||
|
||||
def test_recipe_repo_pagination_by_tags(database: AllRepositories, unique_user: TestUser):
|
||||
slug1, slug2 = [random_string(10) for _ in range(2)]
|
||||
|
||||
tags = [
|
||||
TagSave(group_id=unique_user.group_id, name=slug1, slug=slug1),
|
||||
TagSave(group_id=unique_user.group_id, name=slug2, slug=slug2),
|
||||
]
|
||||
|
||||
created_tags = [database.tags.create(tag) for tag in tags]
|
||||
|
||||
# Bootstrap the database with recipes
|
||||
recipes = []
|
||||
|
||||
for i in range(10):
|
||||
# None of the tags
|
||||
recipes.append(
|
||||
Recipe(
|
||||
user_id=unique_user.user_id,
|
||||
group_id=unique_user.group_id,
|
||||
name=random_string(),
|
||||
)
|
||||
)
|
||||
|
||||
# Only one of the tags
|
||||
recipes.append(
|
||||
Recipe(
|
||||
user_id=unique_user.user_id,
|
||||
group_id=unique_user.group_id,
|
||||
name=random_string(),
|
||||
tags=[created_tags[i % 2]],
|
||||
),
|
||||
)
|
||||
|
||||
# Both of the tags
|
||||
recipes.append(
|
||||
Recipe(
|
||||
user_id=unique_user.user_id,
|
||||
group_id=unique_user.group_id,
|
||||
name=random_string(),
|
||||
tags=created_tags,
|
||||
)
|
||||
)
|
||||
|
||||
for recipe in recipes:
|
||||
database.recipes.create(recipe)
|
||||
|
||||
pagination_query = RecipePaginationQuery(
|
||||
page=1,
|
||||
per_page=-1,
|
||||
)
|
||||
|
||||
# Get all recipes with only one tag by UUID
|
||||
tag_id = created_tags[0].id
|
||||
recipes_with_one_tag = database.recipes.page_all(pagination_query, tags=[tag_id]).items
|
||||
assert len(recipes_with_one_tag) == 15
|
||||
|
||||
for recipe_summary in recipes_with_one_tag:
|
||||
tag_ids = [tag.id for tag in recipe_summary.tags]
|
||||
assert tag_id in tag_ids
|
||||
|
||||
# Get all recipes with only one tag by slug
|
||||
tag_slug = created_tags[1].slug
|
||||
recipes_with_one_tag = database.recipes.page_all(pagination_query, tags=[tag_slug]).items
|
||||
assert len(recipes_with_one_tag) == 15
|
||||
|
||||
for recipe_summary in recipes_with_one_tag:
|
||||
tag_slugs = [tag.slug for tag in recipe_summary.tags]
|
||||
assert tag_slug in tag_slugs
|
||||
|
||||
# Get all recipes with both tags
|
||||
recipes_with_both_tags = database.recipes.page_all(pagination_query, tags=[tag.id for tag in created_tags]).items
|
||||
assert len(recipes_with_both_tags) == 10
|
||||
|
||||
for recipe_summary in recipes_with_both_tags:
|
||||
tag_ids = [tag.id for tag in recipe_summary.tags]
|
||||
for tag in created_tags:
|
||||
assert tag.id in tag_ids
|
||||
|
Loading…
x
Reference in New Issue
Block a user