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: Add the ability to flag a food as "on hand", to exclude from shopping list #3777

Merged
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export default defineComponent({

const shoppingListIngredients: ShoppingListIngredient[] = recipe.recipeIngredient.map((ing) => {
return {
checked: true,
checked: !ing.food?.onHand,
ingredient: ing,
disableAmount: recipe.settings?.disableAmount || false,
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/composables/store/use-food-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ export const useFoodData = function () {
name: "",
description: "",
labelId: undefined,
onHand: false,
});

function reset() {
data.id = "";
data.name = "";
data.description = "";
data.labelId = undefined;
data.onHand = false;
}

return {
Expand Down
3 changes: 2 additions & 1 deletion frontend/lang/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,8 @@
"food-data": "Food Data",
"example-food-singular": "ex: Onion",
"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": {
"seed-dialog-text": "Seed the database with common units based on your local language.",
Expand Down
4 changes: 3 additions & 1 deletion frontend/lib/api/types/recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface CreateIngredientFood {
};
labelId?: string;
aliases?: CreateIngredientFoodAlias[];
onHand?: boolean;
}
export interface CreateIngredientFoodAlias {
name: string;
Expand Down Expand Up @@ -135,6 +136,7 @@ export interface IngredientFood {
label?: MultiPurposeLabelSummary;
createdAt?: string;
updateAt?: string;
onHand?: boolean;
}
export interface IngredientFoodAlias {
name: string;
Expand Down Expand Up @@ -464,7 +466,7 @@ export interface ScrapeRecipe {
export interface ScrapeRecipeTest {
url: string;
}
export interface SlugResponse {}
export interface SlugResponse { }
export interface TagIn {
name: string;
}
Expand Down
26 changes: 26 additions & 0 deletions frontend/pages/group/data/foods.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@
:label="$t('data-pages.foods.food-label')"
>
</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
></BaseDialog>

Expand Down Expand Up @@ -134,6 +142,14 @@
:label="$t('data-pages.foods.food-label')"
>
</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-card-text>
<template #custom-card-action>
Expand Down Expand Up @@ -243,6 +259,11 @@
{{ item.label.name }}
</MultiPurposeLabel>
</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>
<BaseButton @click="seedDialog = true">
<template #icon> {{ $globals.icons.database }} </template>
Expand Down Expand Up @@ -300,6 +321,11 @@ export default defineComponent({
value: "label",
show: true,
},
{
text: i18n.tc("tool.on-hand"),
value: "onHand",
show: true,
},
];

const foodStore = useFoodStore();
Expand Down
25 changes: 21 additions & 4 deletions mealie/db/models/recipe/ingredient.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
"RecipeIngredientModel", back_populates="unit"
)
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
Expand Down Expand Up @@ -144,12 +146,15 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
name: Mapped[str | None] = mapped_column(String)
plural_name: Mapped[str | None] = mapped_column(String)
description: Mapped[str | None] = mapped_column(String)
on_hand: Mapped[bool] = mapped_column(Boolean)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only actual functional change in this file


ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship(
"RecipeIngredientModel", back_populates="food"
)
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")

Expand All @@ -162,7 +167,13 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):

@api_extras
@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:
self.name_normalized = self.normalize(name)
if plural_name is not None:
Expand Down Expand Up @@ -317,7 +328,13 @@ class RecipeIngredientModel(SqlAlchemyBase, BaseMixins):
original_text_normalized: Mapped[str | None] = mapped_column(String, index=True)

@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
if note is not None:
self.note_normalized = self.normalize(note)
Expand Down
11 changes: 9 additions & 2 deletions mealie/schema/recipe/recipe_ingredient.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class UnitFoodBase(MealieModel):
plural_name: str | None = None
description: str = ""
extras: dict | None = {}
on_hand: bool = False
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only actual functional change in this file


@field_validator("id", mode="before")
def convert_empty_id_to_none(cls, v):
Expand Down Expand Up @@ -79,13 +80,19 @@ class IngredientFood(CreateIngredientFood):
created_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
model_config = ConfigDict(from_attributes=True)

@classmethod
def loader_options(cls) -> list[LoaderOption]:
return [joinedload(IngredientFoodModel.extras), joinedload(IngredientFoodModel.label)]
return [
joinedload(IngredientFoodModel.extras),
joinedload(IngredientFoodModel.label),
]


class IngredientFoodPagination(PaginationBase):
Expand Down
Loading