diff --git a/frontend/api/class-interfaces/cookbooks.ts b/frontend/api/class-interfaces/cookbooks.ts index 2a2dc6e18e7e..aa2e884575ce 100644 --- a/frontend/api/class-interfaces/cookbooks.ts +++ b/frontend/api/class-interfaces/cookbooks.ts @@ -11,6 +11,7 @@ export interface CreateCookBook { export interface CookBook extends CreateCookBook { id: number; slug: string; + description: string; position: number; group_id: number; categories: Category[] | CategoryBase[]; diff --git a/frontend/composables/use-cookbooks.ts b/frontend/composables/use-cookbooks.ts index ea8de86f0db9..dad6d41cd8e2 100644 --- a/frontend/composables/use-cookbooks.ts +++ b/frontend/composables/use-cookbooks.ts @@ -5,6 +5,22 @@ import { CookBook } from "~/api/class-interfaces/cookbooks"; let cookbookStore: Ref | null = null; +export const useCookbook = function () { + function getOne(id: string | number) { + const api = useApiSingleton(); + + const units = useAsync(async () => { + const { data } = await api.cookbooks.getOne(id); + + return data; + }, useAsyncKey()); + + return units; + } + + return { getOne }; +}; + export const useCookbooks = function () { const api = useApiSingleton(); const loading = ref(false); @@ -45,10 +61,10 @@ export const useCookbooks = function () { loading.value = true; const { data } = await api.cookbooks.createOne({ // @ts-ignore. I"m thinking this will always be defined. - name: "New Cookbook" + String(cookbookStore?.value?.length + 1 || 1), + name: "Cookbook " + String(cookbookStore?.value?.length + 1 || 1), }); if (data && cookbookStore?.value) { - cookbookStore.value.unshift(data); + cookbookStore.value.push(data); } else { this.refreshAll(); } diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index 2e273bf7dc6e..bd945a881bf9 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -402,7 +402,9 @@ }, "sidebar": { "all-recipes": "All Recipes", + "backups": "Backups", "categories": "Categories", + "cookbooks": "Cookbooks", "dashboard": "Dashboard", "home-page": "Home Page", "manage-users": "Manage Users", @@ -411,8 +413,7 @@ "search": "Search", "site-settings": "Site Settings", "tags": "Tags", - "toolbox": "Toolbox", - "backups": "Backups" + "toolbox": "Toolbox" }, "signup": { "error-signing-up": "Error Signing Up", diff --git a/frontend/layouts/admin.vue b/frontend/layouts/admin.vue index fedbaabb804b..f365b4a4e941 100644 --- a/frontend/layouts/admin.vue +++ b/frontend/layouts/admin.vue @@ -58,8 +58,8 @@ export default defineComponent({ }, { icon: this.$globals.icons.pages, - to: "/user/group/pages", - title: this.$t("settings.pages"), + to: "/user/group/cookbooks", + title: this.$t("sidebar.cookbooks"), }, ], adminLinks: [ diff --git a/frontend/pages/cookbooks/_slug.vue b/frontend/pages/cookbooks/_slug.vue new file mode 100644 index 000000000000..a8bce5ac407b --- /dev/null +++ b/frontend/pages/cookbooks/_slug.vue @@ -0,0 +1,49 @@ + + + + + \ No newline at end of file diff --git a/frontend/pages/user/group/pages.vue b/frontend/pages/user/group/cookbooks.vue similarity index 81% rename from frontend/pages/user/group/pages.vue rename to frontend/pages/user/group/cookbooks.vue index 1f1e3b77bbfa..3c7713f270b8 100644 --- a/frontend/pages/user/group/pages.vue +++ b/frontend/pages/user/group/cookbooks.vue @@ -6,27 +6,33 @@ - {{ cookbook.name }} +
+ + {{ $globals.icons.pages }} + + {{ cookbook.name }} +
+ - +
diff --git a/mealie/db/models/cookbook.py b/mealie/db/models/cookbook.py index 0e1cf187685f..00106c3ec3c6 100644 --- a/mealie/db/models/cookbook.py +++ b/mealie/db/models/cookbook.py @@ -10,6 +10,7 @@ class CookBook(SqlAlchemyBase, BaseMixins): id = Column(Integer, primary_key=True) position = Column(Integer, nullable=False) name = Column(String, nullable=False) + description = Column(String, default="") slug = Column(String, nullable=False) categories = orm.relationship(Category, secondary=cookbooks_to_categories, single_parent=True) diff --git a/mealie/routes/backup_routes.py b/mealie/routes/backup_routes.py index bbcbed8d50a5..b41c4d678277 100644 --- a/mealie/routes/backup_routes.py +++ b/mealie/routes/backup_routes.py @@ -44,7 +44,6 @@ def export_database(background_tasks: BackgroundTasks, data: BackupJob, session: templates=data.templates, export_recipes=data.options.recipes, export_settings=data.options.settings, - export_pages=data.options.pages, export_users=data.options.users, export_groups=data.options.groups, export_notifications=data.options.notifications, @@ -92,7 +91,6 @@ def import_database( archive=import_data.name, import_recipes=import_data.recipes, import_settings=import_data.settings, - import_pages=import_data.pages, import_users=import_data.users, import_groups=import_data.groups, force_import=import_data.force, diff --git a/mealie/routes/groups/cookbooks.py b/mealie/routes/groups/cookbooks.py index 52b339a5f24d..d6dba1b0692e 100644 --- a/mealie/routes/groups/cookbooks.py +++ b/mealie/routes/groups/cookbooks.py @@ -1,7 +1,7 @@ from fastapi import Depends from mealie.routes.routers import UserAPIRouter -from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook +from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook from mealie.services.cookbook import CookbookService user_router = UserAPIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"]) @@ -28,7 +28,7 @@ def update_many(data: list[ReadCookBook], cb_service: CookbookService = Depends( return cb_service.update_many(data) -@user_router.get("/{id}", response_model=ReadCookBook) +@user_router.get("/{id}", response_model=RecipeCookBook) def get_cookbook(cb_service: CookbookService = Depends(CookbookService.write_existing)): """ Get cookbook from the Database """ # Get Item diff --git a/mealie/routes/site_settings/__init__.py b/mealie/routes/site_settings/__init__.py index 7eda166cdb67..432926070909 100644 --- a/mealie/routes/site_settings/__init__.py +++ b/mealie/routes/site_settings/__init__.py @@ -1,10 +1,8 @@ from fastapi import APIRouter -from . import custom_pages, site_settings +from . import site_settings settings_router = APIRouter() -settings_router.include_router(custom_pages.public_router) -settings_router.include_router(custom_pages.admin_router) settings_router.include_router(site_settings.public_router) settings_router.include_router(site_settings.admin_router) diff --git a/mealie/routes/site_settings/custom_pages.py b/mealie/routes/site_settings/custom_pages.py deleted file mode 100644 index 8d0f8d8cc09e..000000000000 --- a/mealie/routes/site_settings/custom_pages.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Union - -from fastapi import APIRouter, Depends -from sqlalchemy.orm.session import Session - -from mealie.db.database import db -from mealie.db.db_setup import generate_session -from mealie.routes.routers import AdminAPIRouter -from mealie.schema.admin import CustomPageBase, CustomPageOut - -public_router = APIRouter(prefix="/api/site-settings/custom-pages", tags=["Settings"]) -admin_router = AdminAPIRouter(prefix="/api/site-settings/custom-pages", tags=["Settings"]) - - -@public_router.get("") -def get_custom_pages(session: Session = Depends(generate_session)): - """ Returns the sites custom pages """ - - return db.custom_pages.get_all(session) - - -@admin_router.post("") -async def create_new_page( - new_page: CustomPageBase, - session: Session = Depends(generate_session), -): - """ Creates a new Custom Page """ - - db.custom_pages.create(session, new_page.dict()) - - -@admin_router.put("") -async def update_multiple_pages(pages: list[CustomPageOut], session: Session = Depends(generate_session)): - """ Update multiple custom pages """ - for page in pages: - db.custom_pages.update(session, page.id, page.dict()) - - -@public_router.get("/{id}") -async def get_single_page( - id: Union[int, str], - session: Session = Depends(generate_session), -): - """ Removes a custom page from the database """ - if isinstance(id, int): - return db.custom_pages.get(session, id) - elif isinstance(id, str): - return db.custom_pages.get(session, id, "slug") - - -@admin_router.put("/{id}") -async def update_single_page( - data: CustomPageOut, - id: int, - session: Session = Depends(generate_session), -): - """ Removes a custom page from the database """ - - return db.custom_pages.update(session, id, data.dict()) - - -@admin_router.delete("/{id}") -async def delete_custom_page( - id: int, - session: Session = Depends(generate_session), -): - """ Removes a custom page from the database """ - - db.custom_pages.delete(session, id) - return diff --git a/mealie/schema/admin/backup.py b/mealie/schema/admin/backup.py index ab3859d4cb7f..4ce669cfed18 100644 --- a/mealie/schema/admin/backup.py +++ b/mealie/schema/admin/backup.py @@ -7,7 +7,6 @@ from pydantic import BaseModel class BackupOptions(BaseModel): recipes: bool = True settings: bool = True - pages: bool = True themes: bool = True groups: bool = True users: bool = True diff --git a/mealie/schema/cookbook/cookbook.py b/mealie/schema/cookbook/cookbook.py index adffcba0e56b..4e3aa7fdd600 100644 --- a/mealie/schema/cookbook/cookbook.py +++ b/mealie/schema/cookbook/cookbook.py @@ -2,11 +2,12 @@ from fastapi_camelcase import CamelModel from pydantic import validator from slugify import slugify -from ..recipe.recipe_category import CategoryBase +from ..recipe.recipe_category import CategoryBase, RecipeCategoryResponse class CreateCookBook(CamelModel): name: str + description: str = "" slug: str = None position: int = 1 categories: list[CategoryBase] = [] @@ -36,3 +37,11 @@ class ReadCookBook(UpdateCookBook): class Config: orm_mode = True + + +class RecipeCookBook(ReadCookBook): + group_id: int + categories: list[RecipeCategoryResponse] + + class Config: + orm_mode = True diff --git a/mealie/schema/recipe/recipe_category.py b/mealie/schema/recipe/recipe_category.py index 7a8b36997254..f7c8a010f777 100644 --- a/mealie/schema/recipe/recipe_category.py +++ b/mealie/schema/recipe/recipe_category.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List from fastapi_camelcase import CamelModel from pydantic.utils import GetterDict @@ -23,9 +23,10 @@ class CategoryBase(CategoryIn): class RecipeCategoryResponse(CategoryBase): - recipes: Optional[List["Recipe"]] + recipes: List["Recipe"] = [] class Config: + orm_mode = True schema_extra = {"example": {"id": 1, "name": "dinner", "recipes": [{}]}} diff --git a/mealie/services/backups/exports.py b/mealie/services/backups/exports.py index 44d8fcf7d677..7ed03942299e 100644 --- a/mealie/services/backups/exports.py +++ b/mealie/services/backups/exports.py @@ -108,7 +108,6 @@ def backup_all( templates=None, export_recipes=True, export_settings=True, - export_pages=True, export_users=True, export_groups=True, export_notifications=True, @@ -136,10 +135,6 @@ def backup_all( all_settings = db.settings.get_all(session) db_export.export_items(all_settings, "settings") - if export_pages: - all_pages = db.custom_pages.get_all(session) - db_export.export_items(all_pages, "pages") - if export_notifications: all_notifications = db.event_notifications.get_all(session) db_export.export_items(all_notifications, "notifications") diff --git a/mealie/services/backups/imports.py b/mealie/services/backups/imports.py index d46a1a475088..daf670c86bdf 100644 --- a/mealie/services/backups/imports.py +++ b/mealie/services/backups/imports.py @@ -11,8 +11,6 @@ from mealie.core.config import app_dirs from mealie.db.database import db from mealie.schema.admin import ( CommentImport, - CustomPageImport, - CustomPageOut, GroupImport, NotificationImport, RecipeImport, @@ -189,19 +187,6 @@ class ImportDatabase: return [import_status] - def import_pages(self): - pages_file = self.import_dir.joinpath("pages", "pages.json") - pages = ImportDatabase.read_models_file(pages_file, CustomPageOut) - - page_imports = [] - for page in pages: - import_stats = self.import_model( - db_table=db.custom_pages, model=page, return_model=CustomPageImport, name_attr="name", search_key="slug" - ) - page_imports.append(import_stats) - - return page_imports - def import_groups(self): groups_file = self.import_dir.joinpath("groups", "groups.json") groups = ImportDatabase.read_models_file(groups_file, UpdateGroup) @@ -326,7 +311,6 @@ def import_database( archive, import_recipes=True, import_settings=True, - import_pages=True, import_users=True, import_groups=True, import_notifications=True, @@ -343,10 +327,6 @@ def import_database( if import_settings: settings_report = import_session.import_settings() - page_report = [] - if import_pages: - page_report = import_session.import_pages() - group_report = [] if import_groups: group_report = import_session.import_groups() @@ -367,7 +347,6 @@ def import_database( return { "recipeImports": recipe_report, "settingsImports": settings_report, - "pageImports": page_report, "groupImports": group_report, "userImports": user_report, "notificationImports": notification_report, diff --git a/mealie/services/cookbook/cookbook_service.py b/mealie/services/cookbook/cookbook_service.py index 2faef9c2f77c..3adc7452c324 100644 --- a/mealie/services/cookbook/cookbook_service.py +++ b/mealie/services/cookbook/cookbook_service.py @@ -1,14 +1,16 @@ +from __future__ import annotations + from fastapi import HTTPException, status from mealie.core.root_logger import get_logger -from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, SaveCookBook +from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook from mealie.services.base_http_service.base_http_service import BaseHttpService from mealie.services.events import create_group_event logger = get_logger(module=__name__) -class CookbookService(BaseHttpService[str, str]): +class CookbookService(BaseHttpService[int, str]): """ Class Methods: `read_existing`: Reads an existing recipe from the database. @@ -39,8 +41,17 @@ class CookbookService(BaseHttpService[str, str]): if self.cookbook.group_id != self.group_id: raise HTTPException(status.HTTP_403_FORBIDDEN) - def populate_cookbook(self, id): - self.cookbook = self.db.cookbooks.get(self.session, id) + def populate_cookbook(self, id: int | str): + try: + id = int(id) + except Exception: + pass + + if isinstance(id, int): + self.cookbook = self.db.cookbooks.get_one(self.session, id, override_schema=RecipeCookBook) + + else: + self.cookbook = self.db.cookbooks.get_one(self.session, id, key="slug", override_schema=RecipeCookBook) def get_all(self) -> list[ReadCookBook]: items = self.db.cookbooks.get(self.session, self.group_id, "group_id", limit=999) diff --git a/mealie/services/scraper/ingredient_nlp/utils.py b/mealie/services/scraper/ingredient_nlp/utils.py index 4777cf1290b9..7146e96d5e17 100644 --- a/mealie/services/scraper/ingredient_nlp/utils.py +++ b/mealie/services/scraper/ingredient_nlp/utils.py @@ -134,7 +134,9 @@ def insideParenthesis(token, tokens): return True else: line = " ".join(tokens) - return re.match(r".*\(.*" + re.escape(token) + ".*\).*", line) is not None + return ( + re.match(r".*\(.*" + re.escape(token) + ".*\).*", line) is not None # noqa: W605 - invalid dscape sequence + ) def displayIngredient(ingredient): @@ -222,7 +224,7 @@ def import_data(lines): # turn B-NAME/123 back into "name" tag, confidence = re.split(r"/", columns[-1], 1) - tag = re.sub("^[BI]\-", "", tag).lower() + tag = re.sub("^[BI]\-", "", tag).lower() # noqa: W605 - invalid dscape sequence # ---- DISPLAY ---- # build a structure which groups each token by its tag, so we can diff --git a/tests/app_routes.py b/tests/app_routes.py index c11420371077..345e353c6a5d 100644 --- a/tests/app_routes.py +++ b/tests/app_routes.py @@ -28,7 +28,7 @@ class AppRoutes: self.meal_plans_this_week = "/api/meal-plans/this-week" self.meal_plans_today = "/api/meal-plans/today" self.meal_plans_today_image = "/api/meal-plans/today/image" - self.site_settings_custom_pages = "/api/site-settings/custom-pages" + self.group_cookbook = "/api/groups/cookbooks" self.site_settings = "/api/site-settings" self.site_settings_webhooks_test = "/api/site-settings/webhooks/test" self.themes = "/api/themes" @@ -95,8 +95,8 @@ class AppRoutes: def meal_plans_id_shopping_list(self, id): return f"{self.prefix}/meal-plans/{id}/shopping-list" - def site_settings_custom_pages_id(self, id): - return f"{self.prefix}/site-settings/custom-pages/{id}" + def group_cookbook_id(self, id): + return f"{self.prefix}/groups/cookbooks/{id}" def themes_id(self, id): return f"{self.prefix}/themes/{id}" diff --git a/tests/integration_tests/_test_custom_pages.py b/tests/integration_tests/_test_custom_pages.py deleted file mode 100644 index 265ec0b190ff..000000000000 --- a/tests/integration_tests/_test_custom_pages.py +++ /dev/null @@ -1,44 +0,0 @@ -import json - -import pytest -from fastapi.testclient import TestClient - -from tests.app_routes import AppRoutes - - -@pytest.fixture() -def page_data(): - return {"name": "My New Page", "position": 0, "categories": []} - - -def test_create_page(api_client: TestClient, api_routes: AppRoutes, admin_token, page_data): - response = api_client.post(api_routes.site_settings_custom_pages, json=page_data, headers=admin_token) - - assert response.status_code == 200 - - -def test_read_page(api_client: TestClient, api_routes: AppRoutes, page_data): - response = api_client.get(api_routes.site_settings_custom_pages_id(1)) - - page_data["id"] = 1 - page_data["slug"] = "my-new-page" - - assert json.loads(response.text) == page_data - - -def test_update_page(api_client: TestClient, api_routes: AppRoutes, page_data, admin_token): - page_data["id"] = 1 - page_data["name"] = "My New Name" - response = api_client.put(api_routes.site_settings_custom_pages_id(1), json=page_data, headers=admin_token) - - assert response.status_code == 200 - - -def test_delete_page(api_client: TestClient, api_routes: AppRoutes, admin_token): - response = api_client.delete(api_routes.site_settings_custom_pages_id(1), headers=admin_token) - - assert response.status_code == 200 - - response = api_client.get(api_routes.site_settings_custom_pages_id(1)) - - assert json.loads(response.text) is None diff --git a/tests/integration_tests/test_cookbooks.py b/tests/integration_tests/test_cookbooks.py new file mode 100644 index 000000000000..e0312a86d67a --- /dev/null +++ b/tests/integration_tests/test_cookbooks.py @@ -0,0 +1,43 @@ +import json + +import pytest +from fastapi.testclient import TestClient + +from tests.app_routes import AppRoutes + + +@pytest.fixture() +def page_data(): + return {"name": "My New Page", "description": "", "position": 0, "categories": [], "groupId": 1} + + +def test_create_cookbook(api_client: TestClient, api_routes: AppRoutes, admin_token, page_data): + response = api_client.post(api_routes.group_cookbook, json=page_data, headers=admin_token) + + assert response.status_code == 200 + + +def test_read_cookbook(api_client: TestClient, api_routes: AppRoutes, page_data, admin_token): + response = api_client.get(api_routes.group_cookbook_id(1), headers=admin_token) + + page_data["id"] = 1 + page_data["slug"] = "my-new-page" + + assert json.loads(response.text) == page_data + + +def test_update_cookbook(api_client: TestClient, api_routes: AppRoutes, page_data, admin_token): + page_data["id"] = 1 + page_data["name"] = "My New Name" + response = api_client.put(api_routes.group_cookbook_id(1), json=page_data, headers=admin_token) + + assert response.status_code == 200 + + +def test_delete_cookbook(api_client: TestClient, api_routes: AppRoutes, admin_token): + response = api_client.delete(api_routes.group_cookbook_id(1), headers=admin_token) + + assert response.status_code == 200 + + response = api_client.get(api_routes.group_cookbook_id(1), headers=admin_token) + assert response.status_code == 404 diff --git a/tests/integration_tests/test_import_routes.py b/tests/integration_tests/test_import_routes.py index 643e90703718..e419b76bd251 100644 --- a/tests/integration_tests/test_import_routes.py +++ b/tests/integration_tests/test_import_routes.py @@ -15,7 +15,6 @@ def backup_data(): "settings": False, # ! Broken "groups": True, "users": True, - "pages": True, }