feat(frontend): Add Meal Tags + UI Improvements (#807)

* feat: 

* fix colors

* add additional support for settings meal tag

* add defaults to recipe

* use group reciep settings

* fix login infinite loading

* disable owner on initial load

* add skeleton loader

* add v-model support

* formatting

* fix overwriting existing values

* feat(frontend):  add markdown preview for steps

* update black plus formatting

* update help text

* fix overwrite error

* remove print

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-11-20 14:30:38 -09:00 committed by GitHub
parent d4bf81dee6
commit 912cc6d956
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 456 additions and 246 deletions

View File

@ -5,10 +5,10 @@
:class="{ 'on-hover': hover }" :class="{ 'on-hover': hover }"
:elevation="hover ? 12 : 2" :elevation="hover ? 12 : 2"
:to="route ? `/recipe/${slug}` : ''" :to="route ? `/recipe/${slug}` : ''"
min-height="275" :min-height="imageHeight + 75"
@click="$emit('click')" @click="$emit('click')"
> >
<RecipeCardImage icon-size="200" :slug="slug" small :image-version="image"> <RecipeCardImage :icon-size="imageHeight" :height="imageHeight" :slug="slug" small :image-version="image">
<v-expand-transition v-if="description"> <v-expand-transition v-if="description">
<div v-if="hover" class="d-flex transition-fast-in-fast-out secondary v-card--reveal" style="height: 100%"> <div v-if="hover" class="d-flex transition-fast-in-fast-out secondary v-card--reveal" style="height: 100%">
<v-card-text class="v-card--text-show white--text"> <v-card-text class="v-card--text-show white--text">
@ -23,6 +23,7 @@
</div> </div>
</v-card-title> </v-card-title>
<slot name="actions">
<v-card-actions> <v-card-actions>
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always /> <RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
<RecipeRating :value="rating" :name="name" :slug="slug" :small="true" /> <RecipeRating :value="rating" :name="name" :slug="slug" :small="true" />
@ -43,6 +44,7 @@
@delete="$emit('delete', slug)" @delete="$emit('delete', slug)"
/> />
</v-card-actions> </v-card-actions>
</slot>
<slot></slot> <slot></slot>
</v-card> </v-card>
</v-hover> </v-hover>
@ -92,6 +94,10 @@ export default {
required: true, required: true,
type: Number, type: Number,
}, },
imageHeight: {
type: Number,
default: 200,
},
}, },
data() { data() {
return { return {

View File

@ -41,6 +41,7 @@
</template> </template>
<v-date-picker v-model="newMealdate" no-title @input="pickerMenu = false"></v-date-picker> <v-date-picker v-model="newMealdate" no-title @input="pickerMenu = false"></v-date-picker>
</v-menu> </v-menu>
<v-select v-model="newMealType" :return-object="false" :items="planTypeOptions" label="Entry Type"></v-select>
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
<v-menu <v-menu
@ -77,6 +78,7 @@ import { defineComponent, reactive, ref, toRefs, useContext, useRouter } from "@
import { useClipboard, useShare } from "@vueuse/core"; import { useClipboard, useShare } from "@vueuse/core";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
import { MealType, planTypeOptions } from "~/composables/use-group-mealplan";
export interface ContextMenuIncludes { export interface ContextMenuIncludes {
delete: boolean; delete: boolean;
@ -153,6 +155,7 @@ export default defineComponent({
loading: false, loading: false,
menuItems: [] as ContextMenuItem[], menuItems: [] as ContextMenuItem[],
newMealdate: "", newMealdate: "",
newMealType: "dinner" as MealType,
pickerMenu: false, pickerMenu: false,
}); });
@ -265,7 +268,7 @@ export default defineComponent({
async function addRecipeToPlan() { async function addRecipeToPlan() {
const { response } = await api.mealplans.createOne({ const { response } = await api.mealplans.createOne({
date: state.newMealdate, date: state.newMealdate,
entryType: "dinner", entryType: state.newMealType,
title: "", title: "",
text: "", text: "",
recipeId: props.recipeId, recipeId: props.recipeId,
@ -310,6 +313,7 @@ export default defineComponent({
domConfirmDelete, domConfirmDelete,
domMealplanDialog, domMealplanDialog,
icon, icon,
planTypeOptions,
}; };
}, },
}); });

View File

@ -31,7 +31,7 @@
<div class="mr-auto"> <div class="mr-auto">
{{ $t("search.results") }} {{ $t("search.results") }}
</div> </div>
<router-link to="/search"> {{ $t("search.advanced-search") }} </router-link> <router-link to="/search?advanced=true"> {{ $t("search.advanced-search") }} </router-link>
</v-card-actions> </v-card-actions>
<RecipeCardMobile <RecipeCardMobile

View File

@ -2,7 +2,7 @@
<section @keyup.ctrl.90="undoMerge"> <section @keyup.ctrl.90="undoMerge">
<!-- Ingredient Link Editor --> <!-- Ingredient Link Editor -->
<v-dialog v-model="dialog" width="600"> <v-dialog v-model="dialog" width="600">
<v-card> <v-card :ripple="false">
<v-app-bar dark color="primary" class="mt-n1 mb-3"> <v-app-bar dark color="primary" class="mt-n1 mb-3">
<v-icon large left> <v-icon large left>
{{ $globals.icons.link }} {{ $globals.icons.link }}
@ -127,8 +127,7 @@
</v-fade-transition> </v-fade-transition>
</v-card-title> </v-card-title>
<v-card-text v-if="edit"> <v-card-text v-if="edit">
<v-textarea :key="'instructions' + index" v-model="value[index]['text']" auto-grow dense rows="4"> <MarkdownEditor v-model="value[index]['text']" />
</v-textarea>
<div <div
v-for="ing in step.ingredientReferences" v-for="ing in step.ingredientReferences"
:key="ing.referenceId" :key="ing.referenceId"
@ -417,4 +416,9 @@ export default defineComponent({
}); });
</script> </script>
<style scoped>
.v-card--link:before {
background: none;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div>
<v-tabs v-model="tab" height="30px" class="my-1">
<v-tab>
<v-icon small left> {{ $globals.icons.edit }}</v-icon>
Edit
</v-tab>
<v-tab>
<v-icon small left> {{ $globals.icons.eye }}</v-icon>
Preview
</v-tab>
</v-tabs>
<v-textarea
v-if="tab == 0"
v-model="inputVal"
:class="label == '' ? '' : 'mt-5'"
:label="label"
auto-grow
dense
rows="4"
></v-textarea>
<VueMarkdown v-else :source="value"> </VueMarkdown>
</div>
</template>
<script lang="ts">
// @ts-ignore
import VueMarkdown from "@adapttive/vue-markdown";
import { defineComponent, reactive, toRefs, computed } from "@nuxtjs/composition-api";
export default defineComponent({
name: "MarkdownEditor",
components: {
VueMarkdown,
},
props: {
value: {
type: String,
required: true,
},
label: {
type: String,
default: "",
},
},
setup(props, context) {
const state = reactive({
tab: 0,
});
const inputVal = computed({
get: () => {
return props.value;
},
set: (val) => {
context.emit("input", val);
},
});
return {
inputVal,
...toRefs(state),
};
},
});
</script>

View File

@ -8,16 +8,26 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { defineComponent } from "@nuxtjs/composition-api";
import { useToggle } from "@vueuse/shared"; import { useToggle } from "@vueuse/shared";
import { watch } from "vue-demi";
export default defineComponent({ export default defineComponent({
props: { props: {
value: {
type: Boolean,
default: false,
},
tag: { tag: {
type: String, type: String,
default: "div", default: "div",
}, },
}, },
setup() { setup(_, context) {
const [state, toggle] = useToggle(); const [state, toggle] = useToggle();
watch(state, () => {
context.emit("input", state);
});
return { return {
state, state,
toggle, toggle,

View File

@ -4,6 +4,15 @@ import { useAsyncKey } from "./use-utils";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { CreateMealPlan, UpdateMealPlan } from "~/api/class-interfaces/group-mealplan"; import { CreateMealPlan, UpdateMealPlan } from "~/api/class-interfaces/group-mealplan";
export type MealType = "breakfast" | "lunch" | "dinner" | "snack";
export const planTypeOptions = [
{ text: "Breakfast", value: "breakfast" },
{ text: "Lunch", value: "lunch" },
{ text: "Dinner", value: "dinner" },
{ text: "Snack", value: "snack" },
];
export const useMealplans = function () { export const useMealplans = function () {
const api = useUserApi(); const api = useUserApi();
const loading = ref(false); const loading = ref(false);
@ -72,9 +81,14 @@ export const useMealplans = function () {
this.refreshAll(); this.refreshAll();
} }
}, },
async setType(payload: UpdateMealPlan, typ: MealType) {
payload.entryType = typ;
await this.updateOne(payload);
},
}; };
const mealplans = actions.getAll(); const mealplans = actions.getAll();
return { mealplans, actions, validForm }; return { mealplans, actions, validForm, loading };
}; };

View File

@ -223,7 +223,7 @@ export default {
background: "#202021", background: "#202021",
}, },
light: { light: {
primary: process.env.THEME_LIGHT_PRIMARY || "#007A99", primary: process.env.THEME_LIGHT_PRIMARY || "#E58325",
accent: process.env.THEME_LIGHT_ACCENT || "#007A99", accent: process.env.THEME_LIGHT_ACCENT || "#007A99",
secondary: process.env.THEME_DARK_SECONDARY || "#973542", secondary: process.env.THEME_DARK_SECONDARY || "#973542",
success: process.env.THEME_DARK_SUCCESS || "#43A047", success: process.env.THEME_DARK_SUCCESS || "#43A047",
@ -237,7 +237,6 @@ export default {
privateRuntimeConfig: {}, privateRuntimeConfig: {},
proxy: { proxy: {
// "http://localhost:9000/*/api",
// See Proxy section // See Proxy section
[`${process.env.SUB_PATH || ""}api`]: { [`${process.env.SUB_PATH || ""}api`]: {
pathRewrite: { pathRewrite: {

View File

@ -193,6 +193,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, useContext, computed, reactive } from "@nuxtjs/composition-api"; import { defineComponent, ref, useContext, computed, reactive } from "@nuxtjs/composition-api";
import { alert } from "~/composables/use-toast";
export default defineComponent({ export default defineComponent({
layout: "basic", layout: "basic",
@ -215,7 +216,16 @@ export default defineComponent({
formData.append("username", form.email); formData.append("username", form.email);
formData.append("password", form.password); formData.append("password", form.password);
try {
await $auth.loginWith("local", { data: formData }); await $auth.loginWith("local", { data: formData });
} catch (error) {
if (error.response.status === 401) {
alert.error("Invalid Credentials");
}
else {
alert.error("Something Went Wrong!")
}
}
loggingIn.value = false; loggingIn.value = false;
} }

View File

@ -35,6 +35,9 @@
<v-date-picker v-model="newMeal.date" no-title @input="pickerMenu = false"></v-date-picker> <v-date-picker v-model="newMeal.date" no-title @input="pickerMenu = false"></v-date-picker>
</v-menu> </v-menu>
<v-card-text> <v-card-text>
<v-select v-model="newMeal.entryType" :return-object="false" :items="planTypeOptions" label="Entry Type">
</v-select>
<v-autocomplete <v-autocomplete
v-if="!dialog.note" v-if="!dialog.note"
v-model="newMeal.recipeId" v-model="newMeal.recipeId"
@ -68,7 +71,7 @@
</div> </div>
</div> </div>
<v-switch v-model="edit" label="Editor"></v-switch> <v-switch v-model="edit" label="Editor"></v-switch>
<v-row class="mt-2"> <v-row class="">
<v-col <v-col
v-for="(plan, index) in mealsByDate" v-for="(plan, index) in mealsByDate"
:key="index" :key="index"
@ -79,9 +82,14 @@
xl="2" xl="2"
class="col-borders my-1 d-flex flex-column" class="col-borders my-1 d-flex flex-column"
> >
<p class="h5 text-center"> <v-sheet class="mb-2 bottom-color-border">
<p class="headline text-center mb-1">
{{ $d(plan.date, "short") }} {{ $d(plan.date, "short") }}
</p> </p>
</v-sheet>
<!-- Day Column Recipes -->
<template v-if="edit">
<draggable <draggable
tag="div" tag="div"
:value="plan.meals" :value="plan.meals"
@ -92,9 +100,14 @@
@end="onMoveCallback" @end="onMoveCallback"
> >
<v-card v-for="mealplan in plan.meals" :key="mealplan.id" v-model="hover[mealplan.id]" class="my-1"> <v-card v-for="mealplan in plan.meals" :key="mealplan.id" v-model="hover[mealplan.id]" class="my-1">
<v-list-item :to="edit ? null : `/recipe/${mealplan.recipe.slug}`"> <v-list-item :to="edit || !mealplan.recipe ? null : `/recipe/${mealplan.recipe.slug}`">
<v-list-item-avatar :rounded="false"> <v-list-item-avatar :rounded="false">
<RecipeCardImage v-if="mealplan.recipe" tiny icon-size="25" :slug="mealplan.recipe.slug" /> <RecipeCardImage
v-if="mealplan.recipe"
tiny
icon-size="25"
:slug="mealplan.recipe ? mealplan.recipe.slug : ''"
/>
<v-icon v-else> <v-icon v-else>
{{ $globals.icons.primary }} {{ $globals.icons.primary }}
</v-icon> </v-icon>
@ -103,31 +116,45 @@
<v-list-item-title class="mb-1"> <v-list-item-title class="mb-1">
{{ mealplan.recipe ? mealplan.recipe.name : mealplan.title }} {{ mealplan.recipe ? mealplan.recipe.name : mealplan.title }}
</v-list-item-title> </v-list-item-title>
<v-list-item-subtitle> <v-list-item-subtitle style="min-height: 16px">
{{ mealplan.recipe ? mealplan.recipe.description : mealplan.text }} {{ mealplan.recipe ? mealplan.recipe.description + " " : mealplan.text }}
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<v-divider v-if="edit" class="mx-2"></v-divider> <v-divider class="mx-2"></v-divider>
<v-card-actions v-if="edit"> <div class="py-2 px-2 d-flex">
<v-btn color="error" icon @click="actions.deleteOne(mealplan.id)"> <v-menu offset-y>
<template #activator="{ on, attrs }">
<v-chip v-bind="attrs" label small color="accent" v-on="on" @click.prevent>
<v-icon left>
{{ $globals.icons.tags }}
</v-icon>
{{ mealplan.entryType }}
</v-chip>
</template>
<v-list>
<v-list-item
v-for="mealType in planTypeOptions"
:key="mealType.value"
@click="actions.setType(mealplan, mealType.value)"
>
<v-list-item-title> {{ mealType.text }} </v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-spacer></v-spacer>
<v-btn color="info" class="mr-2" small icon>
<v-icon>{{ $globals.icons.cartCheck }}</v-icon>
</v-btn>
<v-btn color="error" small icon @click="actions.deleteOne(mealplan.id)">
<v-icon>{{ $globals.icons.delete }}</v-icon> <v-icon>{{ $globals.icons.delete }}</v-icon>
</v-btn> </v-btn>
<v-spacer></v-spacer> </div>
<v-btn
v-if="mealplan.recipe"
color="info"
icon
nuxt
target="_blank"
:to="`/recipe/${mealplan.recipe.slug}`"
>
<v-icon>{{ $globals.icons.openInNew }}</v-icon>
</v-btn>
</v-card-actions>
</v-card> </v-card>
</draggable> </draggable>
<v-card v-if="edit" outlined class="mt-auto">
<!-- Day Column Actions -->
<v-card outlined class="mt-auto">
<v-card-actions class="d-flex"> <v-card-actions class="d-flex">
<div style="width: 50%"> <div style="width: 50%">
<v-btn block text @click="randomMeal(plan.date)"> <v-btn block text @click="randomMeal(plan.date)">
@ -141,6 +168,34 @@
</div> </div>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template>
<template v-else-if="plan.meals">
<RecipeCard
v-for="mealplan in plan.meals"
:key="mealplan.id"
:recipe-id="0"
:image-height="125"
class="mb-2"
:route="mealplan.recipe ? true : false"
:slug="mealplan.recipe ? mealplan.recipe.slug : mealplan.title"
:description="mealplan.recipe ? mealplan.recipe.description : mealplan.text"
:name="mealplan.recipe ? mealplan.recipe.name : mealplan.title"
>
<template #actions>
<v-divider class="mb-0 mt-2 mx-2"></v-divider>
<v-card-actions class="justify-end mt-1">
<v-chip label small color="accent">
<v-icon left>
{{ $globals.icons.tags }}
</v-icon>
{{ mealplan.entryType }}
</v-chip>
</v-card-actions>
</template>
</RecipeCard>
</template>
<v-skeleton-loader v-else elevation="2" type="image, list-item-two-line"></v-skeleton-loader>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
@ -151,17 +206,19 @@ import { computed, defineComponent, reactive, ref, toRefs, watch } from "@nuxtjs
import { isSameDay, addDays, subDays, parseISO, format } from "date-fns"; import { isSameDay, addDays, subDays, parseISO, format } from "date-fns";
import { SortableEvent } from "sortablejs"; // eslint-disable-line import { SortableEvent } from "sortablejs"; // eslint-disable-line
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import { useMealplans } from "~/composables/use-group-mealplan"; import { useMealplans, planTypeOptions } from "~/composables/use-group-mealplan";
import { useRecipes, allRecipes } from "~/composables/recipes"; import { useRecipes, allRecipes } from "~/composables/recipes";
import RecipeCardImage from "~/components/Domain/Recipe/RecipeCardImage.vue"; import RecipeCardImage from "~/components/Domain/Recipe/RecipeCardImage.vue";
import RecipeCard from "~/components/Domain/Recipe/RecipeCard.vue";
export default defineComponent({ export default defineComponent({
components: { components: {
draggable, draggable,
RecipeCardImage, RecipeCardImage,
RecipeCard,
}, },
setup() { setup() {
const { mealplans, actions } = useMealplans(); const { mealplans, actions, loading } = useMealplans();
useRecipes(true, true); useRecipes(true, true);
const state = reactive({ const state = reactive({
@ -242,6 +299,7 @@ export default defineComponent({
// ===================================================== // =====================================================
// New Meal Dialog // New Meal Dialog
const domMealDialog = ref(null); const domMealDialog = ref(null);
const dialog = reactive({ const dialog = reactive({
loading: false, loading: false,
@ -256,11 +314,13 @@ export default defineComponent({
newMeal.title = ""; newMeal.title = "";
newMeal.text = ""; newMeal.text = "";
}); });
const newMeal = reactive({ const newMeal = reactive({
date: "", date: "",
title: "", title: "",
text: "", text: "",
recipeId: null, recipeId: null as Number | null,
entryType: "dinner",
}); });
function openDialog(date: Date) { function openDialog(date: Date) {
@ -273,40 +333,45 @@ export default defineComponent({
newMeal.date = ""; newMeal.date = "";
newMeal.title = ""; newMeal.title = "";
newMeal.text = ""; newMeal.text = "";
newMeal.entryType = "dinner";
newMeal.recipeId = null; newMeal.recipeId = null;
} }
function randomMeal(date: Date) { async function randomMeal(date: Date) {
// TODO: Refactor to use API call to get random recipe // TODO: Refactor to use API call to get random recipe
// @ts-ignore // @ts-ignore
const randomRecipe = allRecipes.value[Math.floor(Math.random() * allRecipes.value.length)]; const randomRecipe = allRecipes.value[Math.floor(Math.random() * allRecipes.value.length)];
newMeal.date = format(date, "yyyy-MM-dd"); newMeal.date = format(date, "yyyy-MM-dd");
// @ts-ignore
newMeal.recipeId = randomRecipe.id; newMeal.recipeId = randomRecipe.id || null;
console.log(newMeal.recipeId, randomRecipe.id);
// @ts-ignore // @ts-ignore
actions.createOne(newMeal); await actions.createOne({ ...newMeal });
resetDialog(); resetDialog();
} }
return { return {
resetDialog, ...toRefs(state),
randomMeal, actions,
allRecipes,
backOneWeek,
days,
dialog, dialog,
domMealDialog, domMealDialog,
openDialog,
mealplans,
actions,
newMeal,
allRecipes,
...toRefs(state),
mealsByDate,
onMoveCallback,
backOneWeek,
forwardOneWeek, forwardOneWeek,
loading,
mealplans,
mealsByDate,
newMeal,
onMoveCallback,
openDialog,
planTypeOptions,
randomMeal,
resetDialog,
weekRange, weekRange,
days,
}; };
}, },
head() { head() {
@ -318,8 +383,16 @@ export default defineComponent({
</script> </script>
<style lang="css"> <style lang="css">
.col-borders { /* .col-borders {
border-top: 1px solid #e0e0e0; border-top: 1px solid #e0e0e0;
} */
.left-color-border {
border-left: 5px solid var(--v-primary-base) !important;
}
.bottom-color-border {
border-bottom: 2px solid var(--v-primary-base) !important;
} }
</style> </style>

View File

@ -110,7 +110,7 @@
<!-- Advanced Editor --> <!-- Advanced Editor -->
<div v-if="form"> <div v-if="form">
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2> <h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<draggable v-model="recipe.recipeIngredient" handle=".handle"> <draggable v-if="recipe.recipeIngredient.length > 0" v-model="recipe.recipeIngredient" handle=".handle">
<RecipeIngredientEditor <RecipeIngredientEditor
v-for="(ingredient, index) in recipe.recipeIngredient" v-for="(ingredient, index) in recipe.recipeIngredient"
:key="ingredient.referenceId" :key="ingredient.referenceId"
@ -119,6 +119,7 @@
@delete="recipe.recipeIngredient.splice(index, 1)" @delete="recipe.recipeIngredient.splice(index, 1)"
/> />
</draggable> </draggable>
<v-skeleton-loader v-else boilerplate elevation="2" type="list-item"> </v-skeleton-loader>
<div class="d-flex justify-end mt-2"> <div class="d-flex justify-end mt-2">
<v-tooltip top color="accent"> <v-tooltip top color="accent">
<template #activator="{ on, attrs }"> <template #activator="{ on, attrs }">

View File

@ -126,7 +126,7 @@ export default defineComponent({
const headers = reactive({ const headers = reactive({
id: false, id: false,
owner: true, owner: false,
tags: true, tags: true,
categories: true, categories: true,
recipeYield: false, recipeYield: false,

View File

@ -4,7 +4,7 @@ ALLOW_SIGNUP=true
# ===================================== # =====================================
# Light Mode Config # Light Mode Config
THEME_LIGHT_PRIMARY=#007A99 THEME_LIGHT_PRIMARY=#E58325
THEME_LIGHT_ACCENT=#007A99 THEME_LIGHT_ACCENT=#007A99
THEME_LIGHT_SECONDARY=#973542 THEME_LIGHT_SECONDARY=#973542
THEME_LIGHT_SUCCESS=#43A047 THEME_LIGHT_SUCCESS=#43A047

View File

@ -1,6 +1,24 @@
from mealie.schema.recipe import Recipe from mealie.schema.recipe import Recipe
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient
from mealie.schema.recipe.recipe_step import RecipeStep
from mealie.schema.user.user import PrivateUser from mealie.schema.user.user import PrivateUser
step_text = """Recipe steps as well as other fields in the recipe page support markdown syntax.
**Add a link**
[My Link](https://beta.mealie.io)
**Imbed an image**
Use the `height="100"` or `width="100"` attributes to set the size of the image.
<img height="100" src="https://images.unsplash.com/photo-1567620905732-2d1ec7ab7445?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=960&q=80"></img>
"""
ingredient_note = "1 Cup Flour"
def recipe_creation_factory(user: PrivateUser, name: str, additional_attrs: dict = None) -> Recipe: def recipe_creation_factory(user: PrivateUser, name: str, additional_attrs: dict = None) -> Recipe:
""" """
@ -13,4 +31,10 @@ def recipe_creation_factory(user: PrivateUser, name: str, additional_attrs: dict
additional_attrs["user_id"] = user.id additional_attrs["user_id"] = user.id
additional_attrs["group_id"] = user.group_id additional_attrs["group_id"] = user.group_id
if not additional_attrs.get("recipe_ingredient"):
additional_attrs["recipe_ingredient"] = [RecipeIngredient(note=ingredient_note)]
if not additional_attrs.get("recipe_instructions"):
additional_attrs["recipe_instructions"] = [RecipeStep(text=step_text)]
return Recipe(**additional_attrs) return Recipe(**additional_attrs)

View File

@ -13,6 +13,7 @@ from mealie.core.dependencies.grouped import PublicDeps, UserDeps
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
from mealie.db.data_access_layer.recipe_access_model import RecipeDataAccessModel from mealie.db.data_access_layer.recipe_access_model import RecipeDataAccessModel
from mealie.schema.recipe.recipe import CreateRecipe, Recipe, RecipeSummary from mealie.schema.recipe.recipe import CreateRecipe, Recipe, RecipeSummary
from mealie.schema.recipe.recipe_settings import RecipeSettings
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_recipe_event from mealie.services.events import create_recipe_event
@ -71,8 +72,25 @@ class RecipeService(CrudHttpMixins[CreateRecipe, Recipe, Recipe], UserHttpServic
return [RecipeSummary.construct(**x) for x in new_items] return [RecipeSummary.construct(**x) for x in new_items]
def create_one(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe: def create_one(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
create_data = recipe_creation_factory(self.user, name=create_data.name, additional_attrs=create_data.dict()) group = self.db.groups.get(self.group_id, "id")
create_data = recipe_creation_factory(
self.user,
name=create_data.name,
additional_attrs=create_data.dict(),
)
create_data.settings = RecipeSettings(
public=group.preferences.recipe_public,
show_nutrition=group.preferences.recipe_show_nutrition,
show_assets=group.preferences.recipe_show_assets,
landscape_view=group.preferences.recipe_landscape_view,
disable_comments=group.preferences.recipe_disable_comments,
disable_amount=group.preferences.recipe_disable_amount,
)
self._create_one(create_data, self.t("generic.server-error"), self.exception_key) self._create_one(create_data, self.t("generic.server-error"), self.exception_key)
self._create_event( self._create_event(
"Recipe Created", "Recipe Created",
f"'{self.item.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.item.slug}", f"'{self.item.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.item.slug}",

72
poetry.lock generated
View File

@ -133,25 +133,30 @@ lxml = ["lxml"]
[[package]] [[package]]
name = "black" name = "black"
version = "20.8b1" version = "21.11b1"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6.2"
[package.dependencies] [package.dependencies]
appdirs = "*"
click = ">=7.1.2" click = ">=7.1.2"
mypy-extensions = ">=0.4.3" mypy-extensions = ">=0.4.3"
pathspec = ">=0.6,<1" pathspec = ">=0.9.0,<1"
regex = ">=2020.1.8" platformdirs = ">=2"
toml = ">=0.10.1" regex = ">=2021.4.4"
typed-ast = ">=1.4.0" tomli = ">=0.2.6,<2.0.0"
typing-extensions = ">=3.7.4" typing-extensions = [
{version = ">=3.10.0.0", markers = "python_version < \"3.10\""},
{version = "!=3.10.0.1", markers = "python_version >= \"3.10\""},
]
[package.extras] [package.extras]
colorama = ["colorama (>=0.4.3)"] colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
python2 = ["typed-ast (>=1.4.3)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]] [[package]]
name = "cachetools" name = "cachetools"
@ -1259,12 +1264,12 @@ optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "typed-ast" name = "tomli"
version = "1.4.3" version = "1.2.2"
description = "a fork of Python 2 and 3 ast modules with type comment support" description = "A lil' TOML parser"
category = "dev" category = "dev"
optional = false optional = false
python-versions = "*" python-versions = ">=3.6"
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
@ -1418,7 +1423,7 @@ pgsql = ["psycopg2-binary"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "31d3ee104998ad61b18322584c0cc84de32dbad0dc7657c9f7b7ae8214dae9c3" content-hash = "597bcfac6b50f5f6e203db40e05546b1a9aaf4c8438790d233424cf66fc84d19"
[metadata.files] [metadata.files]
aiofiles = [ aiofiles = [
@ -1467,7 +1472,8 @@ beautifulsoup4 = [
{file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"},
] ]
black = [ black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, {file = "black-21.11b1-py3-none-any.whl", hash = "sha256:802c6c30b637b28645b7fde282ed2569c0cd777dbe493a41b6a03c1d903f99ac"},
{file = "black-21.11b1.tar.gz", hash = "sha256:a042adbb18b3262faad5aff4e834ff186bb893f95ba3a8013f09de1e5569def2"},
] ]
cachetools = [ cachetools = [
{file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"}, {file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"},
@ -2023,6 +2029,8 @@ psycopg2-binary = [
{file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661509f51531ec125e52357a489ea3806640d0ca37d9dada461ffc69ee1e7b6e"}, {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661509f51531ec125e52357a489ea3806640d0ca37d9dada461ffc69ee1e7b6e"},
{file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:d92272c7c16e105788efe2cfa5d680f07e34e0c29b03c1908f8636f55d5f915a"}, {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:d92272c7c16e105788efe2cfa5d680f07e34e0c29b03c1908f8636f55d5f915a"},
{file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:736b8797b58febabb85494142c627bd182b50d2a7ec65322983e71065ad3034c"}, {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:736b8797b58febabb85494142c627bd182b50d2a7ec65322983e71065ad3034c"},
{file = "psycopg2_binary-2.9.1-cp310-cp310-win32.whl", hash = "sha256:ebccf1123e7ef66efc615a68295bf6fdba875a75d5bba10a05073202598085fc"},
{file = "psycopg2_binary-2.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:1f6ca4a9068f5c5c57e744b4baa79f40e83e3746875cac3c45467b16326bab45"},
{file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"}, {file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"},
{file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698"}, {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698"},
{file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616"}, {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616"},
@ -2339,37 +2347,9 @@ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
] ]
typed-ast = [ tomli = [
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
{file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
{file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
{file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
{file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
{file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
{file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
{file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
{file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
{file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
{file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
{file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
{file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
{file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
{file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
] ]
typing-extensions = [ typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},

View File

@ -40,7 +40,6 @@ python-i18n = "^0.3.9"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pylint = "^2.6.0" pylint = "^2.6.0"
black = "^20.8b1"
pytest = "^6.2.1" pytest = "^6.2.1"
pytest-cov = "^2.11.0" pytest-cov = "^2.11.0"
mkdocs-material = "^7.0.2" mkdocs-material = "^7.0.2"
@ -51,6 +50,7 @@ rich = "^10.7.0"
isort = "^5.9.3" isort = "^5.9.3"
regex = "2021.9.30" # TODO: Remove during Upgrade -> https://github.com/psf/black/issues/2524 regex = "2021.9.30" # TODO: Remove during Upgrade -> https://github.com/psf/black/issues/2524
flake8-print = "^4.0.0" flake8-print = "^4.0.0"
black = "^21.11b1"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]