mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	feat: support require_all property for cookbooks (#1130)
* add direction prop for icon position * add support for require_all properties on cookbook * update type annotations * add and - or filter support * update cookbook API * generate types * implement editor for additional options * update version number
This commit is contained in:
		| @@ -0,0 +1,45 @@ | |||||||
|  | """add require_all for cookbook filters | ||||||
|  |  | ||||||
|  | Revision ID: 09dfc897ad62 | ||||||
|  | Revises: 59eb59135381 | ||||||
|  | Create Date: 2022-04-03 10:48:51.379968 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | import sqlalchemy as sa | ||||||
|  |  | ||||||
|  | import mealie.db.migration_types  # noqa: F401 | ||||||
|  | from alembic import op | ||||||
|  |  | ||||||
|  | # revision identifiers, used by Alembic. | ||||||
|  | revision = "09dfc897ad62" | ||||||
|  | down_revision = "59eb59135381" | ||||||
|  | branch_labels = None | ||||||
|  | depends_on = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def upgrade(): | ||||||
|  |     # ### commands auto generated by Alembic - please adjust! ### | ||||||
|  |     op.add_column("cookbooks", sa.Column("require_all_categories", sa.Boolean(), nullable=True)) | ||||||
|  |     op.add_column("cookbooks", sa.Column("require_all_tags", sa.Boolean(), nullable=True)) | ||||||
|  |     op.add_column("cookbooks", sa.Column("require_all_tools", sa.Boolean(), nullable=True)) | ||||||
|  |  | ||||||
|  |     # Set Defaults for Existing Cookbooks | ||||||
|  |     op.execute( | ||||||
|  |         """ | ||||||
|  |         UPDATE cookbooks | ||||||
|  |         SET require_all_categories = TRUE, | ||||||
|  |             require_all_tags = TRUE, | ||||||
|  |             require_all_tools = TRUE | ||||||
|  |         """ | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # ### end Alembic commands ### | ||||||
|  |     # ### end Alembic commands ### | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def downgrade(): | ||||||
|  |     # ### commands auto generated by Alembic - please adjust! ### | ||||||
|  |     op.drop_column("cookbooks", "require_all_tools") | ||||||
|  |     op.drop_column("cookbooks", "require_all_tags") | ||||||
|  |     op.drop_column("cookbooks", "require_all_categories") | ||||||
|  |     # ### end Alembic commands ### | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="text-center"> |   <div class="text-center"> | ||||||
|     <v-menu top offset-y left open-on-hover> |     <v-menu top offset-y :right="right" :left="!right" open-on-hover> | ||||||
|       <template #activator="{ on, attrs }"> |       <template #activator="{ on, attrs }"> | ||||||
|         <v-btn :small="small" icon v-bind="attrs" v-on="on" @click.stop> |         <v-btn :small="small" icon v-bind="attrs" v-on="on" @click.stop> | ||||||
|           <v-icon :small="small"> {{ $globals.icons.help }} </v-icon> |           <v-icon :small="small"> {{ $globals.icons.help }} </v-icon> | ||||||
| @@ -24,6 +24,10 @@ export default defineComponent({ | |||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       default: false, |       default: false, | ||||||
|     }, |     }, | ||||||
|  |     right: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -41,16 +41,35 @@ | |||||||
|                 :items="allCategories || []" |                 :items="allCategories || []" | ||||||
|                 selector-type="category" |                 selector-type="category" | ||||||
|               /> |               /> | ||||||
|  |  | ||||||
|               <RecipeOrganizerSelector v-model="cookbooks[index].tags" :items="allTags || []" selector-type="tag" /> |               <RecipeOrganizerSelector v-model="cookbooks[index].tags" :items="allTags || []" selector-type="tag" /> | ||||||
|               <RecipeOrganizerSelector v-model="cookbooks[index].tools" :items="tools || []" selector-type="tool" /> |               <RecipeOrganizerSelector v-model="cookbooks[index].tools" :items="tools || []" selector-type="tool" /> | ||||||
|               <v-switch v-model="cookbooks[index].public"> |               <v-switch v-model="cookbooks[index].public" hide-details single-line> | ||||||
|                 <template #label> |                 <template #label> | ||||||
|                   Public Cookbook |                   Public Cookbook | ||||||
|                   <HelpIcon class="ml-4"> |                   <HelpIcon small right class="ml-2"> | ||||||
|                     Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page. |                     Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page. | ||||||
|                   </HelpIcon> |                   </HelpIcon> | ||||||
|                 </template> |                 </template> | ||||||
|               </v-switch> |               </v-switch> | ||||||
|  |               <div class="mt-4"> | ||||||
|  |                 <h3 class="text-subtitle-1 d-flex align-center mb-0 pb-0"> | ||||||
|  |                   Filter Options | ||||||
|  |                   <HelpIcon right small class="ml-2"> | ||||||
|  |                     When require all is selected the cookbook will only include recipes that have all of the items | ||||||
|  |                     selected. This applies to each subset of selectors and not a cross section of the selected items. | ||||||
|  |                   </HelpIcon> | ||||||
|  |                 </h3> | ||||||
|  |                 <v-switch v-model="cookbooks[index].requireAllCategories" class="mt-0" hide-details single-line> | ||||||
|  |                   <template #label> Require All Categories </template> | ||||||
|  |                 </v-switch> | ||||||
|  |                 <v-switch v-model="cookbooks[index].requireAllTags" hide-details single-line> | ||||||
|  |                   <template #label> Require All Tags </template> | ||||||
|  |                 </v-switch> | ||||||
|  |                 <v-switch v-model="cookbooks[index].requireAllTools" hide-details single-line> | ||||||
|  |                   <template #label> Require All Tools </template> | ||||||
|  |                 </v-switch> | ||||||
|  |               </div> | ||||||
|             </v-card-text> |             </v-card-text> | ||||||
|             <v-card-actions> |             <v-card-actions> | ||||||
|               <v-spacer></v-spacer> |               <v-spacer></v-spacer> | ||||||
|   | |||||||
| @@ -19,6 +19,9 @@ export interface CreateCookBook { | |||||||
|   categories?: CategoryBase[]; |   categories?: CategoryBase[]; | ||||||
|   tags?: TagBase[]; |   tags?: TagBase[]; | ||||||
|   tools?: RecipeTool[]; |   tools?: RecipeTool[]; | ||||||
|  |   requireAllCategories?: boolean; | ||||||
|  |   requireAllTags?: boolean; | ||||||
|  |   requireAllTools?: boolean; | ||||||
| } | } | ||||||
| export interface TagBase { | export interface TagBase { | ||||||
|   name: string; |   name: string; | ||||||
| @@ -40,6 +43,9 @@ export interface ReadCookBook { | |||||||
|   categories?: CategoryBase[]; |   categories?: CategoryBase[]; | ||||||
|   tags?: TagBase[]; |   tags?: TagBase[]; | ||||||
|   tools?: RecipeTool[]; |   tools?: RecipeTool[]; | ||||||
|  |   requireAllCategories?: boolean; | ||||||
|  |   requireAllTags?: boolean; | ||||||
|  |   requireAllTools?: boolean; | ||||||
|   groupId: string; |   groupId: string; | ||||||
|   id: string; |   id: string; | ||||||
| } | } | ||||||
| @@ -52,6 +58,9 @@ export interface RecipeCookBook { | |||||||
|   categories?: CategoryBase[]; |   categories?: CategoryBase[]; | ||||||
|   tags?: TagBase[]; |   tags?: TagBase[]; | ||||||
|   tools?: RecipeTool[]; |   tools?: RecipeTool[]; | ||||||
|  |   requireAllCategories?: boolean; | ||||||
|  |   requireAllTags?: boolean; | ||||||
|  |   requireAllTools?: boolean; | ||||||
|   groupId: string; |   groupId: string; | ||||||
|   id: string; |   id: string; | ||||||
|   recipes: RecipeSummary[]; |   recipes: RecipeSummary[]; | ||||||
| @@ -138,6 +147,9 @@ export interface SaveCookBook { | |||||||
|   categories?: CategoryBase[]; |   categories?: CategoryBase[]; | ||||||
|   tags?: TagBase[]; |   tags?: TagBase[]; | ||||||
|   tools?: RecipeTool[]; |   tools?: RecipeTool[]; | ||||||
|  |   requireAllCategories?: boolean; | ||||||
|  |   requireAllTags?: boolean; | ||||||
|  |   requireAllTools?: boolean; | ||||||
|   groupId: string; |   groupId: string; | ||||||
| } | } | ||||||
| export interface UpdateCookBook { | export interface UpdateCookBook { | ||||||
| @@ -149,6 +161,9 @@ export interface UpdateCookBook { | |||||||
|   categories?: CategoryBase[]; |   categories?: CategoryBase[]; | ||||||
|   tags?: TagBase[]; |   tags?: TagBase[]; | ||||||
|   tools?: RecipeTool[]; |   tools?: RecipeTool[]; | ||||||
|  |   requireAllCategories?: boolean; | ||||||
|  |   requireAllTags?: boolean; | ||||||
|  |   requireAllTools?: boolean; | ||||||
|   groupId: string; |   groupId: string; | ||||||
|   id: string; |   id: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -229,7 +229,7 @@ export interface RecipeCommentOut { | |||||||
|   user: UserBase; |   user: UserBase; | ||||||
| } | } | ||||||
| export interface UserBase { | export interface UserBase { | ||||||
|   id: number; |   id: string; | ||||||
|   username?: string; |   username?: string; | ||||||
|   admin: boolean; |   admin: boolean; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | from collections.abc import Generator | ||||||
|  |  | ||||||
| import sqlalchemy as sa | import sqlalchemy as sa | ||||||
| from sqlalchemy.orm import sessionmaker | from sqlalchemy.orm import sessionmaker | ||||||
| from sqlalchemy.orm.session import Session | from sqlalchemy.orm.session import Session | ||||||
| @@ -32,7 +34,7 @@ def create_session() -> Session: | |||||||
|     return SessionLocal() |     return SessionLocal() | ||||||
|  |  | ||||||
|  |  | ||||||
| def generate_session() -> Session: | def generate_session() -> Generator[Session, None, None]: | ||||||
|     global SessionLocal |     global SessionLocal | ||||||
|     db = SessionLocal() |     db = SessionLocal() | ||||||
|     try: |     try: | ||||||
|   | |||||||
| @@ -21,8 +21,13 @@ class CookBook(SqlAlchemyBase, BaseMixins): | |||||||
|     public = Column(Boolean, default=False) |     public = Column(Boolean, default=False) | ||||||
|  |  | ||||||
|     categories = orm.relationship(Category, secondary=cookbooks_to_categories, single_parent=True) |     categories = orm.relationship(Category, secondary=cookbooks_to_categories, single_parent=True) | ||||||
|  |     require_all_categories = Column(Boolean, default=True) | ||||||
|  |  | ||||||
|     tags = orm.relationship(Tag, secondary=cookbooks_to_tags, single_parent=True) |     tags = orm.relationship(Tag, secondary=cookbooks_to_tags, single_parent=True) | ||||||
|  |     require_all_tags = Column(Boolean, default=True) | ||||||
|  |  | ||||||
|     tools = orm.relationship(Tool, secondary=cookbooks_to_tools, single_parent=True) |     tools = orm.relationship(Tool, secondary=cookbooks_to_tools, single_parent=True) | ||||||
|  |     require_all_tools = Column(Boolean, default=True) | ||||||
|  |  | ||||||
|     @auto_init() |     @auto_init() | ||||||
|     def __init__(self, **_) -> None: |     def __init__(self, **_) -> None: | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ from .note import Note | |||||||
| from .nutrition import Nutrition | from .nutrition import Nutrition | ||||||
| from .settings import RecipeSettings | from .settings import RecipeSettings | ||||||
| from .shared import RecipeShareTokenModel | from .shared import RecipeShareTokenModel | ||||||
| from .tag import Tag, recipes_to_tags | from .tag import recipes_to_tags | ||||||
| from .tool import recipes_to_tools | from .tool import recipes_to_tools | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -99,7 +99,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | |||||||
|  |  | ||||||
|     # Mealie Specific |     # Mealie Specific | ||||||
|     settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan") |     settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan") | ||||||
|     tags: list[Tag] = orm.relationship("Tag", secondary=recipes_to_tags, back_populates="recipes") |     tags = orm.relationship("Tag", secondary=recipes_to_tags, back_populates="recipes") | ||||||
|     notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan") |     notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan") | ||||||
|     rating = sa.Column(sa.Integer) |     rating = sa.Column(sa.Integer) | ||||||
|     org_url = sa.Column(sa.String) |     org_url = sa.Column(sa.String) | ||||||
|   | |||||||
| @@ -130,6 +130,9 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]): | |||||||
|         categories: list[CategoryBase] | None = None, |         categories: list[CategoryBase] | None = None, | ||||||
|         tags: list[TagBase] | None = None, |         tags: list[TagBase] | None = None, | ||||||
|         tools: list[RecipeTool] | None = None, |         tools: list[RecipeTool] | None = None, | ||||||
|  |         require_all_categories: bool = True, | ||||||
|  |         require_all_tags: bool = True, | ||||||
|  |         require_all_tools: bool = True, | ||||||
|     ) -> list: |     ) -> list: | ||||||
|         fltr = [ |         fltr = [ | ||||||
|             RecipeModel.group_id == self.group_id, |             RecipeModel.group_id == self.group_id, | ||||||
| @@ -137,15 +140,25 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]): | |||||||
|  |  | ||||||
|         if categories: |         if categories: | ||||||
|             cat_ids = [x.id for x in categories] |             cat_ids = [x.id for x in categories] | ||||||
|  |             if require_all_categories: | ||||||
|                 fltr.extend(RecipeModel.recipe_category.any(Category.id.is_(cat_id)) for cat_id in cat_ids) |                 fltr.extend(RecipeModel.recipe_category.any(Category.id.is_(cat_id)) for cat_id in cat_ids) | ||||||
|  |             else: | ||||||
|  |                 fltr.append(RecipeModel.recipe_category.any(Category.id.in_(cat_ids))) | ||||||
|  |  | ||||||
|         if tags: |         if tags: | ||||||
|             tag_ids = [x.id for x in 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 require_all_tags: | ||||||
|  |                 fltr.extend(RecipeModel.tags.any(Tag.id.is_(tag_id)) for tag_id in tag_ids) | ||||||
|  |             else: | ||||||
|  |                 fltr.append(RecipeModel.tags.any(Tag.id.in_(tag_ids))) | ||||||
|  |  | ||||||
|         if tools: |         if tools: | ||||||
|             tool_ids = [x.id for x in tools] |             tool_ids = [x.id for x in tools] | ||||||
|  |  | ||||||
|  |             if require_all_tools: | ||||||
|                 fltr.extend(RecipeModel.tools.any(Tool.id.is_(tool_id)) for tool_id in tool_ids) |                 fltr.extend(RecipeModel.tools.any(Tool.id.is_(tool_id)) for tool_id in tool_ids) | ||||||
|  |             else: | ||||||
|  |                 fltr.append(RecipeModel.tools.any(Tool.id.in_(tool_ids))) | ||||||
|  |  | ||||||
|         return fltr |         return fltr | ||||||
|  |  | ||||||
| @@ -154,8 +167,13 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]): | |||||||
|         categories: list[CategoryBase] | None = None, |         categories: list[CategoryBase] | None = None, | ||||||
|         tags: list[TagBase] | None = None, |         tags: list[TagBase] | None = None, | ||||||
|         tools: list[RecipeTool] | None = None, |         tools: list[RecipeTool] | None = None, | ||||||
|  |         require_all_categories: bool = True, | ||||||
|  |         require_all_tags: bool = True, | ||||||
|  |         require_all_tools: bool = True, | ||||||
|     ) -> list[Recipe]: |     ) -> list[Recipe]: | ||||||
|         fltr = self._category_tag_filters(categories, tags, tools) |         fltr = self._category_tag_filters( | ||||||
|  |             categories, tags, tools, require_all_categories, require_all_tags, require_all_tools | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         return [self.schema.from_orm(x) for x in self.session.query(RecipeModel).filter(*fltr).all()] |         return [self.schema.from_orm(x) for x in self.session.query(RecipeModel).filter(*fltr).all()] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -64,7 +64,12 @@ class GroupCookbookController(BaseUserController): | |||||||
|         return cookbook.cast( |         return cookbook.cast( | ||||||
|             RecipeCookBook, |             RecipeCookBook, | ||||||
|             recipes=self.repos.recipes.by_group(self.group_id).by_category_and_tags( |             recipes=self.repos.recipes.by_group(self.group_id).by_category_and_tags( | ||||||
|                 cookbook.categories, cookbook.tags, cookbook.tools |                 cookbook.categories, | ||||||
|  |                 cookbook.tags, | ||||||
|  |                 cookbook.tools, | ||||||
|  |                 cookbook.require_all_categories, | ||||||
|  |                 cookbook.require_all_tags, | ||||||
|  |                 cookbook.require_all_tools, | ||||||
|             ), |             ), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,6 +16,9 @@ class CreateCookBook(MealieModel): | |||||||
|     categories: list[CategoryBase] = [] |     categories: list[CategoryBase] = [] | ||||||
|     tags: list[TagBase] = [] |     tags: list[TagBase] = [] | ||||||
|     tools: list[RecipeTool] = [] |     tools: list[RecipeTool] = [] | ||||||
|  |     require_all_categories: bool = True | ||||||
|  |     require_all_tags: bool = True | ||||||
|  |     require_all_tools: bool = True | ||||||
|  |  | ||||||
|     @validator("public", always=True, pre=True) |     @validator("public", always=True, pre=True) | ||||||
|     def validate_public(public: bool | None, values: dict) -> bool:  # type: ignore |     def validate_public(public: bool | None, values: dict) -> bool:  # type: ignore | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from mealie.core.config import get_app_settings | |||||||
| from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter | from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter | ||||||
|  |  | ||||||
| ALEMBIC_VERSIONS = [ | ALEMBIC_VERSIONS = [ | ||||||
|     {"version_num": "59eb59135381"}, |     {"version_num": "09dfc897ad62"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user