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:
Hayden 2022-04-03 16:32:58 -08:00 committed by GitHub
parent c988de1921
commit 10784b6e24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 129 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]
fltr.extend(RecipeModel.recipe_category.any(Category.id.is_(cat_id)) for cat_id in cat_ids) if require_all_categories:
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]
fltr.extend(RecipeModel.tools.any(Tool.id.is_(tool_id)) for tool_id in tool_ids)
if require_all_tools:
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()]

View File

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

View File

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

View File

@ -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"},
] ]