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) => {
return {
checked: true,
checked: !ing.food?.onHand,
ingredient: ing,
disableAmount: recipe.settings?.disableAmount || false,
}

View File

@ -19,6 +19,7 @@ export const useFoodData = function () {
name: "",
description: "",
labelId: undefined,
onHand: false,
});
function reset() {
@ -26,6 +27,7 @@ export const useFoodData = function () {
data.name = "";
data.description = "";
data.labelId = undefined;
data.onHand = false;
}
return {

View File

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

View File

@ -63,6 +63,7 @@ export interface CreateIngredientFood {
};
labelId?: string;
aliases?: CreateIngredientFoodAlias[];
onHand?: boolean;
}
export interface CreateIngredientFoodAlias {
name: string;
@ -135,6 +136,7 @@ export interface IngredientFood {
label?: MultiPurposeLabelSummary;
createdAt?: string;
updateAt?: string;
onHand?: boolean;
}
export interface IngredientFoodAlias {
name: string;
@ -464,7 +466,7 @@ export interface ScrapeRecipe {
export interface ScrapeRecipeTest {
url: string;
}
export interface SlugResponse {}
export interface SlugResponse { }
export interface TagIn {
name: string;
}

View File

@ -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>
@ -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>
@ -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>
@ -300,6 +321,11 @@ export default defineComponent({
value: "label",
show: true,
},
{
text: i18n.tc("tool.on-hand"),
value: "onHand",
show: true,
},
];
const foodStore = useFoodStore();

View File

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

View File

@ -36,6 +36,7 @@ class UnitFoodBase(MealieModel):
plural_name: str | None = None
description: str = ""
extras: dict | None = {}
on_hand: bool = False
@field_validator("id", mode="before")
def convert_empty_id_to_none(cls, v):
@ -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):