mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-01-28 19:43:11 -05:00
feat: additional cookbook features (tags, tools, and public) (#1116)
* migration: add public, tags, and tools * generate frontend types * add help icon * start replacement for tool-tag-category selector * add help icon utility * use generator types * add support for cookbook features * add UI elements for cookbook features * fix tests * fix type error
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String, orm
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init, guid
|
||||
from ..recipe.category import Category, cookbooks_to_categories
|
||||
from ..recipe.tag import Tag, cookbooks_to_tags
|
||||
from ..recipe.tool import Tool, cookbooks_to_tools
|
||||
|
||||
|
||||
class CookBook(SqlAlchemyBase, BaseMixins):
|
||||
@@ -10,14 +12,17 @@ class CookBook(SqlAlchemyBase, BaseMixins):
|
||||
id = Column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
position = Column(Integer, nullable=False, default=1)
|
||||
|
||||
group_id = Column(guid.GUID, ForeignKey("groups.id"))
|
||||
group = orm.relationship("Group", back_populates="cookbooks")
|
||||
|
||||
name = Column(String, nullable=False)
|
||||
slug = Column(String, nullable=False)
|
||||
description = Column(String, default="")
|
||||
public = Column(Boolean, default=False)
|
||||
|
||||
categories = orm.relationship(Category, secondary=cookbooks_to_categories, single_parent=True)
|
||||
|
||||
group_id = Column(guid.GUID, ForeignKey("groups.id"))
|
||||
group = orm.relationship("Group", back_populates="cookbooks")
|
||||
tags = orm.relationship(Tag, secondary=cookbooks_to_tags, single_parent=True)
|
||||
tools = orm.relationship(Tool, secondary=cookbooks_to_tools, single_parent=True)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
|
||||
@@ -23,6 +23,13 @@ plan_rules_to_tags = sa.Table(
|
||||
sa.Column("tag_id", guid.GUID, sa.ForeignKey("tags.id")),
|
||||
)
|
||||
|
||||
cookbooks_to_tags = sa.Table(
|
||||
"cookbooks_to_tags",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("cookbook_id", guid.GUID, sa.ForeignKey("cookbooks.id")),
|
||||
sa.Column("tag_id", guid.GUID, sa.ForeignKey("tags.id")),
|
||||
)
|
||||
|
||||
|
||||
class Tag(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "tags"
|
||||
|
||||
@@ -12,6 +12,13 @@ recipes_to_tools = Table(
|
||||
Column("tool_id", GUID, ForeignKey("tools.id")),
|
||||
)
|
||||
|
||||
cookbooks_to_tools = Table(
|
||||
"cookbooks_to_tools",
|
||||
SqlAlchemyBase.metadata,
|
||||
Column("cookbook_id", GUID, ForeignKey("cookbooks.id")),
|
||||
Column("tool_id", GUID, ForeignKey("tools.id")),
|
||||
)
|
||||
|
||||
|
||||
class Tool(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "tools"
|
||||
|
||||
@@ -13,8 +13,10 @@ from mealie.db.models.recipe.ingredient import RecipeIngredient
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.db.models.recipe.settings import RecipeSettings
|
||||
from mealie.db.models.recipe.tag import Tag
|
||||
from mealie.db.models.recipe.tool import Tool
|
||||
from mealie.schema.recipe import Recipe
|
||||
from mealie.schema.recipe.recipe import RecipeCategory, RecipeSummary, RecipeTag
|
||||
from mealie.schema.recipe.recipe import RecipeCategory, RecipeSummary, RecipeTag, RecipeTool
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase, TagBase
|
||||
|
||||
from .repository_generic import RepositoryGeneric
|
||||
|
||||
@@ -123,6 +125,40 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.all()
|
||||
]
|
||||
|
||||
def _category_tag_filters(
|
||||
self,
|
||||
categories: list[CategoryBase] | None = None,
|
||||
tags: list[TagBase] | None = None,
|
||||
tools: list[RecipeTool] | None = None,
|
||||
) -> list:
|
||||
fltr = [
|
||||
RecipeModel.group_id == self.group_id,
|
||||
]
|
||||
|
||||
if categories:
|
||||
cat_ids = [x.id for x in categories]
|
||||
fltr.extend(RecipeModel.recipe_category.any(Category.id.is_(cat_id)) for cat_id in cat_ids)
|
||||
|
||||
if tags:
|
||||
tag_ids = [x.id for x in tags]
|
||||
fltr.extend(RecipeModel.tags.any(Tag.id.is_(tag_id)) for tag_id in tag_ids) # type:ignore
|
||||
|
||||
if tools:
|
||||
tool_ids = [x.id for x in tools]
|
||||
fltr.extend(RecipeModel.tools.any(Tool.id.is_(tool_id)) for tool_id in tool_ids)
|
||||
|
||||
return fltr
|
||||
|
||||
def by_category_and_tags(
|
||||
self,
|
||||
categories: list[CategoryBase] | None = None,
|
||||
tags: list[TagBase] | None = None,
|
||||
tools: list[RecipeTool] | None = None,
|
||||
) -> list[Recipe]:
|
||||
fltr = self._category_tag_filters(categories, tags, tools)
|
||||
|
||||
return [self.schema.from_orm(x) for x in self.session.query(RecipeModel).filter(*fltr).all()]
|
||||
|
||||
def get_random_by_categories_and_tags(
|
||||
self, categories: list[RecipeCategory], tags: list[RecipeTag]
|
||||
) -> list[Recipe]:
|
||||
@@ -135,17 +171,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
# See Also:
|
||||
# - https://stackoverflow.com/questions/60805/getting-random-row-through-sqlalchemy
|
||||
|
||||
filters = [
|
||||
RecipeModel.group_id == self.group_id,
|
||||
]
|
||||
|
||||
if categories:
|
||||
cat_ids = [x.id for x in categories]
|
||||
filters.extend(RecipeModel.recipe_category.any(Category.id.is_(cat_id)) for cat_id in cat_ids)
|
||||
|
||||
if tags:
|
||||
tag_ids = [x.id for x in tags]
|
||||
filters.extend(RecipeModel.tags.any(Tag.id.is_(tag_id)) for tag_id in tag_ids)
|
||||
filters = self._category_tag_filters(categories, tags) # type: ignore
|
||||
|
||||
return [
|
||||
self.schema.from_orm(x)
|
||||
|
||||
@@ -8,15 +8,10 @@ from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema import mapper
|
||||
from mealie.schema.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
||||
from mealie.schema.recipe.recipe_category import RecipeCategoryResponse
|
||||
|
||||
router = APIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"])
|
||||
|
||||
|
||||
class CookBookRecipeResponse(RecipeCookBook):
|
||||
categories: list[RecipeCategoryResponse]
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupCookbookController(BaseUserController):
|
||||
@cached_property
|
||||
@@ -37,13 +32,13 @@ class GroupCookbookController(BaseUserController):
|
||||
self.registered_exceptions,
|
||||
)
|
||||
|
||||
@router.get("", response_model=list[RecipeCookBook])
|
||||
@router.get("", response_model=list[ReadCookBook])
|
||||
def get_all(self):
|
||||
items = self.repo.get_all()
|
||||
items.sort(key=lambda x: x.position)
|
||||
return items
|
||||
|
||||
@router.post("", response_model=RecipeCookBook, status_code=201)
|
||||
@router.post("", response_model=ReadCookBook, status_code=201)
|
||||
def create_one(self, data: CreateCookBook):
|
||||
data = mapper.cast(data, SaveCookBook, group_id=self.group_id)
|
||||
return self.mixins.create_one(data)
|
||||
@@ -58,20 +53,25 @@ class GroupCookbookController(BaseUserController):
|
||||
|
||||
return updated
|
||||
|
||||
@router.get("/{item_id}", response_model=CookBookRecipeResponse)
|
||||
@router.get("/{item_id}", response_model=RecipeCookBook)
|
||||
def get_one(self, item_id: UUID4 | str):
|
||||
match_attr = "slug" if isinstance(item_id, str) else "id"
|
||||
book = self.repo.get_one(item_id, match_attr, override_schema=CookBookRecipeResponse)
|
||||
cookbook = self.repo.get_one(item_id, match_attr)
|
||||
|
||||
if book is None:
|
||||
if cookbook is None:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
return book
|
||||
return cookbook.cast(
|
||||
RecipeCookBook,
|
||||
recipes=self.repos.recipes.by_group(self.group_id).by_category_and_tags(
|
||||
cookbook.categories, cookbook.tags, cookbook.tools
|
||||
),
|
||||
)
|
||||
|
||||
@router.put("/{item_id}", response_model=RecipeCookBook)
|
||||
@router.put("/{item_id}", response_model=ReadCookBook)
|
||||
def update_one(self, item_id: str, data: CreateCookBook):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
return self.mixins.update_one(data, item_id) # type: ignore
|
||||
|
||||
@router.delete("/{item_id}", response_model=RecipeCookBook)
|
||||
@router.delete("/{item_id}", response_model=ReadCookBook)
|
||||
def delete_one(self, item_id: str):
|
||||
return self.mixins.delete_one(item_id)
|
||||
|
||||
@@ -2,19 +2,23 @@ from pydantic import UUID4, validator
|
||||
from slugify import slugify
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
from mealie.schema.recipe.recipe import RecipeSummary, RecipeTool
|
||||
|
||||
from ..recipe.recipe_category import CategoryBase, RecipeCategoryResponse
|
||||
from ..recipe.recipe_category import CategoryBase, TagBase
|
||||
|
||||
|
||||
class CreateCookBook(MealieModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
slug: str = None
|
||||
slug: str | None = None
|
||||
position: int = 1
|
||||
public: bool = False
|
||||
categories: list[CategoryBase] = []
|
||||
tags: list[TagBase] = []
|
||||
tools: list[RecipeTool] = []
|
||||
|
||||
@validator("slug", always=True, pre=True)
|
||||
def validate_slug(slug: str, values):
|
||||
def validate_slug(slug: str, values): # type: ignore
|
||||
name: str = values["name"]
|
||||
calc_slug: str = slugify(name)
|
||||
|
||||
@@ -42,7 +46,7 @@ class ReadCookBook(UpdateCookBook):
|
||||
|
||||
class RecipeCookBook(ReadCookBook):
|
||||
group_id: UUID4
|
||||
categories: list[RecipeCategoryResponse]
|
||||
recipes: list[RecipeSummary]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
Reference in New Issue
Block a user