mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-04 15:03:10 -05:00
* feat: server side search API (#2112) * refactor repository_recipes filter building * add food filter to recipe repository page_all * fix query type annotations * working search * add tests and make sure title matches are ordered correctly * remove instruction matching again * fix formatting and small issues * fix another linting error * make search test no rely on actual words * fix failing postgres compiled query * revise incorrectly ordered migration * automatically extract latest migration version * test migration orderes * run type generators * new search function * wip: new search page * sortable field options * fix virtual scroll issue * fix search casing bug * finalize search filters/sorts * remove old composable * fix type errors --------- Co-authored-by: Sören <fleshgolem@gmx.net>
This commit is contained in:
@@ -63,7 +63,7 @@ class RecipeIngredient(SqlAlchemyBase, BaseMixins):
|
||||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"))
|
||||
|
||||
title: Mapped[str | None] = mapped_column(String) # Section Header - Shows if Present
|
||||
note: Mapped[str | None] = mapped_column(String) # Force Show Text - Overrides Concat
|
||||
note: Mapped[str | None] = mapped_column(String, index=True) # Force Show Text - Overrides Concat
|
||||
|
||||
# Scaling Items
|
||||
unit_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_units.id"), index=True)
|
||||
@@ -73,7 +73,7 @@ class RecipeIngredient(SqlAlchemyBase, BaseMixins):
|
||||
food: Mapped[IngredientFoodModel | None] = orm.relationship(IngredientFoodModel, uselist=False)
|
||||
quantity: Mapped[float | None] = mapped_column(Float)
|
||||
|
||||
original_text: Mapped[str | None] = mapped_column(String)
|
||||
original_text: Mapped[str | None] = mapped_column(String, index=True)
|
||||
|
||||
reference_id: Mapped[GUID | None] = mapped_column(GUID) # Reference Links
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class RecipeInstruction(SqlAlchemyBase):
|
||||
position: Mapped[int | None] = mapped_column(Integer, index=True)
|
||||
type: Mapped[str | None] = mapped_column(String, default="")
|
||||
title: Mapped[str | None] = mapped_column(String)
|
||||
text: Mapped[str | None] = mapped_column(String)
|
||||
text: Mapped[str | None] = mapped_column(String, index=True)
|
||||
|
||||
ingredient_references: Mapped[list[RecipeIngredientRefLink]] = orm.relationship(
|
||||
RecipeIngredientRefLink, cascade="all, delete-orphan"
|
||||
|
||||
@@ -55,7 +55,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
# General Recipe Properties
|
||||
name: Mapped[str] = mapped_column(sa.String, nullable=False, index=True)
|
||||
description: Mapped[str | None] = mapped_column(sa.String)
|
||||
description: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
image: Mapped[str | None] = mapped_column(sa.String)
|
||||
|
||||
# Time Related Properties
|
||||
|
||||
@@ -4,7 +4,7 @@ from uuid import UUID
|
||||
|
||||
from pydantic import UUID4
|
||||
from slugify import slugify
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy import Select, and_, desc, func, or_, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
@@ -20,13 +20,14 @@ from mealie.schema.recipe.recipe import (
|
||||
RecipeCategory,
|
||||
RecipePagination,
|
||||
RecipeSummary,
|
||||
RecipeSummaryWithIngredients,
|
||||
RecipeTag,
|
||||
RecipeTool,
|
||||
)
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase, TagBase
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
|
||||
from ..db.models._model_base import SqlAlchemyBase
|
||||
from ..schema._mealie.mealie_model import extract_uuids
|
||||
from .repository_generic import RepositoryGeneric
|
||||
|
||||
|
||||
@@ -134,16 +135,59 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
)
|
||||
return self.session.execute(stmt).scalars().all()
|
||||
|
||||
def _uuids_for_items(self, items: list[UUID | str] | None, model: type[SqlAlchemyBase]) -> list[UUID] | None:
|
||||
if not items:
|
||||
return None
|
||||
ids: list[UUID] = []
|
||||
slugs: list[str] = []
|
||||
|
||||
for i in items:
|
||||
if isinstance(i, UUID):
|
||||
ids.append(i)
|
||||
else:
|
||||
slugs.append(i)
|
||||
additional_ids = self.session.execute(select(model.id).filter(model.slug.in_(slugs))).scalars().all()
|
||||
return ids + additional_ids
|
||||
|
||||
def _add_search_to_query(self, query: Select, search: str) -> Select:
|
||||
# I would prefer to just do this in the recipe_ingredient.any part of the main query, but it turns out
|
||||
# that at least sqlite wont use indexes for that correctly anymore and takes a big hit, so prefiltering it is
|
||||
ingredient_ids = (
|
||||
self.session.execute(
|
||||
select(RecipeIngredient.id).filter(
|
||||
or_(RecipeIngredient.note.ilike(f"%{search}%"), RecipeIngredient.original_text.ilike(f"%{search}%"))
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
q = query.filter(
|
||||
or_(
|
||||
RecipeModel.name.ilike(f"%{search}%"),
|
||||
RecipeModel.description.ilike(f"%{search}%"),
|
||||
RecipeModel.recipe_ingredient.any(RecipeIngredient.id.in_(ingredient_ids)),
|
||||
)
|
||||
).order_by(desc(RecipeModel.name.ilike(f"%{search}%")))
|
||||
return q
|
||||
|
||||
def page_all(
|
||||
self,
|
||||
pagination: PaginationQuery,
|
||||
override=None,
|
||||
load_food=False,
|
||||
cookbook: ReadCookBook | None = None,
|
||||
categories: list[UUID4 | str] | None = None,
|
||||
tags: list[UUID4 | str] | None = None,
|
||||
tools: list[UUID4 | str] | None = None,
|
||||
foods: list[UUID4 | str] | None = None,
|
||||
require_all_categories=True,
|
||||
require_all_tags=True,
|
||||
require_all_tools=True,
|
||||
require_all_foods=True,
|
||||
search: str | None = None,
|
||||
) -> RecipePagination:
|
||||
# Copy this, because calling methods (e.g. tests) might rely on it not getting mutated
|
||||
pagination_result = pagination.copy()
|
||||
q = select(self.model)
|
||||
|
||||
args = [
|
||||
@@ -152,57 +196,41 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
joinedload(RecipeModel.tools),
|
||||
]
|
||||
|
||||
item_class: type[RecipeSummary | RecipeSummaryWithIngredients]
|
||||
|
||||
if load_food:
|
||||
args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.food)))
|
||||
args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.unit)))
|
||||
item_class = RecipeSummaryWithIngredients
|
||||
else:
|
||||
item_class = RecipeSummary
|
||||
|
||||
q = q.options(*args)
|
||||
|
||||
fltr = self._filter_builder()
|
||||
q = q.filter_by(**fltr)
|
||||
|
||||
if cookbook:
|
||||
cb_filters = self._category_tag_filters(
|
||||
cookbook.categories,
|
||||
cookbook.tags,
|
||||
cookbook.tools,
|
||||
cookbook.require_all_categories,
|
||||
cookbook.require_all_tags,
|
||||
cookbook.require_all_tools,
|
||||
cb_filters = self._build_recipe_filter(
|
||||
categories=extract_uuids(cookbook.categories),
|
||||
tags=extract_uuids(cookbook.tags),
|
||||
tools=extract_uuids(cookbook.tools),
|
||||
require_all_categories=cookbook.require_all_categories,
|
||||
require_all_tags=cookbook.require_all_tags,
|
||||
require_all_tools=cookbook.require_all_tools,
|
||||
)
|
||||
|
||||
q = q.filter(*cb_filters)
|
||||
else:
|
||||
category_ids = self._uuids_for_items(categories, Category)
|
||||
tag_ids = self._uuids_for_items(tags, Tag)
|
||||
tool_ids = self._uuids_for_items(tools, Tool)
|
||||
filters = self._build_recipe_filter(
|
||||
categories=category_ids,
|
||||
tags=tag_ids,
|
||||
tools=tool_ids,
|
||||
foods=foods,
|
||||
require_all_categories=require_all_categories,
|
||||
require_all_tags=require_all_tags,
|
||||
require_all_tools=require_all_tools,
|
||||
require_all_foods=require_all_foods,
|
||||
)
|
||||
q = q.filter(*filters)
|
||||
if search:
|
||||
q = self._add_search_to_query(q, search)
|
||||
|
||||
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))
|
||||
|
||||
if tools:
|
||||
for tool in tools:
|
||||
if isinstance(tool, UUID):
|
||||
q = q.filter(RecipeModel.tools.any(Tool.id == tool))
|
||||
|
||||
else:
|
||||
q = q.filter(RecipeModel.tools.any(Tool.slug == tool))
|
||||
|
||||
q, count, total_pages = self.add_pagination_to_query(q, pagination)
|
||||
q, count, total_pages = self.add_pagination_to_query(q, pagination_result)
|
||||
|
||||
try:
|
||||
data = self.session.execute(q).scalars().unique().all()
|
||||
@@ -211,10 +239,10 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
self.session.rollback()
|
||||
raise e
|
||||
|
||||
items = [item_class.from_orm(item) for item in data]
|
||||
items = [RecipeSummary.from_orm(item) for item in data]
|
||||
return RecipePagination(
|
||||
page=pagination.page,
|
||||
per_page=pagination.per_page,
|
||||
page=pagination_result.page,
|
||||
per_page=pagination_result.per_page,
|
||||
total=count,
|
||||
total_pages=total_pages,
|
||||
items=items,
|
||||
@@ -233,41 +261,46 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
)
|
||||
return [RecipeSummary.from_orm(x) for x in self.session.execute(stmt).unique().scalars().all()]
|
||||
|
||||
def _category_tag_filters(
|
||||
def _build_recipe_filter(
|
||||
self,
|
||||
categories: list[CategoryBase] | None = None,
|
||||
tags: list[TagBase] | None = None,
|
||||
tools: list[RecipeTool] | None = None,
|
||||
categories: list[UUID4] | None = None,
|
||||
tags: list[UUID4] | None = None,
|
||||
tools: list[UUID4] | None = None,
|
||||
foods: list[UUID4] | None = None,
|
||||
require_all_categories: bool = True,
|
||||
require_all_tags: bool = True,
|
||||
require_all_tools: bool = True,
|
||||
require_all_foods: bool = True,
|
||||
) -> list:
|
||||
fltr = [
|
||||
RecipeModel.group_id == self.group_id,
|
||||
]
|
||||
if self.group_id:
|
||||
fltr = [
|
||||
RecipeModel.group_id == self.group_id,
|
||||
]
|
||||
else:
|
||||
fltr = []
|
||||
|
||||
if categories:
|
||||
cat_ids = [x.id for x in categories]
|
||||
if require_all_categories:
|
||||
fltr.extend(RecipeModel.recipe_category.any(Category.id == cat_id) for cat_id in cat_ids)
|
||||
fltr.extend(RecipeModel.recipe_category.any(Category.id == cat_id) for cat_id in categories)
|
||||
else:
|
||||
fltr.append(RecipeModel.recipe_category.any(Category.id.in_(cat_ids)))
|
||||
fltr.append(RecipeModel.recipe_category.any(Category.id.in_(categories)))
|
||||
|
||||
if tags:
|
||||
tag_ids = [x.id for x in tags]
|
||||
if require_all_tags:
|
||||
fltr.extend(RecipeModel.tags.any(Tag.id.is_(tag_id)) for tag_id in tag_ids)
|
||||
fltr.extend(RecipeModel.tags.any(Tag.id == tag_id) for tag_id in tags)
|
||||
else:
|
||||
fltr.append(RecipeModel.tags.any(Tag.id.in_(tag_ids)))
|
||||
fltr.append(RecipeModel.tags.any(Tag.id.in_(tags)))
|
||||
|
||||
if tools:
|
||||
tool_ids = [x.id for x in tools]
|
||||
|
||||
if require_all_tools:
|
||||
fltr.extend(RecipeModel.tools.any(Tool.id == tool_id) for tool_id in tool_ids)
|
||||
fltr.extend(RecipeModel.tools.any(Tool.id == tool_id) for tool_id in tools)
|
||||
else:
|
||||
fltr.append(RecipeModel.tools.any(Tool.id.in_(tool_ids)))
|
||||
|
||||
fltr.append(RecipeModel.tools.any(Tool.id.in_(tools)))
|
||||
if foods:
|
||||
if require_all_foods:
|
||||
fltr.extend(RecipeModel.recipe_ingredient.any(RecipeIngredient.food_id == food) for food in foods)
|
||||
else:
|
||||
fltr.append(RecipeModel.recipe_ingredient.any(RecipeIngredient.food_id.in_(foods)))
|
||||
return fltr
|
||||
|
||||
def by_category_and_tags(
|
||||
@@ -279,8 +312,13 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
require_all_tags: bool = True,
|
||||
require_all_tools: bool = True,
|
||||
) -> list[Recipe]:
|
||||
fltr = self._category_tag_filters(
|
||||
categories, tags, tools, require_all_categories, require_all_tags, require_all_tools
|
||||
fltr = self._build_recipe_filter(
|
||||
categories=extract_uuids(categories) if categories else None,
|
||||
tags=extract_uuids(tags) if tags else None,
|
||||
tools=extract_uuids(tools) if tools else None,
|
||||
require_all_categories=require_all_categories,
|
||||
require_all_tags=require_all_tags,
|
||||
require_all_tools=require_all_tools,
|
||||
)
|
||||
stmt = select(RecipeModel).filter(*fltr)
|
||||
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
@@ -297,7 +335,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
# See Also:
|
||||
# - https://stackoverflow.com/questions/60805/getting-random-row-through-sqlalchemy
|
||||
|
||||
filters = self._category_tag_filters(categories, tags) # type: ignore
|
||||
filters = self._build_recipe_filter(extract_uuids(categories), extract_uuids(tags)) # type: ignore
|
||||
stmt = (
|
||||
select(RecipeModel).filter(and_(*filters)).order_by(func.random()).limit(1) # Postgres and SQLite specific
|
||||
)
|
||||
|
||||
@@ -24,20 +24,15 @@ from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
|
||||
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
|
||||
from mealie.schema.recipe.recipe import (
|
||||
CreateRecipe,
|
||||
CreateRecipeByUrlBulk,
|
||||
RecipePaginationQuery,
|
||||
RecipeSummary,
|
||||
RecipeSummaryWithIngredients,
|
||||
)
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
|
||||
from mealie.schema.recipe.recipe_asset import RecipeAsset
|
||||
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient
|
||||
from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest
|
||||
from mealie.schema.recipe.recipe_settings import RecipeSettings
|
||||
from mealie.schema.recipe.recipe_step import RecipeStep
|
||||
from mealie.schema.recipe.request_helpers import RecipeDuplicate, RecipeZipTokenResponse, UpdateImageResponse
|
||||
from mealie.schema.response import PaginationBase
|
||||
from mealie.schema.response import PaginationBase, PaginationQuery
|
||||
from mealie.schema.response.pagination import RecipeSearchQuery
|
||||
from mealie.schema.response.responses import ErrorResponse
|
||||
from mealie.services import urls
|
||||
from mealie.services.event_bus_service.event_types import (
|
||||
@@ -238,31 +233,37 @@ class RecipeController(BaseRecipeController):
|
||||
# ==================================================================================================================
|
||||
# CRUD Operations
|
||||
|
||||
@router.get("", response_model=PaginationBase[RecipeSummary | RecipeSummaryWithIngredients])
|
||||
@router.get("", response_model=PaginationBase[RecipeSummary])
|
||||
def get_all(
|
||||
self,
|
||||
request: Request,
|
||||
q: RecipePaginationQuery = Depends(),
|
||||
cookbook: UUID4 | str | None = Query(None),
|
||||
q: PaginationQuery = Depends(),
|
||||
search_query: RecipeSearchQuery = Depends(),
|
||||
categories: list[UUID4 | str] | None = Query(None),
|
||||
tags: list[UUID4 | str] | None = Query(None),
|
||||
tools: list[UUID4 | str] | None = Query(None),
|
||||
foods: list[UUID4 | str] | None = Query(None),
|
||||
):
|
||||
cookbook_data: ReadCookBook | None = None
|
||||
if cookbook:
|
||||
cb_match_attr = "slug" if isinstance(cookbook, str) else "id"
|
||||
cookbook_data = self.cookbooks_repo.get_one(cookbook, cb_match_attr)
|
||||
if search_query.cookbook:
|
||||
cb_match_attr = "slug" if isinstance(search_query.cookbook, str) else "id"
|
||||
cookbook_data = self.cookbooks_repo.get_one(search_query.cookbook, cb_match_attr)
|
||||
|
||||
if cookbook is None:
|
||||
if search_query.cookbook is None:
|
||||
raise HTTPException(status_code=404, detail="cookbook not found")
|
||||
|
||||
pagination_response = self.repo.page_all(
|
||||
pagination=q,
|
||||
load_food=q.load_food,
|
||||
cookbook=cookbook_data,
|
||||
categories=categories,
|
||||
tags=tags,
|
||||
tools=tools,
|
||||
foods=foods,
|
||||
require_all_categories=search_query.require_all_categories,
|
||||
require_all_tags=search_query.require_all_tags,
|
||||
require_all_tools=search_query.require_all_tools,
|
||||
require_all_foods=search_query.require_all_foods,
|
||||
search=search_query.search,
|
||||
)
|
||||
|
||||
# merge default pagination with the request's query params
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# This file is auto-generated by gen_schema_exports.py
|
||||
from .mealie_model import MealieModel
|
||||
from .mealie_model import HasUUID, MealieModel
|
||||
|
||||
__all__ = [
|
||||
"HasUUID",
|
||||
"MealieModel",
|
||||
]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar
|
||||
from collections.abc import Sequence
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
from humps.main import camelize
|
||||
from pydantic import BaseModel
|
||||
from pydantic import UUID4, BaseModel
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
@@ -52,3 +53,11 @@ class MealieModel(BaseModel):
|
||||
val = getattr(src, field)
|
||||
if field in self.__fields__ and (val is not None or replace_null):
|
||||
setattr(self, field, val)
|
||||
|
||||
|
||||
class HasUUID(Protocol):
|
||||
id: UUID4
|
||||
|
||||
|
||||
def extract_uuids(models: Sequence[HasUUID]) -> list[UUID4]:
|
||||
return [x.id for x in models]
|
||||
|
||||
@@ -14,11 +14,7 @@ from .group_events import (
|
||||
from .group_exports import GroupDataExport
|
||||
from .group_migration import DataMigrationCreate, SupportedMigrations
|
||||
from .group_permissions import SetPermissions
|
||||
from .group_preferences import (
|
||||
CreateGroupPreferences,
|
||||
ReadGroupPreferences,
|
||||
UpdateGroupPreferences,
|
||||
)
|
||||
from .group_preferences import CreateGroupPreferences, ReadGroupPreferences, UpdateGroupPreferences
|
||||
from .group_seeder import SeederConfig
|
||||
from .group_shopping_list import (
|
||||
ShoppingListAddRecipeParams,
|
||||
@@ -41,23 +37,19 @@ from .group_shopping_list import (
|
||||
ShoppingListUpdate,
|
||||
)
|
||||
from .group_statistics import GroupStatistics, GroupStorage
|
||||
from .invite_token import (
|
||||
CreateInviteToken,
|
||||
EmailInitationResponse,
|
||||
EmailInvitation,
|
||||
ReadInviteToken,
|
||||
SaveInviteToken,
|
||||
)
|
||||
from .webhook import (
|
||||
CreateWebhook,
|
||||
ReadWebhook,
|
||||
SaveWebhook,
|
||||
WebhookPagination,
|
||||
WebhookType,
|
||||
)
|
||||
from .invite_token import CreateInviteToken, EmailInitationResponse, EmailInvitation, ReadInviteToken, SaveInviteToken
|
||||
from .webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination, WebhookType
|
||||
|
||||
__all__ = [
|
||||
"GroupAdminUpdate",
|
||||
"CreateGroupPreferences",
|
||||
"ReadGroupPreferences",
|
||||
"UpdateGroupPreferences",
|
||||
"GroupDataExport",
|
||||
"CreateWebhook",
|
||||
"ReadWebhook",
|
||||
"SaveWebhook",
|
||||
"WebhookPagination",
|
||||
"WebhookType",
|
||||
"GroupEventNotifierCreate",
|
||||
"GroupEventNotifierOptions",
|
||||
"GroupEventNotifierOptionsOut",
|
||||
@@ -67,13 +59,8 @@ __all__ = [
|
||||
"GroupEventNotifierSave",
|
||||
"GroupEventNotifierUpdate",
|
||||
"GroupEventPagination",
|
||||
"GroupDataExport",
|
||||
"DataMigrationCreate",
|
||||
"SupportedMigrations",
|
||||
"SetPermissions",
|
||||
"CreateGroupPreferences",
|
||||
"ReadGroupPreferences",
|
||||
"UpdateGroupPreferences",
|
||||
"SeederConfig",
|
||||
"ShoppingListAddRecipeParams",
|
||||
"ShoppingListCreate",
|
||||
@@ -83,9 +70,9 @@ __all__ = [
|
||||
"ShoppingListItemRecipeRefCreate",
|
||||
"ShoppingListItemRecipeRefOut",
|
||||
"ShoppingListItemRecipeRefUpdate",
|
||||
"ShoppingListItemsCollectionOut",
|
||||
"ShoppingListItemUpdate",
|
||||
"ShoppingListItemUpdateBulk",
|
||||
"ShoppingListItemsCollectionOut",
|
||||
"ShoppingListOut",
|
||||
"ShoppingListPagination",
|
||||
"ShoppingListRecipeRefOut",
|
||||
@@ -93,6 +80,8 @@ __all__ = [
|
||||
"ShoppingListSave",
|
||||
"ShoppingListSummary",
|
||||
"ShoppingListUpdate",
|
||||
"GroupAdminUpdate",
|
||||
"SetPermissions",
|
||||
"GroupStatistics",
|
||||
"GroupStorage",
|
||||
"CreateInviteToken",
|
||||
@@ -100,9 +89,4 @@ __all__ = [
|
||||
"EmailInvitation",
|
||||
"ReadInviteToken",
|
||||
"SaveInviteToken",
|
||||
"CreateWebhook",
|
||||
"ReadWebhook",
|
||||
"SaveWebhook",
|
||||
"WebhookPagination",
|
||||
"WebhookType",
|
||||
]
|
||||
|
||||
@@ -7,7 +7,6 @@ from .recipe import (
|
||||
RecipeCategory,
|
||||
RecipeCategoryPagination,
|
||||
RecipePagination,
|
||||
RecipePaginationQuery,
|
||||
RecipeSummary,
|
||||
RecipeTag,
|
||||
RecipeTagPagination,
|
||||
@@ -155,7 +154,6 @@ __all__ = [
|
||||
"RecipeCategory",
|
||||
"RecipeCategoryPagination",
|
||||
"RecipePagination",
|
||||
"RecipePaginationQuery",
|
||||
"RecipeSummary",
|
||||
"RecipeTag",
|
||||
"RecipeTagPagination",
|
||||
|
||||
@@ -12,7 +12,7 @@ from slugify import slugify
|
||||
from mealie.core.config import get_app_dirs
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.schema._mealie import MealieModel
|
||||
from mealie.schema.response.pagination import PaginationBase, PaginationQuery
|
||||
from mealie.schema.response.pagination import PaginationBase
|
||||
|
||||
from .recipe_asset import RecipeAsset
|
||||
from .recipe_comments import RecipeCommentOut
|
||||
@@ -102,14 +102,6 @@ class RecipeSummary(MealieModel):
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeSummaryWithIngredients(RecipeSummary):
|
||||
recipe_ingredient: list[RecipeIngredient] | None = []
|
||||
|
||||
|
||||
class RecipePaginationQuery(PaginationQuery):
|
||||
load_food: bool = False
|
||||
|
||||
|
||||
class RecipePagination(PaginationBase):
|
||||
items: list[RecipeSummary]
|
||||
|
||||
@@ -211,5 +203,4 @@ class Recipe(RecipeSummary):
|
||||
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient # noqa: E402
|
||||
|
||||
RecipeSummary.update_forward_refs()
|
||||
RecipeSummaryWithIngredients.update_forward_refs()
|
||||
Recipe.update_forward_refs()
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Any, Generic, TypeVar
|
||||
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
||||
|
||||
from humps import camelize
|
||||
from pydantic import BaseModel
|
||||
from pydantic import UUID4, BaseModel
|
||||
from pydantic.generics import GenericModel
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
@@ -16,6 +16,15 @@ class OrderDirection(str, enum.Enum):
|
||||
desc = "desc"
|
||||
|
||||
|
||||
class RecipeSearchQuery(MealieModel):
|
||||
cookbook: UUID4 | str | None
|
||||
require_all_categories: bool = False
|
||||
require_all_tags: bool = False
|
||||
require_all_tools: bool = False
|
||||
require_all_foods: bool = False
|
||||
search: str | None
|
||||
|
||||
|
||||
class PaginationQuery(MealieModel):
|
||||
page: int = 1
|
||||
per_page: int = 50
|
||||
|
||||
Reference in New Issue
Block a user