1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-24 15:49:42 +02:00

perf(backend): remove validation on recipe summary response (#718)

* count responses

* perf(backend):  remove validation on recipe summary response

use the construct() method from pydantic to reduce get time as well as optimize the SQL query for recipes

* update UI to support new categories/tags

* fix(backend): 🐛 restrict recipes by group

Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-10-02 22:07:29 -08:00 committed by GitHub
parent f9829141c0
commit 568215cf70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 82 additions and 37 deletions

View file

@ -40,7 +40,9 @@ def populate_data(token):
def time_request(url, headers): def time_request(url, headers):
start = time.time() start = time.time()
_ = requests.get(url, headers=headers) r = requests.get(url, headers=headers)
print(f"Total Recipes {len(r.json())}")
end = time.time() end = time.time()
print(end - start) print(end - start)

View file

@ -3,15 +3,15 @@
<h2 v-if="title" class="mt-4">{{ title }}</h2> <h2 v-if="title" class="mt-4">{{ title }}</h2>
<v-chip <v-chip
v-for="category in items.slice(0, limit)" v-for="category in items.slice(0, limit)"
:key="category" :key="category.name"
label label
class="ma-1" class="ma-1"
color="accent" color="accent"
:small="small" :small="small"
dark dark
:to="`/recipes/${urlParam}/${getSlug(category)}`" :to="`/recipes/${urlParam}/${category.slug}`"
> >
{{ truncateText(category) }} {{ truncateText(category.name) }}
</v-chip> </v-chip>
</div> </div>
</template> </template>
@ -56,7 +56,7 @@ export default {
return this.$store.getters.getAllTags || []; return this.$store.getters.getAllTags || [];
}, },
urlParam() { urlParam() {
return this.isCategory ? "category" : "tag"; return this.isCategory ? "categories" : "tags";
}, },
}, },
methods: { methods: {

View file

@ -182,7 +182,7 @@
<RecipeCategoryTagSelector <RecipeCategoryTagSelector
v-if="form" v-if="form"
v-model="recipe.recipeCategory" v-model="recipe.recipeCategory"
:return-object="false" :return-object="true"
:show-add="true" :show-add="true"
:show-label="false" :show-label="false"
/> />
@ -200,7 +200,7 @@
<RecipeCategoryTagSelector <RecipeCategoryTagSelector
v-if="form" v-if="form"
v-model="recipe.tags" v-model="recipe.tags"
:return-object="false" :return-object="true"
:show-add="true" :show-add="true"
:tag-selector="true" :tag-selector="true"
:show-label="false" :show-label="false"

View file

@ -122,10 +122,15 @@ export default defineComponent({
}, },
filteredRecipes() { filteredRecipes() {
return this.allRecipes.filter((recipe) => { return this.allRecipes.filter((recipe) => {
const includesTags = this.check(this.includeTags, recipe.tags, this.tagFilter.matchAny, this.tagFilter.exclude); const includesTags = this.check(
this.includeTags,
recipe.tags.map((x) => x.name),
this.tagFilter.matchAny,
this.tagFilter.exclude
);
const includesCats = this.check( const includesCats = this.check(
this.includeCategories, this.includeCategories,
recipe.recipeCategory, recipe.recipeCategory.map((x) => x.name),
this.catFilter.matchAny, this.catFilter.matchAny,
this.catFilter.exclude this.catFilter.exclude
); );

View file

@ -81,11 +81,11 @@ class Database:
@cached_property @cached_property
def categories(self) -> CategoryDataAccessModel: def categories(self) -> CategoryDataAccessModel:
return CategoryDataAccessModel(self.session, pk_id, Category, RecipeCategoryResponse) return CategoryDataAccessModel(self.session, pk_slug, Category, RecipeCategoryResponse)
@cached_property @cached_property
def tags(self) -> TagsDataAccessModel: def tags(self) -> TagsDataAccessModel:
return TagsDataAccessModel(self.session, pk_id, Tag, RecipeTagResponse) return TagsDataAccessModel(self.session, pk_slug, Tag, RecipeTagResponse)
# ================================================================ # ================================================================
# Site Items # Site Items

View file

@ -1,4 +1,7 @@
from random import randint from random import randint
from typing import Any
from sqlalchemy.orm import joinedload
from mealie.db.models.recipe.recipe import RecipeModel from mealie.db.models.recipe.recipe import RecipeModel
from mealie.db.models.recipe.settings import RecipeSettings from mealie.db.models.recipe.settings import RecipeSettings
@ -57,3 +60,13 @@ class RecipeDataAccessModel(AccessModel[Recipe, RecipeModel]):
count=count, count=count,
override_schema=override_schema, override_schema=override_schema,
) )
def summary(self, group_id, start=0, limit=99999) -> Any:
return (
self.session.query(RecipeModel)
.options(joinedload(RecipeModel.recipe_category), joinedload(RecipeModel.tags))
.filter(RecipeModel.group_id == group_id)
.offset(start)
.limit(limit)
.all()
)

View file

@ -2,6 +2,8 @@ from zipfile import ZipFile
from fastapi import Depends, File from fastapi import Depends, File
from fastapi.datastructures import UploadFile from fastapi.datastructures import UploadFile
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from scrape_schema_recipe import scrape_url from scrape_schema_recipe import scrape_url
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse from starlette.responses import FileResponse
@ -22,7 +24,8 @@ logger = get_logger()
@user_router.get("", response_model=list[RecipeSummary]) @user_router.get("", response_model=list[RecipeSummary])
async def get_all(start=0, limit=None, service: RecipeService = Depends(RecipeService.private)): async def get_all(start=0, limit=None, service: RecipeService = Depends(RecipeService.private)):
return service.get_all(start, limit) json_compatible_item_data = jsonable_encoder(service.get_all(start, limit))
return JSONResponse(content=json_compatible_item_data)
@user_router.post("", status_code=201, response_model=str) @user_router.post("", status_code=201, response_model=str)

View file

@ -30,6 +30,18 @@ class CreateRecipe(CamelModel):
name: str name: str
class RecipeTag(CamelModel):
name: str
slug: str
class Config:
orm_mode = True
class RecipeCategory(RecipeTag):
pass
class RecipeSummary(CamelModel): class RecipeSummary(CamelModel):
id: Optional[int] id: Optional[int]
@ -39,11 +51,18 @@ class RecipeSummary(CamelModel):
name: Optional[str] name: Optional[str]
slug: str = "" slug: str = ""
image: Optional[Any] image: Optional[Any]
recipe_yield: Optional[str]
total_time: Optional[str] = None
prep_time: Optional[str] = None
cook_time: Optional[str] = None
perform_time: Optional[str] = None
description: Optional[str] = "" description: Optional[str] = ""
recipe_category: Optional[list[str]] = [] recipe_category: Optional[list[RecipeTag]] = []
tags: Optional[list[str]] = [] tags: Optional[list[RecipeTag]] = []
rating: Optional[int] rating: Optional[int]
org_url: Optional[str] = Field(None, alias="orgURL")
date_added: Optional[datetime.date] date_added: Optional[datetime.date]
date_updated: Optional[datetime.datetime] date_updated: Optional[datetime.datetime]
@ -51,31 +70,29 @@ class RecipeSummary(CamelModel):
class Config: class Config:
orm_mode = True orm_mode = True
@classmethod @validator("tags", always=True, pre=True)
def getter_dict(_cls, name_orm: RecipeModel): def validate_tags(cats: list[Any], values):
return { if isinstance(cats, list) and cats and isinstance(cats[0], str):
**GetterDict(name_orm), return [RecipeTag(name=c, slug=slugify(c)) for c in cats]
"recipe_category": [x.name for x in name_orm.recipe_category], return cats
"tags": [x.name for x in name_orm.tags],
} @validator("recipe_category", always=True, pre=True)
def validate_categories(cats: list[Any], values):
if isinstance(cats, list) and cats and isinstance(cats[0], str):
return [RecipeCategory(name=c, slug=slugify(c)) for c in cats]
return cats
class Recipe(RecipeSummary): class Recipe(RecipeSummary):
recipe_yield: Optional[str]
recipe_ingredient: Optional[list[RecipeIngredient]] = [] recipe_ingredient: Optional[list[RecipeIngredient]] = []
recipe_instructions: Optional[list[RecipeStep]] = [] recipe_instructions: Optional[list[RecipeStep]] = []
nutrition: Optional[Nutrition] nutrition: Optional[Nutrition]
tools: Optional[list[str]] = [] tools: Optional[list[str]] = []
total_time: Optional[str] = None
prep_time: Optional[str] = None
perform_time: Optional[str] = None
# Mealie Specific # Mealie Specific
settings: Optional[RecipeSettings] = RecipeSettings() settings: Optional[RecipeSettings] = RecipeSettings()
assets: Optional[list[RecipeAsset]] = [] assets: Optional[list[RecipeAsset]] = []
notes: Optional[list[RecipeNote]] = [] notes: Optional[list[RecipeNote]] = []
org_url: Optional[str] = Field(None, alias="orgURL")
extras: Optional[dict] = {} extras: Optional[dict] = {}
comments: Optional[list[CommentOut]] = [] comments: Optional[list[CommentOut]] = []
@ -110,8 +127,8 @@ class Recipe(RecipeSummary):
return { return {
**GetterDict(name_orm), **GetterDict(name_orm),
# "recipe_ingredient": [x.note for x in name_orm.recipe_ingredient], # "recipe_ingredient": [x.note for x in name_orm.recipe_ingredient],
"recipe_category": [x.name for x in name_orm.recipe_category], # "recipe_category": [x.name for x in name_orm.recipe_category],
"tags": [x.name for x in name_orm.tags], # "tags": [x.name for x in name_orm.tags],
"tools": [x.tool for x in name_orm.tools], "tools": [x.tool for x in name_orm.tools],
"extras": {x.key_name: x.value for x in name_orm.extras}, "extras": {x.key_name: x.value for x in name_orm.extras},
} }

View file

@ -55,12 +55,8 @@ class RecipeService(CrudHttpMixins[CreateRecipe, Recipe, Recipe], UserHttpServic
# CRUD METHODS # CRUD METHODS
def get_all(self, start=0, limit=None): def get_all(self, start=0, limit=None):
return self.db.recipes.multi_query( items = self.db.recipes.summary(self.user.group_id, start=start, limit=limit)
{"group_id": self.user.group_id}, return [RecipeSummary.construct(**x.__dict__) for x in items]
start=start,
limit=limit,
override_schema=RecipeSummary,
)
def create_one(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe: def create_one(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
create_data = recipe_creation_factory(self.user, name=create_data.name, additional_attrs=create_data.dict()) create_data = recipe_creation_factory(self.user, name=create_data.name, additional_attrs=create_data.dict())

View file

@ -41,7 +41,11 @@ def test_read_update(
recipe["notes"] = test_notes recipe["notes"] = test_notes
recipe["tools"] = ["one tool", "two tool"] recipe["tools"] = ["one tool", "two tool"]
test_categories = ["one", "two", "three"] test_categories = [
{"name": "one", "slug": "one"},
{"name": "two", "slug": "two"},
{"name": "three", "slug": "three"},
]
recipe["recipeCategory"] = test_categories recipe["recipeCategory"] = test_categories
response = api_client.put(recipe_url, json=recipe, headers=unique_user.token) response = api_client.put(recipe_url, json=recipe, headers=unique_user.token)
@ -54,7 +58,12 @@ def test_read_update(
recipe = json.loads(response.text) recipe = json.loads(response.text)
assert recipe["notes"] == test_notes assert recipe["notes"] == test_notes
assert recipe["recipeCategory"].sort() == test_categories.sort()
assert len(recipe["recipeCategory"]) == len(test_categories)
test_name = [x["name"] for x in test_categories]
for cats in zip(recipe["recipeCategory"], test_categories):
assert cats[0]["name"] in test_name
@pytest.mark.parametrize("recipe_data", recipe_test_data) @pytest.mark.parametrize("recipe_data", recipe_test_data)