Skip to content

Commit

Permalink
feat: Structured Yields (mealie-recipes#4489)
Browse files Browse the repository at this point in the history
Co-authored-by: Kuchenpirat <[email protected]>
  • Loading branch information
michael-genson and Kuchenpirat authored Nov 20, 2024
1 parent c8cd68b commit 327da02
Show file tree
Hide file tree
Showing 39 changed files with 1,017 additions and 550 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import sqlalchemy as sa

import mealie.db.migration_types
from alembic import op

# revision identifiers, used by Alembic.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""add recipe yield quantity
Revision ID: b1020f328e98
Revises: 3897397b4631
Create Date: 2024-10-23 15:50:59.888793
"""

import sqlalchemy as sa
from sqlalchemy import orm

from alembic import op
from mealie.db.models._model_utils.guid import GUID
from mealie.services.scraper.cleaner import clean_yield

# revision identifiers, used by Alembic.
revision = "b1020f328e98"
down_revision: str | None = "3897397b4631"
branch_labels: str | tuple[str, ...] | None = None
depends_on: str | tuple[str, ...] | None = None


# Intermediate table definitions
class SqlAlchemyBase(orm.DeclarativeBase):
pass


class RecipeModel(SqlAlchemyBase):
__tablename__ = "recipes"

id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
recipe_yield: orm.Mapped[str | None] = orm.mapped_column(sa.String)
recipe_yield_quantity: orm.Mapped[float] = orm.mapped_column(sa.Float, index=True, default=0)
recipe_servings: orm.Mapped[float] = orm.mapped_column(sa.Float, index=True, default=0)


def parse_recipe_yields():
bind = op.get_bind()
session = orm.Session(bind=bind)

for recipe in session.query(RecipeModel).all():
try:
recipe.recipe_servings, recipe.recipe_yield_quantity, recipe.recipe_yield = clean_yield(recipe.recipe_yield)
except Exception:
recipe.recipe_servings = 0
recipe.recipe_yield_quantity = 0

session.commit()


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("recipes", schema=None) as batch_op:
batch_op.add_column(sa.Column("recipe_yield_quantity", sa.Float(), nullable=False, server_default="0"))
batch_op.create_index(batch_op.f("ix_recipes_recipe_yield_quantity"), ["recipe_yield_quantity"], unique=False)
batch_op.add_column(sa.Column("recipe_servings", sa.Float(), nullable=False, server_default="0"))
batch_op.create_index(batch_op.f("ix_recipes_recipe_servings"), ["recipe_servings"], unique=False)

# ### end Alembic commands ###

parse_recipe_yields()


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("recipes", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_recipes_recipe_servings"))
batch_op.drop_column("recipe_servings")
batch_op.drop_index(batch_op.f("ix_recipes_recipe_yield_quantity"))
batch_op.drop_column("recipe_yield_quantity")

# ### end Alembic commands ###
12 changes: 11 additions & 1 deletion frontend/components/Domain/Recipe/RecipeDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ interface ShowHeaders {
tags: boolean;
categories: boolean;
tools: boolean;
recipeServings: boolean;
recipeYieldQuantity: boolean;
recipeYield: boolean;
dateAdded: boolean;
}
Expand Down Expand Up @@ -93,6 +95,8 @@ export default defineComponent({
owner: false,
tags: true,
categories: true,
recipeServings: true,
recipeYieldQuantity: true,
recipeYield: true,
dateAdded: true,
};
Expand Down Expand Up @@ -127,8 +131,14 @@ export default defineComponent({
if (props.showHeaders.tools) {
hdrs.push({ text: i18n.t("tool.tools"), value: "tools" });
}
if (props.showHeaders.recipeServings) {
hdrs.push({ text: i18n.t("recipe.servings"), value: "recipeServings" });
}
if (props.showHeaders.recipeYieldQuantity) {
hdrs.push({ text: i18n.t("recipe.yield"), value: "recipeYieldQuantity" });
}
if (props.showHeaders.recipeYield) {
hdrs.push({ text: i18n.t("recipe.yield"), value: "recipeYield" });
hdrs.push({ text: i18n.t("recipe.yield-text"), value: "recipeYield" });
}
if (props.showHeaders.dateAdded) {
hdrs.push({ text: i18n.t("general.date-added"), value: "dateAdded" });
Expand Down
14 changes: 7 additions & 7 deletions frontend/components/Domain/Recipe/RecipeLastMade.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,6 @@
</BaseDialog>
</div>
<div>
<div class="d-flex justify-center flex-wrap">
<BaseButton :small="$vuetify.breakpoint.smAndDown" @click="madeThisDialog = true">
<template #icon> {{ $globals.icons.chefHat }} </template>
{{ $t('recipe.made-this') }}
</BaseButton>
</div>
<div class="d-flex justify-center flex-wrap">
<v-chip
label
Expand All @@ -105,6 +99,12 @@
{{ $t('recipe.last-made-date', { date: value ? new Date(value).toLocaleDateString($i18n.locale) : $t("general.never") } ) }}
</v-chip>
</div>
<div class="d-flex justify-center flex-wrap mt-1">
<BaseButton :small="$vuetify.breakpoint.smAndDown" @click="madeThisDialog = true">
<template #icon> {{ $globals.icons.chefHat }} </template>
{{ $t('recipe.made-this') }}
</BaseButton>
</div>
</div>
</div>
</template>
Expand All @@ -125,7 +125,7 @@ export default defineComponent({
},
recipe: {
type: Object as () => Recipe,
default: null,
required: true,
},
},
setup(props, context) {
Expand Down
16 changes: 8 additions & 8 deletions frontend/components/Domain/Recipe/RecipePage/RecipePage.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div>
<v-container v-show="!isCookMode" key="recipe-page" :class="{ 'pa-0': $vuetify.breakpoint.smAndDown }">
<v-card :flat="$vuetify.breakpoint.smAndDown" class="d-print-none">
<v-card :flat="$vuetify.breakpoint.smAndDown" class="d-print-none">
<RecipePageHeader
:recipe="recipe"
:recipe-scale="scale"
Expand All @@ -21,10 +21,10 @@
a significant amount of prop management. When we move to Vue 3 and have access to some of the newer API's the plan to update this
data management and mutation system we're using.
-->
<RecipePageInfoEditor v-if="isEditMode" :recipe="recipe" :landscape="landscape" />
<RecipePageEditorToolbar v-if="isEditForm" :recipe="recipe" />
<RecipePageTitleContent :recipe="recipe" :landscape="landscape" />
<RecipePageIngredientEditor v-if="isEditForm" :recipe="recipe" />
<RecipePageScale :recipe="recipe" :scale.sync="scale" :landscape="landscape" />
<RecipePageScale :recipe="recipe" :scale.sync="scale" />

<!--
This section contains the 2 column layout for the recipe steps and other content.
Expand Down Expand Up @@ -76,7 +76,7 @@
<v-row style="height: 100%;" no-gutters class="overflow-hidden">
<v-col cols="12" sm="5" class="overflow-y-auto pl-4 pr-3 py-2" style="height: 100%;">
<div class="d-flex align-center">
<RecipePageScale :recipe="recipe" :scale.sync="scale" :landscape="landscape" />
<RecipePageScale :recipe="recipe" :scale.sync="scale" />
</div>
<RecipePageIngredientToolsView v-if="!isEditForm" :recipe="recipe" :scale="scale" :is-cook-mode="isCookMode" />
<v-divider></v-divider>
Expand All @@ -95,7 +95,7 @@
</v-sheet>
<v-sheet v-show="isCookMode && hasLinkedIngredients">
<div class="mt-2 px-2 px-md-4">
<RecipePageScale :recipe="recipe" :scale.sync="scale" :landscape="landscape"/>
<RecipePageScale :recipe="recipe" :scale.sync="scale"/>
</div>
<RecipePageInstructions
v-model="recipe.recipeInstructions"
Expand Down Expand Up @@ -154,7 +154,7 @@ import RecipePageIngredientToolsView from "./RecipePageParts/RecipePageIngredien
import RecipePageInstructions from "./RecipePageParts/RecipePageInstructions.vue";
import RecipePageOrganizers from "./RecipePageParts/RecipePageOrganizers.vue";
import RecipePageScale from "./RecipePageParts/RecipePageScale.vue";
import RecipePageTitleContent from "./RecipePageParts/RecipePageTitleContent.vue";
import RecipePageInfoEditor from "./RecipePageParts/RecipePageInfoEditor.vue";
import RecipePageComments from "./RecipePageParts/RecipePageComments.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipePrintContainer from "~/components/Domain/Recipe/RecipePrintContainer.vue";
Expand Down Expand Up @@ -185,7 +185,7 @@ export default defineComponent({
RecipePageHeader,
RecipePrintContainer,
RecipePageComments,
RecipePageTitleContent,
RecipePageInfoEditor,
RecipePageEditorToolbar,
RecipePageIngredientEditor,
RecipePageOrganizers,
Expand All @@ -195,7 +195,7 @@ export default defineComponent({
RecipeNotes,
RecipePageInstructions,
RecipePageFooter,
RecipeIngredients
RecipeIngredients,
},
props: {
recipe: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,7 @@
<template>
<div>
<div class="d-flex justify-end flex-wrap align-stretch">
<v-card v-if="!landscape" width="50%" flat class="d-flex flex-column justify-center align-center">
<v-card-text>
<v-card-title class="headline pa-0 flex-column align-center">
{{ recipe.name }}
<RecipeRating :key="recipe.slug" :value="recipe.rating" :recipe-id="recipe.id" :slug="recipe.slug" />
</v-card-title>
<v-divider class="my-2"></v-divider>
<SafeMarkdown :source="recipe.description" />
<v-divider></v-divider>
<div v-if="isOwnGroup" class="d-flex justify-center mt-5">
<RecipeLastMade
v-model="recipe.lastMade"
:recipe="recipe"
class="d-flex justify-center flex-wrap"
:class="true ? undefined : 'force-bottom'"
/>
</div>
<div class="d-flex justify-center mt-5">
<RecipeTimeCard
class="d-flex justify-center flex-wrap"
:class="true ? undefined : 'force-bottom'"
:prep-time="recipe.prepTime"
:total-time="recipe.totalTime"
:perform-time="recipe.performTime"
/>
</div>
</v-card-text>
</v-card>
<v-img
:key="imageKey"
:max-width="landscape ? null : '50%'"
min-height="50"
:height="hideImage ? undefined : imageHeight"
:src="recipeImageUrl"
class="d-print-none"
@error="hideImage = true"
>
</v-img>
</div>
<v-divider></v-divider>
<RecipePageInfoCard :recipe="recipe" :recipe-scale="recipeScale" :landscape="landscape" />
<v-divider />
<RecipeActionMenu
:recipe="recipe"
:slug="recipe.slug"
Expand All @@ -65,21 +26,17 @@
import { defineComponent, useContext, computed, ref, watch } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useRecipePermissions } from "~/composables/recipes";
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
import RecipePageInfoCard from "~/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCard.vue";
import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue";
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
import { useStaticRoutes, useUserApi } from "~/composables/api";
import { HouseholdSummary } from "~/lib/api/types/household";
import { Recipe } from "~/lib/api/types/recipe";
import { NoUndefinedField } from "~/lib/api/types/non-generated";
import { usePageState, usePageUser, PageMode, EditorMode } from "~/composables/recipe-page/shared-state";
export default defineComponent({
components: {
RecipeTimeCard,
RecipePageInfoCard,
RecipeActionMenu,
RecipeRating,
RecipeLastMade,
},
props: {
recipe: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<template>
<div>
<div class="d-flex justify-end flex-wrap align-stretch">
<RecipePageInfoCardImage v-if="landscape" :recipe="recipe" />
<v-card
:width="landscape ? '100%' : '50%'"
flat
class="d-flex flex-column justify-center align-center"
>
<v-card-text>
<v-card-title class="headline pa-0 flex-column align-center">
{{ recipe.name }}
<RecipeRating :key="recipe.slug" :value="recipe.rating" :recipe-id="recipe.id" :slug="recipe.slug" />
</v-card-title>
<v-divider class="my-2" />
<SafeMarkdown :source="recipe.description" />
<v-divider />
<v-container class="d-flex flex-row flex-wrap justify-center align-center">
<div class="mx-5">
<v-row no-gutters class="mb-1">
<v-col v-if="recipe.recipeYieldQuantity || recipe.recipeYield" cols="12" class="d-flex flex-wrap justify-center">
<RecipeYield
:yield-quantity="recipe.recipeYieldQuantity"
:yield="recipe.recipeYield"
:scale="recipeScale"
/>
</v-col>
</v-row>
<v-row no-gutters>
<v-col cols="12" class="d-flex flex-wrap justify-center">
<RecipeLastMade
v-if="isOwnGroup"
:value="recipe.lastMade"
:recipe="recipe"
:class="true ? undefined : 'force-bottom'"
/>
</v-col>
</v-row>
</div>
<div class="mx-5">
<RecipeTimeCard
stacked
container-class="d-flex flex-wrap justify-center"
:prep-time="recipe.prepTime"
:total-time="recipe.totalTime"
:perform-time="recipe.performTime"
/>
</div>
</v-container>
</v-card-text>
</v-card>
<RecipePageInfoCardImage v-if="!landscape" :recipe="recipe" max-width="50%" class="my-auto" />
</div>
</div>
</template>

<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
import RecipeYield from "~/components/Domain/Recipe/RecipeYield.vue";
import RecipePageInfoCardImage from "~/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCardImage.vue";
import { Recipe } from "~/lib/api/types/recipe";
import { NoUndefinedField } from "~/lib/api/types/non-generated";
export default defineComponent({
components: {
RecipeRating,
RecipeLastMade,
RecipeTimeCard,
RecipeYield,
RecipePageInfoCardImage,
},
props: {
recipe: {
type: Object as () => NoUndefinedField<Recipe>,
required: true,
},
recipeScale: {
type: Number,
default: 1,
},
landscape: {
type: Boolean,
required: true,
},
},
setup() {
const { $vuetify } = useContext();
const useMobile = computed(() => $vuetify.breakpoint.smAndDown);
const { isOwnGroup } = useLoggedInState();
return {
isOwnGroup,
useMobile,
};
}
});
</script>
Loading

0 comments on commit 327da02

Please sign in to comment.