From e109ac0f47b772878a52e476b6e7a7cc8c93c1d4 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 10 Dec 2021 19:48:06 -0900 Subject: [PATCH] feat(backend): :sparkles: add rename tag, tool, category support (#875) --- frontend/api/class-interfaces/tools.ts | 11 ++ .../Domain/Recipe/RecipeCardSection.vue | 10 +- .../Recipe/RecipeCategoryTagToolPage.vue | 106 ++++++++++++++++ frontend/layouts/default.vue | 5 + frontend/nuxt.config.js | 5 +- frontend/pages/admin/site-settings.vue | 2 +- frontend/pages/recipes/categories/_slug.vue | 88 ++++++++++++- frontend/pages/recipes/categories/index.vue | 74 +++-------- frontend/pages/recipes/tags/_slug.vue | 104 ++++++++++++--- frontend/pages/recipes/tags/index.vue | 81 ++++-------- frontend/pages/recipes/tools/_slug.vue | 119 ++++++++++++++++++ frontend/pages/recipes/tools/index.vue | 33 +++++ frontend/plugins/dark-mode.client.ts | 1 - .../data_access_layer/access_model_factory.py | 3 +- mealie/db/models/recipe/tool.py | 7 +- mealie/routes/categories/categories.py | 3 + mealie/routes/tools/__init__.py | 14 ++- mealie/schema/recipe/__init__.py | 1 + mealie/schema/recipe/recipe.py | 6 +- mealie/schema/recipe/recipe_category.py | 2 +- mealie/schema/recipe/recipe_tool.py | 15 +++ mealie/services/migrations/mealie_alpha.py | 20 +-- mealie/services/migrations/paprika.py | 2 +- mealie/services/recipe/recipe_tool_service.py | 2 +- .../user_recipe_tests/test_recipe_tools.py | 22 +++- 25 files changed, 573 insertions(+), 163 deletions(-) create mode 100644 frontend/components/Domain/Recipe/RecipeCategoryTagToolPage.vue create mode 100644 frontend/pages/recipes/tools/_slug.vue create mode 100644 frontend/pages/recipes/tools/index.vue diff --git a/frontend/api/class-interfaces/tools.ts b/frontend/api/class-interfaces/tools.ts index abc0e98a22cc..21762d99d192 100644 --- a/frontend/api/class-interfaces/tools.ts +++ b/frontend/api/class-interfaces/tools.ts @@ -1,4 +1,5 @@ import { BaseCRUDAPI } from "../_base"; +import { Recipe } from "~/types/api-types/recipe"; const prefix = "/api"; @@ -9,14 +10,24 @@ export interface CreateTool { export interface Tool extends CreateTool { id: number; + slug: string; +} + +export interface RecipeToolResponse extends Tool { + recipes: Recipe[]; } const routes = { tools: `${prefix}/tools`, toolsId: (id: string) => `${prefix}/tools/${id}`, + toolsSlug: (id: string) => `${prefix}/tools/slug/${id}`, }; export class ToolsApi extends BaseCRUDAPI { baseRoute: string = routes.tools; itemRoute = routes.toolsId; + + async byslug(slug: string) { + return await this.requests.get(routes.toolsSlug(slug)); + } } diff --git a/frontend/components/Domain/Recipe/RecipeCardSection.vue b/frontend/components/Domain/Recipe/RecipeCardSection.vue index 5e01bcd42051..4e57fcdcc6ca 100644 --- a/frontend/components/Domain/Recipe/RecipeCardSection.vue +++ b/frontend/components/Domain/Recipe/RecipeCardSection.vue @@ -1,10 +1,12 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/pages/recipes/categories/index.vue b/frontend/pages/recipes/categories/index.vue index 14c144639f64..8e837976a2fa 100644 --- a/frontend/pages/recipes/categories/index.vue +++ b/frontend/pages/recipes/categories/index.vue @@ -1,71 +1,35 @@ - + - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/pages/recipes/tags/_slug.vue b/frontend/pages/recipes/tags/_slug.vue index 8ae0182d3997..0569d25e2e9b 100644 --- a/frontend/pages/recipes/tags/_slug.vue +++ b/frontend/pages/recipes/tags/_slug.vue @@ -1,49 +1,125 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/pages/recipes/tags/index.vue b/frontend/pages/recipes/tags/index.vue index eb174e0c8370..45ed1cfe2760 100644 --- a/frontend/pages/recipes/tags/index.vue +++ b/frontend/pages/recipes/tags/index.vue @@ -1,71 +1,34 @@ - + - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/pages/recipes/tools/_slug.vue b/frontend/pages/recipes/tools/_slug.vue new file mode 100644 index 000000000000..0b9f3976d16d --- /dev/null +++ b/frontend/pages/recipes/tools/_slug.vue @@ -0,0 +1,119 @@ + + + + \ No newline at end of file diff --git a/frontend/pages/recipes/tools/index.vue b/frontend/pages/recipes/tools/index.vue new file mode 100644 index 000000000000..836a236612c2 --- /dev/null +++ b/frontend/pages/recipes/tools/index.vue @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/frontend/plugins/dark-mode.client.ts b/frontend/plugins/dark-mode.client.ts index 008d71c8da42..fb8396593786 100644 --- a/frontend/plugins/dark-mode.client.ts +++ b/frontend/plugins/dark-mode.client.ts @@ -2,7 +2,6 @@ import { useDark } from "@vueuse/core"; export default ({ $vuetify }: any) => { const isDark = useDark(); - console.log("isDark Plugin", isDark); if (isDark.value) { $vuetify.theme.dark = true; diff --git a/mealie/db/data_access_layer/access_model_factory.py b/mealie/db/data_access_layer/access_model_factory.py index cba951a622f5..4f8887e437b2 100644 --- a/mealie/db/data_access_layer/access_model_factory.py +++ b/mealie/db/data_access_layer/access_model_factory.py @@ -28,10 +28,9 @@ from mealie.schema.group.group_preferences import ReadGroupPreferences from mealie.schema.group.invite_token import ReadInviteToken from mealie.schema.group.webhook import ReadWebhook from mealie.schema.meal_plan.new_meal import ReadPlanEntry -from mealie.schema.recipe import Recipe, RecipeCategoryResponse, RecipeCommentOut, RecipeTagResponse +from mealie.schema.recipe import Recipe, RecipeCategoryResponse, RecipeCommentOut, RecipeTagResponse, RecipeTool from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit from mealie.schema.recipe.recipe_share_token import RecipeShareToken -from mealie.schema.recipe.recipe_tool import RecipeTool from mealie.schema.reports.reports import ReportEntryOut, ReportOut from mealie.schema.server import ServerTask from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut diff --git a/mealie/db/models/recipe/tool.py b/mealie/db/models/recipe/tool.py index 5d5506693de4..eb243b95b622 100644 --- a/mealie/db/models/recipe/tool.py +++ b/mealie/db/models/recipe/tool.py @@ -1,3 +1,4 @@ +from slugify import slugify from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Table, orm from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase @@ -14,10 +15,10 @@ recipes_to_tools = Table( class Tool(SqlAlchemyBase, BaseMixins): __tablename__ = "tools" name = Column(String, index=True, unique=True, nullable=False) + slug = Column(String, index=True, unique=True, nullable=False) on_hand = Column(Boolean, default=False) recipes = orm.relationship("RecipeModel", secondary=recipes_to_tools, back_populates="tools") @auto_init() - def __init__(self, name, on_hand, **_) -> None: - self.on_hand = on_hand - self.name = name + def __init__(self, name, **_) -> None: + self.slug = slugify(name) diff --git a/mealie/routes/categories/categories.py b/mealie/routes/categories/categories.py index 9037ad6a85a5..e1a807e04487 100644 --- a/mealie/routes/categories/categories.py +++ b/mealie/routes/categories/categories.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm.session import Session from mealie.core.dependencies import is_logged_in +from mealie.core.root_logger import get_logger from mealie.db.database import get_database from mealie.db.db_setup import generate_session from mealie.routes.routers import AdminAPIRouter, UserAPIRouter @@ -10,6 +11,7 @@ from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse public_router = APIRouter() user_router = UserAPIRouter() admin_router = AdminAPIRouter() +logger = get_logger() @public_router.get("") @@ -61,6 +63,7 @@ async def update_recipe_category(category: str, new_category: CategoryIn, sessio try: return db.categories.update(category, new_category.dict()) except Exception: + logger.exception("Failed to update category") raise HTTPException(status.HTTP_400_BAD_REQUEST) diff --git a/mealie/routes/tools/__init__.py b/mealie/routes/tools/__init__.py index b9470d52c9e4..00b9deed03a7 100644 --- a/mealie/routes/tools/__init__.py +++ b/mealie/routes/tools/__init__.py @@ -1,8 +1,18 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from mealie.schema.recipe.recipe_tool import RecipeToolResponse from mealie.services._base_http_service.router_factory import RouterFactory from mealie.services.recipe.recipe_tool_service import RecipeToolService router = APIRouter() -router.include_router(RouterFactory(RecipeToolService, prefix="/tools", tags=["Recipes: Tools"])) +tools_router = RouterFactory(RecipeToolService, prefix="/tools", tags=["Recipes: Tools"]) + + +@tools_router.get("/slug/{slug}") +async def Func(slug: str, tools_service: RecipeToolService = Depends(RecipeToolService.private)): + """Returns a recipe by slug.""" + return tools_service.db.tools.get_one(slug, "slug", override_schema=RecipeToolResponse) + + +router.include_router(tools_router) diff --git a/mealie/schema/recipe/__init__.py b/mealie/schema/recipe/__init__.py index e98e6f97d682..df5c118fb94d 100644 --- a/mealie/schema/recipe/__init__.py +++ b/mealie/schema/recipe/__init__.py @@ -3,4 +3,5 @@ from .recipe_category import * from .recipe_comments import * from .recipe_image_types import * from .recipe_ingredient import * +from .recipe_tool import * from .request_helpers import * diff --git a/mealie/schema/recipe/recipe.py b/mealie/schema/recipe/recipe.py index db1afd7e7897..f999f1e3edca 100644 --- a/mealie/schema/recipe/recipe.py +++ b/mealie/schema/recipe/recipe.py @@ -18,7 +18,6 @@ from .recipe_notes import RecipeNote from .recipe_nutrition import Nutrition from .recipe_settings import RecipeSettings from .recipe_step import RecipeStep -from .recipe_tool import RecipeTool app_dirs = get_app_dirs() @@ -35,6 +34,11 @@ class RecipeCategory(RecipeTag): pass +class RecipeTool(RecipeTag): + id: int = 0 + on_hand: bool = False + + class CreateRecipeByUrl(BaseModel): url: str diff --git a/mealie/schema/recipe/recipe_category.py b/mealie/schema/recipe/recipe_category.py index f7c8a010f777..92f37a931086 100644 --- a/mealie/schema/recipe/recipe_category.py +++ b/mealie/schema/recipe/recipe_category.py @@ -42,7 +42,7 @@ class RecipeTagResponse(RecipeCategoryResponse): pass -from .recipe import Recipe +from . import Recipe RecipeCategoryResponse.update_forward_refs() RecipeTagResponse.update_forward_refs() diff --git a/mealie/schema/recipe/recipe_tool.py b/mealie/schema/recipe/recipe_tool.py index a7eabcff4565..41cd486baabd 100644 --- a/mealie/schema/recipe/recipe_tool.py +++ b/mealie/schema/recipe/recipe_tool.py @@ -1,3 +1,5 @@ +from typing import List + from fastapi_camelcase import CamelModel @@ -8,6 +10,19 @@ class RecipeToolCreate(CamelModel): class RecipeTool(RecipeToolCreate): id: int + slug: str class Config: orm_mode = True + + +class RecipeToolResponse(RecipeTool): + recipes: List["Recipe"] = [] + + class Config: + orm_mode = True + + +from .recipe import Recipe + +RecipeToolResponse.update_forward_refs() diff --git a/mealie/services/migrations/mealie_alpha.py b/mealie/services/migrations/mealie_alpha.py index 660c56d3f128..766d4f82b242 100644 --- a/mealie/services/migrations/mealie_alpha.py +++ b/mealie/services/migrations/mealie_alpha.py @@ -63,7 +63,7 @@ class MealieAlphaMigrator(BaseMigrator): recipe_lookup: dict[str, Path] = {} recipes_as_dicts = [] - for x in temp_path.rglob("**/[!.]*.json"): + for x in temp_path.rglob("**/recipes/**/[!.]*.json"): if (y := MigrationReaders.json(x)) is not None: recipes_as_dicts.append(y) slug = y["slug"] @@ -76,12 +76,16 @@ class MealieAlphaMigrator(BaseMigrator): recipe_model_lookup = {x.slug: x for x in recipes} for slug, status in results: - if status: - model = recipe_model_lookup.get(slug) - dest_dir = model.directory - source_dir = recipe_lookup.get(slug) + if not status: + continue - if dest_dir.exists(): - shutil.rmtree(dest_dir) + model = recipe_model_lookup.get(slug) + dest_dir = model.directory + source_dir = recipe_lookup.get(slug) - shutil.copytree(source_dir, dest_dir) + if dest_dir.exists(): + shutil.rmtree(dest_dir) + + for dir in source_dir.iterdir(): + if dir.is_dir(): + shutil.copytree(dir, dest_dir / dir.name) diff --git a/mealie/services/migrations/paprika.py b/mealie/services/migrations/paprika.py index d0da688ea042..449abeb69b08 100644 --- a/mealie/services/migrations/paprika.py +++ b/mealie/services/migrations/paprika.py @@ -1,12 +1,12 @@ import base64 import io import json +import re import tempfile import zipfile from gzip import GzipFile from pathlib import Path -import regex as re from slugify import slugify from mealie.schema.recipe import RecipeNote diff --git a/mealie/services/recipe/recipe_tool_service.py b/mealie/services/recipe/recipe_tool_service.py index c24bd7047212..adf39f4d06da 100644 --- a/mealie/services/recipe/recipe_tool_service.py +++ b/mealie/services/recipe/recipe_tool_service.py @@ -2,7 +2,7 @@ from __future__ import annotations from functools import cached_property -from mealie.schema.recipe.recipe_tool import RecipeTool, RecipeToolCreate +from mealie.schema.recipe import RecipeTool, RecipeToolCreate from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins from mealie.services._base_http_service.http_services import UserHttpService from mealie.services.events import create_recipe_event diff --git a/tests/integration_tests/user_recipe_tests/test_recipe_tools.py b/tests/integration_tests/user_recipe_tests/test_recipe_tools.py index ab704eeb7ed4..338827a10c14 100644 --- a/tests/integration_tests/user_recipe_tests/test_recipe_tools.py +++ b/tests/integration_tests/user_recipe_tests/test_recipe_tools.py @@ -22,6 +22,9 @@ class Routes: class TestRecipeTool: id: int name: str + slug: str + on_hand: bool + recipes: list @pytest.fixture(scope="function") @@ -32,7 +35,15 @@ def tool(api_client: TestClient, unique_user: TestUser) -> TestRecipeTool: assert response.status_code == 201 - yield TestRecipeTool(id=response.json()["id"], name=data["name"]) + as_json = response.json() + + yield TestRecipeTool( + id=as_json["id"], + name=data["name"], + slug=as_json["slug"], + on_hand=as_json["onHand"], + recipes=[], + ) try: response = api_client.delete(Routes.item(response.json()["id"]), headers=unique_user.token) @@ -58,7 +69,12 @@ def test_read_tool(api_client: TestClient, tool: TestRecipeTool, unique_user: Te def test_update_tool(api_client: TestClient, tool: TestRecipeTool, unique_user: TestUser): - update_data = {"id": tool.id, "name": random_string(10)} + update_data = { + "id": tool.id, + "name": random_string(10), + "slug": tool.slug, + "on_hand": True, + } response = api_client.put(Routes.item(tool.id), json=update_data, headers=unique_user.token) assert response.status_code == 200 @@ -89,7 +105,7 @@ def test_recipe_tool_association(api_client: TestClient, tool: TestRecipeTool, u as_json = response.json() - as_json["tools"] = [{"id": tool.id, "name": tool.name}] + as_json["tools"] = [{"id": tool.id, "name": tool.name, "slug": tool.slug}] # Update Recipe response = api_client.put(Routes.recipe(slug), json=as_json, headers=unique_user.token)