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:
Michael Genson 2022-08-09 17:01:59 -05:00 committed by GitHub
parent e82e7d0fb3
commit f45e2587a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 219 additions and 19 deletions

View File

@ -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:

View File

@ -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())

View File

@ -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