feature: proper multi-tenant-support (#969)(WIP)

* update naming

* refactor tests to use shared structure

* shorten names

* add tools test case

* refactor to support multi-tenant

* set group_id on creation

* initial refactor for multitenant tags/cats

* spelling

* additional test case for same valued resources

* fix recipe update tests

* apply indexes to foreign keys

* fix performance regressions

* handle unknown exception

* utility decorator for function debugging

* migrate recipe_id to UUID

* GUID for recipes

* remove unused import

* move image functions into package

* move utilities to packages dir

* update import

* linter

* image image and asset routes

* update assets and images to use UUIDs

* fix migration base

* image asset test coverage

* use ids for categories and tag crud functions

* refactor recipe organizer test suite to reduce duplication

* add uuid serlization utility

* organizer base router

* slug routes testing and fixes

* fix postgres error

* adopt UUIDs

* move tags, categories, and tools under "organizers" umbrella

* update composite label

* generate ts types

* fix import error

* update frontend types

* fix type errors

* fix postgres errors

* fix #978

* add null check for title validation

* add note in docs on multi-tenancy
This commit is contained in:
Hayden
2022-02-13 12:23:42 -09:00
committed by GitHub
parent 9a82a172cb
commit c617251f4c
157 changed files with 1866 additions and 1578 deletions

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter
from . import admin, app, auth, categories, comments, groups, parser, recipe, shared, tags, tools, unit_and_foods, users
from . import admin, app, auth, comments, groups, organizers, parser, recipe, shared, unit_and_foods, users
router = APIRouter(prefix="/api")
@@ -9,11 +9,9 @@ router.include_router(auth.router)
router.include_router(users.router)
router.include_router(groups.router)
router.include_router(recipe.router)
router.include_router(organizers.router)
router.include_router(shared.router)
router.include_router(comments.router)
router.include_router(parser.router)
router.include_router(unit_and_foods.router)
router.include_router(tools.router)
router.include_router(categories.router)
router.include_router(tags.router)
router.include_router(admin.router)

View File

@@ -10,13 +10,13 @@ from mealie.core.dependencies import get_current_user
from mealie.core.root_logger import get_logger
from mealie.core.security import create_file_token
from mealie.db.db_setup import generate_session
from mealie.pkgs.stats.fs_stats import pretty_size
from mealie.routes._base.routers import AdminAPIRouter
from mealie.schema.admin import AllBackups, BackupFile, CreateBackup, ImportJob
from mealie.schema.user.user import PrivateUser
from mealie.services.backups import imports
from mealie.services.backups.exports import backup_all
from mealie.services.events import create_backup_event
from mealie.utils.fs_stats import pretty_size
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
logger = get_logger()

View File

@@ -1,6 +0,0 @@
from fastapi import APIRouter
from . import categories
router = APIRouter()
router.include_router(categories.router)

View File

@@ -1,69 +0,0 @@
from functools import cached_property
from fastapi import APIRouter
from pydantic import BaseModel
from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.mixins import CrudMixins
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
from mealie.schema.recipe.recipe_category import CategoryBase
router = APIRouter(prefix="/categories", tags=["Categories: CRUD"])
class CategorySummary(BaseModel):
id: int
slug: str
name: str
class Config:
orm_mode = True
@controller(router)
class RecipeCategoryController(BaseUserController):
# =========================================================================
# CRUD Operations
@cached_property
def mixins(self):
return CrudMixins(self.repos.categories, self.deps.logger)
@router.get("", response_model=list[CategorySummary])
def get_all(self):
"""Returns a list of available categories in the database"""
return self.repos.categories.get_all_limit_columns(fields=["slug", "name"])
@router.post("", status_code=201)
def create_one(self, category: CategoryIn):
"""Creates a Category in the database"""
return self.mixins.create_one(category)
@router.get("/{slug}", response_model=RecipeCategoryResponse)
def get_all_recipes_by_category(self, slug: str):
"""Returns a list of recipes associated with the provided category."""
category_obj = self.repos.categories.get(slug)
category_obj = RecipeCategoryResponse.from_orm(category_obj)
return category_obj
@router.put("/{slug}", response_model=RecipeCategoryResponse)
def update_one(self, slug: str, update_data: CategoryIn):
"""Updates an existing Tag in the database"""
return self.mixins.update_one(update_data, slug)
@router.delete("/{slug}")
def delete_one(self, slug: str):
"""
Removes a recipe category from the database. Deleting a
category does not impact a recipe. The category will be removed
from any recipes that contain it
"""
self.mixins.delete_one(slug)
# =========================================================================
# Read All Operations
@router.get("/empty", response_model=list[CategoryBase])
def get_all_empty(self):
"""Returns a list of categories that do not contain any recipes"""
return self.repos.categories.get_empty()

View File

@@ -143,9 +143,9 @@ class ShoppingListController(BaseUserController):
# Other Operations
@router.post("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut)
def add_recipe_ingredients_to_list(self, item_id: UUID4, recipe_id: int):
def add_recipe_ingredients_to_list(self, item_id: UUID4, recipe_id: UUID4):
return self.service.add_recipe_ingredients_to_list(item_id, recipe_id)
@router.delete("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut)
def remove_recipe_ingredients_from_list(self, item_id: UUID4, recipe_id: int):
def remove_recipe_ingredients_from_list(self, item_id: UUID4, recipe_id: UUID4):
return self.service.remove_recipe_ingredients_from_list(item_id, recipe_id)

View File

@@ -1,6 +1,7 @@
from enum import Enum
from fastapi import APIRouter, HTTPException, status
from pydantic import UUID4
from starlette.responses import FileResponse
from mealie.schema.recipe import Recipe
@@ -19,11 +20,13 @@ class ImageType(str, Enum):
tiny = "tiny-original.webp"
@router.get("/{slug}/images/{file_name}")
async def get_recipe_img(slug: str, file_name: ImageType = ImageType.original):
"""Takes in a recipe slug, returns the static image. This route is proxied in the docker image
and should not hit the API in production"""
recipe_image = Recipe(slug=slug).image_dir.joinpath(file_name.value)
@router.get("/{recipe_id}/images/{file_name}")
async def get_recipe_img(recipe_id: str, file_name: ImageType = ImageType.original):
"""
Takes in a recipe recipe_id, returns the static image. This route is proxied in the docker image
and should not hit the API in production
"""
recipe_image = Recipe.directory_from_id(recipe_id).joinpath("images", file_name.value)
if recipe_image.exists():
return FileResponse(recipe_image)
@@ -31,10 +34,10 @@ async def get_recipe_img(slug: str, file_name: ImageType = ImageType.original):
raise HTTPException(status.HTTP_404_NOT_FOUND)
@router.get("/{slug}/assets/{file_name}")
async def get_recipe_asset(slug: str, file_name: str):
@router.get("/{recipe_id}/assets/{file_name}")
async def get_recipe_asset(recipe_id: UUID4, file_name: str):
"""Returns a recipe asset"""
file = Recipe(slug=slug).asset_dir.joinpath(file_name)
file = Recipe.directory_from_id(recipe_id).joinpath("assets", file_name)
try:
return FileResponse(file)

View File

@@ -0,0 +1,8 @@
from fastapi import APIRouter
from . import controller_categories, controller_tags, controller_tools
router = APIRouter(prefix="/organizers")
router.include_router(controller_categories.router)
router.include_router(controller_tags.router)
router.include_router(controller_tools.router)

View File

@@ -0,0 +1,87 @@
from functools import cached_property
from fastapi import APIRouter
from pydantic import UUID4, BaseModel
from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.mixins import CrudMixins
from mealie.schema import mapper
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
from mealie.schema.recipe.recipe import RecipeCategory
from mealie.schema.recipe.recipe_category import CategoryBase, CategorySave
router = APIRouter(prefix="/categories", tags=["Organizer: Categories"])
class CategorySummary(BaseModel):
id: UUID4
slug: str
name: str
class Config:
orm_mode = True
@controller(router)
class RecipeCategoryController(BaseUserController):
# =========================================================================
# CRUD Operations
@cached_property
def repo(self):
return self.repos.categories.by_group(self.group_id)
@cached_property
def mixins(self):
return CrudMixins(self.repo, self.deps.logger)
@router.get("", response_model=list[CategorySummary])
def get_all(self):
"""Returns a list of available categories in the database"""
return self.repo.get_all(override_schema=CategorySummary)
@router.post("", status_code=201)
def create_one(self, category: CategoryIn):
"""Creates a Category in the database"""
save_data = mapper.cast(category, CategorySave, group_id=self.group_id)
return self.mixins.create_one(save_data)
@router.get("/{item_id}", response_model=CategorySummary)
def get_one(self, item_id: UUID4):
"""Returns a list of recipes associated with the provided category."""
category_obj = self.mixins.get_one(item_id)
category_obj = CategorySummary.from_orm(category_obj)
return category_obj
@router.put("/{item_id}", response_model=CategorySummary)
def update_one(self, item_id: UUID4, update_data: CategoryIn):
"""Updates an existing Tag in the database"""
save_data = mapper.cast(update_data, CategorySave, group_id=self.group_id)
return self.mixins.update_one(save_data, item_id)
@router.delete("/{item_id}")
def delete_one(self, item_id: UUID4):
"""
Removes a recipe category from the database. Deleting a
category does not impact a recipe. The category will be removed
from any recipes that contain it
"""
self.mixins.delete_one(item_id)
# =========================================================================
# Read All Operations
@router.get("/empty", response_model=list[CategoryBase])
def get_all_empty(self):
"""Returns a list of categories that do not contain any recipes"""
return self.repos.categories.get_empty()
@router.get("/slug/{category_slug}")
def get_one_by_slug(self, category_slug: str):
"""Returns a category object with the associated recieps relating to the category"""
category: RecipeCategory = self.mixins.get_one(category_slug, "slug")
return RecipeCategoryResponse.construct(
id=category.id,
slug=category.slug,
name=category.name,
recipes=self.repos.recipes.by_group(self.group_id).get_by_categories([category]),
)

View File

@@ -0,0 +1,66 @@
from functools import cached_property
from fastapi import APIRouter, HTTPException, status
from pydantic import UUID4
from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.mixins import CrudMixins
from mealie.schema import mapper
from mealie.schema.recipe import RecipeTagResponse, TagIn
from mealie.schema.recipe.recipe import RecipeTag
from mealie.schema.recipe.recipe_category import TagSave
router = APIRouter(prefix="/tags", tags=["Organizer: Tags"])
@controller(router)
class TagController(BaseUserController):
@cached_property
def repo(self):
return self.repos.tags.by_group(self.group_id)
@cached_property
def mixins(self):
return CrudMixins(self.repo, self.deps.logger)
@router.get("")
async def get_all(self):
"""Returns a list of available tags in the database"""
return self.repo.get_all(override_schema=RecipeTag)
@router.get("/empty")
def get_empty_tags(self):
"""Returns a list of tags that do not contain any recipes"""
return self.repo.get_empty()
@router.get("/{item_id}", response_model=RecipeTagResponse)
def get_one(self, item_id: UUID4):
"""Returns a list of recipes associated with the provided tag."""
return self.mixins.get_one(item_id)
@router.post("", status_code=201)
def create_one(self, tag: TagIn):
"""Creates a Tag in the database"""
save_data = mapper.cast(tag, TagSave, group_id=self.group_id)
return self.repo.create(save_data)
@router.put("/{item_id}", response_model=RecipeTagResponse)
def update_one(self, item_id: UUID4, new_tag: TagIn):
"""Updates an existing Tag in the database"""
save_data = mapper.cast(new_tag, TagSave, group_id=self.group_id)
return self.repo.update(item_id, save_data)
@router.delete("/{item_id}")
def delete_recipe_tag(self, item_id: UUID4):
"""Removes a recipe tag from the database. Deleting a
tag does not impact a recipe. The tag will be removed
from any recipes that contain it"""
try:
self.repo.delete(item_id)
except Exception as e:
raise HTTPException(status.HTTP_400_BAD_REQUEST) from e
@router.get("/slug/{tag_slug}", response_model=RecipeTagResponse)
async def get_one_by_slug(self, tag_slug: str):
return self.repo.get_one(tag_slug, "slug", override_schema=RecipeTagResponse)

View File

@@ -1,22 +1,24 @@
from functools import cached_property
from fastapi import APIRouter, Depends
from pydantic import UUID4
from mealie.routes._base.abc_controller import BaseUserController
from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import CrudMixins
from mealie.schema import mapper
from mealie.schema.query import GetAll
from mealie.schema.recipe.recipe import RecipeTool
from mealie.schema.recipe.recipe_tool import RecipeToolCreate, RecipeToolResponse
from mealie.schema.recipe.recipe_tool import RecipeToolCreate, RecipeToolResponse, RecipeToolSave
router = APIRouter(prefix="/tools", tags=["Recipes: Tools"])
router = APIRouter(prefix="/tools", tags=["Organizer: Tools"])
@controller(router)
class RecipeToolController(BaseUserController):
@cached_property
def repo(self):
return self.repos.tools
return self.repos.tools.by_group(self.group_id)
@property
def mixins(self) -> CrudMixins:
@@ -28,18 +30,19 @@ class RecipeToolController(BaseUserController):
@router.post("", response_model=RecipeTool, status_code=201)
def create_one(self, data: RecipeToolCreate):
return self.mixins.create_one(data)
save_data = mapper.cast(data, RecipeToolSave, group_id=self.group_id)
return self.mixins.create_one(save_data)
@router.get("/{item_id}", response_model=RecipeTool)
def get_one(self, item_id: int):
def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id)
@router.put("/{item_id}", response_model=RecipeTool)
def update_one(self, item_id: int, data: RecipeToolCreate):
def update_one(self, item_id: UUID4, data: RecipeToolCreate):
return self.mixins.update_one(data, item_id)
@router.delete("/{item_id}", response_model=RecipeTool)
def delete_one(self, item_id: int):
def delete_one(self, item_id: UUID4):
return self.mixins.delete_one(item_id) # type: ignore
@router.get("/slug/{tool_slug}", response_model=RecipeToolResponse)

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter
from . import all_recipe_routes, bulk_actions, comments, image_and_assets, recipe_crud_routes, shared_routes
from . import all_recipe_routes, bulk_actions, comments, recipe_crud_routes, shared_routes
prefix = "/recipes"
@@ -9,7 +9,6 @@ router = APIRouter()
router.include_router(all_recipe_routes.router, prefix=prefix, tags=["Recipe: Query All"])
router.include_router(recipe_crud_routes.router_exports)
router.include_router(recipe_crud_routes.router)
router.include_router(image_and_assets.router, prefix=prefix, tags=["Recipe: Images and Assets"])
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])
router.include_router(bulk_actions.router, prefix=prefix)
router.include_router(bulk_actions.router, prefix=prefix, tags=["Recipe: Bulk Exports"])

View File

@@ -1,68 +0,0 @@
from shutil import copyfileobj
from fastapi import Depends, File, Form, HTTPException, status
from fastapi.datastructures import UploadFile
from pydantic import BaseModel
from slugify import slugify
from sqlalchemy.orm.session import Session
from mealie.db.db_setup import generate_session
from mealie.repos.all_repositories import get_repositories
from mealie.routes._base.routers import UserAPIRouter
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeAsset
from mealie.services.image.image import scrape_image, write_image
router = UserAPIRouter()
class UpdateImageResponse(BaseModel):
image: str
@router.post("/{slug}/image")
def scrape_image_url(slug: str, url: CreateRecipeByUrl):
"""Removes an existing image and replaces it with the incoming file."""
scrape_image(url.url, slug)
@router.put("/{slug}/image", response_model=UpdateImageResponse)
def update_recipe_image(
slug: str,
image: bytes = File(...),
extension: str = Form(...),
session: Session = Depends(generate_session),
):
"""Removes an existing image and replaces it with the incoming file."""
db = get_repositories(session)
write_image(slug, image, extension)
new_version = db.recipes.update_image(slug, extension)
return UpdateImageResponse(image=new_version)
@router.post("/{slug}/assets", response_model=RecipeAsset)
def upload_recipe_asset(
slug: str,
name: str = Form(...),
icon: str = Form(...),
extension: str = Form(...),
file: UploadFile = File(...),
session: Session = Depends(generate_session),
):
"""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 = Recipe(slug=slug).asset_dir.joinpath(file_name)
with dest.open("wb") as buffer:
copyfileobj(file.file, buffer)
if not dest.is_file():
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
db = get_repositories(session)
recipe: Recipe = db.recipes.get(slug)
recipe.assets.append(asset_in)
db.recipes.update(slug, recipe.dict())
return asset_in

View File

@@ -1,12 +1,14 @@
from functools import cached_property
from shutil import copyfileobj
from zipfile import ZipFile
import sqlalchemy
from fastapi import BackgroundTasks, Depends, File, HTTPException
from fastapi import BackgroundTasks, Depends, File, Form, HTTPException, status
from fastapi.datastructures import UploadFile
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from slugify import slugify
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
@@ -22,8 +24,10 @@ from mealie.routes._base.routers import UserAPIRouter
from mealie.schema.query import GetAll
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeImageTypes
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
from mealie.schema.recipe.recipe_asset import RecipeAsset
from mealie.schema.response.responses import ErrorResponse
from mealie.schema.server.tasks import ServerTaskNames
from mealie.services.recipe.recipe_data_service import RecipeDataService
from mealie.services.recipe.recipe_service import RecipeService
from mealie.services.recipe.template_service import TemplateService
from mealie.services.scraper.scraper import create_from_url
@@ -49,6 +53,10 @@ class RecipeGetAll(GetAll):
load_food: bool = False
class UpdateImageResponse(BaseModel):
image: str
class FormatResponse(BaseModel):
jjson: list[str] = Field(..., alias="json")
zip: list[str]
@@ -158,10 +166,9 @@ class RecipeController(BaseRecipeController):
@router.post("/test-scrape-url")
def test_parse_recipe_url(self, url: CreateRecipeByUrl):
# Debugger should produce the same result as the scraper sees before cleaning
scraped_data = RecipeScraperPackage(url.url).scrape_url()
if scraped_data:
if scraped_data := RecipeScraperPackage(url.url).scrape_url():
return scraped_data.schema.data
return "recipe_scrapers was unable to scrape this URL"
@router.post("/create-from-zip", status_code=201)
@@ -217,6 +224,12 @@ class RecipeController(BaseRecipeController):
self.deps.logger.error("SQL Integrity Error on recipe controller action")
raise HTTPException(status_code=400, detail=ErrorResponse.respond(message="Recipe already exists"))
case _:
self.deps.logger.error("Unknown Error on recipe controller action")
raise HTTPException(
status_code=500, detail=ErrorResponse.respond(message="Unknown Error", exception=ex)
)
@router.put("/{slug}")
def update_one(self, slug: str, data: Recipe):
"""Updates a recipe by existing slug and data."""
@@ -243,3 +256,51 @@ class RecipeController(BaseRecipeController):
return self.service.delete_one(slug)
except Exception as e:
self.handle_exceptions(e)
# ==================================================================================================================
# Image and Assets
@router.post("/{slug}/image", tags=["Recipe: Images and Assets"])
def scrape_image_url(self, slug: str, url: CreateRecipeByUrl) -> str:
recipe = self.mixins.get_one(slug)
data_service = RecipeDataService(recipe.id)
data_service.scrape_image(url.url)
@router.put("/{slug}/image", response_model=UpdateImageResponse, tags=["Recipe: Images and Assets"])
def update_recipe_image(self, slug: str, image: bytes = File(...), extension: str = Form(...)):
recipe = self.mixins.get_one(slug)
data_service = RecipeDataService(recipe.id)
data_service.write_image(image, extension)
new_version = self.repo.update_image(slug, extension)
return UpdateImageResponse(image=new_version)
@router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"])
def upload_recipe_asset(
self,
slug: str,
name: str = Form(...),
icon: str = Form(...),
extension: str = Form(...),
file: UploadFile = File(...),
):
"""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)
recipe = self.mixins.get_one(slug)
dest = recipe.asset_dir / file_name
with dest.open("wb") as buffer:
copyfileobj(file.file, buffer)
if not dest.is_file():
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
recipe: Recipe = self.mixins.get_one(slug)
recipe.assets.append(asset_in)
self.mixins.update_one(recipe, slug)
return asset_in

View File

@@ -22,7 +22,7 @@ class RecipeSharedController(BaseUserController):
return CrudMixins[RecipeShareTokenSave, RecipeShareToken, RecipeShareTokenCreate](self.repo, self.deps.logger)
@router.get("", response_model=list[RecipeShareTokenSummary])
def get_all(self, recipe_id: int = None):
def get_all(self, recipe_id: UUID4 = None):
if recipe_id:
return self.repo.multi_query({"recipe_id": recipe_id}, override_schema=RecipeShareTokenSummary)
else:

View File

@@ -1,51 +0,0 @@
from functools import cached_property
from fastapi import APIRouter, HTTPException, status
from mealie.routes._base import BaseUserController, controller
from mealie.schema.recipe import RecipeTagResponse, TagIn
router = APIRouter(prefix="/tags", tags=["Tags: CRUD"])
@controller(router)
class TagController(BaseUserController):
@cached_property
def repo(self):
return self.repos.tags
@router.get("")
async def get_all_recipe_tags(self):
"""Returns a list of available tags in the database"""
return self.repo.get_all_limit_columns(["slug", "name"])
@router.get("/empty")
def get_empty_tags(self):
"""Returns a list of tags that do not contain any recipes"""
return self.repo.get_empty()
@router.get("/{tag_slug}", response_model=RecipeTagResponse)
def get_all_recipes_by_tag(self, tag_slug: str):
"""Returns a list of recipes associated with the provided tag."""
return self.repo.get_one(tag_slug, override_schema=RecipeTagResponse)
@router.post("", status_code=201)
def create_recipe_tag(self, tag: TagIn):
"""Creates a Tag in the database"""
return self.repo.create(tag)
@router.put("/{tag_slug}", response_model=RecipeTagResponse)
def update_recipe_tag(self, tag_slug: str, new_tag: TagIn):
"""Updates an existing Tag in the database"""
return self.repo.update(tag_slug, new_tag)
@router.delete("/{tag_slug}")
def delete_recipe_tag(self, tag_slug: str):
"""Removes a recipe tag from the database. Deleting a
tag does not impact a recipe. The tag will be removed
from any recipes that contain it"""
try:
self.repo.delete(tag_slug)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)

View File

@@ -1,6 +1,7 @@
from functools import cached_property
from fastapi import APIRouter, Depends
from pydantic import UUID4
from mealie.routes._base.abc_controller import BaseUserController
from mealie.routes._base.controller import controller
@@ -36,13 +37,13 @@ class IngredientFoodsController(BaseUserController):
return self.mixins.create_one(save_data)
@router.get("/{item_id}", response_model=IngredientFood)
def get_one(self, item_id: int):
def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id)
@router.put("/{item_id}", response_model=IngredientFood)
def update_one(self, item_id: int, data: CreateIngredientFood):
def update_one(self, item_id: UUID4, data: CreateIngredientFood):
return self.mixins.update_one(data, item_id)
@router.delete("/{item_id}", response_model=IngredientFood)
def delete_one(self, item_id: int):
def delete_one(self, item_id: UUID4):
return self.mixins.delete_one(item_id)

View File

@@ -1,6 +1,7 @@
from functools import cached_property
from fastapi import APIRouter, Depends
from pydantic import UUID4
from mealie.routes._base.abc_controller import BaseUserController
from mealie.routes._base.controller import controller
@@ -36,13 +37,13 @@ class IngredientUnitsController(BaseUserController):
return self.mixins.create_one(save_data)
@router.get("/{item_id}", response_model=IngredientUnit)
def get_one(self, item_id: int):
def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id)
@router.put("/{item_id}", response_model=IngredientUnit)
def update_one(self, item_id: int, data: CreateIngredientUnit):
def update_one(self, item_id: UUID4, data: CreateIngredientUnit):
return self.mixins.update_one(data, item_id)
@router.delete("/{item_id}", response_model=IngredientUnit)
def delete_one(self, item_id: int):
def delete_one(self, item_id: UUID4):
return self.mixins.delete_one(item_id) # type: ignore

View File

@@ -4,13 +4,12 @@ from pathlib import Path
from fastapi import Depends, File, HTTPException, UploadFile, status
from pydantic import UUID4
from mealie import utils
from mealie.core.dependencies.dependencies import temporary_dir
from mealie.pkgs import cache, img
from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import PrivateUser
from mealie.services.image import minify
router = UserAPIRouter(prefix="", tags=["Users: Images"])
@@ -31,12 +30,12 @@ class UserImageController(BaseUserController):
with temp_img.open("wb") as buffer:
shutil.copyfileobj(profile.file, buffer)
image = minify.to_webp(temp_img)
image = img.PillowMinifier.to_webp(temp_img)
dest = PrivateUser.get_directory(id) / "profile.webp"
shutil.copyfile(image, dest)
self.repos.users.patch(id, {"cache_key": utils.new_cache_key()})
self.repos.users.patch(id, {"cache_key": cache.new_key()})
if not dest.is_file:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)