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:
Hayden
2022-04-01 09:50:31 -08:00
committed by GitHub
parent 1092e0ce7c
commit cfaac2e060
23 changed files with 374 additions and 97 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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