mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
feat: added "last-modified" header to supported record types (#1379)
* fixed type error * exposed created/updated timestamps to shopping list schema * added custom route to mix in "last-modified" header when available in CRUD routes * mixed in MealieCrudRoute to APIRouters * added HEAD route for shopping lists/list-items * replaced default serializer with FastAPI's
This commit is contained in:
parent
5db4dedc3f
commit
292bf7068a
@ -1,6 +1,11 @@
|
|||||||
from typing import Optional
|
import json
|
||||||
|
from collections.abc import Callable
|
||||||
|
from enum import Enum
|
||||||
|
from json.decoder import JSONDecodeError
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, Request, Response
|
||||||
|
from fastapi.routing import APIRoute
|
||||||
|
|
||||||
from mealie.core.dependencies import get_admin_user, get_current_user
|
from mealie.core.dependencies import get_admin_user, get_current_user
|
||||||
|
|
||||||
@ -8,20 +13,34 @@ from mealie.core.dependencies import get_admin_user, get_current_user
|
|||||||
class AdminAPIRouter(APIRouter):
|
class AdminAPIRouter(APIRouter):
|
||||||
"""Router for functions to be protected behind admin authentication"""
|
"""Router for functions to be protected behind admin authentication"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, tags: Optional[list[Union[str, Enum]]] = None, prefix: str = "", **kwargs):
|
||||||
self,
|
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)], **kwargs)
|
||||||
tags: Optional[list[str]] = None,
|
|
||||||
prefix: str = "",
|
|
||||||
):
|
|
||||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)])
|
|
||||||
|
|
||||||
|
|
||||||
class UserAPIRouter(APIRouter):
|
class UserAPIRouter(APIRouter):
|
||||||
"""Router for functions to be protected behind user authentication"""
|
"""Router for functions to be protected behind user authentication"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, tags: Optional[list[Union[str, Enum]]] = None, prefix: str = "", **kwargs):
|
||||||
self,
|
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)], **kwargs)
|
||||||
tags: Optional[list[str]] = None,
|
|
||||||
prefix: str = "",
|
|
||||||
):
|
class MealieCrudRoute(APIRoute):
|
||||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)])
|
"""Route class to include the last-modified header when returning a MealieModel, when available"""
|
||||||
|
|
||||||
|
def get_route_handler(self) -> Callable:
|
||||||
|
original_route_handler = super().get_route_handler()
|
||||||
|
|
||||||
|
async def custom_route_handler(request: Request) -> Response:
|
||||||
|
try:
|
||||||
|
response = await original_route_handler(request)
|
||||||
|
response_body = json.loads(response.body)
|
||||||
|
if type(response_body) == dict:
|
||||||
|
if last_modified := response_body.get("updateAt"):
|
||||||
|
response.headers["last-modified"] = last_modified
|
||||||
|
|
||||||
|
except JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
return custom_route_handler
|
||||||
|
@ -6,12 +6,13 @@ from pydantic import UUID4
|
|||||||
from mealie.core.exceptions import mealie_registered_exceptions
|
from mealie.core.exceptions import mealie_registered_exceptions
|
||||||
from mealie.routes._base import BaseUserController, controller
|
from mealie.routes._base import BaseUserController, controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
from mealie.schema import mapper
|
from mealie.schema import mapper
|
||||||
from mealie.schema.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
from mealie.schema.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
||||||
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
|
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
|
||||||
from mealie.services.event_bus_service.message_types import EventTypes
|
from mealie.services.event_bus_service.message_types import EventTypes
|
||||||
|
|
||||||
router = APIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"])
|
router = APIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"], route_class=MealieCrudRoute)
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
@controller(router)
|
||||||
|
@ -6,6 +6,7 @@ from pydantic import UUID4
|
|||||||
from mealie.routes._base.base_controllers import BaseUserController
|
from mealie.routes._base.base_controllers import BaseUserController
|
||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
from mealie.schema.group.group_events import (
|
from mealie.schema.group.group_events import (
|
||||||
GroupEventNotifierCreate,
|
GroupEventNotifierCreate,
|
||||||
GroupEventNotifierOut,
|
GroupEventNotifierOut,
|
||||||
@ -17,7 +18,9 @@ from mealie.schema.mapper import cast
|
|||||||
from mealie.schema.query import GetAll
|
from mealie.schema.query import GetAll
|
||||||
from mealie.services.event_bus_service.event_bus_service import EventBusService
|
from mealie.services.event_bus_service.event_bus_service import EventBusService
|
||||||
|
|
||||||
router = APIRouter(prefix="/groups/events/notifications", tags=["Group: Event Notifications"])
|
router = APIRouter(
|
||||||
|
prefix="/groups/events/notifications", tags=["Group: Event Notifications"], route_class=MealieCrudRoute
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
@controller(router)
|
||||||
|
@ -6,6 +6,7 @@ from pydantic import UUID4
|
|||||||
from mealie.routes._base.base_controllers import BaseUserController
|
from mealie.routes._base.base_controllers import BaseUserController
|
||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
from mealie.schema.labels import (
|
from mealie.schema.labels import (
|
||||||
MultiPurposeLabelCreate,
|
MultiPurposeLabelCreate,
|
||||||
MultiPurposeLabelOut,
|
MultiPurposeLabelOut,
|
||||||
@ -16,7 +17,7 @@ from mealie.schema.labels import (
|
|||||||
from mealie.schema.mapper import cast
|
from mealie.schema.mapper import cast
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.query import GetAll
|
||||||
|
|
||||||
router = APIRouter(prefix="/groups/labels", tags=["Group: Multi Purpose Labels"])
|
router = APIRouter(prefix="/groups/labels", tags=["Group: Multi Purpose Labels"], route_class=MealieCrudRoute)
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
@controller(router)
|
||||||
|
@ -6,6 +6,7 @@ from pydantic import UUID4
|
|||||||
from mealie.routes._base.base_controllers import BaseUserController
|
from mealie.routes._base.base_controllers import BaseUserController
|
||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
from mealie.schema.group.group_shopping_list import (
|
from mealie.schema.group.group_shopping_list import (
|
||||||
ShoppingListCreate,
|
ShoppingListCreate,
|
||||||
ShoppingListItemCreate,
|
ShoppingListItemCreate,
|
||||||
@ -23,7 +24,9 @@ from mealie.services.event_bus_service.event_bus_service import EventBusService,
|
|||||||
from mealie.services.event_bus_service.message_types import EventTypes
|
from mealie.services.event_bus_service.message_types import EventTypes
|
||||||
from mealie.services.group_services.shopping_lists import ShoppingListService
|
from mealie.services.group_services.shopping_lists import ShoppingListService
|
||||||
|
|
||||||
item_router = APIRouter(prefix="/groups/shopping/items", tags=["Group: Shopping List Items"])
|
item_router = APIRouter(
|
||||||
|
prefix="/groups/shopping/items", tags=["Group: Shopping List Items"], route_class=MealieCrudRoute
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@controller(item_router)
|
@controller(item_router)
|
||||||
@ -95,6 +98,7 @@ class ShoppingListItemController(BaseUserController):
|
|||||||
|
|
||||||
return shopping_list_item
|
return shopping_list_item
|
||||||
|
|
||||||
|
@item_router.head("/{item_id}", response_model=ShoppingListItemOut)
|
||||||
@item_router.get("/{item_id}", response_model=ShoppingListItemOut)
|
@item_router.get("/{item_id}", response_model=ShoppingListItemOut)
|
||||||
def get_one(self, item_id: UUID4):
|
def get_one(self, item_id: UUID4):
|
||||||
return self.mixins.get_one(item_id)
|
return self.mixins.get_one(item_id)
|
||||||
@ -144,7 +148,7 @@ class ShoppingListItemController(BaseUserController):
|
|||||||
return shopping_list_item
|
return shopping_list_item
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"])
|
router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"], route_class=MealieCrudRoute)
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
@controller(router)
|
||||||
@ -189,6 +193,7 @@ class ShoppingListController(BaseUserController):
|
|||||||
|
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
@router.head("/{item_id}", response_model=ShoppingListOut)
|
||||||
@router.get("/{item_id}", response_model=ShoppingListOut)
|
@router.get("/{item_id}", response_model=ShoppingListOut)
|
||||||
def get_one(self, item_id: UUID4):
|
def get_one(self, item_id: UUID4):
|
||||||
return self.mixins.get_one(item_id)
|
return self.mixins.get_one(item_id)
|
||||||
|
@ -19,7 +19,7 @@ from mealie.pkgs import cache
|
|||||||
from mealie.repos.repository_recipes import RepositoryRecipes
|
from mealie.repos.repository_recipes import RepositoryRecipes
|
||||||
from mealie.routes._base import BaseUserController, controller
|
from mealie.routes._base import BaseUserController, controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.routes._base.routers import UserAPIRouter
|
from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.query import GetAll
|
||||||
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
|
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
|
||||||
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
|
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
|
||||||
@ -112,7 +112,7 @@ class RecipeExportController(BaseRecipeController):
|
|||||||
return FileResponse(temp_path, filename=f"{slug}.zip")
|
return FileResponse(temp_path, filename=f"{slug}.zip")
|
||||||
|
|
||||||
|
|
||||||
router = UserAPIRouter(prefix="/recipes", tags=["Recipe: CRUD"])
|
router = UserAPIRouter(prefix="/recipes", tags=["Recipe: CRUD"], route_class=MealieCrudRoute)
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
@controller(router)
|
||||||
|
@ -6,12 +6,13 @@ from pydantic import UUID4
|
|||||||
from mealie.routes._base.base_controllers import BaseUserController
|
from mealie.routes._base.base_controllers import BaseUserController
|
||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
from mealie.schema import mapper
|
from mealie.schema import mapper
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.query import GetAll
|
||||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood, MergeFood, SaveIngredientFood
|
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood, MergeFood, SaveIngredientFood
|
||||||
from mealie.schema.response.responses import SuccessResponse
|
from mealie.schema.response.responses import SuccessResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/foods", tags=["Recipes: Foods"])
|
router = APIRouter(prefix="/foods", tags=["Recipes: Foods"], route_class=MealieCrudRoute)
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
@controller(router)
|
||||||
|
@ -6,12 +6,13 @@ from pydantic import UUID4
|
|||||||
from mealie.routes._base.base_controllers import BaseUserController
|
from mealie.routes._base.base_controllers import BaseUserController
|
||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
from mealie.schema import mapper
|
from mealie.schema import mapper
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.query import GetAll
|
||||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit, MergeUnit, SaveIngredientUnit
|
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit, MergeUnit, SaveIngredientUnit
|
||||||
from mealie.schema.response.responses import SuccessResponse
|
from mealie.schema.response.responses import SuccessResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/units", tags=["Recipes: Units"])
|
router = APIRouter(prefix="/units", tags=["Recipes: Units"], route_class=MealieCrudRoute)
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
@controller(router)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from datetime import datetime
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
@ -38,6 +39,9 @@ class ShoppingListItemCreate(MealieModel):
|
|||||||
label_id: Optional[UUID4] = None
|
label_id: Optional[UUID4] = None
|
||||||
recipe_references: list[ShoppingListItemRecipeRef] = []
|
recipe_references: list[ShoppingListItemRecipeRef] = []
|
||||||
|
|
||||||
|
created_at: Optional[datetime]
|
||||||
|
update_at: Optional[datetime]
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListItemUpdate(ShoppingListItemCreate):
|
class ShoppingListItemUpdate(ShoppingListItemCreate):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
@ -45,7 +49,7 @@ class ShoppingListItemUpdate(ShoppingListItemCreate):
|
|||||||
|
|
||||||
class ShoppingListItemOut(ShoppingListItemUpdate):
|
class ShoppingListItemOut(ShoppingListItemUpdate):
|
||||||
label: Optional[MultiPurposeLabelSummary]
|
label: Optional[MultiPurposeLabelSummary]
|
||||||
recipe_references: list[ShoppingListItemRecipeRefOut] = []
|
recipe_references: list[Union[ShoppingListItemRecipeRef, ShoppingListItemRecipeRefOut]] = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
@ -54,6 +58,9 @@ class ShoppingListItemOut(ShoppingListItemUpdate):
|
|||||||
class ShoppingListCreate(MealieModel):
|
class ShoppingListCreate(MealieModel):
|
||||||
name: str = None
|
name: str = None
|
||||||
|
|
||||||
|
created_at: Optional[datetime]
|
||||||
|
update_at: Optional[datetime]
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListRecipeRefOut(MealieModel):
|
class ShoppingListRecipeRefOut(MealieModel):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
|
@ -46,6 +46,8 @@ def serialize_list_items(list_items: list[ShoppingListItemOut]) -> list:
|
|||||||
item_dict["id"] = str(item.id)
|
item_dict["id"] = str(item.id)
|
||||||
as_dict.append(item_dict)
|
as_dict.append(item_dict)
|
||||||
|
|
||||||
|
# the default serializer fails on certain complex objects, so we use FastAPI's serliazer first
|
||||||
|
as_dict = utils.jsonify(as_dict)
|
||||||
return as_dict
|
return as_dict
|
||||||
|
|
||||||
|
|
||||||
@ -151,6 +153,8 @@ def test_shopping_list_items_update_many_reorder(
|
|||||||
as_dict.append(item_dict)
|
as_dict.append(item_dict)
|
||||||
|
|
||||||
# update list
|
# update list
|
||||||
|
# the default serializer fails on certain complex objects, so we use FastAPI's serliazer first
|
||||||
|
as_dict = utils.jsonify(as_dict)
|
||||||
response = api_client.put(Routes.items, json=as_dict, headers=unique_user.token)
|
response = api_client.put(Routes.items, json=as_dict, headers=unique_user.token)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user