mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
feature: proper multi-tenant-support (#969)(WIP)
* update naming * refactor tests to use shared structure * shorten names * add tools test case * refactor to support multi-tenant * set group_id on creation * initial refactor for multitenant tags/cats * spelling * additional test case for same valued resources * fix recipe update tests * apply indexes to foreign keys * fix performance regressions * handle unknown exception * utility decorator for function debugging * migrate recipe_id to UUID * GUID for recipes * remove unused import * move image functions into package * move utilities to packages dir * update import * linter * image image and asset routes * update assets and images to use UUIDs * fix migration base * image asset test coverage * use ids for categories and tag crud functions * refactor recipe organizer test suite to reduce duplication * add uuid serlization utility * organizer base router * slug routes testing and fixes * fix postgres error * adopt UUIDs * move tags, categories, and tools under "organizers" umbrella * update composite label * generate ts types * fix import error * update frontend types * fix type errors * fix postgres errors * fix #978 * add null check for title validation * add note in docs on multi-tenancy
This commit is contained in:
parent
9a82a172cb
commit
c617251f4c
@ -14,6 +14,7 @@
|
|||||||
- User/Group settings are now completely separated from the Administration page.
|
- User/Group settings are now completely separated from the Administration page.
|
||||||
- All settings and configurations pages now have some sort of self-documenting help text. Additional text or descriptions are welcome from PRs
|
- All settings and configurations pages now have some sort of self-documenting help text. Additional text or descriptions are welcome from PRs
|
||||||
- New experimental banner for the site to give users a sense of what features are still "in development" and provide a link to a github issue that provides additional context.
|
- New experimental banner for the site to give users a sense of what features are still "in development" and provide a link to a github issue that provides additional context.
|
||||||
|
- Groups now offer full multi-tenant support so you can all groups have their own set of data.
|
||||||
|
|
||||||
#### ⚙️ Site Settings Page
|
#### ⚙️ Site Settings Page
|
||||||
|
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
import { BaseCRUDAPI } from "../_base";
|
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
|
||||||
|
|
||||||
const prefix = "/api";
|
|
||||||
|
|
||||||
export interface Category {
|
|
||||||
name: string;
|
|
||||||
id: number;
|
|
||||||
slug: string;
|
|
||||||
recipes?: Recipe[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateCategory {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes = {
|
|
||||||
categories: `${prefix}/categories`,
|
|
||||||
categoriesEmpty: `${prefix}/categories/empty`,
|
|
||||||
|
|
||||||
categoriesCategory: (category: string) => `${prefix}/categories/${category}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class CategoriesAPI extends BaseCRUDAPI<Category, CreateCategory> {
|
|
||||||
baseRoute: string = routes.categories;
|
|
||||||
itemRoute = routes.categoriesCategory;
|
|
||||||
|
|
||||||
/** Returns a list of categories that do not contain any recipes
|
|
||||||
*/
|
|
||||||
async getEmptyCategories() {
|
|
||||||
return await this.requests.get(routes.categoriesEmpty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a list of recipes associated with the provided category.
|
|
||||||
*/
|
|
||||||
async getAllRecipesByCategory(category: string) {
|
|
||||||
return await this.requests.get(routes.categoriesCategory(category));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Removes a recipe category from the database. Deleting a
|
|
||||||
* category does not impact a recipe. The category will be removed
|
|
||||||
* from any recipes that contain it
|
|
||||||
*/
|
|
||||||
async deleteRecipeCategory(category: string) {
|
|
||||||
return await this.requests.delete(routes.categoriesCategory(category));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
import { BaseCRUDAPI } from "../_base";
|
import { BaseCRUDAPI } from "../_base";
|
||||||
import { Category } from "./categories";
|
|
||||||
import { CategoryBase } from "~/types/api-types/recipe";
|
import { CategoryBase } from "~/types/api-types/recipe";
|
||||||
|
import { RecipeCategory } from "~/types/api-types/user";
|
||||||
|
|
||||||
const prefix = "/api";
|
const prefix = "/api";
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ export interface CookBook extends CreateCookBook {
|
|||||||
description: string;
|
description: string;
|
||||||
position: number;
|
position: number;
|
||||||
group_id: number;
|
group_id: number;
|
||||||
categories: Category[] | CategoryBase[];
|
categories: RecipeCategory[] | CategoryBase[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
|
@ -16,7 +16,7 @@ export interface CreateMealPlan {
|
|||||||
entryType: PlanEntryType;
|
entryType: PlanEntryType;
|
||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
recipeId?: number;
|
recipeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateMealPlan extends CreateMealPlan {
|
export interface UpdateMealPlan extends CreateMealPlan {
|
||||||
|
@ -12,7 +12,7 @@ const prefix = "/api";
|
|||||||
const routes = {
|
const routes = {
|
||||||
shoppingLists: `${prefix}/groups/shopping/lists`,
|
shoppingLists: `${prefix}/groups/shopping/lists`,
|
||||||
shoppingListsId: (id: string) => `${prefix}/groups/shopping/lists/${id}`,
|
shoppingListsId: (id: string) => `${prefix}/groups/shopping/lists/${id}`,
|
||||||
shoppingListIdAddRecipe: (id: string, recipeId: number) => `${prefix}/groups/shopping/lists/${id}/recipe/${recipeId}`,
|
shoppingListIdAddRecipe: (id: string, recipeId: string) => `${prefix}/groups/shopping/lists/${id}/recipe/${recipeId}`,
|
||||||
|
|
||||||
shoppingListItems: `${prefix}/groups/shopping/items`,
|
shoppingListItems: `${prefix}/groups/shopping/items`,
|
||||||
shoppingListItemsId: (id: string) => `${prefix}/groups/shopping/items/${id}`,
|
shoppingListItemsId: (id: string) => `${prefix}/groups/shopping/items/${id}`,
|
||||||
@ -22,11 +22,11 @@ export class ShoppingListsApi extends BaseCRUDAPI<ShoppingListOut, ShoppingListC
|
|||||||
baseRoute = routes.shoppingLists;
|
baseRoute = routes.shoppingLists;
|
||||||
itemRoute = routes.shoppingListsId;
|
itemRoute = routes.shoppingListsId;
|
||||||
|
|
||||||
async addRecipe(itemId: string, recipeId: number) {
|
async addRecipe(itemId: string, recipeId: string) {
|
||||||
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId, recipeId), {});
|
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId, recipeId), {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeRecipe(itemId: string, recipeId: number) {
|
async removeRecipe(itemId: string, recipeId: string) {
|
||||||
return await this.requests.delete(routes.shoppingListIdAddRecipe(itemId, recipeId));
|
return await this.requests.delete(routes.shoppingListIdAddRecipe(itemId, recipeId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
frontend/api/class-interfaces/organizer-categories.ts
Normal file
20
frontend/api/class-interfaces/organizer-categories.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { BaseCRUDAPI } from "../_base";
|
||||||
|
import { CategoryIn, RecipeCategoryResponse } from "~/types/api-types/recipe";
|
||||||
|
import { config } from "~/api/config";
|
||||||
|
|
||||||
|
const prefix = config.PREFIX + "/organizers";
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
categories: `${prefix}/categories`,
|
||||||
|
categoriesId: (category: string) => `${prefix}/categories/${category}`,
|
||||||
|
categoriesSlug: (category: string) => `${prefix}/categories/slug/${category}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class CategoriesAPI extends BaseCRUDAPI<RecipeCategoryResponse, CategoryIn> {
|
||||||
|
baseRoute: string = routes.categories;
|
||||||
|
itemRoute = routes.categoriesId;
|
||||||
|
|
||||||
|
async bySlug(slug: string) {
|
||||||
|
return await this.requests.get<RecipeCategoryResponse>(routes.categoriesSlug(slug));
|
||||||
|
}
|
||||||
|
}
|
20
frontend/api/class-interfaces/organizer-tags.ts
Normal file
20
frontend/api/class-interfaces/organizer-tags.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { BaseCRUDAPI } from "../_base";
|
||||||
|
import { RecipeTagResponse, TagIn } from "~/types/api-types/recipe";
|
||||||
|
import { config } from "~/api/config";
|
||||||
|
|
||||||
|
const prefix = config.PREFIX + "/organizers";
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
tags: `${prefix}/tags`,
|
||||||
|
tagsId: (tag: string) => `${prefix}/tags/${tag}`,
|
||||||
|
tagsSlug: (tag: string) => `${prefix}/tags/slug/${tag}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TagsAPI extends BaseCRUDAPI<RecipeTagResponse, TagIn> {
|
||||||
|
baseRoute: string = routes.tags;
|
||||||
|
itemRoute = routes.tagsId;
|
||||||
|
|
||||||
|
async bySlug(slug: string) {
|
||||||
|
return await this.requests.get<RecipeTagResponse>(routes.tagsSlug(slug));
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import { BaseCRUDAPI } from "../_base";
|
import { BaseCRUDAPI } from "../_base";
|
||||||
import { RecipeTool, RecipeToolCreate, RecipeToolResponse } from "~/types/api-types/recipe";
|
import { RecipeTool, RecipeToolCreate, RecipeToolResponse } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
const prefix = "/api";
|
import { config } from "~/api/config";
|
||||||
|
|
||||||
|
const prefix = config.PREFIX + "/organizers";
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
tools: `${prefix}/tools`,
|
tools: `${prefix}/tools`,
|
||||||
@ -13,7 +15,7 @@ export class ToolsApi extends BaseCRUDAPI<RecipeTool, RecipeToolCreate> {
|
|||||||
baseRoute: string = routes.tools;
|
baseRoute: string = routes.tools;
|
||||||
itemRoute = routes.toolsId;
|
itemRoute = routes.toolsId;
|
||||||
|
|
||||||
async byslug(slug: string) {
|
async bySlug(slug: string) {
|
||||||
return await this.requests.get<RecipeToolResponse>(routes.toolsSlug(slug));
|
return await this.requests.get<RecipeToolResponse>(routes.toolsSlug(slug));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,22 +1,14 @@
|
|||||||
import { BaseCRUDAPI } from "../_base";
|
import { BaseCRUDAPI } from "../_base";
|
||||||
|
import { CreateIngredientFood, IngredientFood } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
const prefix = "/api";
|
const prefix = "/api";
|
||||||
|
|
||||||
export interface CreateFood {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Food extends CreateFood {
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
food: `${prefix}/foods`,
|
food: `${prefix}/foods`,
|
||||||
foodsFood: (tag: string) => `${prefix}/foods/${tag}`,
|
foodsFood: (tag: string) => `${prefix}/foods/${tag}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FoodAPI extends BaseCRUDAPI<Food, CreateFood> {
|
export class FoodAPI extends BaseCRUDAPI<IngredientFood, CreateIngredientFood> {
|
||||||
baseRoute: string = routes.food;
|
baseRoute: string = routes.food;
|
||||||
itemRoute = routes.foodsFood;
|
itemRoute = routes.foodsFood;
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,12 @@ const routes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface RecipeShareTokenCreate {
|
export interface RecipeShareTokenCreate {
|
||||||
recipeId: number;
|
recipeId: string;
|
||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecipeShareToken {
|
export interface RecipeShareToken {
|
||||||
recipeId: number;
|
recipeId: string;
|
||||||
id: string;
|
id: string;
|
||||||
groupId: number;
|
groupId: number;
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Category } from "../categories";
|
|
||||||
import { Tag } from "../tags";
|
|
||||||
import { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit } from "~/types/api-types/recipe";
|
import { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit } from "~/types/api-types/recipe";
|
||||||
|
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
||||||
|
|
||||||
export type Parser = "nlp" | "brute";
|
export type Parser = "nlp" | "brute";
|
||||||
|
|
||||||
@ -30,8 +29,8 @@ export interface ParsedIngredient {
|
|||||||
|
|
||||||
export interface BulkCreateRecipe {
|
export interface BulkCreateRecipe {
|
||||||
url: string;
|
url: string;
|
||||||
categories: Category[];
|
categories: RecipeCategory[];
|
||||||
tags: Tag[];
|
tags: RecipeTag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BulkCreatePayload {
|
export interface BulkCreatePayload {
|
||||||
@ -50,7 +49,7 @@ export interface CreateAsset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RecipeCommentCreate {
|
export interface RecipeCommentCreate {
|
||||||
recipeId: number;
|
recipeId: string;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
import { BaseCRUDAPI } from "../_base";
|
|
||||||
import { Recipe } from "~/types/api-types/admin";
|
|
||||||
|
|
||||||
const prefix = "/api";
|
|
||||||
|
|
||||||
export interface Tag {
|
|
||||||
name: string;
|
|
||||||
id: number;
|
|
||||||
slug: string;
|
|
||||||
recipes?: Recipe[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateTag {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes = {
|
|
||||||
tags: `${prefix}/tags`,
|
|
||||||
tagsEmpty: `${prefix}/tags/empty`,
|
|
||||||
|
|
||||||
tagsTag: (tag: string) => `${prefix}/tags/${tag}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class TagsAPI extends BaseCRUDAPI<Tag, CreateTag> {
|
|
||||||
baseRoute: string = routes.tags;
|
|
||||||
itemRoute = routes.tagsTag;
|
|
||||||
|
|
||||||
/** Returns a list of categories that do not contain any recipes
|
|
||||||
*/
|
|
||||||
async getEmptyCategories() {
|
|
||||||
return await this.requests.get(routes.tagsEmpty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a list of recipes associated with the provided category.
|
|
||||||
*/
|
|
||||||
async getAllRecipesByCategory(category: string) {
|
|
||||||
return await this.requests.get(routes.tagsTag(category));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Removes a recipe category from the database. Deleting a
|
|
||||||
* category does not impact a recipe. The category will be removed
|
|
||||||
* from any recipes that contain it
|
|
||||||
*/
|
|
||||||
async deleteRecipeCategory(category: string) {
|
|
||||||
return await this.requests.delete(routes.tagsTag(category));
|
|
||||||
}
|
|
||||||
}
|
|
5
frontend/api/config.ts
Normal file
5
frontend/api/config.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const PREFIX = "/api";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
PREFIX,
|
||||||
|
};
|
@ -4,8 +4,8 @@ import { GroupAPI } from "./class-interfaces/groups";
|
|||||||
import { EventsAPI } from "./class-interfaces/events";
|
import { EventsAPI } from "./class-interfaces/events";
|
||||||
import { BackupAPI } from "./class-interfaces/backups";
|
import { BackupAPI } from "./class-interfaces/backups";
|
||||||
import { UploadFile } from "./class-interfaces/upload";
|
import { UploadFile } from "./class-interfaces/upload";
|
||||||
import { CategoriesAPI } from "./class-interfaces/categories";
|
import { CategoriesAPI } from "./class-interfaces/organizer-categories";
|
||||||
import { TagsAPI } from "./class-interfaces/tags";
|
import { TagsAPI } from "./class-interfaces/organizer-tags";
|
||||||
import { UtilsAPI } from "./class-interfaces/utils";
|
import { UtilsAPI } from "./class-interfaces/utils";
|
||||||
import { FoodAPI } from "./class-interfaces/recipe-foods";
|
import { FoodAPI } from "./class-interfaces/recipe-foods";
|
||||||
import { UnitAPI } from "./class-interfaces/recipe-units";
|
import { UnitAPI } from "./class-interfaces/recipe-units";
|
||||||
@ -17,7 +17,7 @@ import { EmailAPI } from "./class-interfaces/email";
|
|||||||
import { BulkActionsAPI } from "./class-interfaces/recipe-bulk-actions";
|
import { BulkActionsAPI } from "./class-interfaces/recipe-bulk-actions";
|
||||||
import { GroupServerTaskAPI } from "./class-interfaces/group-tasks";
|
import { GroupServerTaskAPI } from "./class-interfaces/group-tasks";
|
||||||
import { AdminAPI } from "./admin-api";
|
import { AdminAPI } from "./admin-api";
|
||||||
import { ToolsApi } from "./class-interfaces/tools";
|
import { ToolsApi } from "./class-interfaces/organizer-tools";
|
||||||
import { GroupMigrationApi } from "./class-interfaces/group-migrations";
|
import { GroupMigrationApi } from "./class-interfaces/group-migrations";
|
||||||
import { GroupReportsApi } from "./class-interfaces/group-reports";
|
import { GroupReportsApi } from "./class-interfaces/group-reports";
|
||||||
import { ShoppingApi } from "./class-interfaces/group-shopping-lists";
|
import { ShoppingApi } from "./class-interfaces/group-shopping-lists";
|
||||||
|
@ -24,15 +24,7 @@
|
|||||||
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :slug="slug" show-always />
|
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :slug="slug" show-always />
|
||||||
<v-tooltip v-if="!locked" bottom color="info">
|
<v-tooltip v-if="!locked" bottom color="info">
|
||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
<v-btn
|
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('input', true)">
|
||||||
fab
|
|
||||||
small
|
|
||||||
class="mx-1"
|
|
||||||
color="info"
|
|
||||||
v-bind="attrs"
|
|
||||||
v-on="on"
|
|
||||||
@click="$emit('input', true)"
|
|
||||||
>
|
|
||||||
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
@ -40,14 +32,7 @@
|
|||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<v-tooltip v-else bottom color="info">
|
<v-tooltip v-else bottom color="info">
|
||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
<v-btn
|
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on">
|
||||||
fab
|
|
||||||
small
|
|
||||||
class="mx-1"
|
|
||||||
color="info"
|
|
||||||
v-bind="attrs"
|
|
||||||
v-on="on"
|
|
||||||
>
|
|
||||||
<v-icon> {{ $globals.icons.lock }} </v-icon>
|
<v-icon> {{ $globals.icons.lock }} </v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
@ -93,7 +78,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent, ref, useContext} from "@nuxtjs/composition-api";
|
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
||||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||||
|
|
||||||
@ -123,7 +108,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
recipeId: {
|
recipeId: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Number,
|
type: String,
|
||||||
},
|
},
|
||||||
locked: {
|
locked: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -191,7 +176,7 @@ export default defineComponent({
|
|||||||
editorButtons,
|
editorButtons,
|
||||||
emitHandler,
|
emitHandler,
|
||||||
emitDelete,
|
emitDelete,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -86,6 +86,10 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
recipeId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
value: {
|
value: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
@ -143,7 +147,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const { recipeAssetPath } = useStaticRoutes();
|
const { recipeAssetPath } = useStaticRoutes();
|
||||||
function assetURL(assetName: string) {
|
function assetURL(assetName: string) {
|
||||||
return recipeAssetPath(props.slug, assetName);
|
return recipeAssetPath(props.recipeId, assetName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assetEmbed(name: string) {
|
function assetEmbed(name: string) {
|
||||||
|
@ -8,7 +8,14 @@
|
|||||||
:min-height="imageHeight + 75"
|
:min-height="imageHeight + 75"
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
>
|
>
|
||||||
<RecipeCardImage :icon-size="imageHeight" :height="imageHeight" :slug="slug" small :image-version="image">
|
<RecipeCardImage
|
||||||
|
:icon-size="imageHeight"
|
||||||
|
:height="imageHeight"
|
||||||
|
:slug="slug"
|
||||||
|
:recipe-id="recipeId"
|
||||||
|
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">
|
||||||
@ -95,7 +102,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
recipeId: {
|
recipeId: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Number,
|
type: String,
|
||||||
},
|
},
|
||||||
imageHeight: {
|
imageHeight: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<v-img
|
<v-img
|
||||||
v-if="!fallBackImage"
|
v-if="!fallBackImage"
|
||||||
:height="height"
|
:height="height"
|
||||||
:src="getImage(slug)"
|
:src="getImage(recipeId)"
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
@load="fallBackImage = false"
|
@load="fallBackImage = false"
|
||||||
@error="fallBackImage = true"
|
@error="fallBackImage = true"
|
||||||
@ -18,7 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {computed, defineComponent, ref, watch} from "@nuxtjs/composition-api";
|
import { computed, defineComponent, ref, watch } from "@nuxtjs/composition-api";
|
||||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -43,6 +43,10 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
recipeId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
imageVersion: {
|
imageVersion: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
@ -63,20 +67,23 @@ export default defineComponent({
|
|||||||
if (props.small) return "small";
|
if (props.small) return "small";
|
||||||
if (props.large) return "large";
|
if (props.large) return "large";
|
||||||
return "large";
|
return "large";
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => props.slug, () => {
|
|
||||||
fallBackImage.value = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function getImage(slug: string) {
|
watch(
|
||||||
|
() => props.recipeId,
|
||||||
|
() => {
|
||||||
|
fallBackImage.value = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function getImage(recipeId: string) {
|
||||||
switch (imageSize.value) {
|
switch (imageSize.value) {
|
||||||
case "tiny":
|
case "tiny":
|
||||||
return recipeTinyImage(slug, props.imageVersion);
|
return recipeTinyImage(recipeId, props.imageVersion);
|
||||||
case "small":
|
case "small":
|
||||||
return recipeSmallImage(slug, props.imageVersion);
|
return recipeSmallImage(recipeId, props.imageVersion);
|
||||||
case "large":
|
case "large":
|
||||||
return recipeImage(slug, props.imageVersion);
|
return recipeImage(recipeId, props.imageVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,14 @@
|
|||||||
<v-list-item three-line>
|
<v-list-item three-line>
|
||||||
<slot name="avatar">
|
<slot name="avatar">
|
||||||
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
|
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
|
||||||
<RecipeCardImage :icon-size="100" :height="125" :slug="slug" small :image-version="image"></RecipeCardImage>
|
<RecipeCardImage
|
||||||
|
:icon-size="100"
|
||||||
|
:height="125"
|
||||||
|
:slug="slug"
|
||||||
|
:recipe-id="recipeId"
|
||||||
|
small
|
||||||
|
:image-version="image"
|
||||||
|
></RecipeCardImage>
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
</slot>
|
</slot>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
@ -93,7 +100,7 @@ export default defineComponent({
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
recipeId: {
|
recipeId: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -105,7 +112,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
loggedIn,
|
loggedIn,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -46,8 +46,7 @@
|
|||||||
import { computed, defineComponent, onMounted, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, onMounted, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
|
||||||
import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog.vue";
|
import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog.vue";
|
||||||
import { useTags, useCategories } from "~/composables/recipes";
|
import { useTags, useCategories } from "~/composables/recipes";
|
||||||
import { Category } from "~/api/class-interfaces/categories";
|
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
||||||
import { Tag } from "~/api/class-interfaces/tags";
|
|
||||||
|
|
||||||
const MOUNTED_EVENT = "mounted";
|
const MOUNTED_EVENT = "mounted";
|
||||||
|
|
||||||
@ -57,7 +56,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: Array as () => (Category | Tag | string)[],
|
type: Array as () => (RecipeTag | RecipeCategory | string)[],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
solo: {
|
solo: {
|
||||||
@ -103,9 +102,12 @@ export default defineComponent({
|
|||||||
const state = reactive({
|
const state = reactive({
|
||||||
selected: props.value,
|
selected: props.value,
|
||||||
});
|
});
|
||||||
watch(() => props.value, (val) => {
|
watch(
|
||||||
state.selected = val;
|
() => props.value,
|
||||||
});
|
(val) => {
|
||||||
|
state.selected = val;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { i18n } = useContext();
|
const { i18n } = useContext();
|
||||||
const inputLabel = computed(() => {
|
const inputLabel = computed(() => {
|
||||||
@ -114,14 +116,14 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const activeItems = computed(() => {
|
const activeItems = computed(() => {
|
||||||
let itemObjects: Tag[] | Category[] | null;
|
let itemObjects: RecipeTag[] | RecipeCategory[] | null;
|
||||||
if (props.tagSelector) itemObjects = allTags.value;
|
if (props.tagSelector) itemObjects = allTags.value;
|
||||||
else {
|
else {
|
||||||
itemObjects = allCategories.value;
|
itemObjects = allCategories.value;
|
||||||
}
|
}
|
||||||
if (props.returnObject) return itemObjects;
|
if (props.returnObject) return itemObjects;
|
||||||
else {
|
else {
|
||||||
return itemObjects?.map((x: Tag | Category) => x.name);
|
return itemObjects?.map((x: RecipeTag | RecipeCategory) => x.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -145,7 +147,7 @@ export default defineComponent({
|
|||||||
state.selected.splice(index, 1);
|
state.selected.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushToItem(createdItem: Tag | Category) {
|
function pushToItem(createdItem: RecipeTag | RecipeCategory) {
|
||||||
// TODO: Remove excessive get calls
|
// TODO: Remove excessive get calls
|
||||||
getAllCategories();
|
getAllCategories();
|
||||||
getAllTags();
|
getAllTags();
|
||||||
@ -164,4 +166,3 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ export default defineComponent({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
recipeId: {
|
recipeId: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -114,4 +114,4 @@ export default defineComponent({
|
|||||||
return { api, comments, ...toRefs(state), submitComment, deleteComment };
|
return { api, comments, ...toRefs(state), submitComment, deleteComment };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -168,7 +168,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
recipeId: {
|
recipeId: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Number,
|
type: String,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
|
@ -70,7 +70,7 @@ export default defineComponent({
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
recipeId: {
|
recipeId: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
|
@ -158,14 +158,15 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleUnitEnter() {
|
function handleUnitEnter() {
|
||||||
if (value.unit === undefined || !value.unit.name.includes(unitSearch.value)) {
|
if (value.unit === undefined || value.unit === null || !value.unit.name.includes(unitSearch.value)) {
|
||||||
console.log("Creating");
|
console.log("Creating");
|
||||||
createAssignUnit();
|
createAssignUnit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFoodEnter() {
|
function handleFoodEnter() {
|
||||||
if (value.food === undefined || !value.food.name.includes(foodSearch.value)) {
|
console.log(value.food);
|
||||||
|
if (value.food === undefined || value.food === null || !value.food.name.includes(foodSearch.value)) {
|
||||||
console.log("Creating");
|
console.log("Creating");
|
||||||
createAssignFood();
|
createAssignFood();
|
||||||
}
|
}
|
||||||
@ -190,7 +191,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style >
|
<style>
|
||||||
.v-input__append-outer {
|
.v-input__append-outer {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
@ -50,7 +50,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
function validateTitle(title?: string) {
|
function validateTitle(title?: string) {
|
||||||
return !(title === undefined || title === "");
|
return !(title === undefined || title === "" || title === null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
|
@ -10,20 +10,20 @@ export const useStaticRoutes = () => {
|
|||||||
const fullBase = serverBase + prefix;
|
const fullBase = serverBase + prefix;
|
||||||
|
|
||||||
// Methods to Generate reference urls for assets/images *
|
// Methods to Generate reference urls for assets/images *
|
||||||
function recipeImage(recipeSlug: string, version = "", key = 1) {
|
function recipeImage(recipeId: string, version = "", key = 1) {
|
||||||
return `${fullBase}/media/recipes/${recipeSlug}/images/original.webp?&rnd=${key}&version=${version}`;
|
return `${fullBase}/media/recipes/${recipeId}/images/original.webp?&rnd=${key}&version=${version}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeSmallImage(recipeSlug: string, version = "", key = 1) {
|
function recipeSmallImage(recipeId: string, version = "", key = 1) {
|
||||||
return `${fullBase}/media/recipes/${recipeSlug}/images/min-original.webp?&rnd=${key}&version=${version}`;
|
return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?&rnd=${key}&version=${version}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeTinyImage(recipeSlug: string, version = "", key = 1) {
|
function recipeTinyImage(recipeId: string, version = "", key = 1) {
|
||||||
return `${fullBase}/media/recipes/${recipeSlug}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
|
return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeAssetPath(recipeSlug: string, assetName: string) {
|
function recipeAssetPath(recipeId: string, assetName: string) {
|
||||||
return `${fullBase}/media/recipes/${recipeSlug}/assets/${assetName}`;
|
return `${fullBase}/media/recipes/${recipeId}/assets/${assetName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -12,11 +12,11 @@ export const useFoods = function () {
|
|||||||
const deleteTargetId = ref(0);
|
const deleteTargetId = ref(0);
|
||||||
const validForm = ref(true);
|
const validForm = ref(true);
|
||||||
|
|
||||||
const workingFoodData = reactive({
|
const workingFoodData = reactive<IngredientFood>({
|
||||||
id: 0,
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
labelId: "",
|
labelId: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@ -80,16 +80,16 @@ export const useFoods = function () {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetWorking() {
|
resetWorking() {
|
||||||
workingFoodData.id = 0;
|
workingFoodData.id = "";
|
||||||
workingFoodData.name = "";
|
workingFoodData.name = "";
|
||||||
workingFoodData.description = "";
|
workingFoodData.description = "";
|
||||||
workingFoodData.labelId = "";
|
workingFoodData.labelId = undefined;
|
||||||
},
|
},
|
||||||
setWorking(item: IngredientFood) {
|
setWorking(item: IngredientFood) {
|
||||||
workingFoodData.id = item.id;
|
workingFoodData.id = item.id;
|
||||||
workingFoodData.name = item.name;
|
workingFoodData.name = item.name;
|
||||||
workingFoodData.description = item.description || "";
|
workingFoodData.description = item.description || "";
|
||||||
workingFoodData.labelId = item.labelId || "";
|
workingFoodData.labelId = item.labelId;
|
||||||
},
|
},
|
||||||
flushStore() {
|
flushStore() {
|
||||||
foodStore = null;
|
foodStore = null;
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { Ref } from "@nuxtjs/composition-api";
|
import { Ref } from "@nuxtjs/composition-api";
|
||||||
// import { useStaticRoutes } from "../api";
|
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
export const useRecipeMeta = (recipe: Ref<Recipe | null>) => {
|
export const useRecipeMeta = (recipe: Ref<Recipe | null>) => {
|
||||||
// const { recipeImage } = useStaticRoutes();
|
|
||||||
return () => {
|
return () => {
|
||||||
const imageURL = "";
|
const imageURL = "";
|
||||||
return {
|
return {
|
||||||
|
@ -2,10 +2,11 @@ import { reactive, ref, useAsync } from "@nuxtjs/composition-api";
|
|||||||
import { useAsyncKey } from "../use-utils";
|
import { useAsyncKey } from "../use-utils";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { VForm } from "~/types/vuetify";
|
import { VForm } from "~/types/vuetify";
|
||||||
|
import { RecipeTool } from "~/types/api-types/user";
|
||||||
|
|
||||||
export const useTools = function (eager = true) {
|
export const useTools = function (eager = true) {
|
||||||
const workingToolData = reactive({
|
const workingToolData = reactive<RecipeTool>({
|
||||||
id: 0,
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
slug: "",
|
slug: "",
|
||||||
onHand: false,
|
onHand: false,
|
||||||
@ -72,7 +73,7 @@ export const useTools = function (eager = true) {
|
|||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
workingToolData.name = "";
|
workingToolData.name = "";
|
||||||
workingToolData.id = 0;
|
workingToolData.id = "";
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
validForm.value = true;
|
validForm.value = true;
|
||||||
},
|
},
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import { Ref, ref, useAsync } from "@nuxtjs/composition-api";
|
import { Ref, ref, useAsync } from "@nuxtjs/composition-api";
|
||||||
import { useUserApi } from "../api";
|
import { useUserApi } from "../api";
|
||||||
import { useAsyncKey } from "../use-utils";
|
import { useAsyncKey } from "../use-utils";
|
||||||
import { CategoriesAPI, Category } from "~/api/class-interfaces/categories";
|
import { CategoriesAPI } from "~/api/class-interfaces/organizer-categories";
|
||||||
import { Tag, TagsAPI } from "~/api/class-interfaces/tags";
|
import { TagsAPI } from "~/api/class-interfaces/organizer-tags";
|
||||||
|
import { RecipeTag, RecipeCategory } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
export const allCategories = ref<Category[] | null>([]);
|
export const allCategories = ref<RecipeCategory[] | null>([]);
|
||||||
export const allTags = ref<Tag[] | null>([]);
|
export const allTags = ref<RecipeTag[] | null>([]);
|
||||||
|
|
||||||
function baseTagsCategories(reference: Ref<Category[] | null> | Ref<Tag[] | null>, api: TagsAPI | CategoriesAPI) {
|
function baseTagsCategories(
|
||||||
|
reference: Ref<RecipeCategory[] | null> | Ref<RecipeTag[] | null>,
|
||||||
|
api: TagsAPI | CategoriesAPI
|
||||||
|
) {
|
||||||
function useAsyncGetAll() {
|
function useAsyncGetAll() {
|
||||||
useAsync(async () => {
|
useAsync(async () => {
|
||||||
await refreshItems();
|
await refreshItems();
|
||||||
|
@ -107,6 +107,7 @@
|
|||||||
<v-list-item-avatar :rounded="false">
|
<v-list-item-avatar :rounded="false">
|
||||||
<RecipeCardImage
|
<RecipeCardImage
|
||||||
v-if="mealplan.recipe"
|
v-if="mealplan.recipe"
|
||||||
|
:recipe-id="mealplan.recipe.id"
|
||||||
tiny
|
tiny
|
||||||
icon-size="25"
|
icon-size="25"
|
||||||
:slug="mealplan.recipe ? mealplan.recipe.slug : ''"
|
:slug="mealplan.recipe ? mealplan.recipe.slug : ''"
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
></RecipeCardSection>
|
></RecipeCardSection>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api";
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useRecipes, recentRecipes } from "~/composables/recipes";
|
import { useRecipes, recentRecipes } from "~/composables/recipes";
|
||||||
@ -23,4 +23,3 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -35,7 +35,7 @@
|
|||||||
:max-width="enableLandscape ? null : '50%'"
|
:max-width="enableLandscape ? null : '50%'"
|
||||||
min-height="50"
|
min-height="50"
|
||||||
:height="hideImage ? undefined : imageHeight"
|
:height="hideImage ? undefined : imageHeight"
|
||||||
:src="recipeImage(recipe.slug, imageKey)"
|
:src="recipeImage(recipe.id, imageKey)"
|
||||||
class="d-print-none"
|
class="d-print-none"
|
||||||
@error="hideImage = true"
|
@error="hideImage = true"
|
||||||
>
|
>
|
||||||
@ -284,6 +284,7 @@
|
|||||||
v-model="recipe.assets"
|
v-model="recipe.assets"
|
||||||
:edit="form"
|
:edit="form"
|
||||||
:slug="recipe.slug"
|
:slug="recipe.slug"
|
||||||
|
:recipe-id="recipe.id"
|
||||||
/>
|
/>
|
||||||
</client-only>
|
</client-only>
|
||||||
</div>
|
</div>
|
||||||
@ -362,6 +363,7 @@
|
|||||||
v-model="recipe.assets"
|
v-model="recipe.assets"
|
||||||
:edit="form"
|
:edit="form"
|
||||||
:slug="recipe.slug"
|
:slug="recipe.slug"
|
||||||
|
:recipe-id="recipe.id"
|
||||||
/>
|
/>
|
||||||
</client-only>
|
</client-only>
|
||||||
</div>
|
</div>
|
||||||
@ -562,7 +564,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
const { recipeImage } = useStaticRoutes();
|
const { recipeImage } = useStaticRoutes();
|
||||||
|
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Layout Helpers
|
// Layout Helpers
|
||||||
|
|
||||||
@ -691,9 +692,8 @@ export default defineComponent({
|
|||||||
// Recipe Tools
|
// Recipe Tools
|
||||||
|
|
||||||
async function updateTool(tool: RecipeTool) {
|
async function updateTool(tool: RecipeTool) {
|
||||||
if (tool.id === undefined)
|
if (tool.id === undefined) return;
|
||||||
return;
|
|
||||||
|
|
||||||
const { response } = await api.tools.updateOne(tool.id, tool);
|
const { response } = await api.tools.updateOne(tool.id, tool);
|
||||||
|
|
||||||
if (response?.status === 200) {
|
if (response?.status === 200) {
|
||||||
|
@ -72,7 +72,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const category = useAsync(async () => {
|
const category = useAsync(async () => {
|
||||||
const { data } = await api.categories.getOne(slug);
|
const { data } = await api.categories.bySlug(slug);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.initialValue = data.name;
|
state.initialValue = data.name;
|
||||||
}
|
}
|
||||||
@ -93,7 +93,7 @@ export default defineComponent({
|
|||||||
if (!category.value) {
|
if (!category.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { data } = await api.categories.updateOne(category.value.slug, category.value);
|
const { data } = await api.categories.updateOne(category.value.id, category.value);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
router.push("/recipes/categories/" + data.slug);
|
router.push("/recipes/categories/" + data.slug);
|
||||||
|
@ -72,7 +72,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tags = useAsync(async () => {
|
const tags = useAsync(async () => {
|
||||||
const { data } = await api.tags.getOne(slug);
|
const { data } = await api.tags.bySlug(slug);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.initialValue = data.name;
|
state.initialValue = data.name;
|
||||||
}
|
}
|
||||||
@ -93,7 +93,7 @@ export default defineComponent({
|
|||||||
if (!tags.value) {
|
if (!tags.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { data } = await api.tags.updateOne(tags.value.slug, tags.value);
|
const { data } = await api.tags.updateOne(tags.value.id, tags.value);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
router.push("/recipes/tags/" + data.slug);
|
router.push("/recipes/tags/" + data.slug);
|
||||||
|
@ -66,7 +66,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tools = useAsync(async () => {
|
const tools = useAsync(async () => {
|
||||||
const { data } = await api.tools.byslug(slug);
|
const { data } = await api.tools.bySlug(slug);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.initialValue = data.name;
|
state.initialValue = data.name;
|
||||||
}
|
}
|
||||||
|
@ -115,8 +115,8 @@ import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategory
|
|||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useRecipes, allRecipes, useFoods } from "~/composables/recipes";
|
import { useRecipes, allRecipes, useFoods } from "~/composables/recipes";
|
||||||
import { RecipeSummary } from "~/types/api-types/recipe";
|
import { RecipeSummary } from "~/types/api-types/recipe";
|
||||||
import { Tag } from "~/api/class-interfaces/tags";
|
|
||||||
import { useRouteQuery } from "~/composables/use-router";
|
import { useRouteQuery } from "~/composables/use-router";
|
||||||
|
import { RecipeTag } from "~/types/api-types/user";
|
||||||
|
|
||||||
interface GenericFilter {
|
interface GenericFilter {
|
||||||
exclude: boolean;
|
exclude: boolean;
|
||||||
@ -189,7 +189,7 @@ export default defineComponent({
|
|||||||
state.includeTags,
|
state.includeTags,
|
||||||
|
|
||||||
// @ts-ignore See above
|
// @ts-ignore See above
|
||||||
recipe.tags.map((x: Tag) => x.name),
|
recipe.tags.map((x: RecipeTag) => x.name),
|
||||||
state.tagFilter.matchAny,
|
state.tagFilter.matchAny,
|
||||||
state.tagFilter.exclude
|
state.tagFilter.exclude
|
||||||
);
|
);
|
||||||
|
@ -324,7 +324,7 @@ export default defineComponent({
|
|||||||
if (data) {
|
if (data) {
|
||||||
if (data && data !== undefined) {
|
if (data && data !== undefined) {
|
||||||
console.log("Computed Meta. RefKey=");
|
console.log("Computed Meta. RefKey=");
|
||||||
const imageURL = data.slug ? recipeImage(data.slug) : undefined;
|
const imageURL = data.id ? recipeImage(data.id) : undefined;
|
||||||
title.value = data.name;
|
title.value = data.name;
|
||||||
|
|
||||||
meta.value = [
|
meta.value = [
|
||||||
|
@ -411,7 +411,7 @@ export default defineComponent({
|
|||||||
return shoppingList.value?.recipeReferences?.map((ref) => ref.recipe) ?? [];
|
return shoppingList.value?.recipeReferences?.map((ref) => ref.recipe) ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
async function addRecipeReferenceToList(recipeId: number) {
|
async function addRecipeReferenceToList(recipeId: string) {
|
||||||
if (!shoppingList.value) {
|
if (!shoppingList.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -423,7 +423,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeRecipeReferenceToList(recipeId: number) {
|
async function removeRecipeReferenceToList(recipeId: string) {
|
||||||
if (!shoppingList.value) {
|
if (!shoppingList.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -577,4 +577,3 @@ export default defineComponent({
|
|||||||
max-width: 50px;
|
max-width: 50px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -72,12 +72,12 @@ export interface CustomPageBase {
|
|||||||
}
|
}
|
||||||
export interface RecipeCategoryResponse {
|
export interface RecipeCategoryResponse {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
recipes?: Recipe[];
|
recipes?: RecipeSummary[];
|
||||||
}
|
}
|
||||||
export interface Recipe {
|
export interface RecipeSummary {
|
||||||
id?: number;
|
id?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -97,28 +97,19 @@ export interface Recipe {
|
|||||||
recipeIngredient?: RecipeIngredient[];
|
recipeIngredient?: RecipeIngredient[];
|
||||||
dateAdded?: string;
|
dateAdded?: string;
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
recipeInstructions?: RecipeStep[];
|
|
||||||
nutrition?: Nutrition;
|
|
||||||
settings?: RecipeSettings;
|
|
||||||
assets?: RecipeAsset[];
|
|
||||||
notes?: RecipeNote[];
|
|
||||||
extras?: {
|
|
||||||
[k: string]: unknown;
|
|
||||||
};
|
|
||||||
comments?: RecipeCommentOut[];
|
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTag {
|
export interface RecipeTag {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTool {
|
export interface RecipeTool {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
onHand?: boolean;
|
onHand?: boolean;
|
||||||
@ -137,7 +128,7 @@ export interface IngredientUnit {
|
|||||||
description?: string;
|
description?: string;
|
||||||
fraction?: boolean;
|
fraction?: boolean;
|
||||||
abbreviation?: string;
|
abbreviation?: string;
|
||||||
id: number;
|
id: string;
|
||||||
}
|
}
|
||||||
export interface CreateIngredientUnit {
|
export interface CreateIngredientUnit {
|
||||||
name: string;
|
name: string;
|
||||||
@ -149,7 +140,7 @@ export interface IngredientFood {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
id: number;
|
id: string;
|
||||||
label?: MultiPurposeLabelSummary;
|
label?: MultiPurposeLabelSummary;
|
||||||
}
|
}
|
||||||
export interface MultiPurposeLabelSummary {
|
export interface MultiPurposeLabelSummary {
|
||||||
@ -163,59 +154,6 @@ export interface CreateIngredientFood {
|
|||||||
description?: string;
|
description?: string;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeStep {
|
|
||||||
id?: string;
|
|
||||||
title?: string;
|
|
||||||
text: string;
|
|
||||||
ingredientReferences?: IngredientReferences[];
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* A list of ingredient references.
|
|
||||||
*/
|
|
||||||
export interface IngredientReferences {
|
|
||||||
referenceId?: string;
|
|
||||||
}
|
|
||||||
export interface Nutrition {
|
|
||||||
calories?: string;
|
|
||||||
fatContent?: string;
|
|
||||||
proteinContent?: string;
|
|
||||||
carbohydrateContent?: string;
|
|
||||||
fiberContent?: string;
|
|
||||||
sodiumContent?: string;
|
|
||||||
sugarContent?: string;
|
|
||||||
}
|
|
||||||
export interface RecipeSettings {
|
|
||||||
public?: boolean;
|
|
||||||
showNutrition?: boolean;
|
|
||||||
showAssets?: boolean;
|
|
||||||
landscapeView?: boolean;
|
|
||||||
disableComments?: boolean;
|
|
||||||
disableAmount?: boolean;
|
|
||||||
locked?: boolean;
|
|
||||||
}
|
|
||||||
export interface RecipeAsset {
|
|
||||||
name: string;
|
|
||||||
icon: string;
|
|
||||||
fileName?: string;
|
|
||||||
}
|
|
||||||
export interface RecipeNote {
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
export interface RecipeCommentOut {
|
|
||||||
recipeId: number;
|
|
||||||
text: string;
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
updateAt: string;
|
|
||||||
userId: string;
|
|
||||||
user: UserBase;
|
|
||||||
}
|
|
||||||
export interface UserBase {
|
|
||||||
id: number;
|
|
||||||
username?: string;
|
|
||||||
admin: boolean;
|
|
||||||
}
|
|
||||||
export interface CustomPageImport {
|
export interface CustomPageImport {
|
||||||
name: string;
|
name: string;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
export interface CategoryBase {
|
export interface CategoryBase {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface CreateCookBook {
|
export interface CreateCookBook {
|
||||||
@ -28,12 +28,12 @@ export interface ReadCookBook {
|
|||||||
}
|
}
|
||||||
export interface RecipeCategoryResponse {
|
export interface RecipeCategoryResponse {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
recipes?: Recipe[];
|
recipes?: RecipeSummary[];
|
||||||
}
|
}
|
||||||
export interface Recipe {
|
export interface RecipeSummary {
|
||||||
id?: number;
|
id?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -53,28 +53,19 @@ export interface Recipe {
|
|||||||
recipeIngredient?: RecipeIngredient[];
|
recipeIngredient?: RecipeIngredient[];
|
||||||
dateAdded?: string;
|
dateAdded?: string;
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
recipeInstructions?: RecipeStep[];
|
|
||||||
nutrition?: Nutrition;
|
|
||||||
settings?: RecipeSettings;
|
|
||||||
assets?: RecipeAsset[];
|
|
||||||
notes?: RecipeNote[];
|
|
||||||
extras?: {
|
|
||||||
[k: string]: unknown;
|
|
||||||
};
|
|
||||||
comments?: RecipeCommentOut[];
|
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTag {
|
export interface RecipeTag {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTool {
|
export interface RecipeTool {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
onHand?: boolean;
|
onHand?: boolean;
|
||||||
@ -93,7 +84,7 @@ export interface IngredientUnit {
|
|||||||
description?: string;
|
description?: string;
|
||||||
fraction?: boolean;
|
fraction?: boolean;
|
||||||
abbreviation?: string;
|
abbreviation?: string;
|
||||||
id: number;
|
id: string;
|
||||||
}
|
}
|
||||||
export interface CreateIngredientUnit {
|
export interface CreateIngredientUnit {
|
||||||
name: string;
|
name: string;
|
||||||
@ -105,7 +96,7 @@ export interface IngredientFood {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
id: number;
|
id: string;
|
||||||
label?: MultiPurposeLabelSummary;
|
label?: MultiPurposeLabelSummary;
|
||||||
}
|
}
|
||||||
export interface MultiPurposeLabelSummary {
|
export interface MultiPurposeLabelSummary {
|
||||||
@ -119,59 +110,6 @@ export interface CreateIngredientFood {
|
|||||||
description?: string;
|
description?: string;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeStep {
|
|
||||||
id?: string;
|
|
||||||
title?: string;
|
|
||||||
text: string;
|
|
||||||
ingredientReferences?: IngredientReferences[];
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* A list of ingredient references.
|
|
||||||
*/
|
|
||||||
export interface IngredientReferences {
|
|
||||||
referenceId?: string;
|
|
||||||
}
|
|
||||||
export interface Nutrition {
|
|
||||||
calories?: string;
|
|
||||||
fatContent?: string;
|
|
||||||
proteinContent?: string;
|
|
||||||
carbohydrateContent?: string;
|
|
||||||
fiberContent?: string;
|
|
||||||
sodiumContent?: string;
|
|
||||||
sugarContent?: string;
|
|
||||||
}
|
|
||||||
export interface RecipeSettings {
|
|
||||||
public?: boolean;
|
|
||||||
showNutrition?: boolean;
|
|
||||||
showAssets?: boolean;
|
|
||||||
landscapeView?: boolean;
|
|
||||||
disableComments?: boolean;
|
|
||||||
disableAmount?: boolean;
|
|
||||||
locked?: boolean;
|
|
||||||
}
|
|
||||||
export interface RecipeAsset {
|
|
||||||
name: string;
|
|
||||||
icon: string;
|
|
||||||
fileName?: string;
|
|
||||||
}
|
|
||||||
export interface RecipeNote {
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
export interface RecipeCommentOut {
|
|
||||||
recipeId: number;
|
|
||||||
text: string;
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
updateAt: string;
|
|
||||||
userId: string;
|
|
||||||
user: UserBase;
|
|
||||||
}
|
|
||||||
export interface UserBase {
|
|
||||||
id: number;
|
|
||||||
username?: string;
|
|
||||||
admin: boolean;
|
|
||||||
}
|
|
||||||
export interface RecipeCookBook {
|
export interface RecipeCookBook {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
@ -180,7 +180,7 @@ export interface IngredientFood {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
id: number;
|
id: string;
|
||||||
label?: MultiPurposeLabelSummary;
|
label?: MultiPurposeLabelSummary;
|
||||||
}
|
}
|
||||||
export interface MultiPurposeLabelSummary {
|
export interface MultiPurposeLabelSummary {
|
||||||
@ -194,7 +194,7 @@ export interface IngredientUnit {
|
|||||||
description?: string;
|
description?: string;
|
||||||
fraction?: boolean;
|
fraction?: boolean;
|
||||||
abbreviation?: string;
|
abbreviation?: string;
|
||||||
id: number;
|
id: string;
|
||||||
}
|
}
|
||||||
export interface ReadGroupPreferences {
|
export interface ReadGroupPreferences {
|
||||||
privateGroup?: boolean;
|
privateGroup?: boolean;
|
||||||
@ -222,7 +222,7 @@ export interface ReadWebhook {
|
|||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
export interface RecipeSummary {
|
export interface RecipeSummary {
|
||||||
id?: number;
|
id?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -244,17 +244,17 @@ export interface RecipeSummary {
|
|||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTag {
|
export interface RecipeTag {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTool {
|
export interface RecipeTool {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
onHand?: boolean;
|
onHand?: boolean;
|
||||||
@ -307,15 +307,15 @@ export interface ShoppingListItemCreate {
|
|||||||
isFood?: boolean;
|
isFood?: boolean;
|
||||||
note?: string;
|
note?: string;
|
||||||
quantity?: number;
|
quantity?: number;
|
||||||
unitId?: number;
|
unitId?: string;
|
||||||
unit?: IngredientUnit;
|
unit?: IngredientUnit;
|
||||||
foodId?: number;
|
foodId?: string;
|
||||||
food?: IngredientFood;
|
food?: IngredientFood;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
recipeReferences?: ShoppingListItemRecipeRef[];
|
recipeReferences?: ShoppingListItemRecipeRef[];
|
||||||
}
|
}
|
||||||
export interface ShoppingListItemRecipeRef {
|
export interface ShoppingListItemRecipeRef {
|
||||||
recipeId: number;
|
recipeId: string;
|
||||||
recipeQuantity: number;
|
recipeQuantity: number;
|
||||||
}
|
}
|
||||||
export interface ShoppingListItemOut {
|
export interface ShoppingListItemOut {
|
||||||
@ -325,9 +325,9 @@ export interface ShoppingListItemOut {
|
|||||||
isFood?: boolean;
|
isFood?: boolean;
|
||||||
note?: string;
|
note?: string;
|
||||||
quantity?: number;
|
quantity?: number;
|
||||||
unitId?: number;
|
unitId?: string;
|
||||||
unit?: IngredientUnit;
|
unit?: IngredientUnit;
|
||||||
foodId?: number;
|
foodId?: string;
|
||||||
food?: IngredientFood;
|
food?: IngredientFood;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
recipeReferences?: ShoppingListItemRecipeRefOut[];
|
recipeReferences?: ShoppingListItemRecipeRefOut[];
|
||||||
@ -335,7 +335,7 @@ export interface ShoppingListItemOut {
|
|||||||
label?: MultiPurposeLabelSummary;
|
label?: MultiPurposeLabelSummary;
|
||||||
}
|
}
|
||||||
export interface ShoppingListItemRecipeRefOut {
|
export interface ShoppingListItemRecipeRefOut {
|
||||||
recipeId: number;
|
recipeId: string;
|
||||||
recipeQuantity: number;
|
recipeQuantity: number;
|
||||||
id: string;
|
id: string;
|
||||||
shoppingListItemId: string;
|
shoppingListItemId: string;
|
||||||
@ -347,9 +347,9 @@ export interface ShoppingListItemUpdate {
|
|||||||
isFood?: boolean;
|
isFood?: boolean;
|
||||||
note?: string;
|
note?: string;
|
||||||
quantity?: number;
|
quantity?: number;
|
||||||
unitId?: number;
|
unitId?: string;
|
||||||
unit?: IngredientUnit;
|
unit?: IngredientUnit;
|
||||||
foodId?: number;
|
foodId?: string;
|
||||||
food?: IngredientFood;
|
food?: IngredientFood;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
recipeReferences?: ShoppingListItemRecipeRef[];
|
recipeReferences?: ShoppingListItemRecipeRef[];
|
||||||
@ -365,7 +365,7 @@ export interface ShoppingListOut {
|
|||||||
export interface ShoppingListRecipeRefOut {
|
export interface ShoppingListRecipeRefOut {
|
||||||
id: string;
|
id: string;
|
||||||
shoppingListId: string;
|
shoppingListId: string;
|
||||||
recipeId: number;
|
recipeId: string;
|
||||||
recipeQuantity: number;
|
recipeQuantity: number;
|
||||||
recipe: RecipeSummary;
|
recipe: RecipeSummary;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ export type PlanRulesDay = "monday" | "tuesday" | "wednesday" | "thursday" | "fr
|
|||||||
export type PlanRulesType = "breakfast" | "lunch" | "dinner" | "unset";
|
export type PlanRulesType = "breakfast" | "lunch" | "dinner" | "unset";
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
@ -23,7 +23,7 @@ export interface CreatePlanEntry {
|
|||||||
entryType?: PlanEntryType & string;
|
entryType?: PlanEntryType & string;
|
||||||
title?: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
recipeId?: number;
|
recipeId?: string;
|
||||||
}
|
}
|
||||||
export interface ListItem {
|
export interface ListItem {
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -66,7 +66,7 @@ export interface PlanRulesCreate {
|
|||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
}
|
}
|
||||||
export interface Tag {
|
export interface Tag {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
@ -90,13 +90,13 @@ export interface ReadPlanEntry {
|
|||||||
entryType?: PlanEntryType & string;
|
entryType?: PlanEntryType & string;
|
||||||
title?: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
recipeId?: number;
|
recipeId?: string;
|
||||||
id: number;
|
id: number;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
recipe?: RecipeSummary;
|
recipe?: RecipeSummary;
|
||||||
}
|
}
|
||||||
export interface RecipeSummary {
|
export interface RecipeSummary {
|
||||||
id?: number;
|
id?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -118,17 +118,17 @@ export interface RecipeSummary {
|
|||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTag {
|
export interface RecipeTag {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTool {
|
export interface RecipeTool {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
onHand?: boolean;
|
onHand?: boolean;
|
||||||
@ -147,7 +147,7 @@ export interface IngredientUnit {
|
|||||||
description?: string;
|
description?: string;
|
||||||
fraction?: boolean;
|
fraction?: boolean;
|
||||||
abbreviation?: string;
|
abbreviation?: string;
|
||||||
id: number;
|
id: string;
|
||||||
}
|
}
|
||||||
export interface CreateIngredientUnit {
|
export interface CreateIngredientUnit {
|
||||||
name: string;
|
name: string;
|
||||||
@ -159,7 +159,7 @@ export interface IngredientFood {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
id: number;
|
id: string;
|
||||||
label?: MultiPurposeLabelSummary;
|
label?: MultiPurposeLabelSummary;
|
||||||
}
|
}
|
||||||
export interface MultiPurposeLabelSummary {
|
export interface MultiPurposeLabelSummary {
|
||||||
@ -178,7 +178,7 @@ export interface SavePlanEntry {
|
|||||||
entryType?: PlanEntryType & string;
|
entryType?: PlanEntryType & string;
|
||||||
title?: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
recipeId?: number;
|
recipeId?: string;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
}
|
}
|
||||||
export interface ShoppingListIn {
|
export interface ShoppingListIn {
|
||||||
@ -197,7 +197,7 @@ export interface UpdatePlanEntry {
|
|||||||
entryType?: PlanEntryType & string;
|
entryType?: PlanEntryType & string;
|
||||||
title?: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
recipeId?: number;
|
recipeId?: string;
|
||||||
id: number;
|
id: number;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ export interface AssignCategories {
|
|||||||
}
|
}
|
||||||
export interface CategoryBase {
|
export interface CategoryBase {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface AssignTags {
|
export interface AssignTags {
|
||||||
@ -23,7 +23,7 @@ export interface AssignTags {
|
|||||||
}
|
}
|
||||||
export interface TagBase {
|
export interface TagBase {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface BulkActionError {
|
export interface BulkActionError {
|
||||||
@ -38,6 +38,15 @@ export interface BulkActionsResponse {
|
|||||||
export interface CategoryIn {
|
export interface CategoryIn {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
export interface CategoryOut {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
export interface CategorySave {
|
||||||
|
name: string;
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
export interface CreateIngredientFood {
|
export interface CreateIngredientFood {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@ -58,12 +67,12 @@ export interface CreateRecipeBulk {
|
|||||||
tags?: RecipeTag[];
|
tags?: RecipeTag[];
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTag {
|
export interface RecipeTag {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
@ -95,7 +104,7 @@ export interface IngredientFood {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
id: number;
|
id: string;
|
||||||
label?: MultiPurposeLabelSummary;
|
label?: MultiPurposeLabelSummary;
|
||||||
}
|
}
|
||||||
export interface MultiPurposeLabelSummary {
|
export interface MultiPurposeLabelSummary {
|
||||||
@ -119,7 +128,7 @@ export interface IngredientUnit {
|
|||||||
description?: string;
|
description?: string;
|
||||||
fraction?: boolean;
|
fraction?: boolean;
|
||||||
abbreviation?: string;
|
abbreviation?: string;
|
||||||
id: number;
|
id: string;
|
||||||
}
|
}
|
||||||
export interface IngredientsRequest {
|
export interface IngredientsRequest {
|
||||||
parser?: RegisteredParser & string;
|
parser?: RegisteredParser & string;
|
||||||
@ -149,7 +158,7 @@ export interface RecipeIngredient {
|
|||||||
referenceId?: string;
|
referenceId?: string;
|
||||||
}
|
}
|
||||||
export interface Recipe {
|
export interface Recipe {
|
||||||
id?: number;
|
id?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -180,7 +189,7 @@ export interface Recipe {
|
|||||||
comments?: RecipeCommentOut[];
|
comments?: RecipeCommentOut[];
|
||||||
}
|
}
|
||||||
export interface RecipeTool {
|
export interface RecipeTool {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
onHand?: boolean;
|
onHand?: boolean;
|
||||||
@ -210,7 +219,7 @@ export interface RecipeNote {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
export interface RecipeCommentOut {
|
export interface RecipeCommentOut {
|
||||||
recipeId: number;
|
recipeId: string;
|
||||||
text: string;
|
text: string;
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@ -225,52 +234,12 @@ export interface UserBase {
|
|||||||
}
|
}
|
||||||
export interface RecipeCategoryResponse {
|
export interface RecipeCategoryResponse {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: string;
|
||||||
slug: string;
|
|
||||||
recipes?: Recipe[];
|
|
||||||
}
|
|
||||||
export interface RecipeCommentCreate {
|
|
||||||
recipeId: number;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
export interface RecipeCommentSave {
|
|
||||||
recipeId: number;
|
|
||||||
text: string;
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
export interface RecipeCommentUpdate {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
export interface RecipeShareToken {
|
|
||||||
recipeId: number;
|
|
||||||
expiresAt?: string;
|
|
||||||
groupId: string;
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
recipe: Recipe;
|
|
||||||
}
|
|
||||||
export interface RecipeShareTokenCreate {
|
|
||||||
recipeId: number;
|
|
||||||
expiresAt?: string;
|
|
||||||
}
|
|
||||||
export interface RecipeShareTokenSave {
|
|
||||||
recipeId: number;
|
|
||||||
expiresAt?: string;
|
|
||||||
groupId: string;
|
|
||||||
}
|
|
||||||
export interface RecipeShareTokenSummary {
|
|
||||||
recipeId: number;
|
|
||||||
expiresAt?: string;
|
|
||||||
groupId: string;
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
export interface RecipeSlug {
|
|
||||||
slug: string;
|
slug: string;
|
||||||
|
recipes?: RecipeSummary[];
|
||||||
}
|
}
|
||||||
export interface RecipeSummary {
|
export interface RecipeSummary {
|
||||||
id?: number;
|
id?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -291,16 +260,56 @@ export interface RecipeSummary {
|
|||||||
dateAdded?: string;
|
dateAdded?: string;
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
}
|
}
|
||||||
|
export interface RecipeCommentCreate {
|
||||||
|
recipeId: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
export interface RecipeCommentSave {
|
||||||
|
recipeId: string;
|
||||||
|
text: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
export interface RecipeCommentUpdate {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
export interface RecipeShareToken {
|
||||||
|
recipeId: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
groupId: string;
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
recipe: Recipe;
|
||||||
|
}
|
||||||
|
export interface RecipeShareTokenCreate {
|
||||||
|
recipeId: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
export interface RecipeShareTokenSave {
|
||||||
|
recipeId: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
|
export interface RecipeShareTokenSummary {
|
||||||
|
recipeId: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
groupId: string;
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
export interface RecipeSlug {
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
export interface RecipeTagResponse {
|
export interface RecipeTagResponse {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
recipes?: Recipe[];
|
recipes?: RecipeSummary[];
|
||||||
}
|
}
|
||||||
export interface RecipeTool1 {
|
export interface RecipeTool1 {
|
||||||
name: string;
|
name: string;
|
||||||
onHand?: boolean;
|
onHand?: boolean;
|
||||||
id: number;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeToolCreate {
|
export interface RecipeToolCreate {
|
||||||
@ -310,14 +319,42 @@ export interface RecipeToolCreate {
|
|||||||
export interface RecipeToolResponse {
|
export interface RecipeToolResponse {
|
||||||
name: string;
|
name: string;
|
||||||
onHand?: boolean;
|
onHand?: boolean;
|
||||||
id: number;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
recipes?: Recipe[];
|
recipes?: Recipe[];
|
||||||
}
|
}
|
||||||
|
export interface RecipeToolSave {
|
||||||
|
name: string;
|
||||||
|
onHand?: boolean;
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
|
export interface SaveIngredientFood {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
labelId?: string;
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
|
export interface SaveIngredientUnit {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
fraction?: boolean;
|
||||||
|
abbreviation?: string;
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
export interface SlugResponse {}
|
export interface SlugResponse {}
|
||||||
export interface TagIn {
|
export interface TagIn {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
export interface TagOut {
|
||||||
|
name: string;
|
||||||
|
groupId: string;
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
export interface TagSave {
|
||||||
|
name: string;
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
export interface UnitFoodBase {
|
export interface UnitFoodBase {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
export interface CategoryBase {
|
export interface CategoryBase {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface ChangePassword {
|
export interface ChangePassword {
|
||||||
@ -109,7 +109,7 @@ export interface PrivatePasswordResetToken {
|
|||||||
user: PrivateUser;
|
user: PrivateUser;
|
||||||
}
|
}
|
||||||
export interface RecipeSummary {
|
export interface RecipeSummary {
|
||||||
id?: number;
|
id?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -131,17 +131,17 @@ export interface RecipeSummary {
|
|||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTag {
|
export interface RecipeTag {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
export interface RecipeTool {
|
export interface RecipeTool {
|
||||||
id?: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
onHand?: boolean;
|
onHand?: boolean;
|
||||||
@ -160,7 +160,7 @@ export interface IngredientUnit {
|
|||||||
description?: string;
|
description?: string;
|
||||||
fraction?: boolean;
|
fraction?: boolean;
|
||||||
abbreviation?: string;
|
abbreviation?: string;
|
||||||
id: number;
|
id: string;
|
||||||
}
|
}
|
||||||
export interface CreateIngredientUnit {
|
export interface CreateIngredientUnit {
|
||||||
name: string;
|
name: string;
|
||||||
@ -172,7 +172,7 @@ export interface IngredientFood {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
id: number;
|
id: string;
|
||||||
label?: MultiPurposeLabelSummary;
|
label?: MultiPurposeLabelSummary;
|
||||||
}
|
}
|
||||||
export interface MultiPurposeLabelSummary {
|
export interface MultiPurposeLabelSummary {
|
||||||
|
1
makefile
1
makefile
@ -82,7 +82,6 @@ setup-model: ## 🤖 Get the latest NLP CRF++ Model
|
|||||||
|
|
||||||
backend: ## 🎬 Start Mealie Backend Development Server
|
backend: ## 🎬 Start Mealie Backend Development Server
|
||||||
poetry run python mealie/db/init_db.py && \
|
poetry run python mealie/db/init_db.py && \
|
||||||
poetry run python mealie/services/image/minify.py && \
|
|
||||||
poetry run python mealie/app.py
|
poetry run python mealie/app.py
|
||||||
|
|
||||||
.PHONY: frontend
|
.PHONY: frontend
|
||||||
|
@ -9,8 +9,6 @@ DATA_DIR = determine_data_dir()
|
|||||||
|
|
||||||
from .config import get_app_settings
|
from .config import get_app_settings
|
||||||
|
|
||||||
settings = get_app_settings()
|
|
||||||
|
|
||||||
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
|
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
|
||||||
DATE_FORMAT = "%d-%b-%y %H:%M:%S"
|
DATE_FORMAT = "%d-%b-%y %H:%M:%S"
|
||||||
LOGGER_FORMAT = "%(levelname)s: %(asctime)s \t%(message)s"
|
LOGGER_FORMAT = "%(levelname)s: %(asctime)s \t%(message)s"
|
||||||
@ -27,6 +25,8 @@ class LoggerConfig:
|
|||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def get_logger_config():
|
def get_logger_config():
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
if not settings.PRODUCTION:
|
if not settings.PRODUCTION:
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
@ -69,7 +69,6 @@ def logger_init() -> logging.Logger:
|
|||||||
|
|
||||||
|
|
||||||
root_logger = logger_init()
|
root_logger = logger_init()
|
||||||
root_logger.info("Testing Root Logger")
|
|
||||||
|
|
||||||
|
|
||||||
def get_logger(module=None) -> logging.Logger:
|
def get_logger(module=None) -> logging.Logger:
|
||||||
|
@ -12,7 +12,12 @@ def sql_global_init(db_url: str):
|
|||||||
if "sqlite" in db_url:
|
if "sqlite" in db_url:
|
||||||
connect_args["check_same_thread"] = False
|
connect_args["check_same_thread"] = False
|
||||||
|
|
||||||
engine = sa.create_engine(db_url, echo=False, connect_args=connect_args, pool_pre_ping=True)
|
engine = sa.create_engine(
|
||||||
|
db_url,
|
||||||
|
echo=False,
|
||||||
|
connect_args=connect_args,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
)
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
@ -58,6 +58,9 @@ class Group(SqlAlchemyBase, BaseMixins):
|
|||||||
# Owned Models
|
# Owned Models
|
||||||
ingredient_units = orm.relationship("IngredientUnitModel", **common_args)
|
ingredient_units = orm.relationship("IngredientUnitModel", **common_args)
|
||||||
ingredient_foods = orm.relationship("IngredientFoodModel", **common_args)
|
ingredient_foods = orm.relationship("IngredientFoodModel", **common_args)
|
||||||
|
tools = orm.relationship("Tool", **common_args)
|
||||||
|
tags = orm.relationship("Tag", **common_args)
|
||||||
|
categories = orm.relationship("Category", **common_args)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
exclude = {
|
exclude = {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from sqlalchemy import Column, Date, ForeignKey, String, orm
|
from sqlalchemy import Column, Date, ForeignKey, String, orm
|
||||||
from sqlalchemy.sql.sqltypes import Integer
|
|
||||||
|
|
||||||
from mealie.db.models.recipe.tag import Tag, plan_rules_to_tags
|
from mealie.db.models.recipe.tag import Tag, plan_rules_to_tags
|
||||||
|
|
||||||
@ -36,7 +35,7 @@ class GroupMealPlan(SqlAlchemyBase, BaseMixins):
|
|||||||
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
|
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
|
||||||
group = orm.relationship("Group", back_populates="mealplans")
|
group = orm.relationship("Group", back_populates="mealplans")
|
||||||
|
|
||||||
recipe_id = Column(Integer, ForeignKey("recipes.id"))
|
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True)
|
||||||
recipe = orm.relationship("RecipeModel", back_populates="meal_entries", uselist=False)
|
recipe = orm.relationship("RecipeModel", back_populates="meal_entries", uselist=False)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
|
@ -9,7 +9,7 @@ from .._model_utils import auto_init
|
|||||||
|
|
||||||
class GroupPreferencesModel(SqlAlchemyBase, BaseMixins):
|
class GroupPreferencesModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "group_preferences"
|
__tablename__ = "group_preferences"
|
||||||
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"))
|
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="preferences")
|
group = orm.relationship("Group", back_populates="preferences")
|
||||||
|
|
||||||
private_group: bool = sa.Column(sa.Boolean, default=True)
|
private_group: bool = sa.Column(sa.Boolean, default=True)
|
||||||
|
@ -38,7 +38,7 @@ class ReportModel(SqlAlchemyBase, BaseMixins):
|
|||||||
entries = orm.relationship(ReportEntryModel, back_populates="report", cascade="all, delete-orphan")
|
entries = orm.relationship(ReportEntryModel, back_populates="report", cascade="all, delete-orphan")
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"))
|
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="group_reports", single_parent=True)
|
group = orm.relationship("Group", back_populates="group_reports", single_parent=True)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
@ -13,7 +13,7 @@ class ShoppingListItemRecipeReference(BaseMixins, SqlAlchemyBase):
|
|||||||
id = Column(GUID, primary_key=True, default=GUID.generate)
|
id = Column(GUID, primary_key=True, default=GUID.generate)
|
||||||
|
|
||||||
shopping_list_item_id = Column(GUID, ForeignKey("shopping_list_items.id"), primary_key=True)
|
shopping_list_item_id = Column(GUID, ForeignKey("shopping_list_items.id"), primary_key=True)
|
||||||
recipe_id = Column(Integer, ForeignKey("recipes.id"))
|
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True)
|
||||||
recipe = orm.relationship("RecipeModel", back_populates="shopping_list_item_refs")
|
recipe = orm.relationship("RecipeModel", back_populates="shopping_list_item_refs")
|
||||||
recipe_quantity = Column(Float, nullable=False)
|
recipe_quantity = Column(Float, nullable=False)
|
||||||
|
|
||||||
@ -40,10 +40,10 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
|||||||
is_food = Column(Boolean, default=False)
|
is_food = Column(Boolean, default=False)
|
||||||
|
|
||||||
# Scaling Items
|
# Scaling Items
|
||||||
unit_id = Column(Integer, ForeignKey("ingredient_units.id"))
|
unit_id = Column(GUID, ForeignKey("ingredient_units.id"))
|
||||||
unit = orm.relationship(IngredientUnitModel, uselist=False)
|
unit = orm.relationship(IngredientUnitModel, uselist=False)
|
||||||
|
|
||||||
food_id = Column(Integer, ForeignKey("ingredient_foods.id"))
|
food_id = Column(GUID, ForeignKey("ingredient_foods.id"))
|
||||||
food = orm.relationship(IngredientFoodModel, uselist=False)
|
food = orm.relationship(IngredientFoodModel, uselist=False)
|
||||||
|
|
||||||
label_id = Column(GUID, ForeignKey("multi_purpose_labels.id"))
|
label_id = Column(GUID, ForeignKey("multi_purpose_labels.id"))
|
||||||
@ -66,7 +66,7 @@ class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
|
|||||||
|
|
||||||
shopping_list_id = Column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
|
shopping_list_id = Column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
|
||||||
|
|
||||||
recipe_id = Column(Integer, ForeignKey("recipes.id"))
|
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True)
|
||||||
recipe = orm.relationship("RecipeModel", uselist=False, back_populates="shopping_list_refs")
|
recipe = orm.relationship("RecipeModel", uselist=False, back_populates="shopping_list_refs")
|
||||||
|
|
||||||
recipe_quantity = Column(Float, nullable=False)
|
recipe_quantity = Column(Float, nullable=False)
|
||||||
@ -83,7 +83,7 @@ class ShoppingList(SqlAlchemyBase, BaseMixins):
|
|||||||
__tablename__ = "shopping_lists"
|
__tablename__ = "shopping_lists"
|
||||||
id = Column(GUID, primary_key=True, default=GUID.generate)
|
id = Column(GUID, primary_key=True, default=GUID.generate)
|
||||||
|
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"))
|
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="shopping_lists")
|
group = orm.relationship("Group", back_populates="shopping_lists")
|
||||||
|
|
||||||
name = Column(String)
|
name = Column(String)
|
||||||
|
@ -12,7 +12,7 @@ class MultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
|||||||
name = Column(String(255), nullable=False)
|
name = Column(String(255), nullable=False)
|
||||||
color = Column(String(10), nullable=False, default="")
|
color = Column(String(10), nullable=False, default="")
|
||||||
|
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"))
|
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="labels")
|
group = orm.relationship("Group", back_populates="labels")
|
||||||
|
|
||||||
shopping_list_items = orm.relationship("ShoppingListItem", back_populates="label")
|
shopping_list_items = orm.relationship("ShoppingListItem", back_populates="label")
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
|
|
||||||
|
|
||||||
class ApiExtras(SqlAlchemyBase):
|
class ApiExtras(SqlAlchemyBase):
|
||||||
__tablename__ = "api_extras"
|
__tablename__ = "api_extras"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
|
recipee_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
|
||||||
key_name = sa.Column(sa.String)
|
key_name = sa.Column(sa.String)
|
||||||
value = sa.Column(sa.String)
|
value = sa.Column(sa.String)
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
|
|
||||||
|
|
||||||
class RecipeAsset(SqlAlchemyBase):
|
class RecipeAsset(SqlAlchemyBase):
|
||||||
__tablename__ = "recipe_assets"
|
__tablename__ = "recipe_assets"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
|
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
|
||||||
name = sa.Column(sa.String)
|
name = sa.Column(sa.String)
|
||||||
icon = sa.Column(sa.String)
|
icon = sa.Column(sa.String)
|
||||||
file_name = sa.Column(sa.String)
|
file_name = sa.Column(sa.String)
|
||||||
|
@ -15,47 +15,51 @@ group2categories = sa.Table(
|
|||||||
"group2categories",
|
"group2categories",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
sa.Column("group_id", GUID, sa.ForeignKey("groups.id")),
|
sa.Column("group_id", GUID, sa.ForeignKey("groups.id")),
|
||||||
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
|
sa.Column("category_id", GUID, sa.ForeignKey("categories.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
plan_rules_to_categories = sa.Table(
|
plan_rules_to_categories = sa.Table(
|
||||||
"plan_rules_to_categories",
|
"plan_rules_to_categories",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
sa.Column("group_plan_rule_id", GUID, sa.ForeignKey("group_meal_plan_rules.id")),
|
sa.Column("group_plan_rule_id", GUID, sa.ForeignKey("group_meal_plan_rules.id")),
|
||||||
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
|
sa.Column("category_id", GUID, sa.ForeignKey("categories.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
recipes2categories = sa.Table(
|
recipes_to_categories = sa.Table(
|
||||||
"recipes2categories",
|
"recipes_to_categories",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
sa.Column("recipe_id", GUID, sa.ForeignKey("recipes.id")),
|
||||||
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
|
sa.Column("category_id", GUID, sa.ForeignKey("categories.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
cookbooks_to_categories = sa.Table(
|
cookbooks_to_categories = sa.Table(
|
||||||
"cookbooks_to_categories",
|
"cookbooks_to_categories",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
sa.Column("cookbook_id", sa.Integer, sa.ForeignKey("cookbooks.id")),
|
sa.Column("cookbook_id", sa.Integer, sa.ForeignKey("cookbooks.id")),
|
||||||
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
|
sa.Column("category_id", GUID, sa.ForeignKey("categories.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Category(SqlAlchemyBase, BaseMixins):
|
class Category(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "categories"
|
__tablename__ = "categories"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="category_slug_group_id_key"),)
|
||||||
name = sa.Column(sa.String, index=True, nullable=False)
|
|
||||||
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
|
|
||||||
recipes = orm.relationship("RecipeModel", secondary=recipes2categories, back_populates="recipe_category")
|
|
||||||
|
|
||||||
class Config:
|
# ID Relationships
|
||||||
get_attr = "slug"
|
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
|
group = orm.relationship("Group", back_populates="categories", foreign_keys=[group_id])
|
||||||
|
|
||||||
|
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
|
||||||
|
name = sa.Column(sa.String, index=True, nullable=False)
|
||||||
|
slug = sa.Column(sa.String, index=True, nullable=False)
|
||||||
|
recipes = orm.relationship("RecipeModel", secondary=recipes_to_categories, back_populates="recipe_category")
|
||||||
|
|
||||||
@validates("name")
|
@validates("name")
|
||||||
def validate_name(self, key, name):
|
def validate_name(self, key, name):
|
||||||
assert name != ""
|
assert name != ""
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def __init__(self, name, **_) -> None:
|
def __init__(self, name, group_id, **_) -> None:
|
||||||
|
self.group_id = group_id
|
||||||
self.name = name.strip()
|
self.name = name.strip()
|
||||||
self.slug = slugify(name)
|
self.slug = slugify(name)
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import Column, ForeignKey, Integer, String, orm
|
from sqlalchemy import Column, ForeignKey, String, orm
|
||||||
|
|
||||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils import auto_init
|
from mealie.db.models._model_utils import auto_init
|
||||||
@ -11,7 +11,7 @@ class RecipeComment(SqlAlchemyBase, BaseMixins):
|
|||||||
text = Column(String)
|
text = Column(String)
|
||||||
|
|
||||||
# Recipe Link
|
# Recipe Link
|
||||||
recipe_id = Column(Integer, ForeignKey("recipes.id"), nullable=False)
|
recipe_id = Column(GUID, ForeignKey("recipes.id"), nullable=False)
|
||||||
recipe = orm.relationship("RecipeModel", back_populates="comments")
|
recipe = orm.relationship("RecipeModel", back_populates="comments")
|
||||||
|
|
||||||
# User Link
|
# User Link
|
||||||
|
@ -9,12 +9,12 @@ from .._model_utils.guid import GUID
|
|||||||
|
|
||||||
class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "ingredient_units"
|
__tablename__ = "ingredient_units"
|
||||||
|
id = Column(GUID, primary_key=True, default=GUID.generate)
|
||||||
|
|
||||||
# ID Relationships
|
# ID Relationships
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
|
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
|
||||||
group = orm.relationship("Group", back_populates="ingredient_units", foreign_keys=[group_id])
|
group = orm.relationship("Group", back_populates="ingredient_units", foreign_keys=[group_id])
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
name = Column(String)
|
name = Column(String)
|
||||||
description = Column(String)
|
description = Column(String)
|
||||||
abbreviation = Column(String)
|
abbreviation = Column(String)
|
||||||
@ -28,12 +28,12 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "ingredient_foods"
|
__tablename__ = "ingredient_foods"
|
||||||
|
id = Column(GUID, primary_key=True, default=GUID.generate)
|
||||||
|
|
||||||
# ID Relationships
|
# ID Relationships
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
|
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
|
||||||
group = orm.relationship("Group", back_populates="ingredient_foods", foreign_keys=[group_id])
|
group = orm.relationship("Group", back_populates="ingredient_foods", foreign_keys=[group_id])
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
name = Column(String)
|
name = Column(String)
|
||||||
description = Column(String)
|
description = Column(String)
|
||||||
ingredients = orm.relationship("RecipeIngredient", back_populates="food")
|
ingredients = orm.relationship("RecipeIngredient", back_populates="food")
|
||||||
@ -50,16 +50,16 @@ class RecipeIngredient(SqlAlchemyBase, BaseMixins):
|
|||||||
__tablename__ = "recipes_ingredients"
|
__tablename__ = "recipes_ingredients"
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
position = Column(Integer)
|
position = Column(Integer)
|
||||||
parent_id = Column(Integer, ForeignKey("recipes.id"))
|
recipe_id = Column(GUID, ForeignKey("recipes.id"))
|
||||||
|
|
||||||
title = Column(String) # Section Header - Shows if Present
|
title = Column(String) # Section Header - Shows if Present
|
||||||
note = Column(String) # Force Show Text - Overrides Concat
|
note = Column(String) # Force Show Text - Overrides Concat
|
||||||
|
|
||||||
# Scaling Items
|
# Scaling Items
|
||||||
unit_id = Column(Integer, ForeignKey("ingredient_units.id"))
|
unit_id = Column(GUID, ForeignKey("ingredient_units.id"))
|
||||||
unit = orm.relationship(IngredientUnitModel, uselist=False)
|
unit = orm.relationship(IngredientUnitModel, uselist=False)
|
||||||
|
|
||||||
food_id = Column(Integer, ForeignKey("ingredient_foods.id"))
|
food_id = Column(GUID, ForeignKey("ingredient_foods.id"))
|
||||||
food = orm.relationship(IngredientFoodModel, uselist=False)
|
food = orm.relationship(IngredientFoodModel, uselist=False)
|
||||||
quantity = Column(Integer)
|
quantity = Column(Integer)
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ class RecipeIngredientRefLink(SqlAlchemyBase, BaseMixins):
|
|||||||
class RecipeInstruction(SqlAlchemyBase):
|
class RecipeInstruction(SqlAlchemyBase):
|
||||||
__tablename__ = "recipe_instructions"
|
__tablename__ = "recipe_instructions"
|
||||||
id = Column(GUID, primary_key=True, default=GUID.generate)
|
id = Column(GUID, primary_key=True, default=GUID.generate)
|
||||||
parent_id = Column(Integer, ForeignKey("recipes.id"))
|
recipe_id = Column(GUID, ForeignKey("recipes.id"))
|
||||||
position = Column(Integer)
|
position = Column(Integer)
|
||||||
type = Column(String, default="")
|
type = Column(String, default="")
|
||||||
title = Column(String)
|
title = Column(String)
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
|
|
||||||
|
|
||||||
class Note(SqlAlchemyBase):
|
class Note(SqlAlchemyBase):
|
||||||
__tablename__ = "notes"
|
__tablename__ = "notes"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
|
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
|
||||||
title = sa.Column(sa.String)
|
title = sa.Column(sa.String)
|
||||||
text = sa.Column(sa.String)
|
text = sa.Column(sa.String)
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
|
|
||||||
|
|
||||||
class Nutrition(SqlAlchemyBase):
|
class Nutrition(SqlAlchemyBase):
|
||||||
__tablename__ = "recipe_nutrition"
|
__tablename__ = "recipe_nutrition"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
|
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
|
||||||
calories = sa.Column(sa.String)
|
calories = sa.Column(sa.String)
|
||||||
fat_content = sa.Column(sa.String)
|
fat_content = sa.Column(sa.String)
|
||||||
fiber_content = sa.Column(sa.String)
|
fiber_content = sa.Column(sa.String)
|
||||||
|
@ -13,14 +13,14 @@ from .._model_utils import auto_init
|
|||||||
from ..users import users_to_favorites
|
from ..users import users_to_favorites
|
||||||
from .api_extras import ApiExtras
|
from .api_extras import ApiExtras
|
||||||
from .assets import RecipeAsset
|
from .assets import RecipeAsset
|
||||||
from .category import recipes2categories
|
from .category import recipes_to_categories
|
||||||
from .ingredient import RecipeIngredient
|
from .ingredient import RecipeIngredient
|
||||||
from .instruction import RecipeInstruction
|
from .instruction import RecipeInstruction
|
||||||
from .note import Note
|
from .note import Note
|
||||||
from .nutrition import Nutrition
|
from .nutrition import Nutrition
|
||||||
from .settings import RecipeSettings
|
from .settings import RecipeSettings
|
||||||
from .shared import RecipeShareTokenModel
|
from .shared import RecipeShareTokenModel
|
||||||
from .tag import Tag, recipes2tags
|
from .tag import Tag, recipes_to_tags
|
||||||
from .tool import recipes_to_tools
|
from .tool import recipes_to_tools
|
||||||
|
|
||||||
|
|
||||||
@ -43,13 +43,14 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||||||
__tablename__ = "recipes"
|
__tablename__ = "recipes"
|
||||||
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="recipe_slug_group_id_key"),)
|
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="recipe_slug_group_id_key"),)
|
||||||
|
|
||||||
|
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
|
||||||
slug = sa.Column(sa.String, index=True)
|
slug = sa.Column(sa.String, index=True)
|
||||||
|
|
||||||
# ID Relationships
|
# ID Relationships
|
||||||
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"))
|
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="recipes", foreign_keys=[group_id])
|
group = orm.relationship("Group", back_populates="recipes", foreign_keys=[group_id])
|
||||||
|
|
||||||
user_id = sa.Column(GUID, sa.ForeignKey("users.id"))
|
user_id = sa.Column(GUID, sa.ForeignKey("users.id"), index=True)
|
||||||
user = orm.relationship("User", uselist=False, foreign_keys=[user_id])
|
user = orm.relationship("User", uselist=False, foreign_keys=[user_id])
|
||||||
|
|
||||||
meal_entries = orm.relationship("GroupMealPlan", back_populates="recipe", cascade="all, delete-orphan")
|
meal_entries = orm.relationship("GroupMealPlan", back_populates="recipe", cascade="all, delete-orphan")
|
||||||
@ -72,7 +73,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
assets = orm.relationship("RecipeAsset", cascade="all, delete-orphan")
|
assets = orm.relationship("RecipeAsset", cascade="all, delete-orphan")
|
||||||
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
|
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
|
||||||
recipe_category = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes")
|
recipe_category = orm.relationship("Category", secondary=recipes_to_categories, back_populates="recipes")
|
||||||
tools = orm.relationship("Tool", secondary=recipes_to_tools, back_populates="recipes")
|
tools = orm.relationship("Tool", secondary=recipes_to_tools, back_populates="recipes")
|
||||||
|
|
||||||
recipe_ingredient: list[RecipeIngredient] = orm.relationship(
|
recipe_ingredient: list[RecipeIngredient] = orm.relationship(
|
||||||
@ -96,7 +97,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
# Mealie Specific
|
# Mealie Specific
|
||||||
settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan")
|
settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan")
|
||||||
tags: list[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes")
|
tags: list[Tag] = orm.relationship("Tag", secondary=recipes_to_tags, back_populates="recipes")
|
||||||
notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan")
|
notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan")
|
||||||
rating = sa.Column(sa.Integer)
|
rating = sa.Column(sa.Integer)
|
||||||
org_url = sa.Column(sa.String)
|
org_url = sa.Column(sa.String)
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
|
|
||||||
|
|
||||||
class RecipeSettings(SqlAlchemyBase):
|
class RecipeSettings(SqlAlchemyBase):
|
||||||
__tablename__ = "recipe_settings"
|
__tablename__ = "recipe_settings"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
|
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
|
||||||
public = sa.Column(sa.Boolean)
|
public = sa.Column(sa.Boolean)
|
||||||
show_nutrition = sa.Column(sa.Boolean)
|
show_nutrition = sa.Column(sa.Boolean)
|
||||||
show_assets = sa.Column(sa.Boolean)
|
show_assets = sa.Column(sa.Boolean)
|
||||||
|
@ -15,9 +15,9 @@ class RecipeShareTokenModel(SqlAlchemyBase, BaseMixins):
|
|||||||
__tablename__ = "recipe_share_tokens"
|
__tablename__ = "recipe_share_tokens"
|
||||||
id = sa.Column(GUID, primary_key=True, default=uuid4)
|
id = sa.Column(GUID, primary_key=True, default=uuid4)
|
||||||
|
|
||||||
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"))
|
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
|
|
||||||
recipe_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"), nullable=False)
|
recipe_id = sa.Column(GUID, sa.ForeignKey("recipes.id"), nullable=False)
|
||||||
recipe = sa.orm.relationship("RecipeModel", back_populates="share_tokens", uselist=False)
|
recipe = sa.orm.relationship("RecipeModel", back_populates="share_tokens", uselist=False)
|
||||||
|
|
||||||
expires_at = sa.Column(sa.DateTime, nullable=False)
|
expires_at = sa.Column(sa.DateTime, nullable=False)
|
||||||
|
@ -9,27 +9,33 @@ from mealie.db.models._model_utils import guid
|
|||||||
|
|
||||||
logger = root_logger.get_logger()
|
logger = root_logger.get_logger()
|
||||||
|
|
||||||
recipes2tags = sa.Table(
|
recipes_to_tags = sa.Table(
|
||||||
"recipes2tags",
|
"recipes_to_tags",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
sa.Column("recipe_id", guid.GUID, sa.ForeignKey("recipes.id")),
|
||||||
sa.Column("tag_id", sa.Integer, sa.ForeignKey("tags.id")),
|
sa.Column("tag_id", guid.GUID, sa.ForeignKey("tags.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
plan_rules_to_tags = sa.Table(
|
plan_rules_to_tags = sa.Table(
|
||||||
"plan_rules_to_tags",
|
"plan_rules_to_tags",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
sa.Column("plan_rule_id", guid.GUID, sa.ForeignKey("group_meal_plan_rules.id")),
|
sa.Column("plan_rule_id", guid.GUID, sa.ForeignKey("group_meal_plan_rules.id")),
|
||||||
sa.Column("tag_id", sa.Integer, sa.ForeignKey("tags.id")),
|
sa.Column("tag_id", guid.GUID, sa.ForeignKey("tags.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Tag(SqlAlchemyBase, BaseMixins):
|
class Tag(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "tags"
|
__tablename__ = "tags"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="tags_slug_group_id_key"),)
|
||||||
|
|
||||||
|
# ID Relationships
|
||||||
|
group_id = sa.Column(guid.GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
|
group = orm.relationship("Group", back_populates="tags", foreign_keys=[group_id])
|
||||||
|
|
||||||
|
id = sa.Column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||||
name = sa.Column(sa.String, index=True, nullable=False)
|
name = sa.Column(sa.String, index=True, nullable=False)
|
||||||
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
|
slug = sa.Column(sa.String, index=True, nullable=False)
|
||||||
recipes = orm.relationship("RecipeModel", secondary=recipes2tags, back_populates="tags")
|
recipes = orm.relationship("RecipeModel", secondary=recipes_to_tags, back_populates="tags")
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
get_attr = "slug"
|
get_attr = "slug"
|
||||||
@ -39,7 +45,8 @@ class Tag(SqlAlchemyBase, BaseMixins):
|
|||||||
assert name != ""
|
assert name != ""
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def __init__(self, name, **_) -> None:
|
def __init__(self, name, group_id, **_) -> None:
|
||||||
|
self.group_id = group_id
|
||||||
self.name = name.strip()
|
self.name = name.strip()
|
||||||
self.slug = slugify(self.name)
|
self.slug = slugify(self.name)
|
||||||
|
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Table, orm
|
from sqlalchemy import Boolean, Column, ForeignKey, String, Table, UniqueConstraint, orm
|
||||||
|
|
||||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models._model_utils import auto_init
|
from mealie.db.models._model_utils import auto_init
|
||||||
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
|
|
||||||
recipes_to_tools = Table(
|
recipes_to_tools = Table(
|
||||||
"recipes_to_tools",
|
"recipes_to_tools",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
Column("recipe_id", Integer, ForeignKey("recipes.id")),
|
Column("recipe_id", GUID, ForeignKey("recipes.id")),
|
||||||
Column("tool_id", Integer, ForeignKey("tools.id")),
|
Column("tool_id", GUID, ForeignKey("tools.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Tool(SqlAlchemyBase, BaseMixins):
|
class Tool(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "tools"
|
__tablename__ = "tools"
|
||||||
|
id = Column(GUID, primary_key=True, default=GUID.generate)
|
||||||
|
__table_args__ = (UniqueConstraint("slug", "group_id", name="tools_slug_group_id_key"),)
|
||||||
|
|
||||||
|
# ID Relationships
|
||||||
|
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)
|
||||||
|
group = orm.relationship("Group", back_populates="tools", foreign_keys=[group_id])
|
||||||
|
|
||||||
name = Column(String, index=True, unique=True, nullable=False)
|
name = Column(String, index=True, unique=True, nullable=False)
|
||||||
slug = Column(String, index=True, unique=True, nullable=False)
|
slug = Column(String, index=True, unique=True, nullable=False)
|
||||||
on_hand = Column(Boolean, default=False)
|
on_hand = Column(Boolean, default=False)
|
||||||
|
@ -13,7 +13,7 @@ class ServerTaskModel(SqlAlchemyBase, BaseMixins):
|
|||||||
status = Column(String, nullable=False)
|
status = Column(String, nullable=False)
|
||||||
log = Column(String, nullable=True)
|
log = Column(String, nullable=True)
|
||||||
|
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"))
|
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="server_tasks")
|
group = orm.relationship("Group", back_populates="server_tasks")
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import Column, ForeignKey, Integer, Table
|
from sqlalchemy import Column, ForeignKey, Table
|
||||||
|
|
||||||
from .._model_base import SqlAlchemyBase
|
from .._model_base import SqlAlchemyBase
|
||||||
from .._model_utils import GUID
|
from .._model_utils import GUID
|
||||||
@ -7,5 +7,5 @@ users_to_favorites = Table(
|
|||||||
"users_to_favorites",
|
"users_to_favorites",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
Column("user_id", GUID, ForeignKey("users.id")),
|
Column("user_id", GUID, ForeignKey("users.id")),
|
||||||
Column("recipe_id", Integer, ForeignKey("recipes.id")),
|
Column("recipe_id", GUID, ForeignKey("recipes.id")),
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
from sqlalchemy import Boolean, Column, ForeignKey, String, orm
|
||||||
|
|
||||||
from mealie.core.config import get_app_settings
|
from mealie.core.config import get_app_settings
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
@ -33,7 +33,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||||||
admin = Column(Boolean, default=False)
|
admin = Column(Boolean, default=False)
|
||||||
advanced = Column(Boolean, default=False)
|
advanced = Column(Boolean, default=False)
|
||||||
|
|
||||||
group_id = Column(GUID, ForeignKey("groups.id"))
|
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||||
group = orm.relationship("Group", back_populates="users")
|
group = orm.relationship("Group", back_populates="users")
|
||||||
|
|
||||||
cache_key = Column(String, default="1234")
|
cache_key = Column(String, default="1234")
|
||||||
@ -53,7 +53,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||||||
comments = orm.relationship("RecipeComment", **sp_args)
|
comments = orm.relationship("RecipeComment", **sp_args)
|
||||||
password_reset_tokens = orm.relationship("PasswordResetModel", **sp_args)
|
password_reset_tokens = orm.relationship("PasswordResetModel", **sp_args)
|
||||||
|
|
||||||
owned_recipes_id = Column(Integer, ForeignKey("recipes.id"))
|
owned_recipes_id = Column(GUID, ForeignKey("recipes.id"))
|
||||||
owned_recipes = orm.relationship("RecipeModel", single_parent=True, foreign_keys=[owned_recipes_id])
|
owned_recipes = orm.relationship("RecipeModel", single_parent=True, foreign_keys=[owned_recipes_id])
|
||||||
|
|
||||||
favorite_recipes = orm.relationship("RecipeModel", secondary=users_to_favorites, back_populates="favorited_by")
|
favorite_recipes = orm.relationship("RecipeModel", secondary=users_to_favorites, back_populates="favorited_by")
|
||||||
|
1
mealie/pkgs/cache/__init__.py
vendored
Normal file
1
mealie/pkgs/cache/__init__.py
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .cache_key import *
|
@ -2,8 +2,7 @@ import random
|
|||||||
import string
|
import string
|
||||||
|
|
||||||
|
|
||||||
def new_cache_key(length=4) -> str:
|
def new_key(length=4) -> str:
|
||||||
"""returns a 4 character string to be used as a cache key for frontend data"""
|
"""returns a 4 character string to be used as a cache key for frontend data"""
|
||||||
options = string.ascii_letters + string.digits
|
options = string.ascii_letters + string.digits
|
||||||
|
|
||||||
return "".join(random.choices(options, k=length))
|
return "".join(random.choices(options, k=length))
|
7
mealie/pkgs/dev/__init__.py
Normal file
7
mealie/pkgs/dev/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
This package containers helpful development tools to be used for development and testing. It shouldn't be used for or imported
|
||||||
|
in production
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .lifespan_tracker import *
|
||||||
|
from .timer import *
|
12
mealie/pkgs/dev/timer.py
Normal file
12
mealie/pkgs/dev/timer.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def timer(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
start = time.time()
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
end = time.time()
|
||||||
|
print(f"{func.__name__} took {end - start} seconds") # noqa: T001
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
7
mealie/pkgs/img/__init__.py
Normal file
7
mealie/pkgs/img/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
The img package is a collection of utilities for working with images. While it offers some Mealie specific functionality, libraries
|
||||||
|
within the img package should not be tightly coupled to Mealie.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from .minify import *
|
130
mealie/pkgs/img/minify.py
Normal file
130
mealie/pkgs/img/minify.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from logging import Logger
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
WEBP = ".webp"
|
||||||
|
FORMAT = "WEBP"
|
||||||
|
|
||||||
|
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_format(image: Path) -> str:
|
||||||
|
img = Image.open(image)
|
||||||
|
return img.format
|
||||||
|
|
||||||
|
|
||||||
|
def sizeof_fmt(file_path: Path, decimal_places=2):
|
||||||
|
if not file_path.exists():
|
||||||
|
return "(File Not Found)"
|
||||||
|
size = file_path.stat().st_size
|
||||||
|
for unit in ["B", "kB", "MB", "GB", "TB", "PB"]:
|
||||||
|
if size < 1024.0 or unit == "PiB":
|
||||||
|
break
|
||||||
|
size /= 1024.0
|
||||||
|
return f"{size:.{decimal_places}f} {unit}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MinifierOptions:
|
||||||
|
original: bool = True
|
||||||
|
minature: bool = True
|
||||||
|
tiny: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class ABCMinifier(ABC):
|
||||||
|
def __init__(self, purge=False, opts: MinifierOptions = None, logger: Logger = None):
|
||||||
|
self._purge = purge
|
||||||
|
self._opts = opts or MinifierOptions()
|
||||||
|
self._logger = logger or Logger("Minifier")
|
||||||
|
|
||||||
|
def get_image_sizes(self, org_img: Path, min_img: Path, tiny_img: Path):
|
||||||
|
self._logger.info(
|
||||||
|
f"{org_img.name} Minified: {sizeof_fmt(org_img)} -> {sizeof_fmt(min_img)} -> {sizeof_fmt(tiny_img)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def minify(self, image: Path, force=True):
|
||||||
|
...
|
||||||
|
|
||||||
|
def purge(self, image: Path):
|
||||||
|
if not self._purge:
|
||||||
|
return
|
||||||
|
|
||||||
|
for file in image.parent.glob("*.*"):
|
||||||
|
if file.suffix != WEBP:
|
||||||
|
file.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
class PillowMinifier(ABCMinifier):
|
||||||
|
@staticmethod
|
||||||
|
def to_webp(image_file: Path, dest: Path = None, quality: int = 100) -> Path:
|
||||||
|
"""
|
||||||
|
Converts an image to the webp format in-place. The original image is not
|
||||||
|
removed By default, the quality is set to 100.
|
||||||
|
"""
|
||||||
|
if image_file.suffix == WEBP:
|
||||||
|
return image_file
|
||||||
|
|
||||||
|
img = Image.open(image_file)
|
||||||
|
|
||||||
|
dest = dest or image_file.with_suffix(WEBP)
|
||||||
|
img.save(dest, FORMAT, quality=quality)
|
||||||
|
|
||||||
|
return dest
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def crop_center(pil_img: Image, crop_width=300, crop_height=300):
|
||||||
|
img_width, img_height = pil_img.size
|
||||||
|
return pil_img.crop(
|
||||||
|
(
|
||||||
|
(img_width - crop_width) // 2,
|
||||||
|
(img_height - crop_height) // 2,
|
||||||
|
(img_width + crop_width) // 2,
|
||||||
|
(img_height + crop_height) // 2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def minify(self, image_file: Path, force=True):
|
||||||
|
if not image_file.exists():
|
||||||
|
raise FileNotFoundError(f"{image_file.name} does not exist")
|
||||||
|
|
||||||
|
org_dest = image_file.parent.joinpath("original.webp")
|
||||||
|
min_dest = image_file.parent.joinpath("min-original.webp")
|
||||||
|
tiny_dest = image_file.parent.joinpath("tiny-original.webp")
|
||||||
|
|
||||||
|
if not force and min_dest.exists() and tiny_dest.exists() and org_dest.exists():
|
||||||
|
self._logger.info(f"{image_file.name} already minified")
|
||||||
|
return
|
||||||
|
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if self._opts.original:
|
||||||
|
if not force and org_dest.exists():
|
||||||
|
self._logger.info(f"{image_file.name} already minified")
|
||||||
|
else:
|
||||||
|
PillowMinifier.to_webp(image_file, org_dest, quality=70)
|
||||||
|
success = True
|
||||||
|
|
||||||
|
if self._opts.minature:
|
||||||
|
if not force and min_dest.exists():
|
||||||
|
self._logger.info(f"{image_file.name} already minified")
|
||||||
|
else:
|
||||||
|
PillowMinifier.to_webp(image_file, min_dest, quality=70)
|
||||||
|
self._logger.info(f"{image_file.name} minified")
|
||||||
|
success = True
|
||||||
|
|
||||||
|
if self._opts.tiny:
|
||||||
|
if not force and tiny_dest.exists():
|
||||||
|
self._logger.info(f"{image_file.name} already minified")
|
||||||
|
else:
|
||||||
|
img = Image.open(image_file)
|
||||||
|
tiny_image = PillowMinifier.crop_center(img)
|
||||||
|
tiny_image.save(tiny_dest, FORMAT, quality=70)
|
||||||
|
self._logger.info("Tiny image saved")
|
||||||
|
success = True
|
||||||
|
|
||||||
|
if self._purge and success:
|
||||||
|
self.purge(image_file)
|
1
mealie/pkgs/stats/__init__.py
Normal file
1
mealie/pkgs/stats/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .fs_stats import *
|
@ -46,7 +46,8 @@ from mealie.schema.group.webhook import ReadWebhook
|
|||||||
from mealie.schema.labels import MultiPurposeLabelOut
|
from mealie.schema.labels import MultiPurposeLabelOut
|
||||||
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
|
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
|
||||||
from mealie.schema.meal_plan.plan_rules import PlanRulesOut
|
from mealie.schema.meal_plan.plan_rules import PlanRulesOut
|
||||||
from mealie.schema.recipe import Recipe, RecipeCategoryResponse, RecipeCommentOut, RecipeTagResponse, RecipeTool
|
from mealie.schema.recipe import Recipe, RecipeCommentOut, RecipeTool
|
||||||
|
from mealie.schema.recipe.recipe_category import CategoryOut, TagOut
|
||||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
||||||
from mealie.schema.recipe.recipe_share_token import RecipeShareToken
|
from mealie.schema.recipe.recipe_share_token import RecipeShareToken
|
||||||
from mealie.schema.reports.reports import ReportEntryOut, ReportOut
|
from mealie.schema.reports.reports import ReportEntryOut, ReportOut
|
||||||
@ -67,12 +68,12 @@ PK_TOKEN = "token"
|
|||||||
PK_GROUP_ID = "group_id"
|
PK_GROUP_ID = "group_id"
|
||||||
|
|
||||||
|
|
||||||
class RepositoryCategories(RepositoryGeneric):
|
class RepositoryCategories(RepositoryGeneric[CategoryOut, Category]):
|
||||||
def get_empty(self):
|
def get_empty(self):
|
||||||
return self.session.query(Category).filter(~Category.recipes.any()).all()
|
return self.session.query(Category).filter(~Category.recipes.any()).all()
|
||||||
|
|
||||||
|
|
||||||
class RepositoryTags(RepositoryGeneric):
|
class RepositoryTags(RepositoryGeneric[TagOut, Tag]):
|
||||||
def get_empty(self):
|
def get_empty(self):
|
||||||
return self.session.query(Tag).filter(~Tag.recipes.any()).all()
|
return self.session.query(Tag).filter(~Tag.recipes.any()).all()
|
||||||
|
|
||||||
@ -114,11 +115,11 @@ class AllRepositories:
|
|||||||
@cached_property
|
@cached_property
|
||||||
def categories(self) -> RepositoryCategories:
|
def categories(self) -> RepositoryCategories:
|
||||||
# TODO: Fix Typing for Category Repository
|
# TODO: Fix Typing for Category Repository
|
||||||
return RepositoryCategories(self.session, PK_SLUG, Category, RecipeCategoryResponse)
|
return RepositoryCategories(self.session, PK_ID, Category, CategoryOut)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def tags(self) -> RepositoryTags:
|
def tags(self) -> RepositoryTags:
|
||||||
return RepositoryTags(self.session, PK_SLUG, Tag, RecipeTagResponse)
|
return RepositoryTags(self.session, PK_ID, Tag, TagOut)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def recipe_share_tokens(self) -> RepositoryGeneric[RecipeShareToken, RecipeShareTokenModel]:
|
def recipe_share_tokens(self) -> RepositoryGeneric[RecipeShareToken, RecipeShareTokenModel]:
|
||||||
|
@ -11,7 +11,7 @@ from mealie.db.models.recipe.recipe import RecipeModel
|
|||||||
from mealie.db.models.recipe.settings import RecipeSettings
|
from mealie.db.models.recipe.settings import RecipeSettings
|
||||||
from mealie.db.models.recipe.tag import Tag
|
from mealie.db.models.recipe.tag import Tag
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
from mealie.schema.recipe.recipe import RecipeCategory, RecipeTag
|
from mealie.schema.recipe.recipe import RecipeCategory, RecipeSummary, RecipeTag
|
||||||
|
|
||||||
from .repository_generic import RepositoryGeneric
|
from .repository_generic import RepositoryGeneric
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_by_categories(self, categories: list[RecipeCategory]) -> list[Recipe]:
|
def get_by_categories(self, categories: list[RecipeCategory]) -> list[RecipeSummary]:
|
||||||
"""
|
"""
|
||||||
get_by_categories returns all the Recipes that contain every category provided in the list
|
get_by_categories returns all the Recipes that contain every category provided in the list
|
||||||
"""
|
"""
|
||||||
@ -97,7 +97,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
ids = [x.id for x in categories]
|
ids = [x.id for x in categories]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
self.schema.from_orm(x)
|
RecipeSummary.from_orm(x)
|
||||||
for x in self.session.query(RecipeModel)
|
for x in self.session.query(RecipeModel)
|
||||||
.join(RecipeModel.recipe_category)
|
.join(RecipeModel.recipe_category)
|
||||||
.filter(RecipeModel.recipe_category.any(Category.id.in_(ids)))
|
.filter(RecipeModel.recipe_category.any(Category.id.in_(ids)))
|
||||||
@ -120,13 +120,11 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
|
|
||||||
if categories:
|
if categories:
|
||||||
cat_ids = [x.id for x in categories]
|
cat_ids = [x.id for x in categories]
|
||||||
for cat_id in cat_ids:
|
filters.extend(RecipeModel.recipe_category.any(Category.id.is_(cat_id)) for cat_id in cat_ids)
|
||||||
filters.append(RecipeModel.recipe_category.any(Category.id.is_(cat_id)))
|
|
||||||
|
|
||||||
if tags:
|
if tags:
|
||||||
tag_ids = [x.id for x in tags]
|
tag_ids = [x.id for x in tags]
|
||||||
for tag_id in tag_ids:
|
filters.extend(RecipeModel.tags.any(Tag.id.is_(tag_id)) for tag_id in tag_ids)
|
||||||
filters.append(RecipeModel.tags.any(Tag.id.is_(tag_id)))
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
self.schema.from_orm(x)
|
self.schema.from_orm(x)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from . import admin, app, auth, categories, comments, groups, parser, recipe, shared, tags, tools, unit_and_foods, users
|
from . import admin, app, auth, comments, groups, organizers, parser, recipe, shared, unit_and_foods, users
|
||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
@ -9,11 +9,9 @@ router.include_router(auth.router)
|
|||||||
router.include_router(users.router)
|
router.include_router(users.router)
|
||||||
router.include_router(groups.router)
|
router.include_router(groups.router)
|
||||||
router.include_router(recipe.router)
|
router.include_router(recipe.router)
|
||||||
|
router.include_router(organizers.router)
|
||||||
router.include_router(shared.router)
|
router.include_router(shared.router)
|
||||||
router.include_router(comments.router)
|
router.include_router(comments.router)
|
||||||
router.include_router(parser.router)
|
router.include_router(parser.router)
|
||||||
router.include_router(unit_and_foods.router)
|
router.include_router(unit_and_foods.router)
|
||||||
router.include_router(tools.router)
|
|
||||||
router.include_router(categories.router)
|
|
||||||
router.include_router(tags.router)
|
|
||||||
router.include_router(admin.router)
|
router.include_router(admin.router)
|
||||||
|
@ -10,13 +10,13 @@ from mealie.core.dependencies import get_current_user
|
|||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.core.security import create_file_token
|
from mealie.core.security import create_file_token
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
|
from mealie.pkgs.stats.fs_stats import pretty_size
|
||||||
from mealie.routes._base.routers import AdminAPIRouter
|
from mealie.routes._base.routers import AdminAPIRouter
|
||||||
from mealie.schema.admin import AllBackups, BackupFile, CreateBackup, ImportJob
|
from mealie.schema.admin import AllBackups, BackupFile, CreateBackup, ImportJob
|
||||||
from mealie.schema.user.user import PrivateUser
|
from mealie.schema.user.user import PrivateUser
|
||||||
from mealie.services.backups import imports
|
from mealie.services.backups import imports
|
||||||
from mealie.services.backups.exports import backup_all
|
from mealie.services.backups.exports import backup_all
|
||||||
from mealie.services.events import create_backup_event
|
from mealie.services.events import create_backup_event
|
||||||
from mealie.utils.fs_stats import pretty_size
|
|
||||||
|
|
||||||
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
|
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
|
||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
from fastapi import APIRouter
|
|
||||||
|
|
||||||
from . import categories
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
router.include_router(categories.router)
|
|
@ -1,69 +0,0 @@
|
|||||||
from functools import cached_property
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from mealie.routes._base import BaseUserController, controller
|
|
||||||
from mealie.routes._base.mixins import CrudMixins
|
|
||||||
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
|
|
||||||
from mealie.schema.recipe.recipe_category import CategoryBase
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/categories", tags=["Categories: CRUD"])
|
|
||||||
|
|
||||||
|
|
||||||
class CategorySummary(BaseModel):
|
|
||||||
id: int
|
|
||||||
slug: str
|
|
||||||
name: str
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
|
||||||
class RecipeCategoryController(BaseUserController):
|
|
||||||
# =========================================================================
|
|
||||||
# CRUD Operations
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def mixins(self):
|
|
||||||
return CrudMixins(self.repos.categories, self.deps.logger)
|
|
||||||
|
|
||||||
@router.get("", response_model=list[CategorySummary])
|
|
||||||
def get_all(self):
|
|
||||||
"""Returns a list of available categories in the database"""
|
|
||||||
return self.repos.categories.get_all_limit_columns(fields=["slug", "name"])
|
|
||||||
|
|
||||||
@router.post("", status_code=201)
|
|
||||||
def create_one(self, category: CategoryIn):
|
|
||||||
"""Creates a Category in the database"""
|
|
||||||
return self.mixins.create_one(category)
|
|
||||||
|
|
||||||
@router.get("/{slug}", response_model=RecipeCategoryResponse)
|
|
||||||
def get_all_recipes_by_category(self, slug: str):
|
|
||||||
"""Returns a list of recipes associated with the provided category."""
|
|
||||||
category_obj = self.repos.categories.get(slug)
|
|
||||||
category_obj = RecipeCategoryResponse.from_orm(category_obj)
|
|
||||||
return category_obj
|
|
||||||
|
|
||||||
@router.put("/{slug}", response_model=RecipeCategoryResponse)
|
|
||||||
def update_one(self, slug: str, update_data: CategoryIn):
|
|
||||||
"""Updates an existing Tag in the database"""
|
|
||||||
return self.mixins.update_one(update_data, slug)
|
|
||||||
|
|
||||||
@router.delete("/{slug}")
|
|
||||||
def delete_one(self, slug: str):
|
|
||||||
"""
|
|
||||||
Removes a recipe category from the database. Deleting a
|
|
||||||
category does not impact a recipe. The category will be removed
|
|
||||||
from any recipes that contain it
|
|
||||||
"""
|
|
||||||
self.mixins.delete_one(slug)
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# Read All Operations
|
|
||||||
|
|
||||||
@router.get("/empty", response_model=list[CategoryBase])
|
|
||||||
def get_all_empty(self):
|
|
||||||
"""Returns a list of categories that do not contain any recipes"""
|
|
||||||
return self.repos.categories.get_empty()
|
|
@ -143,9 +143,9 @@ class ShoppingListController(BaseUserController):
|
|||||||
# Other Operations
|
# Other Operations
|
||||||
|
|
||||||
@router.post("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut)
|
@router.post("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut)
|
||||||
def add_recipe_ingredients_to_list(self, item_id: UUID4, recipe_id: int):
|
def add_recipe_ingredients_to_list(self, item_id: UUID4, recipe_id: UUID4):
|
||||||
return self.service.add_recipe_ingredients_to_list(item_id, recipe_id)
|
return self.service.add_recipe_ingredients_to_list(item_id, recipe_id)
|
||||||
|
|
||||||
@router.delete("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut)
|
@router.delete("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut)
|
||||||
def remove_recipe_ingredients_from_list(self, item_id: UUID4, recipe_id: int):
|
def remove_recipe_ingredients_from_list(self, item_id: UUID4, recipe_id: UUID4):
|
||||||
return self.service.remove_recipe_ingredients_from_list(item_id, recipe_id)
|
return self.service.remove_recipe_ingredients_from_list(item_id, recipe_id)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
from pydantic import UUID4
|
||||||
from starlette.responses import FileResponse
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
@ -19,11 +20,13 @@ class ImageType(str, Enum):
|
|||||||
tiny = "tiny-original.webp"
|
tiny = "tiny-original.webp"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{slug}/images/{file_name}")
|
@router.get("/{recipe_id}/images/{file_name}")
|
||||||
async def get_recipe_img(slug: str, file_name: ImageType = ImageType.original):
|
async def get_recipe_img(recipe_id: str, file_name: ImageType = ImageType.original):
|
||||||
"""Takes in a recipe slug, returns the static image. This route is proxied in the docker image
|
"""
|
||||||
and should not hit the API in production"""
|
Takes in a recipe recipe_id, returns the static image. This route is proxied in the docker image
|
||||||
recipe_image = Recipe(slug=slug).image_dir.joinpath(file_name.value)
|
and should not hit the API in production
|
||||||
|
"""
|
||||||
|
recipe_image = Recipe.directory_from_id(recipe_id).joinpath("images", file_name.value)
|
||||||
|
|
||||||
if recipe_image.exists():
|
if recipe_image.exists():
|
||||||
return FileResponse(recipe_image)
|
return FileResponse(recipe_image)
|
||||||
@ -31,10 +34,10 @@ async def get_recipe_img(slug: str, file_name: ImageType = ImageType.original):
|
|||||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{slug}/assets/{file_name}")
|
@router.get("/{recipe_id}/assets/{file_name}")
|
||||||
async def get_recipe_asset(slug: str, file_name: str):
|
async def get_recipe_asset(recipe_id: UUID4, file_name: str):
|
||||||
"""Returns a recipe asset"""
|
"""Returns a recipe asset"""
|
||||||
file = Recipe(slug=slug).asset_dir.joinpath(file_name)
|
file = Recipe.directory_from_id(recipe_id).joinpath("assets", file_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return FileResponse(file)
|
return FileResponse(file)
|
||||||
|
8
mealie/routes/organizers/__init__.py
Normal file
8
mealie/routes/organizers/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from . import controller_categories, controller_tags, controller_tools
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/organizers")
|
||||||
|
router.include_router(controller_categories.router)
|
||||||
|
router.include_router(controller_tags.router)
|
||||||
|
router.include_router(controller_tools.router)
|
87
mealie/routes/organizers/controller_categories.py
Normal file
87
mealie/routes/organizers/controller_categories.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from pydantic import UUID4, BaseModel
|
||||||
|
|
||||||
|
from mealie.routes._base import BaseUserController, controller
|
||||||
|
from mealie.routes._base.mixins import CrudMixins
|
||||||
|
from mealie.schema import mapper
|
||||||
|
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
|
||||||
|
from mealie.schema.recipe.recipe import RecipeCategory
|
||||||
|
from mealie.schema.recipe.recipe_category import CategoryBase, CategorySave
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/categories", tags=["Organizer: Categories"])
|
||||||
|
|
||||||
|
|
||||||
|
class CategorySummary(BaseModel):
|
||||||
|
id: UUID4
|
||||||
|
slug: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
@controller(router)
|
||||||
|
class RecipeCategoryController(BaseUserController):
|
||||||
|
# =========================================================================
|
||||||
|
# CRUD Operations
|
||||||
|
@cached_property
|
||||||
|
def repo(self):
|
||||||
|
return self.repos.categories.by_group(self.group_id)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def mixins(self):
|
||||||
|
return CrudMixins(self.repo, self.deps.logger)
|
||||||
|
|
||||||
|
@router.get("", response_model=list[CategorySummary])
|
||||||
|
def get_all(self):
|
||||||
|
"""Returns a list of available categories in the database"""
|
||||||
|
return self.repo.get_all(override_schema=CategorySummary)
|
||||||
|
|
||||||
|
@router.post("", status_code=201)
|
||||||
|
def create_one(self, category: CategoryIn):
|
||||||
|
"""Creates a Category in the database"""
|
||||||
|
save_data = mapper.cast(category, CategorySave, group_id=self.group_id)
|
||||||
|
return self.mixins.create_one(save_data)
|
||||||
|
|
||||||
|
@router.get("/{item_id}", response_model=CategorySummary)
|
||||||
|
def get_one(self, item_id: UUID4):
|
||||||
|
"""Returns a list of recipes associated with the provided category."""
|
||||||
|
category_obj = self.mixins.get_one(item_id)
|
||||||
|
category_obj = CategorySummary.from_orm(category_obj)
|
||||||
|
return category_obj
|
||||||
|
|
||||||
|
@router.put("/{item_id}", response_model=CategorySummary)
|
||||||
|
def update_one(self, item_id: UUID4, update_data: CategoryIn):
|
||||||
|
"""Updates an existing Tag in the database"""
|
||||||
|
save_data = mapper.cast(update_data, CategorySave, group_id=self.group_id)
|
||||||
|
return self.mixins.update_one(save_data, item_id)
|
||||||
|
|
||||||
|
@router.delete("/{item_id}")
|
||||||
|
def delete_one(self, item_id: UUID4):
|
||||||
|
"""
|
||||||
|
Removes a recipe category from the database. Deleting a
|
||||||
|
category does not impact a recipe. The category will be removed
|
||||||
|
from any recipes that contain it
|
||||||
|
"""
|
||||||
|
self.mixins.delete_one(item_id)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Read All Operations
|
||||||
|
|
||||||
|
@router.get("/empty", response_model=list[CategoryBase])
|
||||||
|
def get_all_empty(self):
|
||||||
|
"""Returns a list of categories that do not contain any recipes"""
|
||||||
|
return self.repos.categories.get_empty()
|
||||||
|
|
||||||
|
@router.get("/slug/{category_slug}")
|
||||||
|
def get_one_by_slug(self, category_slug: str):
|
||||||
|
"""Returns a category object with the associated recieps relating to the category"""
|
||||||
|
category: RecipeCategory = self.mixins.get_one(category_slug, "slug")
|
||||||
|
return RecipeCategoryResponse.construct(
|
||||||
|
id=category.id,
|
||||||
|
slug=category.slug,
|
||||||
|
name=category.name,
|
||||||
|
recipes=self.repos.recipes.by_group(self.group_id).get_by_categories([category]),
|
||||||
|
)
|
66
mealie/routes/organizers/controller_tags.py
Normal file
66
mealie/routes/organizers/controller_tags.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
from pydantic import UUID4
|
||||||
|
|
||||||
|
from mealie.routes._base import BaseUserController, controller
|
||||||
|
from mealie.routes._base.mixins import CrudMixins
|
||||||
|
from mealie.schema import mapper
|
||||||
|
from mealie.schema.recipe import RecipeTagResponse, TagIn
|
||||||
|
from mealie.schema.recipe.recipe import RecipeTag
|
||||||
|
from mealie.schema.recipe.recipe_category import TagSave
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/tags", tags=["Organizer: Tags"])
|
||||||
|
|
||||||
|
|
||||||
|
@controller(router)
|
||||||
|
class TagController(BaseUserController):
|
||||||
|
@cached_property
|
||||||
|
def repo(self):
|
||||||
|
return self.repos.tags.by_group(self.group_id)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def mixins(self):
|
||||||
|
return CrudMixins(self.repo, self.deps.logger)
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def get_all(self):
|
||||||
|
"""Returns a list of available tags in the database"""
|
||||||
|
return self.repo.get_all(override_schema=RecipeTag)
|
||||||
|
|
||||||
|
@router.get("/empty")
|
||||||
|
def get_empty_tags(self):
|
||||||
|
"""Returns a list of tags that do not contain any recipes"""
|
||||||
|
return self.repo.get_empty()
|
||||||
|
|
||||||
|
@router.get("/{item_id}", response_model=RecipeTagResponse)
|
||||||
|
def get_one(self, item_id: UUID4):
|
||||||
|
"""Returns a list of recipes associated with the provided tag."""
|
||||||
|
return self.mixins.get_one(item_id)
|
||||||
|
|
||||||
|
@router.post("", status_code=201)
|
||||||
|
def create_one(self, tag: TagIn):
|
||||||
|
"""Creates a Tag in the database"""
|
||||||
|
save_data = mapper.cast(tag, TagSave, group_id=self.group_id)
|
||||||
|
return self.repo.create(save_data)
|
||||||
|
|
||||||
|
@router.put("/{item_id}", response_model=RecipeTagResponse)
|
||||||
|
def update_one(self, item_id: UUID4, new_tag: TagIn):
|
||||||
|
"""Updates an existing Tag in the database"""
|
||||||
|
save_data = mapper.cast(new_tag, TagSave, group_id=self.group_id)
|
||||||
|
return self.repo.update(item_id, save_data)
|
||||||
|
|
||||||
|
@router.delete("/{item_id}")
|
||||||
|
def delete_recipe_tag(self, item_id: UUID4):
|
||||||
|
"""Removes a recipe tag from the database. Deleting a
|
||||||
|
tag does not impact a recipe. The tag will be removed
|
||||||
|
from any recipes that contain it"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.repo.delete(item_id)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status.HTTP_400_BAD_REQUEST) from e
|
||||||
|
|
||||||
|
@router.get("/slug/{tag_slug}", response_model=RecipeTagResponse)
|
||||||
|
async def get_one_by_slug(self, tag_slug: str):
|
||||||
|
return self.repo.get_one(tag_slug, "slug", override_schema=RecipeTagResponse)
|
@ -1,22 +1,24 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.routes._base.abc_controller import BaseUserController
|
from mealie.routes._base.abc_controller import BaseUserController
|
||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
from mealie.routes._base.mixins import CrudMixins
|
from mealie.routes._base.mixins import CrudMixins
|
||||||
|
from mealie.schema import mapper
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.query import GetAll
|
||||||
from mealie.schema.recipe.recipe import RecipeTool
|
from mealie.schema.recipe.recipe import RecipeTool
|
||||||
from mealie.schema.recipe.recipe_tool import RecipeToolCreate, RecipeToolResponse
|
from mealie.schema.recipe.recipe_tool import RecipeToolCreate, RecipeToolResponse, RecipeToolSave
|
||||||
|
|
||||||
router = APIRouter(prefix="/tools", tags=["Recipes: Tools"])
|
router = APIRouter(prefix="/tools", tags=["Organizer: Tools"])
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
@controller(router)
|
||||||
class RecipeToolController(BaseUserController):
|
class RecipeToolController(BaseUserController):
|
||||||
@cached_property
|
@cached_property
|
||||||
def repo(self):
|
def repo(self):
|
||||||
return self.repos.tools
|
return self.repos.tools.by_group(self.group_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mixins(self) -> CrudMixins:
|
def mixins(self) -> CrudMixins:
|
||||||
@ -28,18 +30,19 @@ class RecipeToolController(BaseUserController):
|
|||||||
|
|
||||||
@router.post("", response_model=RecipeTool, status_code=201)
|
@router.post("", response_model=RecipeTool, status_code=201)
|
||||||
def create_one(self, data: RecipeToolCreate):
|
def create_one(self, data: RecipeToolCreate):
|
||||||
return self.mixins.create_one(data)
|
save_data = mapper.cast(data, RecipeToolSave, group_id=self.group_id)
|
||||||
|
return self.mixins.create_one(save_data)
|
||||||
|
|
||||||
@router.get("/{item_id}", response_model=RecipeTool)
|
@router.get("/{item_id}", response_model=RecipeTool)
|
||||||
def get_one(self, item_id: int):
|
def get_one(self, item_id: UUID4):
|
||||||
return self.mixins.get_one(item_id)
|
return self.mixins.get_one(item_id)
|
||||||
|
|
||||||
@router.put("/{item_id}", response_model=RecipeTool)
|
@router.put("/{item_id}", response_model=RecipeTool)
|
||||||
def update_one(self, item_id: int, data: RecipeToolCreate):
|
def update_one(self, item_id: UUID4, data: RecipeToolCreate):
|
||||||
return self.mixins.update_one(data, item_id)
|
return self.mixins.update_one(data, item_id)
|
||||||
|
|
||||||
@router.delete("/{item_id}", response_model=RecipeTool)
|
@router.delete("/{item_id}", response_model=RecipeTool)
|
||||||
def delete_one(self, item_id: int):
|
def delete_one(self, item_id: UUID4):
|
||||||
return self.mixins.delete_one(item_id) # type: ignore
|
return self.mixins.delete_one(item_id) # type: ignore
|
||||||
|
|
||||||
@router.get("/slug/{tool_slug}", response_model=RecipeToolResponse)
|
@router.get("/slug/{tool_slug}", response_model=RecipeToolResponse)
|
@ -1,6 +1,6 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from . import all_recipe_routes, bulk_actions, comments, image_and_assets, recipe_crud_routes, shared_routes
|
from . import all_recipe_routes, bulk_actions, comments, recipe_crud_routes, shared_routes
|
||||||
|
|
||||||
prefix = "/recipes"
|
prefix = "/recipes"
|
||||||
|
|
||||||
@ -9,7 +9,6 @@ router = APIRouter()
|
|||||||
router.include_router(all_recipe_routes.router, prefix=prefix, tags=["Recipe: Query All"])
|
router.include_router(all_recipe_routes.router, prefix=prefix, tags=["Recipe: Query All"])
|
||||||
router.include_router(recipe_crud_routes.router_exports)
|
router.include_router(recipe_crud_routes.router_exports)
|
||||||
router.include_router(recipe_crud_routes.router)
|
router.include_router(recipe_crud_routes.router)
|
||||||
router.include_router(image_and_assets.router, prefix=prefix, tags=["Recipe: Images and Assets"])
|
|
||||||
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])
|
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])
|
||||||
router.include_router(bulk_actions.router, prefix=prefix)
|
router.include_router(bulk_actions.router, prefix=prefix)
|
||||||
router.include_router(bulk_actions.router, prefix=prefix, tags=["Recipe: Bulk Exports"])
|
router.include_router(bulk_actions.router, prefix=prefix, tags=["Recipe: Bulk Exports"])
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
from shutil import copyfileobj
|
|
||||||
|
|
||||||
from fastapi import Depends, File, Form, HTTPException, status
|
|
||||||
from fastapi.datastructures import UploadFile
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from slugify import slugify
|
|
||||||
from sqlalchemy.orm.session import Session
|
|
||||||
|
|
||||||
from mealie.db.db_setup import generate_session
|
|
||||||
from mealie.repos.all_repositories import get_repositories
|
|
||||||
from mealie.routes._base.routers import UserAPIRouter
|
|
||||||
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeAsset
|
|
||||||
from mealie.services.image.image import scrape_image, write_image
|
|
||||||
|
|
||||||
router = UserAPIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateImageResponse(BaseModel):
|
|
||||||
image: str
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{slug}/image")
|
|
||||||
def scrape_image_url(slug: str, url: CreateRecipeByUrl):
|
|
||||||
"""Removes an existing image and replaces it with the incoming file."""
|
|
||||||
scrape_image(url.url, slug)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{slug}/image", response_model=UpdateImageResponse)
|
|
||||||
def update_recipe_image(
|
|
||||||
slug: str,
|
|
||||||
image: bytes = File(...),
|
|
||||||
extension: str = Form(...),
|
|
||||||
session: Session = Depends(generate_session),
|
|
||||||
):
|
|
||||||
"""Removes an existing image and replaces it with the incoming file."""
|
|
||||||
db = get_repositories(session)
|
|
||||||
write_image(slug, image, extension)
|
|
||||||
new_version = db.recipes.update_image(slug, extension)
|
|
||||||
|
|
||||||
return UpdateImageResponse(image=new_version)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{slug}/assets", response_model=RecipeAsset)
|
|
||||||
def upload_recipe_asset(
|
|
||||||
slug: str,
|
|
||||||
name: str = Form(...),
|
|
||||||
icon: str = Form(...),
|
|
||||||
extension: str = Form(...),
|
|
||||||
file: UploadFile = File(...),
|
|
||||||
session: Session = Depends(generate_session),
|
|
||||||
):
|
|
||||||
"""Upload a file to store as a recipe asset"""
|
|
||||||
file_name = slugify(name) + "." + extension
|
|
||||||
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
|
|
||||||
dest = Recipe(slug=slug).asset_dir.joinpath(file_name)
|
|
||||||
|
|
||||||
with dest.open("wb") as buffer:
|
|
||||||
copyfileobj(file.file, buffer)
|
|
||||||
|
|
||||||
if not dest.is_file():
|
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
db = get_repositories(session)
|
|
||||||
|
|
||||||
recipe: Recipe = db.recipes.get(slug)
|
|
||||||
recipe.assets.append(asset_in)
|
|
||||||
db.recipes.update(slug, recipe.dict())
|
|
||||||
return asset_in
|
|
@ -1,12 +1,14 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from shutil import copyfileobj
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from fastapi import BackgroundTasks, Depends, File, HTTPException
|
from fastapi import BackgroundTasks, Depends, File, Form, HTTPException, status
|
||||||
from fastapi.datastructures import UploadFile
|
from fastapi.datastructures import UploadFile
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
from slugify import slugify
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from starlette.responses import FileResponse
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
@ -22,8 +24,10 @@ from mealie.routes._base.routers import UserAPIRouter
|
|||||||
from mealie.schema.query import GetAll
|
from mealie.schema.query import GetAll
|
||||||
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeImageTypes
|
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeImageTypes
|
||||||
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
|
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
|
||||||
|
from mealie.schema.recipe.recipe_asset import RecipeAsset
|
||||||
from mealie.schema.response.responses import ErrorResponse
|
from mealie.schema.response.responses import ErrorResponse
|
||||||
from mealie.schema.server.tasks import ServerTaskNames
|
from mealie.schema.server.tasks import ServerTaskNames
|
||||||
|
from mealie.services.recipe.recipe_data_service import RecipeDataService
|
||||||
from mealie.services.recipe.recipe_service import RecipeService
|
from mealie.services.recipe.recipe_service import RecipeService
|
||||||
from mealie.services.recipe.template_service import TemplateService
|
from mealie.services.recipe.template_service import TemplateService
|
||||||
from mealie.services.scraper.scraper import create_from_url
|
from mealie.services.scraper.scraper import create_from_url
|
||||||
@ -49,6 +53,10 @@ class RecipeGetAll(GetAll):
|
|||||||
load_food: bool = False
|
load_food: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateImageResponse(BaseModel):
|
||||||
|
image: str
|
||||||
|
|
||||||
|
|
||||||
class FormatResponse(BaseModel):
|
class FormatResponse(BaseModel):
|
||||||
jjson: list[str] = Field(..., alias="json")
|
jjson: list[str] = Field(..., alias="json")
|
||||||
zip: list[str]
|
zip: list[str]
|
||||||
@ -158,10 +166,9 @@ class RecipeController(BaseRecipeController):
|
|||||||
@router.post("/test-scrape-url")
|
@router.post("/test-scrape-url")
|
||||||
def test_parse_recipe_url(self, url: CreateRecipeByUrl):
|
def test_parse_recipe_url(self, url: CreateRecipeByUrl):
|
||||||
# Debugger should produce the same result as the scraper sees before cleaning
|
# Debugger should produce the same result as the scraper sees before cleaning
|
||||||
scraped_data = RecipeScraperPackage(url.url).scrape_url()
|
if scraped_data := RecipeScraperPackage(url.url).scrape_url():
|
||||||
|
|
||||||
if scraped_data:
|
|
||||||
return scraped_data.schema.data
|
return scraped_data.schema.data
|
||||||
|
|
||||||
return "recipe_scrapers was unable to scrape this URL"
|
return "recipe_scrapers was unable to scrape this URL"
|
||||||
|
|
||||||
@router.post("/create-from-zip", status_code=201)
|
@router.post("/create-from-zip", status_code=201)
|
||||||
@ -217,6 +224,12 @@ class RecipeController(BaseRecipeController):
|
|||||||
self.deps.logger.error("SQL Integrity Error on recipe controller action")
|
self.deps.logger.error("SQL Integrity Error on recipe controller action")
|
||||||
raise HTTPException(status_code=400, detail=ErrorResponse.respond(message="Recipe already exists"))
|
raise HTTPException(status_code=400, detail=ErrorResponse.respond(message="Recipe already exists"))
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.deps.logger.error("Unknown Error on recipe controller action")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail=ErrorResponse.respond(message="Unknown Error", exception=ex)
|
||||||
|
)
|
||||||
|
|
||||||
@router.put("/{slug}")
|
@router.put("/{slug}")
|
||||||
def update_one(self, slug: str, data: Recipe):
|
def update_one(self, slug: str, data: Recipe):
|
||||||
"""Updates a recipe by existing slug and data."""
|
"""Updates a recipe by existing slug and data."""
|
||||||
@ -243,3 +256,51 @@ class RecipeController(BaseRecipeController):
|
|||||||
return self.service.delete_one(slug)
|
return self.service.delete_one(slug)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.handle_exceptions(e)
|
self.handle_exceptions(e)
|
||||||
|
|
||||||
|
# ==================================================================================================================
|
||||||
|
# Image and Assets
|
||||||
|
|
||||||
|
@router.post("/{slug}/image", tags=["Recipe: Images and Assets"])
|
||||||
|
def scrape_image_url(self, slug: str, url: CreateRecipeByUrl) -> str:
|
||||||
|
recipe = self.mixins.get_one(slug)
|
||||||
|
data_service = RecipeDataService(recipe.id)
|
||||||
|
data_service.scrape_image(url.url)
|
||||||
|
|
||||||
|
@router.put("/{slug}/image", response_model=UpdateImageResponse, tags=["Recipe: Images and Assets"])
|
||||||
|
def update_recipe_image(self, slug: str, image: bytes = File(...), extension: str = Form(...)):
|
||||||
|
recipe = self.mixins.get_one(slug)
|
||||||
|
data_service = RecipeDataService(recipe.id)
|
||||||
|
data_service.write_image(image, extension)
|
||||||
|
|
||||||
|
new_version = self.repo.update_image(slug, extension)
|
||||||
|
return UpdateImageResponse(image=new_version)
|
||||||
|
|
||||||
|
@router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"])
|
||||||
|
def upload_recipe_asset(
|
||||||
|
self,
|
||||||
|
slug: str,
|
||||||
|
name: str = Form(...),
|
||||||
|
icon: str = Form(...),
|
||||||
|
extension: str = Form(...),
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
):
|
||||||
|
"""Upload a file to store as a recipe asset"""
|
||||||
|
file_name = slugify(name) + "." + extension
|
||||||
|
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
|
||||||
|
|
||||||
|
recipe = self.mixins.get_one(slug)
|
||||||
|
|
||||||
|
dest = recipe.asset_dir / file_name
|
||||||
|
|
||||||
|
with dest.open("wb") as buffer:
|
||||||
|
copyfileobj(file.file, buffer)
|
||||||
|
|
||||||
|
if not dest.is_file():
|
||||||
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
recipe: Recipe = self.mixins.get_one(slug)
|
||||||
|
recipe.assets.append(asset_in)
|
||||||
|
|
||||||
|
self.mixins.update_one(recipe, slug)
|
||||||
|
|
||||||
|
return asset_in
|
||||||
|
@ -22,7 +22,7 @@ class RecipeSharedController(BaseUserController):
|
|||||||
return CrudMixins[RecipeShareTokenSave, RecipeShareToken, RecipeShareTokenCreate](self.repo, self.deps.logger)
|
return CrudMixins[RecipeShareTokenSave, RecipeShareToken, RecipeShareTokenCreate](self.repo, self.deps.logger)
|
||||||
|
|
||||||
@router.get("", response_model=list[RecipeShareTokenSummary])
|
@router.get("", response_model=list[RecipeShareTokenSummary])
|
||||||
def get_all(self, recipe_id: int = None):
|
def get_all(self, recipe_id: UUID4 = None):
|
||||||
if recipe_id:
|
if recipe_id:
|
||||||
return self.repo.multi_query({"recipe_id": recipe_id}, override_schema=RecipeShareTokenSummary)
|
return self.repo.multi_query({"recipe_id": recipe_id}, override_schema=RecipeShareTokenSummary)
|
||||||
else:
|
else:
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
from functools import cached_property
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
|
||||||
|
|
||||||
from mealie.routes._base import BaseUserController, controller
|
|
||||||
from mealie.schema.recipe import RecipeTagResponse, TagIn
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/tags", tags=["Tags: CRUD"])
|
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
|
||||||
class TagController(BaseUserController):
|
|
||||||
@cached_property
|
|
||||||
def repo(self):
|
|
||||||
return self.repos.tags
|
|
||||||
|
|
||||||
@router.get("")
|
|
||||||
async def get_all_recipe_tags(self):
|
|
||||||
"""Returns a list of available tags in the database"""
|
|
||||||
return self.repo.get_all_limit_columns(["slug", "name"])
|
|
||||||
|
|
||||||
@router.get("/empty")
|
|
||||||
def get_empty_tags(self):
|
|
||||||
"""Returns a list of tags that do not contain any recipes"""
|
|
||||||
return self.repo.get_empty()
|
|
||||||
|
|
||||||
@router.get("/{tag_slug}", response_model=RecipeTagResponse)
|
|
||||||
def get_all_recipes_by_tag(self, tag_slug: str):
|
|
||||||
"""Returns a list of recipes associated with the provided tag."""
|
|
||||||
return self.repo.get_one(tag_slug, override_schema=RecipeTagResponse)
|
|
||||||
|
|
||||||
@router.post("", status_code=201)
|
|
||||||
def create_recipe_tag(self, tag: TagIn):
|
|
||||||
"""Creates a Tag in the database"""
|
|
||||||
return self.repo.create(tag)
|
|
||||||
|
|
||||||
@router.put("/{tag_slug}", response_model=RecipeTagResponse)
|
|
||||||
def update_recipe_tag(self, tag_slug: str, new_tag: TagIn):
|
|
||||||
"""Updates an existing Tag in the database"""
|
|
||||||
return self.repo.update(tag_slug, new_tag)
|
|
||||||
|
|
||||||
@router.delete("/{tag_slug}")
|
|
||||||
def delete_recipe_tag(self, tag_slug: str):
|
|
||||||
"""Removes a recipe tag from the database. Deleting a
|
|
||||||
tag does not impact a recipe. The tag will be removed
|
|
||||||
from any recipes that contain it"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.repo.delete(tag_slug)
|
|
||||||
except Exception:
|
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
|
@ -1,6 +1,7 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.routes._base.abc_controller import BaseUserController
|
from mealie.routes._base.abc_controller import BaseUserController
|
||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
@ -36,13 +37,13 @@ class IngredientFoodsController(BaseUserController):
|
|||||||
return self.mixins.create_one(save_data)
|
return self.mixins.create_one(save_data)
|
||||||
|
|
||||||
@router.get("/{item_id}", response_model=IngredientFood)
|
@router.get("/{item_id}", response_model=IngredientFood)
|
||||||
def get_one(self, item_id: int):
|
def get_one(self, item_id: UUID4):
|
||||||
return self.mixins.get_one(item_id)
|
return self.mixins.get_one(item_id)
|
||||||
|
|
||||||
@router.put("/{item_id}", response_model=IngredientFood)
|
@router.put("/{item_id}", response_model=IngredientFood)
|
||||||
def update_one(self, item_id: int, data: CreateIngredientFood):
|
def update_one(self, item_id: UUID4, data: CreateIngredientFood):
|
||||||
return self.mixins.update_one(data, item_id)
|
return self.mixins.update_one(data, item_id)
|
||||||
|
|
||||||
@router.delete("/{item_id}", response_model=IngredientFood)
|
@router.delete("/{item_id}", response_model=IngredientFood)
|
||||||
def delete_one(self, item_id: int):
|
def delete_one(self, item_id: UUID4):
|
||||||
return self.mixins.delete_one(item_id)
|
return self.mixins.delete_one(item_id)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.routes._base.abc_controller import BaseUserController
|
from mealie.routes._base.abc_controller import BaseUserController
|
||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
@ -36,13 +37,13 @@ class IngredientUnitsController(BaseUserController):
|
|||||||
return self.mixins.create_one(save_data)
|
return self.mixins.create_one(save_data)
|
||||||
|
|
||||||
@router.get("/{item_id}", response_model=IngredientUnit)
|
@router.get("/{item_id}", response_model=IngredientUnit)
|
||||||
def get_one(self, item_id: int):
|
def get_one(self, item_id: UUID4):
|
||||||
return self.mixins.get_one(item_id)
|
return self.mixins.get_one(item_id)
|
||||||
|
|
||||||
@router.put("/{item_id}", response_model=IngredientUnit)
|
@router.put("/{item_id}", response_model=IngredientUnit)
|
||||||
def update_one(self, item_id: int, data: CreateIngredientUnit):
|
def update_one(self, item_id: UUID4, data: CreateIngredientUnit):
|
||||||
return self.mixins.update_one(data, item_id)
|
return self.mixins.update_one(data, item_id)
|
||||||
|
|
||||||
@router.delete("/{item_id}", response_model=IngredientUnit)
|
@router.delete("/{item_id}", response_model=IngredientUnit)
|
||||||
def delete_one(self, item_id: int):
|
def delete_one(self, item_id: UUID4):
|
||||||
return self.mixins.delete_one(item_id) # type: ignore
|
return self.mixins.delete_one(item_id) # type: ignore
|
||||||
|
@ -4,13 +4,12 @@ from pathlib import Path
|
|||||||
from fastapi import Depends, File, HTTPException, UploadFile, status
|
from fastapi import Depends, File, HTTPException, UploadFile, status
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie import utils
|
|
||||||
from mealie.core.dependencies.dependencies import temporary_dir
|
from mealie.core.dependencies.dependencies import temporary_dir
|
||||||
|
from mealie.pkgs import cache, img
|
||||||
from mealie.routes._base import BaseUserController, controller
|
from mealie.routes._base import BaseUserController, controller
|
||||||
from mealie.routes._base.routers import UserAPIRouter
|
from mealie.routes._base.routers import UserAPIRouter
|
||||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||||
from mealie.schema.user import PrivateUser
|
from mealie.schema.user import PrivateUser
|
||||||
from mealie.services.image import minify
|
|
||||||
|
|
||||||
router = UserAPIRouter(prefix="", tags=["Users: Images"])
|
router = UserAPIRouter(prefix="", tags=["Users: Images"])
|
||||||
|
|
||||||
@ -31,12 +30,12 @@ class UserImageController(BaseUserController):
|
|||||||
with temp_img.open("wb") as buffer:
|
with temp_img.open("wb") as buffer:
|
||||||
shutil.copyfileobj(profile.file, buffer)
|
shutil.copyfileobj(profile.file, buffer)
|
||||||
|
|
||||||
image = minify.to_webp(temp_img)
|
image = img.PillowMinifier.to_webp(temp_img)
|
||||||
dest = PrivateUser.get_directory(id) / "profile.webp"
|
dest = PrivateUser.get_directory(id) / "profile.webp"
|
||||||
|
|
||||||
shutil.copyfile(image, dest)
|
shutil.copyfile(image, dest)
|
||||||
|
|
||||||
self.repos.users.patch(id, {"cache_key": utils.new_cache_key()})
|
self.repos.users.patch(id, {"cache_key": cache.new_key()})
|
||||||
|
|
||||||
if not dest.is_file:
|
if not dest.is_file:
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user