mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	feat: random sort option for front page (#2363)
* Add hook for random sorting * Add random sorting to front page * Add multiple tests for random sorting. * Be extra sure that all recipes are returned. * Too stable random. seed doesn't reach backend. * add timestamp to useRecipeSearch * Update randomization tests for timestamp seeding * ruff cleanup * pass timestamp separately in getAll * remove debugging log items * remove timestamp from address bar * remove defaults from backend timestamps * timestamp should be optional * fix edge case: query without timestamp * similar edge case: no timestamp in pagination * ruff :/ * better edge case handling * stabilize random search test w/more recipes * better pagination seeding * update pagination seed test * remove redundant random/seed check * Test for api routes to random sorting. * please the typing gods * hack to make query parameters throw correct exc * ruff * fix validator message typo * black reformatting --------- Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
		| @@ -31,6 +31,7 @@ export function useRecipeSearch(api: UserApi): UseRecipeSearchReturn { | |||||||
|       orderBy: "name", |       orderBy: "name", | ||||||
|       orderDirection: "asc", |       orderDirection: "asc", | ||||||
|       perPage: 20, |       perPage: 20, | ||||||
|  |       _searchSeed: Date.now().toString(), | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (error) { |     if (error) { | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { useAsync, ref } from "@nuxtjs/composition-api"; | import { useAsync, ref } from "@nuxtjs/composition-api"; | ||||||
| import { useAsyncKey } from "../use-utils"; | import { useAsyncKey } from "../use-utils"; | ||||||
| import { useUserApi } from "~/composables/api"; | import { useUserApi } from "~/composables/api"; | ||||||
| import {Recipe} from "~/lib/api/types/recipe"; | import { Recipe } from "~/lib/api/types/recipe"; | ||||||
| import {RecipeSearchQuery} from "~/lib/api/user/recipes/recipe"; | import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe"; | ||||||
|  |  | ||||||
| export const allRecipes = ref<Recipe[]>([]); | export const allRecipes = ref<Recipe[]>([]); | ||||||
| export const recentRecipes = ref<Recipe[]>([]); | export const recentRecipes = ref<Recipe[]>([]); | ||||||
| @@ -23,6 +23,8 @@ export const useLazyRecipes = function () { | |||||||
|     const { data } = await api.recipes.getAll(page, perPage, { |     const { data } = await api.recipes.getAll(page, perPage, { | ||||||
|       orderBy, |       orderBy, | ||||||
|       orderDirection, |       orderDirection, | ||||||
|  |       paginationSeed: query?._searchSeed, // propagate searchSeed to stabilize random order pagination | ||||||
|  |       searchSeed: query?._searchSeed, // unused, but pass it along for completeness of data | ||||||
|       search: query?.search, |       search: query?.search, | ||||||
|       cookbook: query?.cookbook, |       cookbook: query?.cookbook, | ||||||
|       categories: query?.categories, |       categories: query?.categories, | ||||||
|   | |||||||
| @@ -78,6 +78,8 @@ export type RecipeSearchQuery = { | |||||||
|   page?: number; |   page?: number; | ||||||
|   perPage?: number; |   perPage?: number; | ||||||
|   orderBy?: string; |   orderBy?: string; | ||||||
|  |  | ||||||
|  |   _searchSeed?: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> { | export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> { | ||||||
|   | |||||||
| @@ -212,7 +212,6 @@ export default defineComponent({ | |||||||
|           foods: toIDArray(selectedFoods.value), |           foods: toIDArray(selectedFoods.value), | ||||||
|           tags: toIDArray(selectedTags.value), |           tags: toIDArray(selectedTags.value), | ||||||
|           tools: toIDArray(selectedTools.value), |           tools: toIDArray(selectedTools.value), | ||||||
|  |  | ||||||
|           // Only add the query param if it's or not default |           // Only add the query param if it's or not default | ||||||
|           ...{ |           ...{ | ||||||
|             auto: state.value.auto ? undefined : "false", |             auto: state.value.auto ? undefined : "false", | ||||||
| @@ -239,6 +238,7 @@ export default defineComponent({ | |||||||
|         requireAllFoods: state.value.requireAllFoods, |         requireAllFoods: state.value.requireAllFoods, | ||||||
|         orderBy: state.value.orderBy, |         orderBy: state.value.orderBy, | ||||||
|         orderDirection: state.value.orderDirection, |         orderDirection: state.value.orderDirection, | ||||||
|  |         _searchSeed: Date.now().toString() | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -303,6 +303,11 @@ export default defineComponent({ | |||||||
|         name: i18n.tc("general.updated"), |         name: i18n.tc("general.updated"), | ||||||
|         value: "update_at", |         value: "update_at", | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.diceMultiple, | ||||||
|  |         name: i18n.tc("general.random"), | ||||||
|  |         value: "random", | ||||||
|  |       }, | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|     onMounted(() => { |     onMounted(() => { | ||||||
|   | |||||||
| @@ -1,12 +1,13 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import random | ||||||
| from collections.abc import Iterable | from collections.abc import Iterable | ||||||
| from math import ceil | from math import ceil | ||||||
| from typing import Any, Generic, TypeVar | from typing import Any, Generic, TypeVar | ||||||
|  |  | ||||||
| from fastapi import HTTPException | from fastapi import HTTPException | ||||||
| from pydantic import UUID4, BaseModel | from pydantic import UUID4, BaseModel | ||||||
| from sqlalchemy import Select, delete, func, select | from sqlalchemy import Select, case, delete, func, select | ||||||
| from sqlalchemy.orm.session import Session | from sqlalchemy.orm.session import Session | ||||||
| from sqlalchemy.sql import sqltypes | from sqlalchemy.sql import sqltypes | ||||||
|  |  | ||||||
| @@ -378,4 +379,16 @@ class RepositoryGeneric(Generic[Schema, Model]): | |||||||
|  |  | ||||||
|                 query = query.order_by(order_attr) |                 query = query.order_by(order_attr) | ||||||
|  |  | ||||||
|  |             elif pagination.order_by == "random": | ||||||
|  |                 # randomize outside of database, since not all db's can set random seeds | ||||||
|  |                 # this solution is db-independent & stable to paging | ||||||
|  |                 temp_query = query.with_only_columns(self.model.id) | ||||||
|  |                 allids = self.session.execute(temp_query).scalars().all()  # fast because id is indexed | ||||||
|  |                 order = list(range(len(allids))) | ||||||
|  |                 random.seed(pagination.pagination_seed) | ||||||
|  |                 random.shuffle(order) | ||||||
|  |                 random_dict = dict(zip(allids, order, strict=True)) | ||||||
|  |                 case_stmt = case(random_dict, value=self.model.id) | ||||||
|  |                 query = query.order_by(case_stmt) | ||||||
|  |  | ||||||
|         return query.limit(pagination.per_page).offset((pagination.page - 1) * pagination.per_page), count, total_pages |         return query.limit(pagination.per_page).offset((pagination.page - 1) * pagination.per_page), count, total_pages | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ from mealie.routes._base import BaseCrudController, controller | |||||||
| from mealie.routes._base.mixins import HttpRepo | from mealie.routes._base.mixins import HttpRepo | ||||||
| from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter | from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter | ||||||
| from mealie.schema.cookbook.cookbook import ReadCookBook | from mealie.schema.cookbook.cookbook import ReadCookBook | ||||||
|  | from mealie.schema.make_dependable import make_dependable | ||||||
| from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe | from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe | ||||||
| from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeLastMade, RecipeSummary | from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeLastMade, RecipeSummary | ||||||
| from mealie.schema.recipe.recipe_asset import RecipeAsset | from mealie.schema.recipe.recipe_asset import RecipeAsset | ||||||
| @@ -237,8 +238,8 @@ class RecipeController(BaseRecipeController): | |||||||
|     def get_all( |     def get_all( | ||||||
|         self, |         self, | ||||||
|         request: Request, |         request: Request, | ||||||
|         q: PaginationQuery = Depends(), |         q: PaginationQuery = Depends(make_dependable(PaginationQuery)), | ||||||
|         search_query: RecipeSearchQuery = Depends(), |         search_query: RecipeSearchQuery = Depends(make_dependable(RecipeSearchQuery)), | ||||||
|         categories: list[UUID4 | str] | None = Query(None), |         categories: list[UUID4 | str] | None = Query(None), | ||||||
|         tags: list[UUID4 | str] | None = Query(None), |         tags: list[UUID4 | str] | None = Query(None), | ||||||
|         tools: list[UUID4 | str] | None = Query(None), |         tools: list[UUID4 | str] | None = Query(None), | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								mealie/schema/make_dependable.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								mealie/schema/make_dependable.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | from inspect import signature | ||||||
|  |  | ||||||
|  | from fastapi.exceptions import HTTPException, ValidationError | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def make_dependable(cls): | ||||||
|  |     """ | ||||||
|  |     Pydantic BaseModels are very powerful because we get lots of validations and type checking right out of the box. | ||||||
|  |     FastAPI can accept a BaseModel as a route Dependency and it will automatically handle things like documentation | ||||||
|  |     and error handling. However, if we define custom validators then the errors they raise are not handled, leading | ||||||
|  |     to HTTP 500's being returned. | ||||||
|  |  | ||||||
|  |     To better understand this issue, you can visit https://github.com/tiangolo/fastapi/issues/1474 for context. | ||||||
|  |  | ||||||
|  |     A workaround proposed there adds a classmethod which attempts to init the BaseModel and handles formatting of | ||||||
|  |     any raised ValidationErrors, custom or otherwise. However, this means essentially duplicating the class's | ||||||
|  |     signature. This function automates the creation of a workaround method with a matching signature so that you | ||||||
|  |     can avoid code duplication. | ||||||
|  |  | ||||||
|  |     usage: | ||||||
|  |     async def fetch(thing_request: ThingRequest = Depends(make_dependable(ThingRequest))): | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def init_cls_and_handle_errors(*args, **kwargs): | ||||||
|  |         try: | ||||||
|  |             signature(init_cls_and_handle_errors).bind(*args, **kwargs) | ||||||
|  |             return cls(*args, **kwargs) | ||||||
|  |         except ValidationError as e: | ||||||
|  |             for error in e.errors(): | ||||||
|  |                 error["loc"] = ["query"] + list(error["loc"]) | ||||||
|  |             raise HTTPException(422, detail=e.errors()) from None | ||||||
|  |  | ||||||
|  |     init_cls_and_handle_errors.__signature__ = signature(cls) | ||||||
|  |     return init_cls_and_handle_errors | ||||||
| @@ -3,7 +3,7 @@ from typing import Any, Generic, TypeVar | |||||||
| from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit | from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit | ||||||
|  |  | ||||||
| from humps import camelize | from humps import camelize | ||||||
| from pydantic import UUID4, BaseModel | from pydantic import UUID4, BaseModel, validator | ||||||
| from pydantic.generics import GenericModel | from pydantic.generics import GenericModel | ||||||
|  |  | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
| @@ -23,6 +23,7 @@ class RecipeSearchQuery(MealieModel): | |||||||
|     require_all_tools: bool = False |     require_all_tools: bool = False | ||||||
|     require_all_foods: bool = False |     require_all_foods: bool = False | ||||||
|     search: str | None |     search: str | None | ||||||
|  |     _search_seed: str | None = None | ||||||
|  |  | ||||||
|  |  | ||||||
| class PaginationQuery(MealieModel): | class PaginationQuery(MealieModel): | ||||||
| @@ -31,6 +32,13 @@ class PaginationQuery(MealieModel): | |||||||
|     order_by: str = "created_at" |     order_by: str = "created_at" | ||||||
|     order_direction: OrderDirection = OrderDirection.desc |     order_direction: OrderDirection = OrderDirection.desc | ||||||
|     query_filter: str | None = None |     query_filter: str | None = None | ||||||
|  |     pagination_seed: str | None = None | ||||||
|  |  | ||||||
|  |     @validator("pagination_seed", always=True, pre=True) | ||||||
|  |     def validate_randseed(cls, pagination_seed, values): | ||||||
|  |         if values.get("order_by") == "random" and not pagination_seed: | ||||||
|  |             raise ValueError("paginationSeed is required when orderBy is random") | ||||||
|  |         return pagination_seed | ||||||
|  |  | ||||||
|  |  | ||||||
| class PaginationBase(GenericModel, Generic[DataT]): | class PaginationBase(GenericModel, Generic[DataT]): | ||||||
|   | |||||||
| @@ -424,3 +424,28 @@ def test_get_recipe_by_slug_or_id(api_client: TestClient, unique_user: utils.Tes | |||||||
|         recipe_data = response.json() |         recipe_data = response.json() | ||||||
|         assert recipe_data["slug"] == slug |         assert recipe_data["slug"] == slug | ||||||
|         assert recipe_data["id"] == recipe_id |         assert recipe_data["id"] == recipe_id | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_get_random_order(api_client: TestClient, unique_user: utils.TestUser): | ||||||
|  |     # Create more recipes for stable random ordering | ||||||
|  |     slugs = [random_string(10) for _ in range(7)] | ||||||
|  |     for slug in slugs: | ||||||
|  |         response = api_client.post(api_routes.recipes, json={"name": slug}, headers=unique_user.token) | ||||||
|  |         assert response.status_code == 201 | ||||||
|  |         assert json.loads(response.text) == slug | ||||||
|  |  | ||||||
|  |     goodparams: dict[str, int | str] = {"page": 1, "perPage": -1, "orderBy": "random", "paginationSeed": "abcdefg"} | ||||||
|  |     response = api_client.get(api_routes.recipes, params=goodparams, headers=unique_user.token) | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |  | ||||||
|  |     seed1_params: dict[str, int | str] = {"page": 1, "perPage": -1, "orderBy": "random", "paginationSeed": "abcdefg"} | ||||||
|  |     seed2_params: dict[str, int | str] = {"page": 1, "perPage": -1, "orderBy": "random", "paginationSeed": "gfedcba"} | ||||||
|  |     data1 = api_client.get(api_routes.recipes, params=seed1_params, headers=unique_user.token).json() | ||||||
|  |     data2 = api_client.get(api_routes.recipes, params=seed2_params, headers=unique_user.token).json() | ||||||
|  |     data1_new = api_client.get(api_routes.recipes, params=seed1_params, headers=unique_user.token).json() | ||||||
|  |     assert data1["items"][0]["slug"] != data2["items"][0]["slug"]  # new seed -> new order | ||||||
|  |     assert data1["items"][0]["slug"] == data1_new["items"][0]["slug"]  # same seed -> same order | ||||||
|  |  | ||||||
|  |     badparams: dict[str, int | str] = {"page": 1, "perPage": -1, "orderBy": "random"} | ||||||
|  |     response = api_client.get(api_routes.recipes, params=badparams, headers=unique_user.token) | ||||||
|  |     assert response.status_code == 422 | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | from datetime import datetime | ||||||
| from typing import cast | from typing import cast | ||||||
|  |  | ||||||
| from mealie.repos.repository_factory import AllRepositories | from mealie.repos.repository_factory import AllRepositories | ||||||
| @@ -200,6 +201,20 @@ def test_recipe_repo_pagination_by_categories(database: AllRepositories, unique_ | |||||||
|         for category in created_categories: |         for category in created_categories: | ||||||
|             assert category.id in category_ids |             assert category.id in category_ids | ||||||
|  |  | ||||||
|  |     # Test random ordering with category filter | ||||||
|  |     pagination_query = PaginationQuery( | ||||||
|  |         page=1, | ||||||
|  |         per_page=-1, | ||||||
|  |         order_by="random", | ||||||
|  |         pagination_seed=str(datetime.now()), | ||||||
|  |         order_direction=OrderDirection.asc, | ||||||
|  |     ) | ||||||
|  |     random_ordered = [] | ||||||
|  |     for i in range(5): | ||||||
|  |         pagination_query.pagination_seed = str(datetime.now()) | ||||||
|  |         random_ordered.append(database.recipes.page_all(pagination_query, categories=[category_slug]).items) | ||||||
|  |     assert not all(i == random_ordered[0] for i in random_ordered) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_recipe_repo_pagination_by_tags(database: AllRepositories, unique_user: TestUser): | def test_recipe_repo_pagination_by_tags(database: AllRepositories, unique_user: TestUser): | ||||||
|     slug1, slug2 = (random_string(10) for _ in range(2)) |     slug1, slug2 = (random_string(10) for _ in range(2)) | ||||||
| @@ -279,6 +294,21 @@ def test_recipe_repo_pagination_by_tags(database: AllRepositories, unique_user: | |||||||
|         for tag in created_tags: |         for tag in created_tags: | ||||||
|             assert tag.id in tag_ids |             assert tag.id in tag_ids | ||||||
|  |  | ||||||
|  |     # Test random ordering with tag filter | ||||||
|  |     pagination_query = PaginationQuery( | ||||||
|  |         page=1, | ||||||
|  |         per_page=-1, | ||||||
|  |         order_by="random", | ||||||
|  |         pagination_seed=str(datetime.now()), | ||||||
|  |         order_direction=OrderDirection.asc, | ||||||
|  |     ) | ||||||
|  |     random_ordered = [] | ||||||
|  |     for i in range(5): | ||||||
|  |         pagination_query.pagination_seed = str(datetime.now()) | ||||||
|  |         random_ordered.append(database.recipes.page_all(pagination_query, tags=[tag_slug]).items) | ||||||
|  |     assert len(random_ordered[0]) == 15 | ||||||
|  |     assert not all(i == random_ordered[0] for i in random_ordered) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_recipe_repo_pagination_by_tools(database: AllRepositories, unique_user: TestUser): | def test_recipe_repo_pagination_by_tools(database: AllRepositories, unique_user: TestUser): | ||||||
|     slug1, slug2 = (random_string(10) for _ in range(2)) |     slug1, slug2 = (random_string(10) for _ in range(2)) | ||||||
| @@ -360,6 +390,21 @@ def test_recipe_repo_pagination_by_tools(database: AllRepositories, unique_user: | |||||||
|         for tool in created_tools: |         for tool in created_tools: | ||||||
|             assert tool.id in tool_ids |             assert tool.id in tool_ids | ||||||
|  |  | ||||||
|  |     # Test random ordering with tools filter | ||||||
|  |     pagination_query = PaginationQuery( | ||||||
|  |         page=1, | ||||||
|  |         per_page=-1, | ||||||
|  |         order_by="random", | ||||||
|  |         pagination_seed=str(datetime.now()), | ||||||
|  |         order_direction=OrderDirection.asc, | ||||||
|  |     ) | ||||||
|  |     random_ordered = [] | ||||||
|  |     for i in range(5): | ||||||
|  |         pagination_query.pagination_seed = str(datetime.now()) | ||||||
|  |         random_ordered.append(database.recipes.page_all(pagination_query, tools=[tool_id]).items) | ||||||
|  |     assert len(random_ordered[0]) == 15 | ||||||
|  |     assert not all(i == random_ordered[0] for i in random_ordered) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_recipe_repo_pagination_by_foods(database: AllRepositories, unique_user: TestUser): | def test_recipe_repo_pagination_by_foods(database: AllRepositories, unique_user: TestUser): | ||||||
|     slug1, slug2 = (random_string(10) for _ in range(2)) |     slug1, slug2 = (random_string(10) for _ in range(2)) | ||||||
| @@ -430,6 +475,20 @@ def test_recipe_repo_pagination_by_foods(database: AllRepositories, unique_user: | |||||||
|  |  | ||||||
|     assert len(recipes_with_either_food) == 20 |     assert len(recipes_with_either_food) == 20 | ||||||
|  |  | ||||||
|  |     pagination_query = PaginationQuery( | ||||||
|  |         page=1, | ||||||
|  |         per_page=-1, | ||||||
|  |         order_by="random", | ||||||
|  |         pagination_seed=str(datetime.now()), | ||||||
|  |         order_direction=OrderDirection.asc, | ||||||
|  |     ) | ||||||
|  |     random_ordered = [] | ||||||
|  |     for i in range(5): | ||||||
|  |         pagination_query.pagination_seed = str(datetime.now()) | ||||||
|  |         random_ordered.append(database.recipes.page_all(pagination_query, foods=[food_id]).items) | ||||||
|  |     assert len(random_ordered[0]) == 15 | ||||||
|  |     assert not all(i == random_ordered[0] for i in random_ordered) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_recipe_repo_search(database: AllRepositories, unique_user: TestUser): | def test_recipe_repo_search(database: AllRepositories, unique_user: TestUser): | ||||||
|     recipes = [ |     recipes = [ | ||||||
| @@ -461,6 +520,37 @@ def test_recipe_repo_search(database: AllRepositories, unique_user: TestUser): | |||||||
|             group_id=unique_user.group_id, |             group_id=unique_user.group_id, | ||||||
|             name="Rátàtôuile", |             name="Rátàtôuile", | ||||||
|         ), |         ), | ||||||
|  |         # Add a bunch of recipes for stable randomization | ||||||
|  |         Recipe( | ||||||
|  |             user_id=unique_user.user_id, | ||||||
|  |             group_id=unique_user.group_id, | ||||||
|  |             name=f"{random_string(10)} soup", | ||||||
|  |         ), | ||||||
|  |         Recipe( | ||||||
|  |             user_id=unique_user.user_id, | ||||||
|  |             group_id=unique_user.group_id, | ||||||
|  |             name=f"{random_string(10)} soup", | ||||||
|  |         ), | ||||||
|  |         Recipe( | ||||||
|  |             user_id=unique_user.user_id, | ||||||
|  |             group_id=unique_user.group_id, | ||||||
|  |             name=f"{random_string(10)} soup", | ||||||
|  |         ), | ||||||
|  |         Recipe( | ||||||
|  |             user_id=unique_user.user_id, | ||||||
|  |             group_id=unique_user.group_id, | ||||||
|  |             name=f"{random_string(10)} soup", | ||||||
|  |         ), | ||||||
|  |         Recipe( | ||||||
|  |             user_id=unique_user.user_id, | ||||||
|  |             group_id=unique_user.group_id, | ||||||
|  |             name=f"{random_string(10)} soup", | ||||||
|  |         ), | ||||||
|  |         Recipe( | ||||||
|  |             user_id=unique_user.user_id, | ||||||
|  |             group_id=unique_user.group_id, | ||||||
|  |             name=f"{random_string(10)} soup", | ||||||
|  |         ), | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     for recipe in recipes: |     for recipe in recipes: | ||||||
| @@ -520,3 +610,17 @@ def test_recipe_repo_search(database: AllRepositories, unique_user: TestUser): | |||||||
|         fuzzy_result = database.recipes.page_all(pagination_query, search="Steinbuck").items |         fuzzy_result = database.recipes.page_all(pagination_query, search="Steinbuck").items | ||||||
|         assert len(fuzzy_result) == 1 |         assert len(fuzzy_result) == 1 | ||||||
|         assert fuzzy_result[0].name == "Steinbock Sloop" |         assert fuzzy_result[0].name == "Steinbock Sloop" | ||||||
|  |  | ||||||
|  |     # Test random ordering with search | ||||||
|  |     pagination_query = PaginationQuery( | ||||||
|  |         page=1, | ||||||
|  |         per_page=-1, | ||||||
|  |         order_by="random", | ||||||
|  |         pagination_seed=str(datetime.now()), | ||||||
|  |         order_direction=OrderDirection.asc, | ||||||
|  |     ) | ||||||
|  |     random_ordered = [] | ||||||
|  |     for i in range(5): | ||||||
|  |         pagination_query.pagination_seed = str(datetime.now()) | ||||||
|  |         random_ordered.append(database.recipes.page_all(pagination_query, search="soup").items) | ||||||
|  |     assert not all(i == random_ordered[0] for i in random_ordered) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user