feat: Add the ability to flag a food as "on hand", to exclude from shopping list (#3777)

This commit is contained in:
boc-the-git 2024-06-29 01:16:04 +10:00 committed by GitHub
parent 4831adb0f3
commit a062a4beaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 112 additions and 9 deletions

View File

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

View File

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

View File

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

View File

@ -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.",

View File

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

View File

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

View File

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

View File

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