mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
feat: Add the ability to flag a food as "on hand", to exclude from shopping list (#3777)
This commit is contained in:
parent
4831adb0f3
commit
a062a4beaa
@ -0,0 +1,48 @@
|
|||||||
|
"""Add staple flag to foods
|
||||||
|
|
||||||
|
Revision ID: 32d69327997b
|
||||||
|
Revises: 7788478a0338
|
||||||
|
Create Date: 2024-06-22 10:17:03.323966
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "32d69327997b"
|
||||||
|
down_revision = "7788478a0338"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def is_postgres():
|
||||||
|
return op.get_context().dialect.name == "postgresql"
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
with op.batch_alter_table("ingredient_foods") as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("on_hand", sa.Boolean(), nullable=True, default=False))
|
||||||
|
|
||||||
|
bind = op.get_bind()
|
||||||
|
session = orm.Session(bind=bind)
|
||||||
|
|
||||||
|
with session:
|
||||||
|
if is_postgres():
|
||||||
|
stmt = "UPDATE ingredient_foods SET on_hand = FALSE;"
|
||||||
|
else:
|
||||||
|
stmt = "UPDATE ingredient_foods SET on_hand = 0;"
|
||||||
|
|
||||||
|
session.execute(sa.text(stmt))
|
||||||
|
|
||||||
|
# forbid nulls after migration
|
||||||
|
with op.batch_alter_table("ingredient_foods") as batch_op:
|
||||||
|
batch_op.alter_column("on_hand", nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
with op.batch_alter_table("ingredient_foods") as batch_op:
|
||||||
|
batch_op.drop_column("on_hand")
|
@ -231,7 +231,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const shoppingListIngredients: ShoppingListIngredient[] = recipe.recipeIngredient.map((ing) => {
|
const shoppingListIngredients: ShoppingListIngredient[] = recipe.recipeIngredient.map((ing) => {
|
||||||
return {
|
return {
|
||||||
checked: true,
|
checked: !ing.food?.onHand,
|
||||||
ingredient: ing,
|
ingredient: ing,
|
||||||
disableAmount: recipe.settings?.disableAmount || false,
|
disableAmount: recipe.settings?.disableAmount || false,
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ export const useFoodData = function () {
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
labelId: undefined,
|
labelId: undefined,
|
||||||
|
onHand: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
@ -26,6 +27,7 @@ export const useFoodData = function () {
|
|||||||
data.name = "";
|
data.name = "";
|
||||||
data.description = "";
|
data.description = "";
|
||||||
data.labelId = undefined;
|
data.labelId = undefined;
|
||||||
|
data.onHand = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -988,7 +988,8 @@
|
|||||||
"food-data": "Food Data",
|
"food-data": "Food Data",
|
||||||
"example-food-singular": "ex: Onion",
|
"example-food-singular": "ex: Onion",
|
||||||
"example-food-plural": "ex: Onions",
|
"example-food-plural": "ex: Onions",
|
||||||
"label-overwrite-warning": "This will assign the chosen label to all selected foods and potentially overwrite your existing labels."
|
"label-overwrite-warning": "This will assign the chosen label to all selected foods and potentially overwrite your existing labels.",
|
||||||
|
"on-hand-checkbox-label": "Setting this flag will make this food unchecked by default when adding a recipe to a shopping list."
|
||||||
},
|
},
|
||||||
"units": {
|
"units": {
|
||||||
"seed-dialog-text": "Seed the database with common units based on your local language.",
|
"seed-dialog-text": "Seed the database with common units based on your local language.",
|
||||||
|
@ -63,6 +63,7 @@ export interface CreateIngredientFood {
|
|||||||
};
|
};
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
aliases?: CreateIngredientFoodAlias[];
|
aliases?: CreateIngredientFoodAlias[];
|
||||||
|
onHand?: boolean;
|
||||||
}
|
}
|
||||||
export interface CreateIngredientFoodAlias {
|
export interface CreateIngredientFoodAlias {
|
||||||
name: string;
|
name: string;
|
||||||
@ -135,6 +136,7 @@ export interface IngredientFood {
|
|||||||
label?: MultiPurposeLabelSummary;
|
label?: MultiPurposeLabelSummary;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updateAt?: string;
|
updateAt?: string;
|
||||||
|
onHand?: boolean;
|
||||||
}
|
}
|
||||||
export interface IngredientFoodAlias {
|
export interface IngredientFoodAlias {
|
||||||
name: string;
|
name: string;
|
||||||
@ -464,7 +466,7 @@ export interface ScrapeRecipe {
|
|||||||
export interface ScrapeRecipeTest {
|
export interface ScrapeRecipeTest {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
export interface SlugResponse {}
|
export interface SlugResponse { }
|
||||||
export interface TagIn {
|
export interface TagIn {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,14 @@
|
|||||||
:label="$t('data-pages.foods.food-label')"
|
:label="$t('data-pages.foods.food-label')"
|
||||||
>
|
>
|
||||||
</v-autocomplete>
|
</v-autocomplete>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="createTarget.onHand"
|
||||||
|
hide-details
|
||||||
|
:label="$t('tool.on-hand')"
|
||||||
|
/>
|
||||||
|
<p class="text-caption mt-1">
|
||||||
|
{{ $t("data-pages.foods.on-hand-checkbox-label") }}
|
||||||
|
</p>
|
||||||
</v-form> </v-card-text
|
</v-form> </v-card-text
|
||||||
></BaseDialog>
|
></BaseDialog>
|
||||||
|
|
||||||
@ -134,6 +142,14 @@
|
|||||||
:label="$t('data-pages.foods.food-label')"
|
:label="$t('data-pages.foods.food-label')"
|
||||||
>
|
>
|
||||||
</v-autocomplete>
|
</v-autocomplete>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="editTarget.onHand"
|
||||||
|
hide-details
|
||||||
|
:label="$t('tool.on-hand')"
|
||||||
|
/>
|
||||||
|
<p class="text-caption mt-1">
|
||||||
|
{{ $t("data-pages.foods.on-hand-checkbox-label") }}
|
||||||
|
</p>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<template #custom-card-action>
|
<template #custom-card-action>
|
||||||
@ -243,6 +259,11 @@
|
|||||||
{{ item.label.name }}
|
{{ item.label.name }}
|
||||||
</MultiPurposeLabel>
|
</MultiPurposeLabel>
|
||||||
</template>
|
</template>
|
||||||
|
<template #item.onHand="{ item }">
|
||||||
|
<v-icon :color="item.onHand ? 'success' : undefined">
|
||||||
|
{{ item.onHand ? $globals.icons.check : $globals.icons.close }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
<template #button-bottom>
|
<template #button-bottom>
|
||||||
<BaseButton @click="seedDialog = true">
|
<BaseButton @click="seedDialog = true">
|
||||||
<template #icon> {{ $globals.icons.database }} </template>
|
<template #icon> {{ $globals.icons.database }} </template>
|
||||||
@ -300,6 +321,11 @@ export default defineComponent({
|
|||||||
value: "label",
|
value: "label",
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: i18n.tc("tool.on-hand"),
|
||||||
|
value: "onHand",
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const foodStore = useFoodStore();
|
const foodStore = useFoodStore();
|
||||||
|
@ -36,7 +36,9 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
|||||||
"RecipeIngredientModel", back_populates="unit"
|
"RecipeIngredientModel", back_populates="unit"
|
||||||
)
|
)
|
||||||
aliases: Mapped[list["IngredientUnitAliasModel"]] = orm.relationship(
|
aliases: Mapped[list["IngredientUnitAliasModel"]] = orm.relationship(
|
||||||
"IngredientUnitAliasModel", back_populates="unit", cascade="all, delete, delete-orphan"
|
"IngredientUnitAliasModel",
|
||||||
|
back_populates="unit",
|
||||||
|
cascade="all, delete, delete-orphan",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||||
@ -144,12 +146,15 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
|||||||
name: Mapped[str | None] = mapped_column(String)
|
name: Mapped[str | None] = mapped_column(String)
|
||||||
plural_name: Mapped[str | None] = mapped_column(String)
|
plural_name: Mapped[str | None] = mapped_column(String)
|
||||||
description: Mapped[str | None] = mapped_column(String)
|
description: Mapped[str | None] = mapped_column(String)
|
||||||
|
on_hand: Mapped[bool] = mapped_column(Boolean)
|
||||||
|
|
||||||
ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship(
|
ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship(
|
||||||
"RecipeIngredientModel", back_populates="food"
|
"RecipeIngredientModel", back_populates="food"
|
||||||
)
|
)
|
||||||
aliases: Mapped[list["IngredientFoodAliasModel"]] = orm.relationship(
|
aliases: Mapped[list["IngredientFoodAliasModel"]] = orm.relationship(
|
||||||
"IngredientFoodAliasModel", back_populates="food", cascade="all, delete, delete-orphan"
|
"IngredientFoodAliasModel",
|
||||||
|
back_populates="food",
|
||||||
|
cascade="all, delete, delete-orphan",
|
||||||
)
|
)
|
||||||
extras: Mapped[list[IngredientFoodExtras]] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan")
|
extras: Mapped[list[IngredientFoodExtras]] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan")
|
||||||
|
|
||||||
@ -162,7 +167,13 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
@api_extras
|
@api_extras
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, session: Session, name: str | None = None, plural_name: str | None = None, **_) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
session: Session,
|
||||||
|
name: str | None = None,
|
||||||
|
plural_name: str | None = None,
|
||||||
|
**_,
|
||||||
|
) -> None:
|
||||||
if name is not None:
|
if name is not None:
|
||||||
self.name_normalized = self.normalize(name)
|
self.name_normalized = self.normalize(name)
|
||||||
if plural_name is not None:
|
if plural_name is not None:
|
||||||
@ -317,7 +328,13 @@ class RecipeIngredientModel(SqlAlchemyBase, BaseMixins):
|
|||||||
original_text_normalized: Mapped[str | None] = mapped_column(String, index=True)
|
original_text_normalized: Mapped[str | None] = mapped_column(String, index=True)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, session: Session, note: str | None = None, orginal_text: str | None = None, **_) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
session: Session,
|
||||||
|
note: str | None = None,
|
||||||
|
orginal_text: str | None = None,
|
||||||
|
**_,
|
||||||
|
) -> None:
|
||||||
# SQLAlchemy events do not seem to register things that are set during auto_init
|
# SQLAlchemy events do not seem to register things that are set during auto_init
|
||||||
if note is not None:
|
if note is not None:
|
||||||
self.note_normalized = self.normalize(note)
|
self.note_normalized = self.normalize(note)
|
||||||
|
@ -36,6 +36,7 @@ class UnitFoodBase(MealieModel):
|
|||||||
plural_name: str | None = None
|
plural_name: str | None = None
|
||||||
description: str = ""
|
description: str = ""
|
||||||
extras: dict | None = {}
|
extras: dict | None = {}
|
||||||
|
on_hand: bool = False
|
||||||
|
|
||||||
@field_validator("id", mode="before")
|
@field_validator("id", mode="before")
|
||||||
def convert_empty_id_to_none(cls, v):
|
def convert_empty_id_to_none(cls, v):
|
||||||
@ -79,13 +80,19 @@ class IngredientFood(CreateIngredientFood):
|
|||||||
created_at: datetime.datetime | None = None
|
created_at: datetime.datetime | None = None
|
||||||
update_at: datetime.datetime | None = None
|
update_at: datetime.datetime | None = None
|
||||||
|
|
||||||
_searchable_properties: ClassVar[list[str]] = ["name_normalized", "plural_name_normalized"]
|
_searchable_properties: ClassVar[list[str]] = [
|
||||||
|
"name_normalized",
|
||||||
|
"plural_name_normalized",
|
||||||
|
]
|
||||||
_normalize_search: ClassVar[bool] = True
|
_normalize_search: ClassVar[bool] = True
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def loader_options(cls) -> list[LoaderOption]:
|
def loader_options(cls) -> list[LoaderOption]:
|
||||||
return [joinedload(IngredientFoodModel.extras), joinedload(IngredientFoodModel.label)]
|
return [
|
||||||
|
joinedload(IngredientFoodModel.extras),
|
||||||
|
joinedload(IngredientFoodModel.label),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class IngredientFoodPagination(PaginationBase):
|
class IngredientFoodPagination(PaginationBase):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user