-
- mdi-plus
+
+ mdi-plus
@@ -11,7 +11,7 @@
- Create a Category
+ {{ title }}
@@ -21,8 +21,8 @@
@@ -31,7 +31,7 @@
{{ $t("general.cancel") }}
-
+
{{ $t("general.create") }}
@@ -43,31 +43,56 @@
diff --git a/frontend/src/pages/SearchPage/index.vue b/frontend/src/pages/SearchPage/index.vue
index 50533313ac24..697f21de0238 100644
--- a/frontend/src/pages/SearchPage/index.vue
+++ b/frontend/src/pages/SearchPage/index.vue
@@ -28,7 +28,7 @@
Category Filter
-
Tag Filter
-
@@ -74,16 +76,14 @@
import Fuse from "fuse.js";
import RecipeCard from "@/components/Recipe/RecipeCard";
import CategorySidebar from "@/components/UI/CategorySidebar";
-import CategorySelector from "@/components/FormHelpers/CategorySelector";
-import TagSelector from "@/components/FormHelpers/TagSelector";
+import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
import FilterSelector from "./FilterSelector.vue";
export default {
components: {
RecipeCard,
CategorySidebar,
- CategorySelector,
- TagSelector,
+ CategoryTagSelector,
FilterSelector,
},
data() {
diff --git a/mealie/core/config.py b/mealie/core/config.py
index 363b329835d9..b876ae9bb93b 100644
--- a/mealie/core/config.py
+++ b/mealie/core/config.py
@@ -127,5 +127,3 @@ class AppSettings(BaseSettings):
settings = AppSettings()
-
-print(settings.dict())
diff --git a/mealie/db/database.py b/mealie/db/database.py
index b2543579852b..69e3830d1eb9 100644
--- a/mealie/db/database.py
+++ b/mealie/db/database.py
@@ -9,7 +9,8 @@ from mealie.db.models.users import User
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
from mealie.schema.meal import MealPlanInDB
from mealie.schema.recipe import Recipe
-from mealie.schema.settings import CustomPageOut, SiteSettings as SiteSettingsSchema
+from mealie.schema.settings import CustomPageOut
+from mealie.schema.settings import SiteSettings as SiteSettingsSchema
from mealie.schema.sign_up import SignUpOut
from mealie.schema.theme import SiteTheme
from mealie.schema.user import GroupInDB, UserInDB
diff --git a/mealie/db/models/recipe/recipe.py b/mealie/db/models/recipe/recipe.py
index e83134f99fee..28e512db0c9b 100644
--- a/mealie/db/models/recipe/recipe.py
+++ b/mealie/db/models/recipe/recipe.py
@@ -60,7 +60,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
@validates("name")
def validate_name(self, key, name):
- assert not name == ""
+ assert name != ""
return name
def __init__(
@@ -92,11 +92,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
self.image = image
self.recipeCuisine = recipeCuisine
- if self.nutrition:
- self.nutrition = Nutrition(**nutrition)
- else:
- self.nutrition = Nutrition()
-
+ self.nutrition = Nutrition(**nutrition) if self.nutrition else Nutrition()
self.tools = [Tool(tool=x) for x in tools] if tools else []
self.recipeYield = recipeYield
diff --git a/mealie/db/models/recipe/tag.py b/mealie/db/models/recipe/tag.py
index 1977055b9821..8c7e5ad46864 100644
--- a/mealie/db/models/recipe/tag.py
+++ b/mealie/db/models/recipe/tag.py
@@ -1,7 +1,7 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
-from mealie.db.models.model_base import SqlAlchemyBase
from fastapi.logger import logger
+from mealie.db.models.model_base import SqlAlchemyBase
from slugify import slugify
from sqlalchemy.orm import validates
@@ -25,7 +25,7 @@ class Tag(SqlAlchemyBase):
assert name != ""
return name
- def __init__(self, name) -> None:
+ def __init__(self, name, session=None) -> None:
self.name = name.strip()
self.slug = slugify(self.name)
diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py
index e696f87d9a2d..b02cec248546 100644
--- a/mealie/routes/recipe/recipe_crud_routes.py
+++ b/mealie/routes/recipe/recipe_crud_routes.py
@@ -7,7 +7,7 @@ from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.recipe import Recipe, RecipeURLIn
from mealie.schema.snackbar import SnackResponse
-from mealie.services.image.image import IMG_OPTIONS, delete_image, read_image, write_image
+from mealie.services.image.image import IMG_OPTIONS, delete_image, read_image, rename_image, write_image
from mealie.services.scraper.scraper import create_from_url
from sqlalchemy.orm.session import Session
@@ -61,6 +61,9 @@ def update_recipe(
recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict())
+ if recipe_slug != recipe.slug:
+ rename_image(original_slug=recipe_slug, new_slug=recipe.slug)
+
return recipe.slug
diff --git a/mealie/routes/recipe/tag_routes.py b/mealie/routes/recipe/tag_routes.py
index 11572316f82a..65dfbdc1916e 100644
--- a/mealie/routes/recipe/tag_routes.py
+++ b/mealie/routes/recipe/tag_routes.py
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends
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.category import RecipeTagResponse
+from mealie.schema.category import RecipeTagResponse, TagIn
from mealie.schema.snackbar import SnackResponse
from sqlalchemy.orm.session import Session
@@ -19,6 +19,14 @@ async def get_all_recipe_tags(session: Session = Depends(generate_session)):
""" Returns a list of available tags in the database """
return db.tags.get_all_limit_columns(session, ["slug", "name"])
+@router.post("")
+async def create_recipe_tag(
+ tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
+):
+ """ Creates a Tag in the database """
+
+ return db.tags.create(session, tag.dict())
+
@router.get("/{tag}", response_model=RecipeTagResponse)
def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)):
diff --git a/mealie/routes/users/crud.py b/mealie/routes/users/crud.py
index f15d5dd8caf9..774d867c3a9a 100644
--- a/mealie/routes/users/crud.py
+++ b/mealie/routes/users/crud.py
@@ -4,7 +4,7 @@ from datetime import timedelta
from fastapi import APIRouter, Depends, File, UploadFile
from fastapi.responses import FileResponse
from mealie.core import security
-from mealie.core.config import settings, app_dirs
+from mealie.core.config import app_dirs, settings
from mealie.core.security import get_password_hash, verify_password
from mealie.db.database import db
from mealie.db.db_setup import generate_session
diff --git a/mealie/run.sh b/mealie/run.sh
index a29b4b189619..12a5d142d674 100755
--- a/mealie/run.sh
+++ b/mealie/run.sh
@@ -3,24 +3,30 @@
# Get Reload Arg `run.sh reload` for dev server
ARG1=${1:-production}
-# Initialize Database Prerun
-python mealie/db/init_db.py
-python mealie/services/image/minify.py
+# Set Script Directory - Used for running the script from a different directory.
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
-## Migrations
+# # Initialize Database Prerun
+poetry run python $DIR/db/init_db.py
+poetry run python $DIR/services/image/minify.py
+
+# Migrations
# TODO
+ # Migrations
+ # Set Port from ENV Variable
-if [ "$ARG1" = "reload" ]
+if [[ "$ARG1" = "reload" ]]
then
- echo "Hot reload"
+ echo "Hot Reload!"
# Start API
uvicorn mealie.app:app --host 0.0.0.0 --port 9000 --reload
else
- echo "Production config"
+ echo "Production"
# Web Server
caddy start --config ./Caddyfile
# Start API
uvicorn mealie.app:app --host 0.0.0.0 --port 9000
-fi
\ No newline at end of file
+fi
+
diff --git a/mealie/schema/category.py b/mealie/schema/category.py
index 36c6d7ec1f1d..5e7f1c842a05 100644
--- a/mealie/schema/category.py
+++ b/mealie/schema/category.py
@@ -23,6 +23,10 @@ class RecipeCategoryResponse(CategoryBase):
schema_extra = {"example": {"id": 1, "name": "dinner", "recipes": [{}]}}
+class TagIn(CategoryIn):
+ pass
+
+
class TagBase(CategoryBase):
pass
diff --git a/mealie/services/image/image.py b/mealie/services/image/image.py
index d85536b9fcc5..04e01a492456 100644
--- a/mealie/services/image/image.py
+++ b/mealie/services/image/image.py
@@ -29,7 +29,6 @@ def read_image(recipe_slug: str, image_type: str = "original") -> Path:
Returns:
Path: [description]
"""
- print(image_type)
recipe_slug = recipe_slug.split(".")[0] # Incase of File Name
recipe_image_dir = app_dirs.IMG_DIR.joinpath(recipe_slug)
@@ -39,6 +38,18 @@ def read_image(recipe_slug: str, image_type: str = "original") -> Path:
return None
+def rename_image(original_slug, new_slug) -> Path:
+ current_path = app_dirs.IMG_DIR.joinpath(original_slug)
+ new_path = app_dirs.IMG_DIR.joinpath(new_slug)
+
+ try:
+ new_path = current_path.rename(new_path)
+ except FileNotFoundError:
+ logger.error(f"Image Directory {original_slug} Doesn't Exist")
+
+ return new_path
+
+
def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name:
try:
delete_image(recipe_slug)
diff --git a/mealie/services/image/minify.py b/mealie/services/image/minify.py
index 35215cf08822..024656a50314 100644
--- a/mealie/services/image/minify.py
+++ b/mealie/services/image/minify.py
@@ -1,8 +1,12 @@
import shutil
from pathlib import Path
+from fastapi.logger import logger
from mealie.core.config import app_dirs
-from PIL import Image, UnidentifiedImageError
+from mealie.db.database import db
+from mealie.db.db_setup import create_session
+from PIL import Image
+from sqlalchemy.orm.session import Session
def minify_image(image_file: Path, min_dest: Path, tiny_dest: Path):
@@ -24,7 +28,7 @@ def minify_image(image_file: Path, min_dest: Path, tiny_dest: Path):
tiny_image = crop_center(img)
tiny_image.save(tiny_dest, quality=70)
- except:
+ except Exception:
shutil.copy(image_file, min_dest)
shutil.copy(image_file, tiny_dest)
@@ -59,6 +63,28 @@ def move_all_images():
image_file.rename(new_folder.joinpath(f"original{image_file.suffix}"))
+def validate_slugs_in_database(session: Session = None):
+ def check_image_path(image_name: str, slug_path: str) -> bool:
+ existing_path: Path = app_dirs.IMG_DIR.joinpath(image_name)
+ slug_path: Path = app_dirs.IMG_DIR.joinpath(slug_path)
+
+ if existing_path.is_dir():
+ slug_path.rename(existing_path)
+ else:
+ logger.info("No Image Found")
+
+ session = session or create_session()
+ all_recipes = db.recipes.get_all(session)
+
+ slugs_and_images = [(x.slug, x.image) for x in all_recipes]
+
+ for slug, image in slugs_and_images:
+ image_slug = image.split(".")[0] # Remove Extension
+ if slug != image_slug:
+ logger.info(f"{slug}, Doesn't Match '{image_slug}'")
+ check_image_path(image, slug)
+
+
def migrate_images():
print("Checking for Images to Minify...")
@@ -77,10 +103,11 @@ def migrate_images():
org_size = sizeof_fmt(image.stat().st_size)
dest_size = sizeof_fmt(min_dest.stat().st_size)
tiny_size = sizeof_fmt(tiny_dest.stat().st_size)
- print(f"{image.name} Minified: {org_size} -> {dest_size} -> {tiny_size}")
+ logger.info(f"{image.name} Minified: {org_size} -> {dest_size} -> {tiny_size}")
- print("Finished Minification Check")
+ logger.info("Finished Minification Check")
if __name__ == "__main__":
migrate_images()
+ validate_slugs_in_database()
diff --git a/mealie/services/migrations/chowdown.py b/mealie/services/migrations/chowdown.py
index 0f09b4fa685d..b1c0bb7de8d3 100644
--- a/mealie/services/migrations/chowdown.py
+++ b/mealie/services/migrations/chowdown.py
@@ -6,6 +6,7 @@ from fastapi.logger import logger
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.schema.recipe import Recipe
+from mealie.services.image.minify import migrate_images
from mealie.utils.unzip import unpack_zip
from sqlalchemy.orm.session import Session
@@ -89,4 +90,5 @@ def chowdown_migrate(session: Session, zip_file: Path):
failed_images.append(image.name)
report = {"successful": successful_recipes, "failed": failed_recipes}
+ migrate_images()
return report
diff --git a/mealie/services/migrations/nextcloud.py b/mealie/services/migrations/nextcloud.py
index dd20821b03b1..08bf93966072 100644
--- a/mealie/services/migrations/nextcloud.py
+++ b/mealie/services/migrations/nextcloud.py
@@ -7,6 +7,7 @@ from pathlib import Path
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.schema.recipe import Recipe
+from mealie.services.image import minify
from mealie.services.scraper.cleaner import Cleaner
@@ -23,39 +24,43 @@ def process_selection(selection: Path) -> Path:
return None
+def clean_nextcloud_tags(nextcloud_tags: str):
+ if not isinstance(nextcloud_tags, str):
+ return None
+
+ return [x.title().lstrip() for x in nextcloud_tags.split(",") if x != ""]
+
+
def import_recipes(recipe_dir: Path) -> Recipe:
image = False
+
for file in recipe_dir.glob("full.*"):
image = file
+ break
for file in recipe_dir.glob("*.json"):
recipe_file = file
+ break
with open(recipe_file, "r") as f:
recipe_dict = json.loads(f.read())
recipe_data = Cleaner.clean(recipe_dict)
- image_name = None
- if image:
- image_name = recipe_data["slug"] + image.suffix
- recipe_data["image"] = image_name
- else:
- recipe_data["image"] = "none"
+ image_name = recipe_data["slug"]
+ recipe_data["image"] = recipe_data["slug"]
+ recipe_data["tags"] = clean_nextcloud_tags(recipe_data.get("keywords"))
recipe = Recipe(**recipe_data)
if image:
- shutil.copy(image, app_dirs.IMG_DIR.joinpath(image_name))
+ shutil.copy(image, app_dirs.IMG_DIR.joinpath(image_name + image.suffix))
return recipe
def prep():
- try:
- shutil.rmtree(app_dirs.TEMP_DIR)
- except:
- pass
+ shutil.rmtree(app_dirs.TEMP_DIR, ignore_errors=True)
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
@@ -80,11 +85,13 @@ def migrate(session, selection: str):
db.recipes.create(session, recipe.dict())
successful_imports.append(recipe.name)
- except:
+ except Exception:
+ session.rollback()
logging.error(f"Failed Nextcloud Import: {dir.name}")
logging.exception("")
failed_imports.append(dir.name)
cleanup()
+ minify.migrate_images()
return {"successful": successful_imports, "failed": failed_imports}
diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py
index d434e12adfff..c2641b428c43 100644
--- a/tests/unit_tests/test_config.py
+++ b/tests/unit_tests/test_config.py
@@ -29,7 +29,7 @@ def test_non_default_settings(monkeypatch):
monkeypatch.setenv("DEFAULT_GROUP", "Test Group")
monkeypatch.setenv("DEFAULT_PASSWORD", "Test Password")
monkeypatch.setenv("API_PORT", "8000")
- monkeypatch.setenv("API_DOCS", False)
+ monkeypatch.setenv("API_DOCS", 'False')
app_settings = AppSettings()
diff --git a/tests/unit_tests/test_nextcloud.py b/tests/unit_tests/test_nextcloud.py
index 3e446e76c695..bb773fc39078 100644
--- a/tests/unit_tests/test_nextcloud.py
+++ b/tests/unit_tests/test_nextcloud.py
@@ -1,3 +1,4 @@
+import shutil
from pathlib import Path
import pytest
@@ -36,4 +37,4 @@ def test_zip_extraction(file_name: str, final_path: Path):
def test_nextcloud_migration(recipe_dir: Path):
recipe = import_recipes(recipe_dir)
assert isinstance(recipe, Recipe)
- app_dirs.IMG_DIR.joinpath(recipe.image).unlink(missing_ok=True)
+ shutil.rmtree(app_dirs.IMG_DIR.joinpath(recipe.image), ignore_errors=True)