mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
feat: Add Household Filter to Meal Plan Rules (#4231)
This commit is contained in:
parent
38502e82d4
commit
4712994242
@ -0,0 +1,53 @@
|
|||||||
|
"""add households filter to meal plans
|
||||||
|
|
||||||
|
Revision ID: 1fe4bd37ccc8
|
||||||
|
Revises: be568e39ffdf
|
||||||
|
Create Date: 2024-09-18 14:52:55.831540
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
import mealie.db.migration_types
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "1fe4bd37ccc8"
|
||||||
|
down_revision: str | None = "be568e39ffdf"
|
||||||
|
branch_labels: str | tuple[str, ...] | None = None
|
||||||
|
depends_on: str | tuple[str, ...] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
"plan_rules_to_households",
|
||||||
|
sa.Column("group_plan_rule_id", mealie.db.migration_types.GUID(), nullable=True),
|
||||||
|
sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["group_plan_rule_id"],
|
||||||
|
["group_meal_plan_rules.id"],
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["household_id"],
|
||||||
|
["households.id"],
|
||||||
|
),
|
||||||
|
sa.UniqueConstraint("group_plan_rule_id", "household_id", name="group_plan_rule_id_household_id_key"),
|
||||||
|
)
|
||||||
|
with op.batch_alter_table("plan_rules_to_households", schema=None) as batch_op:
|
||||||
|
batch_op.create_index(
|
||||||
|
batch_op.f("ix_plan_rules_to_households_group_plan_rule_id"), ["group_plan_rule_id"], unique=False
|
||||||
|
)
|
||||||
|
batch_op.create_index(batch_op.f("ix_plan_rules_to_households_household_id"), ["household_id"], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table("plan_rules_to_households", schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f("ix_plan_rules_to_households_household_id"))
|
||||||
|
batch_op.drop_index(batch_op.f("ix_plan_rules_to_households_group_plan_rule_id"))
|
||||||
|
|
||||||
|
op.drop_table("plan_rules_to_households")
|
||||||
|
# ### end Alembic commands ###
|
@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<v-select
|
||||||
|
v-model="selected"
|
||||||
|
:items="households"
|
||||||
|
:label="label"
|
||||||
|
:hint="description"
|
||||||
|
:persistent-hint="!!description"
|
||||||
|
item-text="name"
|
||||||
|
:multiple="multiselect"
|
||||||
|
:prepend-inner-icon="$globals.icons.household"
|
||||||
|
return-object
|
||||||
|
>
|
||||||
|
<template #selection="data">
|
||||||
|
<v-chip
|
||||||
|
:key="data.index"
|
||||||
|
class="ma-1"
|
||||||
|
:input-value="data.selected"
|
||||||
|
small
|
||||||
|
close
|
||||||
|
label
|
||||||
|
color="accent"
|
||||||
|
dark
|
||||||
|
@click:close="removeByIndex(data.index)"
|
||||||
|
>
|
||||||
|
{{ data.item.name || data.item }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, onMounted, useContext } from "@nuxtjs/composition-api";
|
||||||
|
import { useHouseholdStore } from "~/composables/store/use-household-store";
|
||||||
|
|
||||||
|
interface HouseholdLike {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array as () => HouseholdLike[],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
multiselect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, context) {
|
||||||
|
const selected = computed({
|
||||||
|
get: () => props.value,
|
||||||
|
set: (val) => {
|
||||||
|
context.emit("input", val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (selected.value === undefined) {
|
||||||
|
selected.value = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { i18n } = useContext();
|
||||||
|
const label = computed(
|
||||||
|
() => props.multiselect ? i18n.tc("household.households") : i18n.tc("household.household")
|
||||||
|
);
|
||||||
|
|
||||||
|
const { store: households } = useHouseholdStore();
|
||||||
|
function removeByIndex(index: number) {
|
||||||
|
if (selected.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||||
|
selected.value = [...newSelected];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selected,
|
||||||
|
label,
|
||||||
|
households,
|
||||||
|
removeByIndex,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -5,8 +5,15 @@
|
|||||||
<v-select v-model="inputEntryType" :items="MEAL_TYPE_OPTIONS" :label="$t('meal-plan.meal-type')"></v-select>
|
<v-select v-model="inputEntryType" :items="MEAL_TYPE_OPTIONS" :label="$t('meal-plan.meal-type')"></v-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
<RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
|
<RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
|
||||||
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
|
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
|
||||||
|
<GroupHouseholdSelector
|
||||||
|
v-model="inputHouseholds"
|
||||||
|
multiselect
|
||||||
|
:description="$tc('meal-plan.mealplan-households-description')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- TODO: proper pluralization of inputDay -->
|
<!-- TODO: proper pluralization of inputDay -->
|
||||||
{{ $t('meal-plan.this-rule-will-apply', {
|
{{ $t('meal-plan.this-rule-will-apply', {
|
||||||
@ -18,11 +25,13 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
|
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
|
||||||
|
import GroupHouseholdSelector from "~/components/Domain/Household/GroupHouseholdSelector.vue";
|
||||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||||
import { RecipeTag, RecipeCategory } from "~/lib/api/types/recipe";
|
import { PlanCategory, PlanHousehold, PlanTag } from "~/lib/api/types/meal-plan";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
GroupHouseholdSelector,
|
||||||
RecipeOrganizerSelector,
|
RecipeOrganizerSelector,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@ -35,11 +44,15 @@ export default defineComponent({
|
|||||||
default: "unset",
|
default: "unset",
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
type: Array as () => RecipeCategory[],
|
type: Array as () => PlanCategory[],
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
type: Array as () => RecipeTag[],
|
type: Array as () => PlanTag[],
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
households: {
|
||||||
|
type: Array as () => PlanHousehold[],
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
showHelp: {
|
showHelp: {
|
||||||
@ -105,6 +118,15 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inputHouseholds = computed({
|
||||||
|
get: () => {
|
||||||
|
return props.households;
|
||||||
|
},
|
||||||
|
set: (val) => {
|
||||||
|
context.emit("update:households", val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
MEAL_TYPE_OPTIONS,
|
MEAL_TYPE_OPTIONS,
|
||||||
MEAL_DAY_OPTIONS,
|
MEAL_DAY_OPTIONS,
|
||||||
@ -112,6 +134,7 @@ export default defineComponent({
|
|||||||
inputEntryType,
|
inputEntryType,
|
||||||
inputCategories,
|
inputCategories,
|
||||||
inputTags,
|
inputTags,
|
||||||
|
inputHouseholds,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -315,6 +315,10 @@
|
|||||||
"mealplan-settings": "Mealplan Settings",
|
"mealplan-settings": "Mealplan Settings",
|
||||||
"mealplan-update-failed": "Mealplan update failed",
|
"mealplan-update-failed": "Mealplan update failed",
|
||||||
"mealplan-updated": "Mealplan Updated",
|
"mealplan-updated": "Mealplan Updated",
|
||||||
|
"mealplan-households-description": "If no household is selected, recipes can be added from any household",
|
||||||
|
"any-category": "Any Category",
|
||||||
|
"any-tag": "Any Tag",
|
||||||
|
"any-household": "Any Household",
|
||||||
"no-meal-plan-defined-yet": "No meal plan defined yet",
|
"no-meal-plan-defined-yet": "No meal plan defined yet",
|
||||||
"no-meal-planned-for-today": "No meal planned for today",
|
"no-meal-planned-for-today": "No meal planned for today",
|
||||||
"numberOfDays-hint": "Number of days on page load",
|
"numberOfDays-hint": "Number of days on page load",
|
||||||
|
@ -31,13 +31,24 @@ export interface ListItem {
|
|||||||
quantity?: number;
|
quantity?: number;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
}
|
}
|
||||||
|
export interface PlanCategory {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
export interface PlanHousehold {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
export interface PlanRulesCreate {
|
export interface PlanRulesCreate {
|
||||||
day?: PlanRulesDay & string;
|
day?: PlanRulesDay & string;
|
||||||
entryType?: PlanRulesType & string;
|
entryType?: PlanRulesType & string;
|
||||||
categories?: Category[];
|
categories?: PlanCategory[];
|
||||||
tags?: Tag[];
|
tags?: PlanTag[];
|
||||||
|
households?: PlanHousehold[];
|
||||||
}
|
}
|
||||||
export interface Tag {
|
export interface PlanTag {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@ -45,8 +56,9 @@ export interface Tag {
|
|||||||
export interface PlanRulesOut {
|
export interface PlanRulesOut {
|
||||||
day?: PlanRulesDay & string;
|
day?: PlanRulesDay & string;
|
||||||
entryType?: PlanRulesType & string;
|
entryType?: PlanRulesType & string;
|
||||||
categories?: Category[];
|
categories?: PlanCategory[];
|
||||||
tags?: Tag[];
|
tags?: PlanTag[];
|
||||||
|
households?: PlanHousehold[];
|
||||||
groupId: string;
|
groupId: string;
|
||||||
householdId: string;
|
householdId: string;
|
||||||
id: string;
|
id: string;
|
||||||
@ -54,8 +66,9 @@ export interface PlanRulesOut {
|
|||||||
export interface PlanRulesSave {
|
export interface PlanRulesSave {
|
||||||
day?: PlanRulesDay & string;
|
day?: PlanRulesDay & string;
|
||||||
entryType?: PlanRulesType & string;
|
entryType?: PlanRulesType & string;
|
||||||
categories?: Category[];
|
categories?: PlanCategory[];
|
||||||
tags?: Tag[];
|
tags?: PlanTag[];
|
||||||
|
households?: PlanHousehold[];
|
||||||
groupId: string;
|
groupId: string;
|
||||||
householdId: string;
|
householdId: string;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
:entry-type.sync="createData.entryType"
|
:entry-type.sync="createData.entryType"
|
||||||
:categories.sync="createData.categories"
|
:categories.sync="createData.categories"
|
||||||
:tags.sync="createData.tags"
|
:tags.sync="createData.tags"
|
||||||
|
:households.sync="createData.households"
|
||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions class="justify-end">
|
<v-card-actions class="justify-end">
|
||||||
@ -58,12 +59,58 @@
|
|||||||
<template v-if="!editState[rule.id]">
|
<template v-if="!editState[rule.id]">
|
||||||
<div v-if="rule.categories">
|
<div v-if="rule.categories">
|
||||||
<h4 class="py-1">{{ $t("category.categories") }}:</h4>
|
<h4 class="py-1">{{ $t("category.categories") }}:</h4>
|
||||||
<RecipeChips :items="rule.categories" small />
|
<RecipeChips v-if="rule.categories.length" :items="rule.categories" small class="pb-3" />
|
||||||
|
<v-card-text
|
||||||
|
v-else
|
||||||
|
label
|
||||||
|
class="ma-0 px-0 pt-0 pb-3"
|
||||||
|
text-color="accent"
|
||||||
|
small
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
{{ $tc("meal-plan.any-category") }}
|
||||||
|
</v-card-text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="rule.tags">
|
<div v-if="rule.tags">
|
||||||
<h4 class="py-1">{{ $t("tag.tags") }}:</h4>
|
<h4 class="py-1">{{ $t("tag.tags") }}:</h4>
|
||||||
<RecipeChips :items="rule.tags" url-prefix="tags" small />
|
<RecipeChips v-if="rule.tags.length" :items="rule.tags" url-prefix="tags" small class="pb-3" />
|
||||||
|
<v-card-text
|
||||||
|
v-else
|
||||||
|
label
|
||||||
|
class="ma-0 px-0 pt-0 pb-3"
|
||||||
|
text-color="accent"
|
||||||
|
small
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
{{ $tc("meal-plan.any-tag") }}
|
||||||
|
</v-card-text>
|
||||||
|
</div>
|
||||||
|
<div v-if="rule.households">
|
||||||
|
<h4 class="py-1">{{ $t("household.households") }}:</h4>
|
||||||
|
<div v-if="rule.households.length">
|
||||||
|
<v-chip
|
||||||
|
v-for="household in rule.households"
|
||||||
|
:key="household.id"
|
||||||
|
label
|
||||||
|
class="ma-1"
|
||||||
|
color="accent"
|
||||||
|
small
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
{{ household.name }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
<v-card-text
|
||||||
|
v-else
|
||||||
|
label
|
||||||
|
class="ma-0 px-0 pt-0 pb-3"
|
||||||
|
text-color="accent"
|
||||||
|
small
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
{{ $tc("meal-plan.any-household") }}
|
||||||
|
</v-card-text>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -72,6 +119,7 @@
|
|||||||
:entry-type.sync="allRules[idx].entryType"
|
:entry-type.sync="allRules[idx].entryType"
|
||||||
:categories.sync="allRules[idx].categories"
|
:categories.sync="allRules[idx].categories"
|
||||||
:tags.sync="allRules[idx].tags"
|
:tags.sync="allRules[idx].tags"
|
||||||
|
:households.sync="allRules[idx].households"
|
||||||
/>
|
/>
|
||||||
<div class="d-flex justify-end">
|
<div class="d-flex justify-end">
|
||||||
<BaseButton update @click="updateRule(rule)" />
|
<BaseButton update @click="updateRule(rule)" />
|
||||||
@ -138,6 +186,7 @@ export default defineComponent({
|
|||||||
day: "unset",
|
day: "unset",
|
||||||
categories: [],
|
categories: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
|
households: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createRule() {
|
async function createRule() {
|
||||||
@ -149,6 +198,7 @@ export default defineComponent({
|
|||||||
day: "unset",
|
day: "unset",
|
||||||
categories: [],
|
categories: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
|
households: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from sqlalchemy import Date, ForeignKey, String, orm
|
from sqlalchemy import Column, Date, ForeignKey, String, Table, UniqueConstraint, orm
|
||||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
@ -18,6 +18,14 @@ if TYPE_CHECKING:
|
|||||||
from ..users import User
|
from ..users import User
|
||||||
from .household import Household
|
from .household import Household
|
||||||
|
|
||||||
|
plan_rules_to_households = Table(
|
||||||
|
"plan_rules_to_households",
|
||||||
|
SqlAlchemyBase.metadata,
|
||||||
|
Column("group_plan_rule_id", GUID, ForeignKey("group_meal_plan_rules.id"), index=True),
|
||||||
|
Column("household_id", GUID, ForeignKey("households.id"), index=True),
|
||||||
|
UniqueConstraint("group_plan_rule_id", "household_id", name="group_plan_rule_id_household_id_key"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
||||||
__tablename__ = "group_meal_plan_rules"
|
__tablename__ = "group_meal_plan_rules"
|
||||||
@ -33,8 +41,10 @@ class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
|||||||
String, nullable=False, default=""
|
String, nullable=False, default=""
|
||||||
) # "breakfast", "lunch", "dinner", "side"
|
) # "breakfast", "lunch", "dinner", "side"
|
||||||
|
|
||||||
|
# Filters
|
||||||
categories: Mapped[list[Category]] = orm.relationship(Category, secondary=plan_rules_to_categories)
|
categories: Mapped[list[Category]] = orm.relationship(Category, secondary=plan_rules_to_categories)
|
||||||
tags: Mapped[list[Tag]] = orm.relationship(Tag, secondary=plan_rules_to_tags)
|
tags: Mapped[list[Tag]] = orm.relationship(Tag, secondary=plan_rules_to_tags)
|
||||||
|
households: Mapped[list["Household"]] = orm.relationship("Household", secondary=plan_rules_to_households)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, **_) -> None:
|
def __init__(self, **_) -> None:
|
||||||
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
from datetime import datetime, timezone
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Any, Generic, TypeVar
|
from typing import Any, Generic, TypeVar
|
||||||
|
|
||||||
@ -62,6 +63,9 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
def household_id(self) -> UUID4 | None:
|
def household_id(self) -> UUID4 | None:
|
||||||
return self._household_id
|
return self._household_id
|
||||||
|
|
||||||
|
def _random_seed(self) -> str:
|
||||||
|
return str(datetime.now(tz=timezone.utc))
|
||||||
|
|
||||||
def _log_exception(self, e: Exception) -> None:
|
def _log_exception(self, e: Exception) -> None:
|
||||||
self.logger.error(f"Error processing query for Repo model={self.model.__name__} schema={self.schema.__name__}")
|
self.logger.error(f"Error processing query for Repo model={self.model.__name__} schema={self.schema.__name__}")
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
@ -409,6 +413,9 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
# this solution is db-independent & stable to paging
|
# this solution is db-independent & stable to paging
|
||||||
temp_query = query.with_only_columns(self.model.id)
|
temp_query = query.with_only_columns(self.model.id)
|
||||||
allids = self.session.execute(temp_query).scalars().all() # fast because id is indexed
|
allids = self.session.execute(temp_query).scalars().all() # fast because id is indexed
|
||||||
|
if not allids:
|
||||||
|
return query
|
||||||
|
|
||||||
order = list(range(len(allids)))
|
order = list(range(len(allids)))
|
||||||
random.seed(pagination.pagination_seed)
|
random.seed(pagination.pagination_seed)
|
||||||
random.shuffle(order)
|
random.shuffle(order)
|
||||||
|
@ -23,7 +23,6 @@ from mealie.schema.recipe.recipe import (
|
|||||||
RecipeCategory,
|
RecipeCategory,
|
||||||
RecipePagination,
|
RecipePagination,
|
||||||
RecipeSummary,
|
RecipeSummary,
|
||||||
RecipeTag,
|
|
||||||
RecipeTool,
|
RecipeTool,
|
||||||
)
|
)
|
||||||
from mealie.schema.recipe.recipe_category import CategoryBase, TagBase
|
from mealie.schema.recipe.recipe_category import CategoryBase, TagBase
|
||||||
@ -99,6 +98,9 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
ids.append(i_as_uuid)
|
ids.append(i_as_uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
slugs.append(i)
|
slugs.append(i)
|
||||||
|
|
||||||
|
if not slugs:
|
||||||
|
return ids
|
||||||
additional_ids = self.session.execute(sa.select(model.id).filter(model.slug.in_(slugs))).scalars().all()
|
additional_ids = self.session.execute(sa.select(model.id).filter(model.slug.in_(slugs))).scalars().all()
|
||||||
return ids + additional_ids
|
return ids + additional_ids
|
||||||
|
|
||||||
@ -308,27 +310,6 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
stmt = sa.select(RecipeModel).filter(*fltr)
|
stmt = sa.select(RecipeModel).filter(*fltr)
|
||||||
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||||
|
|
||||||
def get_random_by_categories_and_tags(
|
|
||||||
self, categories: list[RecipeCategory], tags: list[RecipeTag]
|
|
||||||
) -> list[Recipe]:
|
|
||||||
"""
|
|
||||||
get_random_by_categories returns a single random Recipe that contains every category provided
|
|
||||||
in the list. This uses a function built in to Postgres and SQLite to get a random row limited
|
|
||||||
to 1 entry.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# See Also:
|
|
||||||
# - https://stackoverflow.com/questions/60805/getting-random-row-through-sqlalchemy
|
|
||||||
|
|
||||||
filters = self._build_recipe_filter(extract_uuids(categories), extract_uuids(tags)) # type: ignore
|
|
||||||
stmt = (
|
|
||||||
sa.select(RecipeModel)
|
|
||||||
.filter(sa.and_(*filters))
|
|
||||||
.order_by(sa.func.random())
|
|
||||||
.limit(1) # Postgres and SQLite specific
|
|
||||||
)
|
|
||||||
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
|
||||||
|
|
||||||
def get_random(self, limit=1) -> list[Recipe]:
|
def get_random(self, limit=1) -> list[Recipe]:
|
||||||
stmt = sa.select(RecipeModel).order_by(sa.func.random()).limit(limit) # Postgres and SQLite specific
|
stmt = sa.select(RecipeModel).order_by(sa.func.random()).limit(limit) # Postgres and SQLite specific
|
||||||
if self.group_id:
|
if self.group_id:
|
||||||
|
@ -4,14 +4,15 @@ from functools import cached_property
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from mealie.core.exceptions import mealie_registered_exceptions
|
from mealie.core.exceptions import mealie_registered_exceptions
|
||||||
|
from mealie.repos.all_repositories import get_repositories
|
||||||
from mealie.repos.repository_meals import RepositoryMeals
|
from mealie.repos.repository_meals import RepositoryMeals
|
||||||
from mealie.routes._base import controller
|
from mealie.routes._base import controller
|
||||||
from mealie.routes._base.base_controllers import BaseCrudController
|
from mealie.routes._base.base_controllers import BaseCrudController
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.schema import mapper
|
from mealie.schema import mapper
|
||||||
from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry
|
from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry
|
||||||
from mealie.schema.meal_plan.new_meal import CreateRandomEntry, PlanEntryPagination
|
from mealie.schema.meal_plan.new_meal import CreateRandomEntry, PlanEntryPagination, PlanEntryType
|
||||||
from mealie.schema.meal_plan.plan_rules import PlanRulesDay
|
from mealie.schema.meal_plan.plan_rules import PlanCategory, PlanHousehold, PlanRulesDay, PlanTag
|
||||||
from mealie.schema.recipe.recipe import Recipe
|
from mealie.schema.recipe.recipe import Recipe
|
||||||
from mealie.schema.response.pagination import PaginationQuery
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.schema.response.responses import ErrorResponse
|
from mealie.schema.response.responses import ErrorResponse
|
||||||
@ -40,6 +41,47 @@ class GroupMealplanController(BaseCrudController):
|
|||||||
self.registered_exceptions,
|
self.registered_exceptions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_random_recipes_from_mealplan(
|
||||||
|
self, plan_date: date, entry_type: PlanEntryType, limit: int = 1
|
||||||
|
) -> list[Recipe]:
|
||||||
|
"""
|
||||||
|
Gets rules for a mealplan and returns a list of random recipes based on the rules.
|
||||||
|
May return zero recipes if no recipes match the filter criteria.
|
||||||
|
|
||||||
|
Recipes from all households are included unless the rules specify a household filter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rules = self.repos.group_meal_plan_rules.get_rules(PlanRulesDay.from_date(plan_date), entry_type.value)
|
||||||
|
cross_household_recipes = get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
|
||||||
|
|
||||||
|
tags: list[PlanTag] = []
|
||||||
|
categories: list[PlanCategory] = []
|
||||||
|
households: list[PlanHousehold] = []
|
||||||
|
for rule in rules:
|
||||||
|
if rule.tags:
|
||||||
|
tags.extend(rule.tags)
|
||||||
|
if rule.categories:
|
||||||
|
categories.extend(rule.categories)
|
||||||
|
if rule.households:
|
||||||
|
households.extend(rule.households)
|
||||||
|
|
||||||
|
if not (tags or categories or households):
|
||||||
|
return cross_household_recipes.get_random(limit=limit)
|
||||||
|
|
||||||
|
category_ids = [category.id for category in categories] or None
|
||||||
|
tag_ids = [tag.id for tag in tags] or None
|
||||||
|
household_ids = [household.id for household in households] or None
|
||||||
|
|
||||||
|
recipes_data = cross_household_recipes.page_all(
|
||||||
|
pagination=PaginationQuery(
|
||||||
|
page=1, per_page=limit, order_by="random", pagination_seed=self.repo._random_seed()
|
||||||
|
),
|
||||||
|
categories=category_ids,
|
||||||
|
tags=tag_ids,
|
||||||
|
households=household_ids,
|
||||||
|
)
|
||||||
|
return recipes_data.items
|
||||||
|
|
||||||
@router.get("/today")
|
@router.get("/today")
|
||||||
def get_todays_meals(self):
|
def get_todays_meals(self):
|
||||||
return self.repo.get_today()
|
return self.repo.get_today()
|
||||||
@ -47,36 +89,19 @@ class GroupMealplanController(BaseCrudController):
|
|||||||
@router.post("/random", response_model=ReadPlanEntry)
|
@router.post("/random", response_model=ReadPlanEntry)
|
||||||
def create_random_meal(self, data: CreateRandomEntry):
|
def create_random_meal(self, data: CreateRandomEntry):
|
||||||
"""
|
"""
|
||||||
create_random_meal is a route that provides the randomized functionality for mealplaners.
|
`create_random_meal` is a route that provides the randomized functionality for mealplaners.
|
||||||
It operates by following the rules setout in the Groups mealplan settings. If not settings
|
It operates by following the rules set out in the household's mealplan settings. If no settings
|
||||||
are set, it will default return any random meal.
|
are set, it will return any random meal.
|
||||||
|
|
||||||
Refer to the mealplan settings routes for more information on how rules can be applied
|
Refer to the mealplan settings routes for more information on how rules can be applied
|
||||||
to the random meal selector.
|
to the random meal selector.
|
||||||
"""
|
"""
|
||||||
# Get relevant group rules
|
random_recipes = self._get_random_recipes_from_mealplan(data.date, data.entry_type)
|
||||||
rules = self.repos.group_meal_plan_rules.get_rules(PlanRulesDay.from_date(data.date), data.entry_type.value)
|
if not random_recipes:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404, detail=ErrorResponse.respond(message=self.t("mealplan.no-recipes-match-your-rules"))
|
||||||
|
)
|
||||||
|
|
||||||
recipe_repo = self.repos.recipes
|
|
||||||
random_recipes: list[Recipe] = []
|
|
||||||
|
|
||||||
if not rules: # If no rules are set, return any random recipe from the group
|
|
||||||
random_recipes = recipe_repo.get_random()
|
|
||||||
else: # otherwise construct a query based on the rules
|
|
||||||
tags = []
|
|
||||||
categories = []
|
|
||||||
for rule in rules:
|
|
||||||
if rule.tags:
|
|
||||||
tags.extend(rule.tags)
|
|
||||||
if rule.categories:
|
|
||||||
categories.extend(rule.categories)
|
|
||||||
|
|
||||||
if tags or categories:
|
|
||||||
random_recipes = self.repos.recipes.get_random_by_categories_and_tags(categories, tags)
|
|
||||||
else:
|
|
||||||
random_recipes = recipe_repo.get_random()
|
|
||||||
|
|
||||||
try:
|
|
||||||
recipe = random_recipes[0]
|
recipe = random_recipes[0]
|
||||||
return self.mixins.create_one(
|
return self.mixins.create_one(
|
||||||
SavePlanEntry(
|
SavePlanEntry(
|
||||||
@ -87,10 +112,6 @@ class GroupMealplanController(BaseCrudController):
|
|||||||
user_id=self.user.id,
|
user_id=self.user.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except IndexError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=404, detail=ErrorResponse.respond(message=self.t("mealplan.no-recipes-match-your-rules"))
|
|
||||||
) from e
|
|
||||||
|
|
||||||
@router.get("", response_model=PlanEntryPagination)
|
@router.get("", response_model=PlanEntryPagination)
|
||||||
def get_all(
|
def get_all(
|
||||||
|
@ -9,14 +9,16 @@ from .new_meal import (
|
|||||||
UpdatePlanEntry,
|
UpdatePlanEntry,
|
||||||
)
|
)
|
||||||
from .plan_rules import (
|
from .plan_rules import (
|
||||||
Category,
|
BasePlanRuleFilter,
|
||||||
|
PlanCategory,
|
||||||
|
PlanHousehold,
|
||||||
PlanRulesCreate,
|
PlanRulesCreate,
|
||||||
PlanRulesDay,
|
PlanRulesDay,
|
||||||
PlanRulesOut,
|
PlanRulesOut,
|
||||||
PlanRulesPagination,
|
PlanRulesPagination,
|
||||||
PlanRulesSave,
|
PlanRulesSave,
|
||||||
PlanRulesType,
|
PlanRulesType,
|
||||||
Tag,
|
PlanTag,
|
||||||
)
|
)
|
||||||
from .shopping_list import ListItem, ShoppingListIn, ShoppingListOut
|
from .shopping_list import ListItem, ShoppingListIn, ShoppingListOut
|
||||||
|
|
||||||
@ -31,12 +33,14 @@ __all__ = [
|
|||||||
"ReadPlanEntry",
|
"ReadPlanEntry",
|
||||||
"SavePlanEntry",
|
"SavePlanEntry",
|
||||||
"UpdatePlanEntry",
|
"UpdatePlanEntry",
|
||||||
"Category",
|
"BasePlanRuleFilter",
|
||||||
|
"PlanCategory",
|
||||||
|
"PlanHousehold",
|
||||||
"PlanRulesCreate",
|
"PlanRulesCreate",
|
||||||
"PlanRulesDay",
|
"PlanRulesDay",
|
||||||
"PlanRulesOut",
|
"PlanRulesOut",
|
||||||
"PlanRulesPagination",
|
"PlanRulesPagination",
|
||||||
"PlanRulesSave",
|
"PlanRulesSave",
|
||||||
"PlanRulesType",
|
"PlanRulesType",
|
||||||
"Tag",
|
"PlanTag",
|
||||||
]
|
]
|
||||||
|
@ -5,19 +5,27 @@ from pydantic import UUID4, ConfigDict
|
|||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from sqlalchemy.orm.interfaces import LoaderOption
|
from sqlalchemy.orm.interfaces import LoaderOption
|
||||||
|
|
||||||
from mealie.db.models.household import GroupMealPlanRules
|
from mealie.db.models.household import GroupMealPlanRules, Household
|
||||||
|
from mealie.db.models.recipe import Category, Tag
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
from mealie.schema.response.pagination import PaginationBase
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
|
|
||||||
class Category(MealieModel):
|
class BasePlanRuleFilter(MealieModel):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
name: str
|
name: str
|
||||||
slug: str
|
slug: str
|
||||||
|
|
||||||
|
|
||||||
|
class PlanCategory(BasePlanRuleFilter):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
class Tag(Category):
|
class PlanTag(BasePlanRuleFilter):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PlanHousehold(BasePlanRuleFilter):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
@ -51,8 +59,9 @@ class PlanRulesType(str, Enum):
|
|||||||
class PlanRulesCreate(MealieModel):
|
class PlanRulesCreate(MealieModel):
|
||||||
day: PlanRulesDay = PlanRulesDay.unset
|
day: PlanRulesDay = PlanRulesDay.unset
|
||||||
entry_type: PlanRulesType = PlanRulesType.unset
|
entry_type: PlanRulesType = PlanRulesType.unset
|
||||||
categories: list[Category] = []
|
categories: list[PlanCategory] = []
|
||||||
tags: list[Tag] = []
|
tags: list[PlanTag] = []
|
||||||
|
households: list[PlanHousehold] = []
|
||||||
|
|
||||||
|
|
||||||
class PlanRulesSave(PlanRulesCreate):
|
class PlanRulesSave(PlanRulesCreate):
|
||||||
@ -66,7 +75,23 @@ class PlanRulesOut(PlanRulesSave):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def loader_options(cls) -> list[LoaderOption]:
|
def loader_options(cls) -> list[LoaderOption]:
|
||||||
return [joinedload(GroupMealPlanRules.categories), joinedload(GroupMealPlanRules.tags)]
|
return [
|
||||||
|
joinedload(GroupMealPlanRules.categories).load_only(
|
||||||
|
Category.id,
|
||||||
|
Category.name,
|
||||||
|
Category.slug,
|
||||||
|
),
|
||||||
|
joinedload(GroupMealPlanRules.tags).load_only(
|
||||||
|
Tag.id,
|
||||||
|
Tag.name,
|
||||||
|
Tag.slug,
|
||||||
|
),
|
||||||
|
joinedload(GroupMealPlanRules.households).load_only(
|
||||||
|
Household.id,
|
||||||
|
Household.name,
|
||||||
|
Household.slug,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PlanRulesPagination(PaginationBase):
|
class PlanRulesPagination(PaginationBase):
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
|
import random
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from mealie.schema.household.household import HouseholdSummary
|
||||||
from mealie.schema.meal_plan.new_meal import CreatePlanEntry
|
from mealie.schema.meal_plan.new_meal import CreatePlanEntry
|
||||||
|
from mealie.schema.meal_plan.plan_rules import PlanRulesDay, PlanRulesOut, PlanRulesSave, PlanRulesType
|
||||||
|
from mealie.schema.recipe.recipe import Recipe
|
||||||
|
from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagOut, TagSave
|
||||||
from tests.utils import api_routes
|
from tests.utils import api_routes
|
||||||
from tests.utils.factories import random_string
|
from tests.utils.factories import random_string
|
||||||
from tests.utils.fixture_schemas import TestUser
|
from tests.utils.fixture_schemas import TestUser
|
||||||
@ -14,6 +20,39 @@ def route_all_slice(page: int, perPage: int, start_date: str, end_date: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_recipe(unique_user: TestUser, tags: list[TagOut] | None = None, categories: list[CategoryOut] | None = None):
|
||||||
|
return unique_user.repos.recipes.create(
|
||||||
|
Recipe(
|
||||||
|
user_id=unique_user.user_id,
|
||||||
|
group_id=UUID(unique_user.group_id),
|
||||||
|
name=random_string(),
|
||||||
|
tags=tags or [],
|
||||||
|
recipe_category=categories or [],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_rule(
|
||||||
|
unique_user: TestUser,
|
||||||
|
day: PlanRulesDay,
|
||||||
|
entry_type: PlanRulesType,
|
||||||
|
tags: list[TagOut] | None = None,
|
||||||
|
categories: list[CategoryOut] | None = None,
|
||||||
|
households: list[HouseholdSummary] | None = None,
|
||||||
|
):
|
||||||
|
return unique_user.repos.group_meal_plan_rules.create(
|
||||||
|
PlanRulesSave(
|
||||||
|
group_id=UUID(unique_user.group_id),
|
||||||
|
household_id=UUID(unique_user.household_id),
|
||||||
|
day=day,
|
||||||
|
entry_type=entry_type,
|
||||||
|
tags=tags or [],
|
||||||
|
categories=categories or [],
|
||||||
|
households=households or [],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_mealplan_no_recipe(api_client: TestClient, unique_user: TestUser):
|
def test_create_mealplan_no_recipe(api_client: TestClient, unique_user: TestUser):
|
||||||
title = random_string(length=25)
|
title = random_string(length=25)
|
||||||
text = random_string(length=25)
|
text = random_string(length=25)
|
||||||
@ -167,3 +206,128 @@ def test_get_mealplan_today(api_client: TestClient, unique_user: TestUser):
|
|||||||
|
|
||||||
for meal_plan in response_json:
|
for meal_plan in response_json:
|
||||||
assert meal_plan["date"] == datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
|
assert meal_plan["date"] == datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_mealplan_with_rules_categories_and_tags_filter(api_client: TestClient, unique_user: TestUser):
|
||||||
|
tags = [
|
||||||
|
unique_user.repos.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id)) for _ in range(4)
|
||||||
|
]
|
||||||
|
categories = [
|
||||||
|
unique_user.repos.categories.create(CategorySave(name=random_string(), group_id=unique_user.group_id))
|
||||||
|
for _ in range(4)
|
||||||
|
]
|
||||||
|
[
|
||||||
|
create_recipe(unique_user, tags=[tag], categories=[category])
|
||||||
|
for tag, category in zip(tags, categories, strict=True)
|
||||||
|
]
|
||||||
|
[create_recipe(unique_user) for _ in range(5)]
|
||||||
|
|
||||||
|
i = random.randint(0, 3)
|
||||||
|
tag = tags[i]
|
||||||
|
category = categories[i]
|
||||||
|
rule = create_rule(
|
||||||
|
unique_user,
|
||||||
|
day=PlanRulesDay.saturday,
|
||||||
|
entry_type=PlanRulesType.breakfast,
|
||||||
|
tags=[tag],
|
||||||
|
categories=[category],
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = {"date": "2023-02-25", "entryType": "breakfast"}
|
||||||
|
response = api_client.post(api_routes.households_mealplans_random, json=payload, headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
recipe_data = response.json()["recipe"]
|
||||||
|
assert recipe_data["tags"][0]["name"] == tag.name
|
||||||
|
assert recipe_data["recipeCategory"][0]["name"] == category.name
|
||||||
|
finally:
|
||||||
|
unique_user.repos.group_meal_plan_rules.delete(rule.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_mealplan_with_rules_date_and_type_filter(api_client: TestClient, unique_user: TestUser):
|
||||||
|
tags = [
|
||||||
|
unique_user.repos.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id)) for _ in range(4)
|
||||||
|
]
|
||||||
|
recipes = [create_recipe(unique_user, tags=[tag]) for tag in tags]
|
||||||
|
[create_recipe(unique_user) for _ in range(5)]
|
||||||
|
|
||||||
|
rules: list[PlanRulesOut] = []
|
||||||
|
rules.append(
|
||||||
|
create_rule(unique_user, day=PlanRulesDay.saturday, entry_type=PlanRulesType.breakfast, tags=[tags[0]])
|
||||||
|
)
|
||||||
|
rules.append(create_rule(unique_user, day=PlanRulesDay.saturday, entry_type=PlanRulesType.dinner, tags=[tags[1]]))
|
||||||
|
rules.append(create_rule(unique_user, day=PlanRulesDay.sunday, entry_type=PlanRulesType.breakfast, tags=[tags[2]]))
|
||||||
|
rules.append(create_rule(unique_user, day=PlanRulesDay.sunday, entry_type=PlanRulesType.dinner, tags=[tags[3]]))
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = {"date": "2023-02-25", "entryType": "breakfast"}
|
||||||
|
response = api_client.post(api_routes.households_mealplans_random, json=payload, headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["recipe"]["slug"] == recipes[0].slug
|
||||||
|
finally:
|
||||||
|
for rule in rules:
|
||||||
|
unique_user.repos.group_meal_plan_rules.delete(rule.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_mealplan_with_rules_includes_other_households(
|
||||||
|
api_client: TestClient, unique_user: TestUser, h2_user: TestUser
|
||||||
|
):
|
||||||
|
tag = h2_user.repos.tags.create(TagSave(name=random_string(), group_id=h2_user.group_id))
|
||||||
|
recipe = create_recipe(h2_user, tags=[tag])
|
||||||
|
rule = create_rule(unique_user, day=PlanRulesDay.saturday, entry_type=PlanRulesType.breakfast, tags=[tag])
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = {"date": "2023-02-25", "entryType": "breakfast"}
|
||||||
|
response = api_client.post(api_routes.households_mealplans_random, json=payload, headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["recipe"]["slug"] == recipe.slug
|
||||||
|
finally:
|
||||||
|
unique_user.repos.group_meal_plan_rules.delete(rule.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_mealplan_with_rules_households_filter(api_client: TestClient, unique_user: TestUser, h2_user: TestUser):
|
||||||
|
tag = unique_user.repos.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id))
|
||||||
|
recipe = create_recipe(unique_user, tags=[tag])
|
||||||
|
[create_recipe(h2_user, tags=[tag]) for _ in range(10)]
|
||||||
|
|
||||||
|
household = unique_user.repos.households.get_by_slug_or_id(unique_user.household_id)
|
||||||
|
assert household
|
||||||
|
|
||||||
|
rule = create_rule(
|
||||||
|
unique_user, day=PlanRulesDay.saturday, entry_type=PlanRulesType.breakfast, tags=[tag], households=[household]
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = {"date": "2023-02-25", "entryType": "breakfast"}
|
||||||
|
response = api_client.post(api_routes.households_mealplans_random, json=payload, headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["recipe"]["slug"] == recipe.slug
|
||||||
|
finally:
|
||||||
|
unique_user.repos.group_meal_plan_rules.delete(rule.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_mealplan_with_rules_households_filter_includes_any_households(
|
||||||
|
api_client: TestClient, unique_user: TestUser, h2_user: TestUser
|
||||||
|
):
|
||||||
|
tag = unique_user.repos.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id))
|
||||||
|
recipe = create_recipe(h2_user, tags=[tag])
|
||||||
|
|
||||||
|
household = unique_user.repos.households.get_by_slug_or_id(unique_user.household_id)
|
||||||
|
assert household
|
||||||
|
h2_household = unique_user.repos.households.get_by_slug_or_id(h2_user.household_id)
|
||||||
|
assert h2_household
|
||||||
|
rule = create_rule(
|
||||||
|
unique_user,
|
||||||
|
day=PlanRulesDay.saturday,
|
||||||
|
entry_type=PlanRulesType.breakfast,
|
||||||
|
tags=[tag],
|
||||||
|
households=[household, h2_household],
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = {"date": "2023-02-25", "entryType": "breakfast"}
|
||||||
|
response = api_client.post(api_routes.households_mealplans_random, json=payload, headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["recipe"]["slug"] == recipe.slug
|
||||||
|
finally:
|
||||||
|
unique_user.repos.group_meal_plan_rules.delete(rule.id)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user