mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-08 17:03:11 -05:00
feature/category-tag-crud (#354)
* update tag route * search.and * offset for mobile * relative imports * get settings * new page * category/tag CRUD * bulk assign frontend * Bulk assign * debounce search * remove dev data * recipe store refactor * fix mobile view * fix failing tests * commit test data Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
from logging import getLogger
|
||||
|
||||
from mealie.db.db_base import BaseDocument
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.mealplan import MealPlanModel
|
||||
@@ -16,12 +18,13 @@ from mealie.schema.theme import SiteTheme
|
||||
from mealie.schema.user import GroupInDB, UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
logger = getLogger()
|
||||
|
||||
|
||||
class _Recipes(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "slug"
|
||||
self.sql_model: RecipeModel = RecipeModel
|
||||
self.orm_mode = True
|
||||
self.schema: Recipe = Recipe
|
||||
|
||||
def update_image(self, session: Session, slug: str, extension: str = None) -> str:
|
||||
@@ -36,23 +39,26 @@ class _Categories(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "slug"
|
||||
self.sql_model = Category
|
||||
self.orm_mode = True
|
||||
self.schema = RecipeCategoryResponse
|
||||
|
||||
def get_empty(self, session: Session):
|
||||
return session.query(Category).filter(~Category.recipes.any()).all()
|
||||
|
||||
|
||||
class _Tags(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "slug"
|
||||
self.sql_model = Tag
|
||||
self.orm_mode = True
|
||||
self.schema = RecipeTagResponse
|
||||
|
||||
def get_empty(self, session: Session):
|
||||
return session.query(Tag).filter(~Tag.recipes.any()).all()
|
||||
|
||||
|
||||
class _Meals(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "uid"
|
||||
self.sql_model = MealPlanModel
|
||||
self.orm_mode = True
|
||||
self.schema = MealPlanInDB
|
||||
|
||||
|
||||
@@ -60,7 +66,6 @@ class _Settings(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "id"
|
||||
self.sql_model = SiteSettings
|
||||
self.orm_mode = True
|
||||
self.schema = SiteSettingsSchema
|
||||
|
||||
|
||||
@@ -68,7 +73,6 @@ class _Themes(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "name"
|
||||
self.sql_model = SiteThemeModel
|
||||
self.orm_mode = True
|
||||
self.schema = SiteTheme
|
||||
|
||||
|
||||
@@ -76,7 +80,6 @@ class _Users(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "id"
|
||||
self.sql_model = User
|
||||
self.orm_mode = True
|
||||
self.schema = UserInDB
|
||||
|
||||
def update_password(self, session, id, password: str):
|
||||
@@ -91,7 +94,6 @@ class _Groups(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "id"
|
||||
self.sql_model = Group
|
||||
self.orm_mode = True
|
||||
self.schema = GroupInDB
|
||||
|
||||
def get_meals(self, session: Session, match_value: str, match_key: str = "name") -> list[MealPlanInDB]:
|
||||
@@ -116,7 +118,6 @@ class _SignUps(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "token"
|
||||
self.sql_model = SignUp
|
||||
self.orm_mode = True
|
||||
self.schema = SignUpOut
|
||||
|
||||
|
||||
@@ -124,7 +125,6 @@ class _CustomPages(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "id"
|
||||
self.sql_model = CustomPage
|
||||
self.orm_mode = True
|
||||
self.schema = CustomPageOut
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ class BaseDocument:
|
||||
self.primary_key: str
|
||||
self.store: str
|
||||
self.sql_model: SqlAlchemyBase
|
||||
self.orm_mode = False
|
||||
self.schema: BaseModel
|
||||
|
||||
# TODO: Improve Get All Query Functionality
|
||||
@@ -138,3 +137,4 @@ class BaseDocument:
|
||||
|
||||
session.delete(result)
|
||||
session.commit()
|
||||
|
||||
|
||||
@@ -11,28 +11,28 @@ site_settings2categories = sa.Table(
|
||||
"site_settings2categoories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("sidebar_id", sa.Integer, sa.ForeignKey("site_settings.id")),
|
||||
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
|
||||
sa.Column("category_id", sa.String, sa.ForeignKey("categories.id")),
|
||||
)
|
||||
|
||||
group2categories = sa.Table(
|
||||
"group2categories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("group_id", sa.Integer, sa.ForeignKey("groups.id")),
|
||||
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
|
||||
sa.Column("category_id", sa.String, sa.ForeignKey("categories.id")),
|
||||
)
|
||||
|
||||
recipes2categories = sa.Table(
|
||||
"recipes2categories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
||||
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
|
||||
sa.Column("category_id", sa.String, sa.ForeignKey("categories.id")),
|
||||
)
|
||||
|
||||
custom_pages2categories = sa.Table(
|
||||
"custom_pages2categories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("custom_page_id", sa.Integer, sa.ForeignKey("custom_pages.id")),
|
||||
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
|
||||
sa.Column("category_id", sa.String, sa.ForeignKey("categories.id")),
|
||||
)
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ class Category(SqlAlchemyBase):
|
||||
self.name = name.strip()
|
||||
self.slug = slugify(name)
|
||||
|
||||
def update(self, name, session=None) -> None:
|
||||
self.__init__(name, session)
|
||||
|
||||
@staticmethod
|
||||
def get_ref(session, slug: str):
|
||||
return session.query(Category).filter(Category.slug == slug).one()
|
||||
|
||||
@@ -86,6 +86,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
rating: int = None,
|
||||
orgURL: str = None,
|
||||
extras: dict = None,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
@@ -139,6 +141,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
rating: int = None,
|
||||
orgURL: str = None,
|
||||
extras: dict = None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ recipes2tags = sa.Table(
|
||||
"recipes2tags",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
||||
sa.Column("tag_slug", sa.Integer, sa.ForeignKey("tags.slug")),
|
||||
sa.Column("tag_id", sa.Integer, sa.ForeignKey("tags.id")),
|
||||
)
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ class Tag(SqlAlchemyBase):
|
||||
self.name = name.strip()
|
||||
self.slug = slugify(self.name)
|
||||
|
||||
def update(self, name, session=None) -> None:
|
||||
self.__init__(name, session)
|
||||
|
||||
@staticmethod
|
||||
def create_if_not_exist(session, name: str = None):
|
||||
test_slug = slugify(name)
|
||||
|
||||
@@ -18,6 +18,18 @@ async def get_all_recipe_categories(session: Session = Depends(generate_session)
|
||||
return db.categories.get_all_limit_columns(session, ["slug", "name"])
|
||||
|
||||
|
||||
@router.get("/empty")
|
||||
def get_empty_categories(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of categories that do not contain any recipes"""
|
||||
return db.categories.get_empty(session)
|
||||
|
||||
|
||||
@router.get("/{category}", response_model=RecipeCategoryResponse)
|
||||
def get_all_recipes_by_category(category: str, session: Session = Depends(generate_session)):
|
||||
""" Returns a list of recipes associated with the provided category. """
|
||||
return db.categories.get(session, category)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_recipe_category(
|
||||
category: CategoryIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
|
||||
@@ -27,10 +39,16 @@ async def create_recipe_category(
|
||||
return db.categories.create(session, category.dict())
|
||||
|
||||
|
||||
@router.get("/{category}", response_model=RecipeCategoryResponse)
|
||||
def get_all_recipes_by_category(category: str, session: Session = Depends(generate_session)):
|
||||
""" Returns a list of recipes associated with the provided category. """
|
||||
return db.categories.get(session, category)
|
||||
@router.put("/{category}", response_model=RecipeCategoryResponse)
|
||||
async def update_recipe_category(
|
||||
category: str,
|
||||
new_category: CategoryIn,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
""" Updates an existing Tag in the database """
|
||||
|
||||
return db.categories.update(session, category, new_category.dict())
|
||||
|
||||
|
||||
@router.delete("/{category}")
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import shutil
|
||||
from enum import Enum
|
||||
|
||||
import requests
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from mealie.db.database import db
|
||||
@@ -13,10 +11,7 @@ from mealie.services.image.image import IMG_OPTIONS, delete_image, read_image, r
|
||||
from mealie.services.scraper.scraper import create_from_url
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/recipes",
|
||||
tags=["Recipe CRUD"],
|
||||
)
|
||||
router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
|
||||
|
||||
|
||||
@router.post("/create", status_code=201, response_model=str)
|
||||
@@ -66,7 +61,30 @@ def update_recipe(
|
||||
if recipe_slug != recipe.slug:
|
||||
rename_image(original_slug=recipe_slug, new_slug=recipe.slug)
|
||||
|
||||
return recipe.slug
|
||||
return recipe
|
||||
|
||||
|
||||
@router.patch("/{recipe_slug}")
|
||||
def update_recipe(
|
||||
recipe_slug: str,
|
||||
data: dict,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
""" Updates a recipe by existing slug and data. """
|
||||
|
||||
existing_entry: Recipe = db.recipes.get(session, recipe_slug)
|
||||
|
||||
entry_dict = existing_entry.dict()
|
||||
entry_dict.update(data)
|
||||
updated_entry = Recipe(**entry_dict) # ! Surely there's a better way?
|
||||
|
||||
recipe: Recipe = db.recipes.update(session, recipe_slug, updated_entry.dict())
|
||||
|
||||
if recipe_slug != recipe.slug:
|
||||
rename_image(original_slug=recipe_slug, new_slug=recipe.slug)
|
||||
|
||||
return recipe
|
||||
|
||||
|
||||
@router.delete("/{recipe_slug}")
|
||||
|
||||
@@ -20,6 +20,18 @@ async def get_all_recipe_tags(session: Session = Depends(generate_session)):
|
||||
return db.tags.get_all_limit_columns(session, ["slug", "name"])
|
||||
|
||||
|
||||
@router.get("/empty")
|
||||
def get_empty_tags(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of tags that do not contain any recipes"""
|
||||
return db.tags.get_empty(session)
|
||||
|
||||
|
||||
@router.get("/{tag}", response_model=RecipeTagResponse)
|
||||
def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)):
|
||||
""" Returns a list of recipes associated with the provided tag. """
|
||||
return db.tags.get(session, tag)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_recipe_tag(
|
||||
tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
|
||||
@@ -29,10 +41,13 @@ async def create_recipe_tag(
|
||||
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)):
|
||||
""" Returns a list of recipes associated with the provided tag. """
|
||||
return db.tags.get(session, tag)
|
||||
@router.put("/{tag}", response_model=RecipeTagResponse)
|
||||
async def update_recipe_tag(
|
||||
tag: str, new_tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
|
||||
):
|
||||
""" Updates an existing Tag in the database """
|
||||
|
||||
return db.tags.update(session, tag, new_tag.dict())
|
||||
|
||||
|
||||
@router.delete("/{tag}")
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import List, Optional
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from mealie.schema.recipe import Recipe
|
||||
from pydantic.utils import GetterDict
|
||||
|
||||
|
||||
class CategoryIn(CamelModel):
|
||||
@@ -15,6 +16,13 @@ class CategoryBase(CategoryIn):
|
||||
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]]
|
||||
|
||||
@@ -35,6 +35,7 @@ class Nutrition(BaseModel):
|
||||
|
||||
|
||||
class RecipeSummary(BaseModel):
|
||||
id: Optional[int]
|
||||
name: str
|
||||
slug: Optional[str] = ""
|
||||
image: Optional[Any]
|
||||
|
||||
Reference in New Issue
Block a user