Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Query Filter Builder for Cookbooks and Meal Plans #4346

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
37adb8e
create field editor
michael-genson Oct 2, 2024
c385ef5
added dynamic rendering and qf string builder
michael-genson Oct 6, 2024
aeaf81b
extend parenthesis
michael-genson Oct 7, 2024
d09fd39
fix switching from multiselect
michael-genson Oct 7, 2024
7e79557
converted date picker to menu
michael-genson Oct 7, 2024
540c5aa
Made fields draggable
michael-genson Oct 7, 2024
8b86ada
tweaked rendering
michael-genson Oct 7, 2024
31d6f75
more style tweaks
michael-genson Oct 7, 2024
2baaa83
add delete and format actions
michael-genson Oct 7, 2024
d38525c
add organizers
michael-genson Oct 8, 2024
fffb10d
Merge remote-tracking branch 'upstream/mealie-next' into feat/query-f…
michael-genson Oct 8, 2024
387bf08
better types
michael-genson Oct 8, 2024
479d969
add support for food/household organizers
michael-genson Oct 8, 2024
fd41a3b
add foods/households to qf builder
michael-genson Oct 8, 2024
ab69ba4
fixed type import
michael-genson Oct 8, 2024
71b41a0
added qf string to models
michael-genson Oct 8, 2024
7359d3e
added json models for query filters
michael-genson Oct 8, 2024
9c5e085
updated schemas, repos, and controllers
michael-genson Oct 8, 2024
36f99be
dev:generate
michael-genson Oct 8, 2024
699b5f3
added support for injecting queryfilter from API
michael-genson Oct 9, 2024
824c766
adjustments for small screens
michael-genson Oct 9, 2024
89399a1
fix pydantic validators
michael-genson Oct 9, 2024
0cba0ac
fix fetching cookbook recipes
michael-genson Oct 9, 2024
f4b6a72
updated migrations to convert existing rules to qfs
michael-genson Oct 9, 2024
b4b6ba8
slim builder a bit
michael-genson Oct 9, 2024
a3b8d89
fix reactivity issues
michael-genson Oct 9, 2024
835dfaa
fix cookbooks composable
michael-genson Oct 9, 2024
7a4555e
add qf builder to cb editor
michael-genson Oct 9, 2024
cdbb77c
replace rule editor with qf builder
michael-genson Oct 9, 2024
3bbaa33
renamed qf builder
michael-genson Oct 9, 2024
8e69498
delete new cbs with invalid qfs
michael-genson Oct 9, 2024
0430062
updated descriptions
michael-genson Oct 9, 2024
a4f7d02
removed console log
michael-genson Oct 9, 2024
3fc8f1f
fixed a bunch of type issues
michael-genson Oct 9, 2024
fa952b5
added additional field options
michael-genson Oct 9, 2024
0becb6c
translated labels
michael-genson Oct 9, 2024
e48c9a8
made operators translatable
michael-genson Oct 10, 2024
09aefe2
extracted functionality to composable
michael-genson Oct 10, 2024
6a6faf7
tweaked style
michael-genson Oct 10, 2024
d52d580
added QueryFilterJSON test
michael-genson Oct 10, 2024
d9fc7d2
clean up .temp dir before initializing testing
michael-genson Oct 10, 2024
5bd7cba
fix pytest warning
michael-genson Oct 10, 2024
decafa4
add tests for new migration
michael-genson Oct 10, 2024
ffc8728
fix public cookbook recipes
michael-genson Oct 10, 2024
2afec37
added/updated cookbook tests
michael-genson Oct 10, 2024
5d75509
updated mealplan tests
michael-genson Oct 10, 2024
c5bca5b
fixed public recipe cookbook route
michael-genson Oct 10, 2024
14c797c
added/updated recipe tests
michael-genson Oct 10, 2024
2141535
improved query filter handling
michael-genson Oct 10, 2024
89f9f21
lint
michael-genson Oct 10, 2024
11c429c
Merge branch 'mealie-next' into feat/query-filter-builder
michael-genson Oct 10, 2024
0068e1e
more lint
michael-genson Oct 10, 2024
23f08fd
even more lint
michael-genson Oct 10, 2024
4f5f3c7
removed unused method
michael-genson Oct 10, 2024
da09be8
Merge remote-tracking branch 'upstream/mealie-next' into feat/query-f…
michael-genson Oct 13, 2024
a115903
fix migration
michael-genson Oct 13, 2024
f2d8665
added missing submit disable
michael-genson Oct 15, 2024
71eb805
bad imports
michael-genson Oct 15, 2024
c72e78d
formatting tweaks to add more space
michael-genson Oct 15, 2024
72bf544
increased field value size
michael-genson Oct 15, 2024
028ae7e
added/updated translations for all relations
michael-genson Oct 15, 2024
aa4d129
removed unnecessary logical operator nulling
michael-genson Oct 15, 2024
3fbaef6
Merge branch 'mealie-next' into feat/query-filter-builder
michael-genson Oct 15, 2024
dc9e5c3
slightly less zealous size
michael-genson Oct 15, 2024
68b5c00
slightly less slightly less zealous size
michael-genson Oct 15, 2024
ad79887
Merge remote-tracking branch 'upstream/mealie-next' into feat/query-f…
michael-genson Oct 17, 2024
e82414d
added validations
michael-genson Oct 17, 2024
37b8be2
added tests for new validations
michael-genson Oct 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"""added query_filter_string to cookbook and mealplan

Revision ID: 86054b40fd06
Revises: 602927e1013e
Create Date: 2024-10-08 21:17:31.601903

"""

import sqlalchemy as sa
from sqlalchemy import orm

from alembic import op
from mealie.db.models._model_utils import guid

# revision identifiers, used by Alembic.
revision = "86054b40fd06"
down_revision: str | None = "602927e1013e"
branch_labels: str | tuple[str, ...] | None = None
depends_on: str | tuple[str, ...] | None = None


# Intermediate table definitions
class SqlAlchemyBase(orm.DeclarativeBase):
pass


class Category(SqlAlchemyBase):
__tablename__ = "categories"
id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)


class Tag(SqlAlchemyBase):
__tablename__ = "tags"
id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)


class Tool(SqlAlchemyBase):
__tablename__ = "tools"
id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)


class Household(SqlAlchemyBase):
__tablename__ = "households"
id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)


cookbooks_to_categories = sa.Table(
"cookbooks_to_categories",
SqlAlchemyBase.metadata,
sa.Column("cookbook_id", guid.GUID, sa.ForeignKey("cookbooks.id"), index=True),
sa.Column("category_id", guid.GUID, sa.ForeignKey("categories.id"), index=True),
)

cookbooks_to_tags = sa.Table(
"cookbooks_to_tags",
SqlAlchemyBase.metadata,
sa.Column("cookbook_id", guid.GUID, sa.ForeignKey("cookbooks.id"), index=True),
sa.Column("tag_id", guid.GUID, sa.ForeignKey("tags.id"), index=True),
)

cookbooks_to_tools = sa.Table(
"cookbooks_to_tools",
SqlAlchemyBase.metadata,
sa.Column("cookbook_id", guid.GUID, sa.ForeignKey("cookbooks.id"), index=True),
sa.Column("tool_id", guid.GUID, sa.ForeignKey("tools.id"), index=True),
)

plan_rules_to_categories = sa.Table(
"plan_rules_to_categories",
SqlAlchemyBase.metadata,
sa.Column("group_plan_rule_id", guid.GUID, sa.ForeignKey("group_meal_plan_rules.id"), index=True),
sa.Column("category_id", guid.GUID, sa.ForeignKey("categories.id"), index=True),
)

plan_rules_to_tags = sa.Table(
"plan_rules_to_tags",
SqlAlchemyBase.metadata,
sa.Column("plan_rule_id", guid.GUID, sa.ForeignKey("group_meal_plan_rules.id"), index=True),
sa.Column("tag_id", guid.GUID, sa.ForeignKey("tags.id"), index=True),
)

plan_rules_to_households = sa.Table(
"plan_rules_to_households",
SqlAlchemyBase.metadata,
sa.Column("group_plan_rule_id", guid.GUID, sa.ForeignKey("group_meal_plan_rules.id"), index=True),
sa.Column("household_id", guid.GUID, sa.ForeignKey("households.id"), index=True),
)


class CookBook(SqlAlchemyBase):
__tablename__ = "cookbooks"

id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
query_filter_string: orm.Mapped[str] = orm.mapped_column(sa.String, nullable=False, default="")

categories: orm.Mapped[list[Category]] = orm.relationship(
Category, secondary=cookbooks_to_categories, single_parent=True
)
require_all_categories: orm.Mapped[bool | None] = orm.mapped_column(sa.Boolean, default=True)

tags: orm.Mapped[list[Tag]] = orm.relationship(Tag, secondary=cookbooks_to_tags, single_parent=True)
require_all_tags: orm.Mapped[bool | None] = orm.mapped_column(sa.Boolean, default=True)

tools: orm.Mapped[list[Tool]] = orm.relationship(Tool, secondary=cookbooks_to_tools, single_parent=True)
require_all_tools: orm.Mapped[bool | None] = orm.mapped_column(sa.Boolean, default=True)


class GroupMealPlanRules(SqlAlchemyBase):
__tablename__ = "group_meal_plan_rules"

id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
query_filter_string: orm.Mapped[str] = orm.mapped_column(sa.String, nullable=False, default="")

categories: orm.Mapped[list[Category]] = orm.relationship(Category, secondary=plan_rules_to_categories)
tags: orm.Mapped[list[Tag]] = orm.relationship(Tag, secondary=plan_rules_to_tags)
households: orm.Mapped[list["Household"]] = orm.relationship("Household", secondary=plan_rules_to_households)


def migrate_cookbooks():
bind = op.get_bind()
session = orm.Session(bind=bind)

cookbooks = session.query(CookBook).all()
for cookbook in cookbooks:
parts = []
if cookbook.categories:
relop = "CONTAINS ALL" if cookbook.require_all_categories else "IN"
vals = ",".join([f'"{cat.id}"' for cat in cookbook.categories])
parts.append(f"recipe_category.id {relop} [{vals}]")
if cookbook.tags:
relop = "CONTAINS ALL" if cookbook.require_all_tags else "IN"
vals = ",".join([f'"{tag.id}"' for tag in cookbook.tags])
parts.append(f"tags.id {relop} [{vals}]")
if cookbook.tools:
relop = "CONTAINS ALL" if cookbook.require_all_tools else "IN"
vals = ",".join([f'"{tool.id}"' for tool in cookbook.tools])
parts.append(f"tools.id {relop} [{vals}]")

cookbook.query_filter_string = " AND ".join(parts)

session.commit()


def migrate_mealplan_rules():
bind = op.get_bind()
session = orm.Session(bind=bind)

rules = session.query(GroupMealPlanRules).all()
for rule in rules:
parts = []
if rule.categories:
vals = ",".join([f'"{cat.id}"' for cat in rule.categories])
parts.append(f"recipe_category.id CONTAINS ALL [{vals}]")
if rule.tags:
vals = ",".join([f'"{tag.id}"' for tag in rule.tags])
parts.append(f"tags.id CONTAINS ALL [{vals}]")
if rule.households:
vals = ",".join([f'"{household.id}"' for household in rule.households])
parts.append(f"household_id IN [{vals}]")

rule.query_filter_string = " AND ".join(parts)

session.commit()


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("cookbooks", schema=None) as batch_op:
batch_op.add_column(sa.Column("query_filter_string", sa.String(), nullable=False, server_default=""))

with op.batch_alter_table("group_meal_plan_rules", schema=None) as batch_op:
batch_op.add_column(sa.Column("query_filter_string", sa.String(), nullable=False, server_default=""))

# ### end Alembic commands ###

migrate_cookbooks()
migrate_mealplan_rules()


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("group_meal_plan_rules", schema=None) as batch_op:
batch_op.drop_column("query_filter_string")

with op.batch_alter_table("cookbooks", schema=None) as batch_op:
batch_op.drop_column("query_filter_string")

# ### end Alembic commands ###
81 changes: 57 additions & 24 deletions frontend/components/Domain/Cookbook/CookbookEditor.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<template>
<div>
<v-card-text v-if="cookbook">
<v-card-text v-if="cookbook" class="px-1">
<v-text-field v-model="cookbook.name" :label="$t('cookbook.cookbook-name')"></v-text-field>
<v-textarea v-model="cookbook.description" auto-grow :rows="2" :label="$t('recipe.description')"></v-textarea>
<RecipeOrganizerSelector v-model="cookbook.categories" selector-type="categories" />
<RecipeOrganizerSelector v-model="cookbook.tags" selector-type="tags" />
<RecipeOrganizerSelector v-model="cookbook.tools" selector-type="tools" />
<QueryFilterBuilder
:field-defs="fieldDefs"
:initial-query-filter="cookbook.queryFilter"
@input="handleInput"
/>
<v-switch v-model="cookbook.public" hide-details single-line>
<template #label>
{{ $t('cookbook.public-cookbook') }}
Expand All @@ -14,33 +16,19 @@
</HelpIcon>
</template>
</v-switch>
<div class="mt-4">
<h3 class="text-subtitle-1 d-flex align-center mb-0 pb-0">
{{ $t('cookbook.filter-options') }}
<HelpIcon right small class="ml-2">
{{ $t('cookbook.filter-options-description') }}
</HelpIcon>
</h3>
<v-switch v-model="cookbook.requireAllCategories" class="mt-0" hide-details single-line>
<template #label> {{ $t('cookbook.require-all-categories') }} </template>
</v-switch>
<v-switch v-model="cookbook.requireAllTags" hide-details single-line>
<template #label> {{ $t('cookbook.require-all-tags') }} </template>
</v-switch>
<v-switch v-model="cookbook.requireAllTools" hide-details single-line>
<template #label> {{ $t('cookbook.require-all-tools') }} </template>
</v-switch>
</div>
</v-card-text>
</div>
</template>

<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
import { defineComponent, useContext } from "@nuxtjs/composition-api";
import { ReadCookBook } from "~/lib/api/types/cookbook";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { Organizer } from "~/lib/api/types/non-generated";
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
import { FieldDefinition } from "~/composables/use-query-filter-builder";

export default defineComponent({
components: { RecipeOrganizerSelector },
components: { QueryFilterBuilder },
props: {
cookbook: {
type: Object as () => ReadCookBook,
Expand All @@ -51,5 +39,50 @@ export default defineComponent({
required: true,
},
},
setup(props) {
const { i18n } = useContext();

function handleInput(value: string | undefined) {
props.cookbook.queryFilterString = value || "";
}

const fieldDefs: FieldDefinition[] = [
{
name: "recipe_category.id",
label: i18n.tc("category.categories"),
type: Organizer.Category,
},
{
name: "tags.id",
label: i18n.tc("tag.tags"),
type: Organizer.Tag,
},
{
name: "tools.id",
label: i18n.tc("tool.tools"),
type: Organizer.Tool,
},
{
name: "household_id",
label: i18n.tc("household.households"),
type: Organizer.Household,
},
{
name: "created_at",
label: i18n.tc("general.date-created"),
type: "date",
},
{
name: "updated_at",
label: i18n.tc("general.date-updated"),
type: "date",
},
];

return {
handleInput,
fieldDefs,
};
},
});
</script>
4 changes: 3 additions & 1 deletion frontend/components/Domain/Cookbook/CookbookPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
<BaseDialog
michael-genson marked this conversation as resolved.
Show resolved Hide resolved
v-if="editTarget"
v-model="dialogStates.edit"
:width="650"
width="100%"
max-width="1100px"
:icon="$globals.icons.pages"
Kuchenpirat marked this conversation as resolved.
Show resolved Hide resolved
:title="$t('general.edit')"
:submit-icon="$globals.icons.save"
:submit-text="$tc('general.save')"
:submit-disabled="!editTarget.queryFilterString"
@submit="editCookbook"
>
<v-card-text>
Expand Down
Loading
Loading