From 292bf7068af10e7765ec49eb5abc09192fcce7c1 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 21 Jun 2022 12:41:14 -0500 Subject: [PATCH] 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 --- mealie/routes/_base/routers.py | 47 +++++++++++++------ mealie/routes/groups/controller_cookbooks.py | 3 +- .../groups/controller_group_notifications.py | 5 +- mealie/routes/groups/controller_labels.py | 3 +- .../groups/controller_shopping_lists.py | 9 +++- mealie/routes/recipe/recipe_crud_routes.py | 4 +- mealie/routes/unit_and_foods/foods.py | 3 +- mealie/routes/unit_and_foods/units.py | 3 +- mealie/schema/group/group_shopping_list.py | 11 ++++- .../test_group_shopping_list_items.py | 4 ++ 10 files changed, 67 insertions(+), 25 deletions(-) diff --git a/mealie/routes/_base/routers.py b/mealie/routes/_base/routers.py index cf0d24250283..8672dccdbf27 100644 --- a/mealie/routes/_base/routers.py +++ b/mealie/routes/_base/routers.py @@ -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 @@ -8,20 +13,34 @@ from mealie.core.dependencies import get_admin_user, get_current_user class AdminAPIRouter(APIRouter): """Router for functions to be protected behind admin authentication""" - def __init__( - self, - tags: Optional[list[str]] = None, - prefix: str = "", - ): - super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)]) + def __init__(self, tags: Optional[list[Union[str, Enum]]] = None, prefix: str = "", **kwargs): + super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)], **kwargs) class UserAPIRouter(APIRouter): """Router for functions to be protected behind user authentication""" - def __init__( - self, - tags: Optional[list[str]] = None, - prefix: str = "", - ): - super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)]) + def __init__(self, tags: Optional[list[Union[str, Enum]]] = None, prefix: str = "", **kwargs): + super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)], **kwargs) + + +class MealieCrudRoute(APIRoute): + """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 diff --git a/mealie/routes/groups/controller_cookbooks.py b/mealie/routes/groups/controller_cookbooks.py index d12a110a13ef..a371e75e6d9b 100644 --- a/mealie/routes/groups/controller_cookbooks.py +++ b/mealie/routes/groups/controller_cookbooks.py @@ -6,12 +6,13 @@ from pydantic import UUID4 from mealie.core.exceptions import mealie_registered_exceptions from mealie.routes._base import BaseUserController, controller from mealie.routes._base.mixins import HttpRepo +from mealie.routes._base.routers import MealieCrudRoute from mealie.schema import mapper 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.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) diff --git a/mealie/routes/groups/controller_group_notifications.py b/mealie/routes/groups/controller_group_notifications.py index 62c54688fc79..f501344b91f6 100644 --- a/mealie/routes/groups/controller_group_notifications.py +++ b/mealie/routes/groups/controller_group_notifications.py @@ -6,6 +6,7 @@ from pydantic import UUID4 from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.controller import controller from mealie.routes._base.mixins import HttpRepo +from mealie.routes._base.routers import MealieCrudRoute from mealie.schema.group.group_events import ( GroupEventNotifierCreate, GroupEventNotifierOut, @@ -17,7 +18,9 @@ from mealie.schema.mapper import cast from mealie.schema.query import GetAll 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) diff --git a/mealie/routes/groups/controller_labels.py b/mealie/routes/groups/controller_labels.py index fe8e4f4df4df..7d888f7e8f44 100644 --- a/mealie/routes/groups/controller_labels.py +++ b/mealie/routes/groups/controller_labels.py @@ -6,6 +6,7 @@ from pydantic import UUID4 from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.controller import controller from mealie.routes._base.mixins import HttpRepo +from mealie.routes._base.routers import MealieCrudRoute from mealie.schema.labels import ( MultiPurposeLabelCreate, MultiPurposeLabelOut, @@ -16,7 +17,7 @@ from mealie.schema.labels import ( from mealie.schema.mapper import cast 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) diff --git a/mealie/routes/groups/controller_shopping_lists.py b/mealie/routes/groups/controller_shopping_lists.py index a76781ec3a7b..6cd8f34d4a4a 100644 --- a/mealie/routes/groups/controller_shopping_lists.py +++ b/mealie/routes/groups/controller_shopping_lists.py @@ -6,6 +6,7 @@ from pydantic import UUID4 from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.controller import controller from mealie.routes._base.mixins import HttpRepo +from mealie.routes._base.routers import MealieCrudRoute from mealie.schema.group.group_shopping_list import ( ShoppingListCreate, 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.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) @@ -95,6 +98,7 @@ class ShoppingListItemController(BaseUserController): return shopping_list_item + @item_router.head("/{item_id}", response_model=ShoppingListItemOut) @item_router.get("/{item_id}", response_model=ShoppingListItemOut) def get_one(self, item_id: UUID4): return self.mixins.get_one(item_id) @@ -144,7 +148,7 @@ class ShoppingListItemController(BaseUserController): 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) @@ -189,6 +193,7 @@ class ShoppingListController(BaseUserController): return val + @router.head("/{item_id}", response_model=ShoppingListOut) @router.get("/{item_id}", response_model=ShoppingListOut) def get_one(self, item_id: UUID4): return self.mixins.get_one(item_id) diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py index 0613637ae264..d558a2cc9ba1 100644 --- a/mealie/routes/recipe/recipe_crud_routes.py +++ b/mealie/routes/recipe/recipe_crud_routes.py @@ -19,7 +19,7 @@ from mealie.pkgs import cache from mealie.repos.repository_recipes import RepositoryRecipes from mealie.routes._base import BaseUserController, controller 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.recipe import Recipe, RecipeImageTypes, ScrapeRecipe from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary @@ -112,7 +112,7 @@ class RecipeExportController(BaseRecipeController): 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) diff --git a/mealie/routes/unit_and_foods/foods.py b/mealie/routes/unit_and_foods/foods.py index a5b4c88d7311..b8fbdffd1399 100644 --- a/mealie/routes/unit_and_foods/foods.py +++ b/mealie/routes/unit_and_foods/foods.py @@ -6,12 +6,13 @@ from pydantic import UUID4 from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.controller import controller from mealie.routes._base.mixins import HttpRepo +from mealie.routes._base.routers import MealieCrudRoute from mealie.schema import mapper from mealie.schema.query import GetAll from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood, MergeFood, SaveIngredientFood 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) diff --git a/mealie/routes/unit_and_foods/units.py b/mealie/routes/unit_and_foods/units.py index 7314ea464388..f9e1e616fa5f 100644 --- a/mealie/routes/unit_and_foods/units.py +++ b/mealie/routes/unit_and_foods/units.py @@ -6,12 +6,13 @@ from pydantic import UUID4 from mealie.routes._base.base_controllers import BaseUserController from mealie.routes._base.controller import controller from mealie.routes._base.mixins import HttpRepo +from mealie.routes._base.routers import MealieCrudRoute from mealie.schema import mapper from mealie.schema.query import GetAll from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit, MergeUnit, SaveIngredientUnit 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) diff --git a/mealie/schema/group/group_shopping_list.py b/mealie/schema/group/group_shopping_list.py index 82058f357132..cbd014523753 100644 --- a/mealie/schema/group/group_shopping_list.py +++ b/mealie/schema/group/group_shopping_list.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Optional +from datetime import datetime +from typing import Optional, Union from pydantic import UUID4 @@ -38,6 +39,9 @@ class ShoppingListItemCreate(MealieModel): label_id: Optional[UUID4] = None recipe_references: list[ShoppingListItemRecipeRef] = [] + created_at: Optional[datetime] + update_at: Optional[datetime] + class ShoppingListItemUpdate(ShoppingListItemCreate): id: UUID4 @@ -45,7 +49,7 @@ class ShoppingListItemUpdate(ShoppingListItemCreate): class ShoppingListItemOut(ShoppingListItemUpdate): label: Optional[MultiPurposeLabelSummary] - recipe_references: list[ShoppingListItemRecipeRefOut] = [] + recipe_references: list[Union[ShoppingListItemRecipeRef, ShoppingListItemRecipeRefOut]] = [] class Config: orm_mode = True @@ -54,6 +58,9 @@ class ShoppingListItemOut(ShoppingListItemUpdate): class ShoppingListCreate(MealieModel): name: str = None + created_at: Optional[datetime] + update_at: Optional[datetime] + class ShoppingListRecipeRefOut(MealieModel): id: UUID4 diff --git a/tests/integration_tests/user_group_tests/test_group_shopping_list_items.py b/tests/integration_tests/user_group_tests/test_group_shopping_list_items.py index 55e9da74b6d4..a882146b8baf 100644 --- a/tests/integration_tests/user_group_tests/test_group_shopping_list_items.py +++ b/tests/integration_tests/user_group_tests/test_group_shopping_list_items.py @@ -46,6 +46,8 @@ def serialize_list_items(list_items: list[ShoppingListItemOut]) -> list: item_dict["id"] = str(item.id) 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 @@ -151,6 +153,8 @@ def test_shopping_list_items_update_many_reorder( as_dict.append(item_dict) # 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) assert response.status_code == 200