diff --git a/.gitignore b/.gitignore index 3b2646bfc..e356f2f9d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ mealie/temp/* mealie/temp/api.html .temp/ .secret +!*/components/Recipe/Parts dev/data/backups/* dev/data/debug/* @@ -159,3 +160,4 @@ scratch.py dev/data/backups/dev_sample_data*.zip dev/data/backups/dev_sample_data*.zip !dev/data/backups/test*.zip +dev/data/recipes/* diff --git a/frontend/src/api/recipe.js b/frontend/src/api/recipe.js index f6a08f8c9..09ba2adfb 100644 --- a/frontend/src/api/recipe.js +++ b/frontend/src/api/recipe.js @@ -16,6 +16,7 @@ const recipeURLs = { delete: slug => prefix + slug, recipeImage: slug => `${prefix}${slug}/image`, updateImage: slug => `${prefix}${slug}/image`, + createAsset: slug => `${prefix}${slug}/asset`, }; export const recipeAPI = { @@ -78,26 +79,16 @@ export const recipeAPI = { ); }, - updateImagebyURL(slug, url) { - return apiReq.post( - recipeURLs.updateImage(slug), - { url: url }, - function() { return i18n.t('general.image-upload-failed'); }, - function() { return i18n.t('recipe.recipe-image-updated'); } - ); + async updateImagebyURL(slug, url) { + const response = apiReq.post(recipeURLs.updateImage(slug), { url: url }); + return response; }, async update(data) { - let response = await apiReq.put( - recipeURLs.update(data.slug), - data, - function() { return i18n.t('recipe.recipe-update-failed'); }, - function() { return i18n.t('recipe.recipe-updated'); } - ); - if(response) { - store.dispatch("patchRecipe", response.data); - return response.data.slug; // ! Temporary until I rewrite to refresh page without additional request - } + console.log(data) + let response = await apiReq.put(recipeURLs.update(data.slug), data); + store.dispatch("patchRecipe", response.data); + return response.data.slug; // ! Temporary until I rewrite to refresh page without additional request }, async patch(data) { diff --git a/frontend/src/components/Recipe/Parts/Assets.vue b/frontend/src/components/Recipe/Parts/Assets.vue new file mode 100644 index 000000000..65d4ec0ae --- /dev/null +++ b/frontend/src/components/Recipe/Parts/Assets.vue @@ -0,0 +1,160 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Recipe/RecipeEditor/BulkAdd.vue b/frontend/src/components/Recipe/Parts/Helpers/BulkAdd.vue similarity index 98% rename from frontend/src/components/Recipe/RecipeEditor/BulkAdd.vue rename to frontend/src/components/Recipe/Parts/Helpers/BulkAdd.vue index 72439417f..aba3835c5 100644 --- a/frontend/src/components/Recipe/RecipeEditor/BulkAdd.vue +++ b/frontend/src/components/Recipe/Parts/Helpers/BulkAdd.vue @@ -3,7 +3,7 @@ - - \ No newline at end of file diff --git a/frontend/src/pages/Admin/Backup/index.vue b/frontend/src/pages/Admin/Backup/index.vue index c9a59e7f8..71090c196 100644 --- a/frontend/src/pages/Admin/Backup/index.vue +++ b/frontend/src/pages/Admin/Backup/index.vue @@ -71,17 +71,6 @@ export default { this.availableBackups = response.imports; this.availableTemplates = response.templates; }, - deleteBackup() { - if (this.$refs.form.validate()) { - this.backupLoading = true; - - api.backups.delete(this.selectedBackup); - this.getAvailableBackups(); - - this.selectedBackup = null; - this.backupLoading = false; - } - }, processFinished(data) { this.getAvailableBackups(); this.backupLoading = false; diff --git a/frontend/src/pages/Admin/ToolBox/CategoryTagEditor/RemoveUnused.vue b/frontend/src/pages/Admin/ToolBox/CategoryTagEditor/RemoveUnused.vue index 12554c1a1..2bc52d2a9 100644 --- a/frontend/src/pages/Admin/ToolBox/CategoryTagEditor/RemoveUnused.vue +++ b/frontend/src/pages/Admin/ToolBox/CategoryTagEditor/RemoveUnused.vue @@ -68,7 +68,6 @@ export default { }, async openDialog() { this.$refs.deleteDialog.open(); - console.log(this.isTags); if (this.isTags) { this.deleteList = await api.tags.getEmpty(); } else { diff --git a/frontend/src/pages/Recipe/ViewRecipe.vue b/frontend/src/pages/Recipe/ViewRecipe.vue index 6df53d5b1..56fdbd7e6 100644 --- a/frontend/src/pages/Recipe/ViewRecipe.vue +++ b/frontend/src/pages/Recipe/ViewRecipe.vue @@ -51,6 +51,8 @@ :yields="recipeDetails.recipeYield" :orgURL="recipeDetails.orgURL" :nutrition="recipeDetails.nutrition" + :assets="recipeDetails.assets" + :slug="recipeDetails.slug" /> (a.dateAdded > b.dateAdded ? -1 : 1)); - console.log(payload); const hash = Object.fromEntries(payload.map(e => [e.id, e])); this.commit("setRecentRecipes", hash); }, @@ -44,7 +43,6 @@ const actions = { const all = getters.getAllRecipes; const payload = await api.recipes.allSummary(all.length, 9999); const hash = Object.fromEntries([...all, ...payload].map(e => [e.id, e])); - console.log(hash); this.commit("setAllRecipes", hash); }, diff --git a/mealie/app.py b/mealie/app.py index 0d16c615b..75b2bb7a6 100644 --- a/mealie/app.py +++ b/mealie/app.py @@ -2,13 +2,14 @@ import uvicorn from fastapi import FastAPI from mealie.core import root_logger - -# import utils.startup as startup from mealie.core.config import APP_VERSION, settings -from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes +from mealie.routes import (backup_routes, debug_routes, migration_routes, + theme_routes, utility_routes) from mealie.routes.groups import groups from mealie.routes.mealplans import mealplans -from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes +from mealie.routes.recipe import (all_recipe_routes, category_routes, + recipe_assets, recipe_crud_routes, + tag_routes) from mealie.routes.site_settings import all_settings from mealie.routes.users import users @@ -37,6 +38,7 @@ def api_routers(): app.include_router(category_routes.router) app.include_router(tag_routes.router) app.include_router(recipe_crud_routes.router) + app.include_router(recipe_assets.router) # Meal Routes app.include_router(mealplans.router) # Settings Routes @@ -50,11 +52,11 @@ def api_routers(): api_routers() -start_scheduler() @app.on_event("startup") def system_startup(): + start_scheduler() logger.info("-----SYSTEM STARTUP----- \n") logger.info("------APP SETTINGS------") logger.info(settings.json(indent=4, exclude={"SECRET", "DEFAULT_PASSWORD", "SFTP_PASSWORD", "SFTP_USERNAME"})) diff --git a/mealie/db/models/mealplan.py b/mealie/db/models/mealplan.py index 84e70d9c2..8ee0cacf3 100644 --- a/mealie/db/models/mealplan.py +++ b/mealie/db/models/mealplan.py @@ -9,7 +9,7 @@ from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase class Meal(SqlAlchemyBase): __tablename__ = "meal" id = sa.Column(sa.Integer, primary_key=True) - parent_id = sa.Column(sa.String, sa.ForeignKey("mealplan.uid")) + parent_id = sa.Column(sa.Integer, sa.ForeignKey("mealplan.uid")) slug = sa.Column(sa.String) name = sa.Column(sa.String) date = sa.Column(sa.Date) diff --git a/mealie/db/models/recipe/api_extras.py b/mealie/db/models/recipe/api_extras.py index 222bb6c2c..c4172cb4c 100644 --- a/mealie/db/models/recipe/api_extras.py +++ b/mealie/db/models/recipe/api_extras.py @@ -5,7 +5,7 @@ from mealie.db.models.model_base import SqlAlchemyBase class ApiExtras(SqlAlchemyBase): __tablename__ = "api_extras" id = sa.Column(sa.Integer, primary_key=True) - parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id")) + parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id")) key_name = sa.Column(sa.String, unique=True) value = sa.Column(sa.String) diff --git a/mealie/db/models/recipe/assets.py b/mealie/db/models/recipe/assets.py new file mode 100644 index 000000000..7fb1dab0a --- /dev/null +++ b/mealie/db/models/recipe/assets.py @@ -0,0 +1,22 @@ +import sqlalchemy as sa +from mealie.db.models.model_base import SqlAlchemyBase + + +class RecipeAsset(SqlAlchemyBase): + __tablename__ = "recipe_assets" + id = sa.Column(sa.Integer, primary_key=True) + parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id")) + name = sa.Column(sa.String) + icon = sa.Column(sa.String) + file_name = sa.Column(sa.String) + + def __init__( + self, + name=None, + icon=None, + file_name=None, + ) -> None: + print("Asset Saved", name) + self.name = name + self.file_name = file_name + self.icon = icon diff --git a/mealie/db/models/recipe/category.py b/mealie/db/models/recipe/category.py index ffaa48638..bacb50fc9 100644 --- a/mealie/db/models/recipe/category.py +++ b/mealie/db/models/recipe/category.py @@ -41,7 +41,7 @@ class Category(SqlAlchemyBase): id = sa.Column(sa.Integer, primary_key=True) 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="recipeCategory") + recipes = orm.relationship("RecipeModel", secondary=recipes2categories, back_populates="recipe_category") @validates("name") def validate_name(self, key, name): diff --git a/mealie/db/models/recipe/ingredient.py b/mealie/db/models/recipe/ingredient.py index 75c20d2d0..b00f4c60a 100644 --- a/mealie/db/models/recipe/ingredient.py +++ b/mealie/db/models/recipe/ingredient.py @@ -6,7 +6,7 @@ class RecipeIngredient(SqlAlchemyBase): __tablename__ = "recipes_ingredients" id = sa.Column(sa.Integer, primary_key=True) position = sa.Column(sa.Integer) - parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id")) + parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id")) ingredient = sa.Column(sa.String) def update(self, ingredient): diff --git a/mealie/db/models/recipe/instruction.py b/mealie/db/models/recipe/instruction.py index 2cb2e38a6..080ebcbdb 100644 --- a/mealie/db/models/recipe/instruction.py +++ b/mealie/db/models/recipe/instruction.py @@ -5,7 +5,8 @@ from mealie.db.models.model_base import SqlAlchemyBase class RecipeInstruction(SqlAlchemyBase): __tablename__ = "recipe_instructions" id = sa.Column(sa.Integer, primary_key=True) - parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id")) + parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id")) position = sa.Column(sa.Integer) type = sa.Column(sa.String, default="") text = sa.Column(sa.String) + title = sa.Column(sa.String) diff --git a/mealie/db/models/recipe/note.py b/mealie/db/models/recipe/note.py index 8d37db582..28ee46303 100644 --- a/mealie/db/models/recipe/note.py +++ b/mealie/db/models/recipe/note.py @@ -5,7 +5,7 @@ from mealie.db.models.model_base import SqlAlchemyBase class Note(SqlAlchemyBase): __tablename__ = "notes" id = sa.Column(sa.Integer, primary_key=True) - parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id")) + parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id")) title = sa.Column(sa.String) text = sa.Column(sa.String) diff --git a/mealie/db/models/recipe/nutrition.py b/mealie/db/models/recipe/nutrition.py index 2ded5bdb2..5856e3de6 100644 --- a/mealie/db/models/recipe/nutrition.py +++ b/mealie/db/models/recipe/nutrition.py @@ -5,26 +5,29 @@ from mealie.db.models.model_base import SqlAlchemyBase class Nutrition(SqlAlchemyBase): __tablename__ = "recipe_nutrition" id = sa.Column(sa.Integer, primary_key=True) - parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id")) + parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id")) calories = sa.Column(sa.String) - fatContent = sa.Column(sa.String) - fiberContent = sa.Column(sa.String) - proteinContent = sa.Column(sa.String) - sodiumContent = sa.Column(sa.String) - sugarContent = sa.Column(sa.String) + fat_content = sa.Column(sa.String) + fiber_content = sa.Column(sa.String) + protein_content = sa.Column(sa.String) + carbohydrate_content = sa.Column(sa.String) + sodium_content = sa.Column(sa.String) + sugar_content = sa.Column(sa.String) def __init__( self, calories=None, - fatContent=None, - fiberContent=None, - proteinContent=None, - sodiumContent=None, - sugarContent=None, + fat_content=None, + fiber_content=None, + protein_content=None, + sodium_content=None, + sugar_content=None, + carbohydrate_content=None, ) -> None: self.calories = calories - self.fatContent = fatContent - self.fiberContent = fiberContent - self.proteinContent = proteinContent - self.sodiumContent = sodiumContent - self.sugarContent = sugarContent + self.fat_content = fat_content + self.fiber_content = fiber_content + self.protein_content = protein_content + self.sodium_content = sodium_content + self.sugar_content = sugar_content + self.carbohydrate_content = carbohydrate_content diff --git a/mealie/db/models/recipe/recipe.py b/mealie/db/models/recipe/recipe.py index 3127a840b..b9ffbedf3 100644 --- a/mealie/db/models/recipe/recipe.py +++ b/mealie/db/models/recipe/recipe.py @@ -1,16 +1,17 @@ import datetime from datetime import date -from typing import List import sqlalchemy as sa import sqlalchemy.orm as orm from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase from mealie.db.models.recipe.api_extras import ApiExtras +from mealie.db.models.recipe.assets import RecipeAsset from mealie.db.models.recipe.category import Category, recipes2categories from mealie.db.models.recipe.ingredient import RecipeIngredient from mealie.db.models.recipe.instruction import RecipeInstruction from mealie.db.models.recipe.note import Note from mealie.db.models.recipe.nutrition import Nutrition +from mealie.db.models.recipe.settings import RecipeSettings from mealie.db.models.recipe.tag import Tag, recipes2tags from mealie.db.models.recipe.tool import Tool from sqlalchemy.ext.orderinglist import ordering_list @@ -26,23 +27,24 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): name = sa.Column(sa.String, nullable=False) description = sa.Column(sa.String) image = sa.Column(sa.String) - totalTime = sa.Column(sa.String) - prepTime = sa.Column(sa.String) - performTime = sa.Column(sa.String) + total_time = sa.Column(sa.String) + prep_time = sa.Column(sa.String) + perform_time = sa.Column(sa.String) cookTime = sa.Column(sa.String) - recipeYield = sa.Column(sa.String) + recipe_yield = sa.Column(sa.String) recipeCuisine = sa.Column(sa.String) - tools: List[Tool] = orm.relationship("Tool", cascade="all, delete-orphan") + tools: list[Tool] = orm.relationship("Tool", cascade="all, delete-orphan") + assets: list[RecipeAsset] = orm.relationship("RecipeAsset", cascade="all, delete-orphan") nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan") - recipeCategory: List = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes") + recipe_category: list = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes") - recipeIngredient: List[RecipeIngredient] = orm.relationship( + recipe_ingredient: list[RecipeIngredient] = orm.relationship( "RecipeIngredient", cascade="all, delete-orphan", order_by="RecipeIngredient.position", collection_class=ordering_list("position"), ) - recipeInstructions: List[RecipeInstruction] = orm.relationship( + recipe_instructions: list[RecipeInstruction] = orm.relationship( "RecipeInstruction", cascade="all, delete-orphan", order_by="RecipeInstruction.position", @@ -51,12 +53,13 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): # Mealie Specific slug = sa.Column(sa.String, index=True, unique=True) - tags: List[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes") - dateAdded = sa.Column(sa.Date, default=date.today) - notes: List[Note] = orm.relationship("Note", 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") + date_added = sa.Column(sa.Date, default=date.today) + notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan") rating = sa.Column(sa.Integer) - orgURL = sa.Column(sa.String) - extras: List[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan") + org_url = sa.Column(sa.String) + extras: list[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan") @validates("name") def validate_name(self, key, name): @@ -69,23 +72,25 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): name: str = None, description: str = None, image: str = None, - recipeYield: str = None, - recipeIngredient: List[str] = None, - recipeInstructions: List[dict] = None, + recipe_yield: str = None, + recipe_ingredient: list[str] = None, + recipe_instructions: list[dict] = None, recipeCuisine: str = None, - totalTime: str = None, - prepTime: str = None, + total_time: str = None, + prep_time: str = None, nutrition: dict = None, - tools: list[str] = [], - performTime: str = None, + tools: list[str] = None, + perform_time: str = None, slug: str = None, - recipeCategory: List[str] = None, - tags: List[str] = None, - dateAdded: datetime.date = None, - notes: List[dict] = None, + recipe_category: list[str] = None, + tags: list[str] = None, + date_added: datetime.date = None, + notes: list[dict] = None, rating: int = None, - orgURL: str = None, + org_url: str = None, extras: dict = None, + assets: list = None, + settings: dict = None, *args, **kwargs ) -> None: @@ -95,77 +100,33 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): self.recipeCuisine = recipeCuisine self.nutrition = Nutrition(**nutrition) if self.nutrition else Nutrition() + self.tools = [Tool(tool=x) for x in tools] if tools else [] - self.recipeYield = recipeYield - self.recipeIngredient = [RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient] - self.recipeInstructions = [ - RecipeInstruction(text=instruc.get("text"), type=instruc.get("@type", None)) - for instruc in recipeInstructions + self.recipe_yield = recipe_yield + self.recipe_ingredient = [RecipeIngredient(ingredient=ingr) for ingr in recipe_ingredient] + self.assets = [RecipeAsset(**a) for a in assets] + self.recipe_instructions = [ + RecipeInstruction(text=instruc.get("text"), title=instruc.get("title"), type=instruc.get("@type", None)) + for instruc in recipe_instructions ] - self.totalTime = totalTime - self.prepTime = prepTime - self.performTime = performTime + self.total_time = total_time + self.prep_time = prep_time + self.perform_time = perform_time - self.recipeCategory = [Category.create_if_not_exist(session=session, name=cat) for cat in recipeCategory] + self.recipe_category = [Category.create_if_not_exist(session=session, name=cat) for cat in recipe_category] # Mealie Specific + self.settings = RecipeSettings(**settings) if settings else RecipeSettings() + print(self.settings) self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags] self.slug = slug - self.dateAdded = dateAdded + self.date_added = date_added self.notes = [Note(**note) for note in notes] self.rating = rating - self.orgURL = orgURL + self.org_url = org_url self.extras = [ApiExtras(key=key, value=value) for key, value in extras.items()] - def update( - self, - session, - name: str = None, - description: str = None, - image: str = None, - recipeYield: str = None, - recipeIngredient: List[str] = None, - recipeInstructions: List[dict] = None, - recipeCuisine: str = None, - totalTime: str = None, - tools: list[str] = [], - prepTime: str = None, - performTime: str = None, - nutrition: dict = None, - slug: str = None, - recipeCategory: List[str] = None, - tags: List[str] = None, - dateAdded: datetime.date = None, - notes: List[dict] = None, - rating: int = None, - orgURL: str = None, - extras: dict = None, - *args, - **kwargs - ): + def update(self, *args, **kwargs): """Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions""" - - self.__init__( - session=session, - name=name, - description=description, - image=image, - recipeYield=recipeYield, - recipeIngredient=recipeIngredient, - recipeInstructions=recipeInstructions, - totalTime=totalTime, - recipeCuisine=recipeCuisine, - prepTime=prepTime, - performTime=performTime, - nutrition=nutrition, - tools=tools, - slug=slug, - recipeCategory=recipeCategory, - tags=tags, - dateAdded=dateAdded, - notes=notes, - rating=rating, - orgURL=orgURL, - extras=extras, - ) + self.__init__(*args, **kwargs) diff --git a/mealie/db/models/recipe/settings.py b/mealie/db/models/recipe/settings.py new file mode 100644 index 000000000..fa7f75ebb --- /dev/null +++ b/mealie/db/models/recipe/settings.py @@ -0,0 +1,18 @@ +import sqlalchemy as sa +from mealie.db.models.model_base import SqlAlchemyBase + + +class RecipeSettings(SqlAlchemyBase): + __tablename__ = "recipe_settings" + id = sa.Column(sa.Integer, primary_key=True) + parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id")) + public = sa.Column(sa.Boolean) + show_nutrition = sa.Column(sa.Boolean) + show_assets = sa.Column(sa.Boolean) + landscape_view = sa.Column(sa.Boolean) + + def __init__(self, public=True, show_nutrition=True, show_assets=True, landscape_view=True) -> None: + self.public = public + self.show_nutrition = show_nutrition + self.show_assets = show_assets + self.landscape_view = landscape_view diff --git a/mealie/db/models/recipe/tool.py b/mealie/db/models/recipe/tool.py index 6b65c9e5d..2406864f3 100644 --- a/mealie/db/models/recipe/tool.py +++ b/mealie/db/models/recipe/tool.py @@ -5,7 +5,7 @@ from mealie.db.models.model_base import SqlAlchemyBase class Tool(SqlAlchemyBase): __tablename__ = "tools" id = sa.Column(sa.Integer, primary_key=True) - parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id")) + parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id")) tool = sa.Column(sa.String) def __init__(self, tool) -> None: diff --git a/mealie/routes/mealplans/helpers.py b/mealie/routes/mealplans/helpers.py index 1e64cb504..15ce140ba 100644 --- a/mealie/routes/mealplans/helpers.py +++ b/mealie/routes/mealplans/helpers.py @@ -21,4 +21,4 @@ def get_shopping_list( mealplan: MealPlanInDB slugs = [x.slug for x in mealplan.meals] recipes: list[Recipe] = [db.recipes.get(session, x) for x in slugs] - return [{"name": x.name, "recipeIngredient": x.recipeIngredient} for x in recipes if x] + return [{"name": x.name, "recipe_ingredient": x.recipe_ingredient} for x in recipes if x] diff --git a/mealie/routes/recipe/all_recipe_routes.py b/mealie/routes/recipe/all_recipe_routes.py index da07ed18e..0a8556afc 100644 --- a/mealie/routes/recipe/all_recipe_routes.py +++ b/mealie/routes/recipe/all_recipe_routes.py @@ -50,11 +50,11 @@ def get_all_recipes( - description - image - recipeYield - - totalTime - - prepTime - - performTime + - total_time + - prep_time + - perform_time - rating - - orgURL + - org_url **Note:** You may experience problems with with query parameters. As an alternative you may also use the post method and provide a body. @@ -78,11 +78,11 @@ def get_all_recipes_post(body: AllRecipeRequest, session: Session = Depends(gene - description - image - recipeYield - - totalTime - - prepTime - - performTime + - total_time + - prep_time + - perform_time - rating - - orgURL + - org_url Refer to the body example for data formats. diff --git a/mealie/routes/recipe/recipe_assets.py b/mealie/routes/recipe/recipe_assets.py new file mode 100644 index 000000000..216a6af0d --- /dev/null +++ b/mealie/routes/recipe/recipe_assets.py @@ -0,0 +1,50 @@ +import shutil + +from fastapi import APIRouter, Depends, File, Form +from fastapi.datastructures import UploadFile +from mealie.core.config import app_dirs +from mealie.db.database import db +from mealie.db.db_setup import generate_session +from mealie.routes.deps import get_current_user +from mealie.schema.recipe import Recipe, RecipeAsset +from mealie.schema.snackbar import SnackResponse +from slugify import slugify +from sqlalchemy.orm.session import Session +from starlette.responses import FileResponse + +router = APIRouter(prefix="/api/recipes", tags=["Recipe Assets"]) + + +@router.get("/{recipe_slug}/asset") +async def get_recipe_asset(recipe_slug, file_name: str): + """ Returns a recipe asset """ + file = app_dirs.RECIPE_DATA_DIR.joinpath(recipe_slug, file_name) + return FileResponse(file) + + +@router.post("/{recipe_slug}/asset", response_model=RecipeAsset) +def upload_recipe_asset( + recipe_slug: str, + name: str = Form(...), + icon: str = Form(...), + extension: str = Form(...), + file: UploadFile = File(...), + session: Session = Depends(generate_session), + current_user=Depends(get_current_user), +): + """ 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 = app_dirs.RECIPE_DATA_DIR.joinpath(recipe_slug, file_name) + dest.parent.mkdir(exist_ok=True, parents=True) + + with dest.open("wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + if dest.is_file(): + recipe: Recipe = db.recipes.get(session, recipe_slug) + recipe.assets.append(asset_in) + db.recipes.update(session, recipe_slug, recipe.dict()) + return asset_in + else: + return SnackResponse.error("Failure uploading file") diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py index e6d5062f9..1c04bd94e 100644 --- a/mealie/routes/recipe/recipe_crud_routes.py +++ b/mealie/routes/recipe/recipe_crud_routes.py @@ -56,6 +56,7 @@ def update_recipe( """ Updates a recipe by existing slug and data. """ recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict()) + print(recipe.assets) if recipe_slug != recipe.slug: rename_image(original_slug=recipe_slug, new_slug=recipe.slug) @@ -64,7 +65,7 @@ def update_recipe( @router.patch("/{recipe_slug}") -def update_recipe( +def patch_recipe( recipe_slug: str, data: dict, session: Session = Depends(generate_session), diff --git a/mealie/schema/recipe.py b/mealie/schema/recipe.py index 994255a4b..1ff2aabf7 100644 --- a/mealie/schema/recipe.py +++ b/mealie/schema/recipe.py @@ -1,12 +1,23 @@ import datetime -from typing import Any, List, Optional +from typing import Any, Optional +from fastapi_camelcase import CamelModel from mealie.db.models.recipe.recipe import RecipeModel -from pydantic import BaseModel, validator +from pydantic import BaseModel, Field, validator from pydantic.utils import GetterDict from slugify import slugify +class RecipeSettings(CamelModel): + public: bool = True + show_nutrition: bool = True + show_assets: bool = True + landscape_view: bool = True + + class Config: + orm_mode = True + + class RecipeNote(BaseModel): title: str text: str @@ -15,34 +26,45 @@ class RecipeNote(BaseModel): orm_mode = True -class RecipeStep(BaseModel): +class RecipeStep(CamelModel): + title: Optional[str] = "" text: str class Config: orm_mode = True -class Nutrition(BaseModel): - calories: Optional[str] - fatContent: Optional[str] - fiberContent: Optional[str] - proteinContent: Optional[str] - sodiumContent: Optional[str] - sugarContent: Optional[str] +class RecipeAsset(CamelModel): + name: str + icon: str + file_name: Optional[str] class Config: orm_mode = True -class RecipeSummary(BaseModel): +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 RecipeSummary(CamelModel): id: Optional[int] name: str slug: Optional[str] = "" image: Optional[Any] description: Optional[str] - recipeCategory: Optional[List[str]] = [] - tags: Optional[List[str]] = [] + recipe_category: Optional[list[str]] = [] + tags: Optional[list[str]] = [] rating: Optional[int] class Config: @@ -52,26 +74,28 @@ class RecipeSummary(BaseModel): def getter_dict(_cls, name_orm: RecipeModel): return { **GetterDict(name_orm), - "recipeCategory": [x.name for x in name_orm.recipeCategory], + "recipe_category": [x.name for x in name_orm.recipe_category], "tags": [x.name for x in name_orm.tags], } class Recipe(RecipeSummary): - recipeYield: Optional[str] - recipeIngredient: Optional[list[str]] - recipeInstructions: Optional[list[RecipeStep]] + recipe_yield: Optional[str] + recipe_ingredient: Optional[list[str]] + recipe_instructions: Optional[list[RecipeStep]] nutrition: Optional[Nutrition] tools: Optional[list[str]] = [] - totalTime: Optional[str] = None - prepTime: Optional[str] = None - performTime: Optional[str] = None + total_time: Optional[str] = None + prep_time: Optional[str] = None + perform_time: Optional[str] = None # Mealie Specific - dateAdded: Optional[datetime.date] - notes: Optional[List[RecipeNote]] = [] - orgURL: Optional[str] + settings: Optional[RecipeSettings] + assets: Optional[list[RecipeAsset]] = [] + date_added: Optional[datetime.date] + notes: Optional[list[RecipeNote]] = [] + org_url: Optional[str] = Field(None, alias="orgURL") extras: Optional[dict] = {} class Config: @@ -81,8 +105,8 @@ class Recipe(RecipeSummary): def getter_dict(_cls, name_orm: RecipeModel): return { **GetterDict(name_orm), - "recipeIngredient": [x.ingredient for x in name_orm.recipeIngredient], - "recipeCategory": [x.name for x in name_orm.recipeCategory], + "recipe_ingredient": [x.ingredient 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}, @@ -93,22 +117,22 @@ class Recipe(RecipeSummary): "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", - "recipeYield": "4 Servings", - "recipeIngredient": [ + "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", ], - "recipeInstructions": [ + "recipe_instructions": [ { "text": "Season chicken with salt and pepper.", }, ], "slug": "chicken-and-rice-with-leeks-and-salsa-verde", "tags": ["favorite", "yummy!"], - "recipeCategory": ["Dinner", "Pasta"], + "recipe_category": ["Dinner", "Pasta"], "notes": [{"title": "Watch Out!", "text": "Prep the day before!"}], - "orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde", + "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!"}, } @@ -126,7 +150,7 @@ class Recipe(RecipeSummary): class AllRecipeRequest(BaseModel): - properties: List[str] + properties: list[str] limit: Optional[int] class Config: diff --git a/mealie/services/backups/imports.py b/mealie/services/backups/imports.py index 2b8bf33e5..3d35a843a 100644 --- a/mealie/services/backups/imports.py +++ b/mealie/services/backups/imports.py @@ -83,7 +83,7 @@ class ImportDatabase: del recipe_dict["categories"] try: del recipe_dict["_id"] - del recipe_dict["dateAdded"] + del recipe_dict["date_added"] except: pass # Migration from list to Object Type Data diff --git a/mealie/services/migrations/_migration_base.py b/mealie/services/migrations/_migration_base.py index ea5d4f7aa..15434d5b2 100644 --- a/mealie/services/migrations/_migration_base.py +++ b/mealie/services/migrations/_migration_base.py @@ -144,7 +144,7 @@ class MigrationBase(BaseModel): """Calls the rewrite_alias function and the Cleaner.clean function on a dictionary and returns the result unpacked into a Recipe object""" recipe_dict = self.rewrite_alias(recipe_dict) - recipe_dict = Cleaner.clean(recipe_dict, url=recipe_dict.get("orgURL", None)) + recipe_dict = Cleaner.clean(recipe_dict, url=recipe_dict.get("org_url", None)) return Recipe(**recipe_dict) diff --git a/mealie/services/migrations/nextcloud.py b/mealie/services/migrations/nextcloud.py index 48461e618..c8b589c93 100644 --- a/mealie/services/migrations/nextcloud.py +++ b/mealie/services/migrations/nextcloud.py @@ -37,7 +37,7 @@ class NextcloudDir: class NextcloudMigration(MigrationBase): key_aliases: Optional[list[MigrationAlias]] = [ MigrationAlias(key="tags", alias="keywords", func=helpers.split_by_comma), - MigrationAlias(key="orgURL", alias="url", func=None), + MigrationAlias(key="org_url", alias="url", func=None), ] diff --git a/tests/conftest.py b/tests/conftest.py index 36107adc6..74b34c497 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,16 @@ +from mealie.core.config import app_dirs, settings + +#! I don't like it either! +SQLITE_FILE = app_dirs.SQLITE_DIR.joinpath("test.db") +SQLITE_FILE.unlink(missing_ok=True) + +settings.SQLITE_FILE = SQLITE_FILE + import json import requests from fastapi.testclient import TestClient from mealie.app import app -from mealie.core.config import app_dirs, settings from mealie.db.db_setup import generate_session, sql_global_init from mealie.db.init_db import init_db from pytest import fixture @@ -12,10 +19,6 @@ from tests.app_routes import AppRoutes from tests.test_config import TEST_DATA from tests.utils.recipe_data import build_recipe_store, get_raw_no_image, get_raw_recipe -SQLITE_FILE = app_dirs.SQLITE_DIR.joinpath("test.db") -SQLITE_FILE.unlink(missing_ok=True) - - TestSessionLocal = sql_global_init(SQLITE_FILE, check_thread=False) init_db(TestSessionLocal())