feat(backend): add rename tag, tool, category support (#875)

This commit is contained in:
Hayden 2021-12-10 19:48:06 -09:00 committed by GitHub
parent 8d77f4b31e
commit e109ac0f47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 573 additions and 163 deletions

View File

@ -1,4 +1,5 @@
import { BaseCRUDAPI } from "../_base";
import { Recipe } from "~/types/api-types/recipe";
const prefix = "/api";
@ -9,14 +10,24 @@ export interface CreateTool {
export interface Tool extends CreateTool {
id: number;
slug: string;
}
export interface RecipeToolResponse extends Tool {
recipes: Recipe[];
}
const routes = {
tools: `${prefix}/tools`,
toolsId: (id: string) => `${prefix}/tools/${id}`,
toolsSlug: (id: string) => `${prefix}/tools/slug/${id}`,
};
export class ToolsApi extends BaseCRUDAPI<Tool, CreateTool> {
baseRoute: string = routes.tools;
itemRoute = routes.toolsId;
async byslug(slug: string) {
return await this.requests.get<Tool>(routes.toolsSlug(slug));
}
}

View File

@ -1,10 +1,12 @@
<template>
<div>
<v-app-bar v-if="!disableToolbar" color="transparent" flat class="mt-n1 flex-sm-wrap rounded">
<v-icon v-if="title" large left>
{{ displayTitleIcon }}
</v-icon>
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
<slot name="title">
<v-icon v-if="title" large left>
{{ displayTitleIcon }}
</v-icon>
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
</slot>
<v-spacer></v-spacer>
<v-btn :icon="$vuetify.breakpoint.xsOnly" text :disabled="recipes.length === 0" @click="navigateRandom">
<v-icon :left="!$vuetify.breakpoint.xsOnly">

View File

@ -0,0 +1,106 @@
<template>
<div v-if="items">
<v-app-bar color="transparent" flat class="mt-n1 rounded">
<v-icon large left>
{{ icon }}
</v-icon>
<v-toolbar-title class="headline"> {{ headline }} </v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<section v-for="(itms, key, idx) in itemsSorted" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
<v-row>
<v-col v-for="(item, index) in itms" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
<v-card hover :to="`/recipes/${itemType}/${item.slug}`">
<v-card-actions>
<v-icon>
{{ icon }}
</v-icon>
<v-card-title class="py-1">{{ item.name }}</v-card-title>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</section>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, useContext, computed } from "@nuxtjs/composition-api";
type ItemType = "tags" | "categories" | "tools";
const ItemTypes = {
tag: "tags",
category: "categories",
tool: "tools",
};
interface GenericItem {
name: string;
slug: string;
}
export default defineComponent({
props: {
itemType: {
type: String as () => ItemType,
required: true,
},
items: {
type: Array as () => GenericItem[],
required: true,
},
},
setup(props) {
// @ts-ignore
const { i18n, $globals } = useContext();
const state = reactive({
headline: "tags",
icon: $globals.icons.tags,
});
switch (props.itemType) {
case ItemTypes.tag:
state.headline = i18n.t("tag.tags") as string;
break;
case ItemTypes.category:
state.headline = i18n.t("recipe.categories") as string;
break;
case ItemTypes.tool:
state.headline = "Tools";
state.icon = $globals.icons.potSteam;
break;
default:
break;
}
const itemsSorted = computed(() => {
const byLetter: { [key: string]: Array<any> } = {};
if (!props.items) return byLetter;
props.items.forEach((item) => {
const letter = item.name[0].toUpperCase();
if (!byLetter[letter]) {
byLetter[letter] = [];
}
byLetter[letter].push(item);
});
return byLetter;
});
return {
...toRefs(state),
itemsSorted,
};
},
head: {
title: "vbase-nuxt",
},
});
</script>

View File

@ -181,6 +181,11 @@ export default defineComponent({
to: "/recipes/tags",
title: this.$t("sidebar.tags"),
},
{
icon: this.$globals.icons.potSteam,
to: "/recipes/tools",
title: "Tools",
},
],
};
},

View File

@ -317,7 +317,10 @@ export default {
// https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-build
analyze: process.env.NODE_ENV !== "production",
babel: {
plugins: [["@babel/plugin-proposal-private-property-in-object", { loose: true }]],
plugins: [
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
["@nuxtjs/composition-api/dist/babel-plugin"],
],
},
transpile: process.env.NODE_ENV !== "production" ? [/@vue[\\/]composition-api/] : null,
},

View File

@ -27,7 +27,7 @@
</section>
<section>
<BaseCardSectionTitle class="pt-2" :icon="$globals.icons.email" title="Email Configuration" />
<v-alert :key="idx" border="left" colored-border :type="getColor(appConfig.emailReady)" elevation="2">
<v-alert border="left" colored-border :type="getColor(appConfig.emailReady)" elevation="2">
<div class="font-weight-medium">Email Configuration Status</div>
<div>
{{ appConfig.emailReady ? "Ready" : "Not Ready - Check Environmental Variables" }}

View File

@ -6,12 +6,54 @@
:title="category.name"
:recipes="category.recipes"
@sort="assignSorted"
></RecipeCardSection>
>
<template #title>
<v-btn icon class="mr-1">
<v-icon dark large @click="reset">
{{ $globals.icons.tags }}
</v-icon>
</v-btn>
<template v-if="edit">
<v-text-field
v-model="category.name"
autofocus
single-line
dense
hide-details
class="headline"
@keyup.enter="updateCategory"
>
</v-text-field>
<v-btn icon @click="updateCategory">
<v-icon size="28">
{{ $globals.icons.save }}
</v-icon>
</v-btn>
<v-btn icon class="mr-1" @click="reset">
<v-icon size="28">
{{ $globals.icons.close }}
</v-icon>
</v-btn>
</template>
<template v-else>
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true">
{{ category.name }}
</v-toolbar-title>
</template>
<span> Click to Edit </span>
</v-tooltip>
</template>
</template>
</RecipeCardSection>
</v-container>
</template>
<script lang="ts">
import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api";
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useUserApi } from "~/composables/api";
import { Recipe } from "~/types/api-types/recipe";
@ -21,13 +63,49 @@ export default defineComponent({
setup() {
const api = useUserApi();
const route = useRoute();
const router = useRouter();
const slug = route.value.params.slug;
const state = reactive({
initialValue: "",
edit: false,
});
const category = useAsync(async () => {
const { data } = await api.categories.getOne(slug);
if (data) {
state.initialValue = data.name;
}
return data;
}, slug);
return { category };
function reset() {
state.edit = false;
if (category.value) {
category.value.name = state.initialValue;
}
}
async function updateCategory() {
state.edit = false;
if (!category.value) {
return;
}
const { data } = await api.categories.updateOne(category.value.slug, category.value);
if (data) {
router.push("/recipes/categories/" + data.slug);
}
}
return {
category,
reset,
...toRefs(state),
updateCategory,
};
},
head() {
return {
@ -44,6 +122,4 @@ export default defineComponent({
},
});
</script>
<style scoped>
</style>

View File

@ -1,71 +1,35 @@
<template>
<v-container v-if="categories">
<v-app-bar color="transparent" flat class="mt-n1 rounded">
<v-icon large left>
{{ $globals.icons.tags }}
</v-icon>
<v-toolbar-title class="headline"> {{ $t("recipe.categories") }} </v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<section v-for="(items, key, idx) in categoriesByLetter" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
<v-row>
<v-col v-for="(item, index) in items" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
<v-card hover :to="`/recipes/categories/${item.slug}`">
<v-card-actions>
<v-icon>
{{ $globals.icons.tags }}
</v-icon>
<v-card-title class="py-1">{{ item.name }}</v-card-title>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</section>
<v-container>
<RecipeCategoryTagToolPage v-if="categories" :items="categories" item-type="categories" />
</v-container>
</template>
<script lang="ts">
import { computed, defineComponent, useAsync } from "@nuxtjs/composition-api";
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({
components: {
RecipeCategoryTagToolPage,
},
setup() {
const api = useUserApi();
const userApi = useUserApi();
const categories = useAsync(async () => {
const { data } = await api.categories.getAll();
return data;
}, useAsyncKey());
const { data } = await userApi.categories.getAll();
const categoriesByLetter: any = computed(() => {
const catsByLetter: { [key: string]: Array<any> } = {};
if (!categories.value) return catsByLetter;
categories.value.forEach((item) => {
const letter = item.name[0].toUpperCase();
if (!catsByLetter[letter]) {
catsByLetter[letter] = [];
}
catsByLetter[letter].push(item);
});
return catsByLetter;
if (data) {
return data;
}
});
return { categories, api, categoriesByLetter };
},
head() {
return {
title: this.$t("sidebar.categories") as string,
categories,
};
},
// head: {
// // @ts-ignore
// title: this.$t("sidebar.categories") as string,
// },
});
</script>
<style scoped>
</style>
</script>

View File

@ -1,49 +1,125 @@
<template>
<v-container>
<RecipeCardSection
v-if="tag"
v-if="tags"
:icon="$globals.icons.tags"
:title="tag.name"
:recipes="tag.recipes"
:title="tags.name"
:recipes="tags.recipes"
@sort="assignSorted"
></RecipeCardSection>
>
<template #title>
<v-btn icon class="mr-1">
<v-icon dark large @click="reset">
{{ $globals.icons.tags }}
</v-icon>
</v-btn>
<template v-if="edit">
<v-text-field
v-model="tags.name"
autofocus
single-line
dense
hide-details
class="headline"
@keyup.enter="updateTags"
>
</v-text-field>
<v-btn icon @click="updateTags">
<v-icon size="28">
{{ $globals.icons.save }}
</v-icon>
</v-btn>
<v-btn icon class="mr-1" @click="reset">
<v-icon size="28">
{{ $globals.icons.close }}
</v-icon>
</v-btn>
</template>
<template v-else>
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true">
{{ tags.name }}
</v-toolbar-title>
</template>
<span> Click to Edit </span>
</v-tooltip>
</template>
</template>
</RecipeCardSection>
</v-container>
</template>
<script lang="ts">
import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api";
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useUserApi } from "~/composables/api";
import { Recipe } from "~/types/api-types/admin";
import { Recipe } from "~/types/api-types/recipe";
export default defineComponent({
components: { RecipeCardSection },
setup() {
const api = useUserApi();
const route = useRoute();
const router = useRouter();
const slug = route.value.params.slug;
const tag = useAsync(async () => {
const state = reactive({
initialValue: "",
edit: false,
});
const tags = useAsync(async () => {
const { data } = await api.tags.getOne(slug);
if (data) {
state.initialValue = data.name;
}
return data;
}, slug);
return { tag };
function reset() {
state.edit = false;
if (tags.value) {
tags.value.name = state.initialValue;
}
}
async function updateTags() {
state.edit = false;
if (!tags.value) {
return;
}
const { data } = await api.tags.updateOne(tags.value.slug, tags.value);
if (data) {
router.push("/recipes/tags/" + data.slug);
}
}
return {
tags,
reset,
...toRefs(state),
updateTags,
};
},
head() {
return {
title: this.$t("sidebar.tags") as string,
title: this.$t("sidebar.categories") as string,
};
},
methods: {
assignSorted(val: Array<Recipe>) {
if (this.tag) {
if (this.tags) {
// @ts-ignore
this.tag.recipes = val;
this.tags.recipes = val;
}
},
},
});
</script>
<style scoped>
</style>

View File

@ -1,71 +1,34 @@
<template>
<v-container v-if="tags">
<v-app-bar color="transparent" flat class="mt-n1 rounded">
<v-icon large left>
{{ $globals.icons.tags }}
</v-icon>
<v-toolbar-title class="headline"> {{ $t("tag.tags") }} </v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<section v-for="(items, key, idx) in tagsByLetter" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
<v-row>
<v-col v-for="(item, index) in items" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
<v-card hover :to="`/recipes/tags/${item.slug}`">
<v-card-actions>
<v-icon>
{{ $globals.icons.tags }}
</v-icon>
<v-card-title class="py-1">{{ item.name }}</v-card-title>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</section>
<v-container>
<RecipeCategoryTagToolPage v-if="tools" :items="tools" item-type="tags" />
</v-container>
</template>
<script lang="ts">
import { defineComponent, useAsync, computed } from "@nuxtjs/composition-api";
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({
setup() {
const api = useUserApi();
const tags = useAsync(async () => {
const { data } = await api.tags.getAll();
return data;
}, useAsyncKey());
const tagsByLetter: any = computed(() => {
const tagsByLetter: { [key: string]: Array<any> } = {};
if (!tags.value) return tagsByLetter;
tags.value.forEach((item) => {
const letter = item.name[0].toUpperCase();
if (!tagsByLetter[letter]) {
tagsByLetter[letter] = [];
}
tagsByLetter[letter].push(item);
});
return tagsByLetter;
});
return { tags, api, tagsByLetter };
components: {
RecipeCategoryTagToolPage,
},
head() {
setup() {
const userApi = useUserApi();
const tools = useAsync(async () => {
const { data } = await userApi.tags.getAll();
if (data) {
return data;
}
});
return {
title: this.$t("sidebar.tags") as string,
tools,
};
},
// head: {
// // @ts-ignore
// title: this.$t("sidebar.tags") as string,
// },
});
</script>
<style scoped>
</style>
</script>

View File

@ -0,0 +1,119 @@
<template>
<v-container>
<RecipeCardSection v-if="tools" :title="tools.name" :recipes="tools.recipes" @sort="assignSorted">
<template #title>
<v-btn icon class="mr-1">
<v-icon dark large @click="reset">
{{ $globals.icons.potSteam }}
</v-icon>
</v-btn>
<template v-if="edit">
<v-text-field
v-model="tools.name"
autofocus
single-line
dense
hide-details
class="headline"
@keyup.enter="updateTools"
>
</v-text-field>
<v-btn icon @click="updateTools">
<v-icon size="28">
{{ $globals.icons.save }}
</v-icon>
</v-btn>
<v-btn icon class="mr-1" @click="reset">
<v-icon size="28">
{{ $globals.icons.close }}
</v-icon>
</v-btn>
</template>
<template v-else>
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true">
{{ tools.name }}
</v-toolbar-title>
</template>
<span> Click to Edit </span>
</v-tooltip>
</template>
</template>
</RecipeCardSection>
</v-container>
</template>
<script lang="ts">
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useUserApi } from "~/composables/api";
import { Recipe } from "~/types/api-types/recipe";
export default defineComponent({
components: { RecipeCardSection },
setup() {
const api = useUserApi();
const route = useRoute();
const router = useRouter();
const slug = route.value.params.slug;
const state = reactive({
initialValue: "",
edit: false,
});
const tools = useAsync(async () => {
const { data } = await api.tools.byslug(slug);
if (data) {
state.initialValue = data.name;
}
return data;
}, slug);
function reset() {
state.edit = false;
if (tools.value) {
tools.value.name = state.initialValue;
}
}
async function updateTools() {
state.edit = false;
if (!tools.value) {
return;
}
const { data } = await api.tools.updateOne(tools.value.id, tools.value);
if (data) {
router.push("/recipes/tools/" + data.slug);
}
}
return {
tools,
reset,
...toRefs(state),
updateTools,
};
},
head() {
return {
title: this.$t("sidebar.categories") as string,
};
},
methods: {
assignSorted(val: Array<Recipe>) {
if (this.tools) {
// @ts-ignore
this.tools.recipes = val;
}
},
},
});
</script>

View File

@ -0,0 +1,33 @@
<template>
<v-container>
<RecipeCategoryTagToolPage v-if="tools" :items="tools" item-type="tools" />
</v-container>
</template>
<script lang="ts">
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
import { useUserApi } from "~/composables/api";
export default defineComponent({
components: {
RecipeCategoryTagToolPage,
},
setup() {
const userApi = useUserApi();
const tools = useAsync(async () => {
const { data } = await userApi.tools.getAll();
if (data) {
return data;
}
});
return {
tools,
};
},
head: {
title: "Tools",
},
});
</script>

View File

@ -2,7 +2,6 @@ import { useDark } from "@vueuse/core";
export default ({ $vuetify }: any) => {
const isDark = useDark();
console.log("isDark Plugin", isDark);
if (isDark.value) {
$vuetify.theme.dark = true;

View File

@ -28,10 +28,9 @@ from mealie.schema.group.group_preferences import ReadGroupPreferences
from mealie.schema.group.invite_token import ReadInviteToken
from mealie.schema.group.webhook import ReadWebhook
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
from mealie.schema.recipe import Recipe, RecipeCategoryResponse, RecipeCommentOut, RecipeTagResponse
from mealie.schema.recipe import Recipe, RecipeCategoryResponse, RecipeCommentOut, RecipeTagResponse, RecipeTool
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
from mealie.schema.recipe.recipe_share_token import RecipeShareToken
from mealie.schema.recipe.recipe_tool import RecipeTool
from mealie.schema.reports.reports import ReportEntryOut, ReportOut
from mealie.schema.server import ServerTask
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut

View File

@ -1,3 +1,4 @@
from slugify import slugify
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Table, orm
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
@ -14,10 +15,10 @@ recipes_to_tools = Table(
class Tool(SqlAlchemyBase, BaseMixins):
__tablename__ = "tools"
name = Column(String, index=True, unique=True, nullable=False)
slug = Column(String, index=True, unique=True, nullable=False)
on_hand = Column(Boolean, default=False)
recipes = orm.relationship("RecipeModel", secondary=recipes_to_tools, back_populates="tools")
@auto_init()
def __init__(self, name, on_hand, **_) -> None:
self.on_hand = on_hand
self.name = name
def __init__(self, name, **_) -> None:
self.slug = slugify(name)

View File

@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import is_logged_in
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
@ -10,6 +11,7 @@ from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
public_router = APIRouter()
user_router = UserAPIRouter()
admin_router = AdminAPIRouter()
logger = get_logger()
@public_router.get("")
@ -61,6 +63,7 @@ async def update_recipe_category(category: str, new_category: CategoryIn, sessio
try:
return db.categories.update(category, new_category.dict())
except Exception:
logger.exception("Failed to update category")
raise HTTPException(status.HTTP_400_BAD_REQUEST)

View File

@ -1,8 +1,18 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from mealie.schema.recipe.recipe_tool import RecipeToolResponse
from mealie.services._base_http_service.router_factory import RouterFactory
from mealie.services.recipe.recipe_tool_service import RecipeToolService
router = APIRouter()
router.include_router(RouterFactory(RecipeToolService, prefix="/tools", tags=["Recipes: Tools"]))
tools_router = RouterFactory(RecipeToolService, prefix="/tools", tags=["Recipes: Tools"])
@tools_router.get("/slug/{slug}")
async def Func(slug: str, tools_service: RecipeToolService = Depends(RecipeToolService.private)):
"""Returns a recipe by slug."""
return tools_service.db.tools.get_one(slug, "slug", override_schema=RecipeToolResponse)
router.include_router(tools_router)

View File

@ -3,4 +3,5 @@ from .recipe_category import *
from .recipe_comments import *
from .recipe_image_types import *
from .recipe_ingredient import *
from .recipe_tool import *
from .request_helpers import *

View File

@ -18,7 +18,6 @@ from .recipe_notes import RecipeNote
from .recipe_nutrition import Nutrition
from .recipe_settings import RecipeSettings
from .recipe_step import RecipeStep
from .recipe_tool import RecipeTool
app_dirs = get_app_dirs()
@ -35,6 +34,11 @@ class RecipeCategory(RecipeTag):
pass
class RecipeTool(RecipeTag):
id: int = 0
on_hand: bool = False
class CreateRecipeByUrl(BaseModel):
url: str

View File

@ -42,7 +42,7 @@ class RecipeTagResponse(RecipeCategoryResponse):
pass
from .recipe import Recipe
from . import Recipe
RecipeCategoryResponse.update_forward_refs()
RecipeTagResponse.update_forward_refs()

View File

@ -1,3 +1,5 @@
from typing import List
from fastapi_camelcase import CamelModel
@ -8,6 +10,19 @@ class RecipeToolCreate(CamelModel):
class RecipeTool(RecipeToolCreate):
id: int
slug: str
class Config:
orm_mode = True
class RecipeToolResponse(RecipeTool):
recipes: List["Recipe"] = []
class Config:
orm_mode = True
from .recipe import Recipe
RecipeToolResponse.update_forward_refs()

View File

@ -63,7 +63,7 @@ class MealieAlphaMigrator(BaseMigrator):
recipe_lookup: dict[str, Path] = {}
recipes_as_dicts = []
for x in temp_path.rglob("**/[!.]*.json"):
for x in temp_path.rglob("**/recipes/**/[!.]*.json"):
if (y := MigrationReaders.json(x)) is not None:
recipes_as_dicts.append(y)
slug = y["slug"]
@ -76,12 +76,16 @@ class MealieAlphaMigrator(BaseMigrator):
recipe_model_lookup = {x.slug: x for x in recipes}
for slug, status in results:
if status:
model = recipe_model_lookup.get(slug)
dest_dir = model.directory
source_dir = recipe_lookup.get(slug)
if not status:
continue
if dest_dir.exists():
shutil.rmtree(dest_dir)
model = recipe_model_lookup.get(slug)
dest_dir = model.directory
source_dir = recipe_lookup.get(slug)
shutil.copytree(source_dir, dest_dir)
if dest_dir.exists():
shutil.rmtree(dest_dir)
for dir in source_dir.iterdir():
if dir.is_dir():
shutil.copytree(dir, dest_dir / dir.name)

View File

@ -1,12 +1,12 @@
import base64
import io
import json
import re
import tempfile
import zipfile
from gzip import GzipFile
from pathlib import Path
import regex as re
from slugify import slugify
from mealie.schema.recipe import RecipeNote

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from functools import cached_property
from mealie.schema.recipe.recipe_tool import RecipeTool, RecipeToolCreate
from mealie.schema.recipe import RecipeTool, RecipeToolCreate
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_recipe_event

View File

@ -22,6 +22,9 @@ class Routes:
class TestRecipeTool:
id: int
name: str
slug: str
on_hand: bool
recipes: list
@pytest.fixture(scope="function")
@ -32,7 +35,15 @@ def tool(api_client: TestClient, unique_user: TestUser) -> TestRecipeTool:
assert response.status_code == 201
yield TestRecipeTool(id=response.json()["id"], name=data["name"])
as_json = response.json()
yield TestRecipeTool(
id=as_json["id"],
name=data["name"],
slug=as_json["slug"],
on_hand=as_json["onHand"],
recipes=[],
)
try:
response = api_client.delete(Routes.item(response.json()["id"]), headers=unique_user.token)
@ -58,7 +69,12 @@ def test_read_tool(api_client: TestClient, tool: TestRecipeTool, unique_user: Te
def test_update_tool(api_client: TestClient, tool: TestRecipeTool, unique_user: TestUser):
update_data = {"id": tool.id, "name": random_string(10)}
update_data = {
"id": tool.id,
"name": random_string(10),
"slug": tool.slug,
"on_hand": True,
}
response = api_client.put(Routes.item(tool.id), json=update_data, headers=unique_user.token)
assert response.status_code == 200
@ -89,7 +105,7 @@ def test_recipe_tool_association(api_client: TestClient, tool: TestRecipeTool, u
as_json = response.json()
as_json["tools"] = [{"id": tool.id, "name": tool.name}]
as_json["tools"] = [{"id": tool.id, "name": tool.name, "slug": tool.slug}]
# Update Recipe
response = api_client.put(Routes.recipe(slug), json=as_json, headers=unique_user.token)