mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-12 13:25:14 -05:00
feat(backend): ✨ add initial cookbook support
This commit is contained in:
1
mealie/schema/cookbook/__init__.py
Normal file
1
mealie/schema/cookbook/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .cookbook import *
|
||||
38
mealie/schema/cookbook/cookbook.py
Normal file
38
mealie/schema/cookbook/cookbook.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic import validator
|
||||
from slugify import slugify
|
||||
|
||||
from ..recipe.recipe_category import CategoryBase
|
||||
|
||||
|
||||
class CreateCookBook(CamelModel):
|
||||
name: str
|
||||
slug: str = None
|
||||
position: int = 1
|
||||
categories: list[CategoryBase] = []
|
||||
|
||||
@validator("slug", always=True, pre=True)
|
||||
def validate_slug(slug: str, values):
|
||||
name: str = values["name"]
|
||||
calc_slug: str = slugify(name)
|
||||
|
||||
if slug != calc_slug:
|
||||
slug = calc_slug
|
||||
|
||||
return slug
|
||||
|
||||
|
||||
class UpdateCookBook(CreateCookBook):
|
||||
id: int
|
||||
|
||||
|
||||
class SaveCookBook(CreateCookBook):
|
||||
group_id: int
|
||||
|
||||
|
||||
class ReadCookBook(UpdateCookBook):
|
||||
group_id: int
|
||||
categories: list[CategoryBase] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
@@ -19,7 +19,6 @@ class CategoryBase(CategoryIn):
|
||||
def getter_dict(_cls, name_orm):
|
||||
return {
|
||||
**GetterDict(name_orm),
|
||||
"total_recipes": len(name_orm.recipes),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from .category import *
|
||||
from .comments import *
|
||||
from .helpers import *
|
||||
from .recipe import *
|
||||
@@ -1,48 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic.utils import GetterDict
|
||||
|
||||
|
||||
class CategoryIn(CamelModel):
|
||||
name: str
|
||||
|
||||
|
||||
class CategoryBase(CategoryIn):
|
||||
id: int
|
||||
slug: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@classmethod
|
||||
def getter_dict(_cls, name_orm):
|
||||
return {
|
||||
**GetterDict(name_orm),
|
||||
"total_recipes": len(name_orm.recipes),
|
||||
}
|
||||
|
||||
|
||||
class RecipeCategoryResponse(CategoryBase):
|
||||
recipes: Optional[List["Recipe"]]
|
||||
|
||||
class Config:
|
||||
schema_extra = {"example": {"id": 1, "name": "dinner", "recipes": [{}]}}
|
||||
|
||||
|
||||
class TagIn(CategoryIn):
|
||||
pass
|
||||
|
||||
|
||||
class TagBase(CategoryBase):
|
||||
pass
|
||||
|
||||
|
||||
class RecipeTagResponse(RecipeCategoryResponse):
|
||||
pass
|
||||
|
||||
|
||||
from .recipe import Recipe
|
||||
|
||||
RecipeCategoryResponse.update_forward_refs()
|
||||
RecipeTagResponse.update_forward_refs()
|
||||
@@ -1,44 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic.utils import GetterDict
|
||||
|
||||
|
||||
class UserBase(CamelModel):
|
||||
id: int
|
||||
username: Optional[str]
|
||||
admin: bool
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class CommentIn(CamelModel):
|
||||
text: str
|
||||
|
||||
|
||||
class CommentSaveToDB(CommentIn):
|
||||
recipe_slug: str
|
||||
user: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class CommentOut(CommentIn):
|
||||
id: int
|
||||
uuid: str
|
||||
recipe_slug: str
|
||||
date_added: datetime
|
||||
user: UserBase
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@classmethod
|
||||
def getter_dict(_cls, name_orm):
|
||||
return {
|
||||
**GetterDict(name_orm),
|
||||
"recipe_slug": name_orm.recipe.slug,
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
|
||||
class RecipeSlug(CamelModel):
|
||||
slug: str
|
||||
@@ -1,243 +0,0 @@
|
||||
import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from pydantic.utils import GetterDict
|
||||
from slugify import slugify
|
||||
|
||||
from mealie.core.config import app_dirs, settings
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
|
||||
from .comments import CommentOut
|
||||
from .units_and_foods import IngredientFood, IngredientUnit
|
||||
|
||||
|
||||
class CreateRecipe(CamelModel):
|
||||
name: str
|
||||
|
||||
|
||||
class RecipeImageTypes(str, Enum):
|
||||
original = "original.webp"
|
||||
min = "min-original.webp"
|
||||
tiny = "tiny-original.webp"
|
||||
|
||||
|
||||
class RecipeSettings(CamelModel):
|
||||
public: bool = settings.RECIPE_PUBLIC
|
||||
show_nutrition: bool = settings.RECIPE_SHOW_NUTRITION
|
||||
show_assets: bool = settings.RECIPE_SHOW_ASSETS
|
||||
landscape_view: bool = settings.RECIPE_LANDSCAPE_VIEW
|
||||
disable_comments: bool = settings.RECIPE_DISABLE_COMMENTS
|
||||
disable_amount: bool = settings.RECIPE_DISABLE_AMOUNT
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeNote(BaseModel):
|
||||
title: str
|
||||
text: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeStep(CamelModel):
|
||||
title: Optional[str] = ""
|
||||
text: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeAsset(CamelModel):
|
||||
name: str
|
||||
icon: str
|
||||
file_name: Optional[str]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class Nutrition(CamelModel):
|
||||
calories: Optional[str]
|
||||
fat_content: Optional[str]
|
||||
protein_content: Optional[str]
|
||||
carbohydrate_content: Optional[str]
|
||||
fiber_content: Optional[str]
|
||||
sodium_content: Optional[str]
|
||||
sugar_content: Optional[str]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeIngredient(CamelModel):
|
||||
title: Optional[str]
|
||||
note: Optional[str]
|
||||
unit: Optional[IngredientUnit]
|
||||
food: Optional[IngredientFood]
|
||||
disable_amount: bool = True
|
||||
quantity: int = 1
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeSummary(CamelModel):
|
||||
id: Optional[int]
|
||||
name: Optional[str]
|
||||
slug: str = ""
|
||||
image: Optional[Any]
|
||||
|
||||
description: Optional[str]
|
||||
recipe_category: Optional[list[str]] = []
|
||||
tags: Optional[list[str]] = []
|
||||
rating: Optional[int]
|
||||
|
||||
date_added: Optional[datetime.date]
|
||||
date_updated: Optional[datetime.datetime]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@classmethod
|
||||
def getter_dict(_cls, name_orm: RecipeModel):
|
||||
return {
|
||||
**GetterDict(name_orm),
|
||||
"recipe_category": [x.name for x in name_orm.recipe_category],
|
||||
"tags": [x.name for x in name_orm.tags],
|
||||
}
|
||||
|
||||
|
||||
class Recipe(RecipeSummary):
|
||||
recipe_yield: Optional[str]
|
||||
recipe_ingredient: Optional[list[RecipeIngredient]] = []
|
||||
recipe_instructions: Optional[list[RecipeStep]] = []
|
||||
nutrition: Optional[Nutrition]
|
||||
tools: Optional[list[str]] = []
|
||||
|
||||
total_time: Optional[str] = None
|
||||
prep_time: Optional[str] = None
|
||||
perform_time: Optional[str] = None
|
||||
|
||||
# Mealie Specific
|
||||
settings: Optional[RecipeSettings] = RecipeSettings()
|
||||
assets: Optional[list[RecipeAsset]] = []
|
||||
notes: Optional[list[RecipeNote]] = []
|
||||
org_url: Optional[str] = Field(None, alias="orgURL")
|
||||
extras: Optional[dict] = {}
|
||||
|
||||
comments: Optional[list[CommentOut]] = []
|
||||
|
||||
@staticmethod
|
||||
def directory_from_slug(slug) -> Path:
|
||||
return app_dirs.RECIPE_DATA_DIR.joinpath(slug)
|
||||
|
||||
@property
|
||||
def directory(self) -> Path:
|
||||
dir = app_dirs.RECIPE_DATA_DIR.joinpath(self.slug)
|
||||
dir.mkdir(exist_ok=True, parents=True)
|
||||
return dir
|
||||
|
||||
@property
|
||||
def asset_dir(self) -> Path:
|
||||
dir = self.directory.joinpath("assets")
|
||||
dir.mkdir(exist_ok=True, parents=True)
|
||||
return dir
|
||||
|
||||
@property
|
||||
def image_dir(self) -> Path:
|
||||
dir = self.directory.joinpath("images")
|
||||
dir.mkdir(exist_ok=True, parents=True)
|
||||
return dir
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@classmethod
|
||||
def getter_dict(_cls, name_orm: RecipeModel):
|
||||
return {
|
||||
**GetterDict(name_orm),
|
||||
# "recipe_ingredient": [x.note for x in name_orm.recipe_ingredient],
|
||||
"recipe_category": [x.name for x in name_orm.recipe_category],
|
||||
"tags": [x.name for x in name_orm.tags],
|
||||
"tools": [x.tool for x in name_orm.tools],
|
||||
"extras": {x.key_name: x.value for x in name_orm.extras},
|
||||
}
|
||||
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "Chicken and Rice With Leeks and Salsa Verde",
|
||||
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
|
||||
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
|
||||
"recipe_yield": "4 Servings",
|
||||
"recipe_ingredient": [
|
||||
"1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
|
||||
"Kosher salt, freshly ground pepper",
|
||||
"3 Tbsp. unsalted butter, divided",
|
||||
],
|
||||
"recipe_instructions": [
|
||||
{
|
||||
"text": "Season chicken with salt and pepper.",
|
||||
},
|
||||
],
|
||||
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"tags": ["favorite", "yummy!"],
|
||||
"recipe_category": ["Dinner", "Pasta"],
|
||||
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
|
||||
"org_url": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"rating": 3,
|
||||
"extras": {"message": "Don't forget to defrost the chicken!"},
|
||||
}
|
||||
}
|
||||
|
||||
@validator("slug", always=True, pre=True)
|
||||
def validate_slug(slug: str, values):
|
||||
if not values["name"]:
|
||||
return slug
|
||||
name: str = values["name"]
|
||||
calc_slug: str = slugify(name)
|
||||
|
||||
if slug != calc_slug:
|
||||
slug = calc_slug
|
||||
|
||||
return slug
|
||||
|
||||
@validator("recipe_ingredient", always=True, pre=True)
|
||||
def validate_ingredients(recipe_ingredient, values):
|
||||
if not recipe_ingredient or not isinstance(recipe_ingredient, list):
|
||||
return recipe_ingredient
|
||||
|
||||
if all(isinstance(elem, str) for elem in recipe_ingredient):
|
||||
return [RecipeIngredient(note=x) for x in recipe_ingredient]
|
||||
|
||||
return recipe_ingredient
|
||||
|
||||
|
||||
class AllRecipeRequest(BaseModel):
|
||||
properties: list[str]
|
||||
limit: Optional[int]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"properties": ["name", "slug", "image"],
|
||||
"limit": 100,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RecipeURLIn(BaseModel):
|
||||
url: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
|
||||
|
||||
|
||||
class SlugResponse(BaseModel):
|
||||
class Config:
|
||||
schema_extra = {"example": "adult-mac-and-cheese"}
|
||||
@@ -1,24 +0,0 @@
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
|
||||
class CreateIngredientFood(CamelModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
class CreateIngredientUnit(CreateIngredientFood):
|
||||
abbreviation: str = ""
|
||||
|
||||
|
||||
class IngredientFood(CreateIngredientFood):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class IngredientUnit(CreateIngredientUnit):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
Reference in New Issue
Block a user