mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-05-25 11:10:26 -04:00
fix: sort recipe names accent-folded for locale-aware ordering (#6853)
Recipe name sorting applied lower() to the raw name, so accented and umlaut characters sorted by raw code point and landed after "Z" (e.g. "Über" after "Zebra"). Sort by the existing unidecode-normalized name column instead, so accented characters sort next to their base letter. Works identically on SQLite and Postgres since the sort key is a pre-computed ASCII column rather than a DB collation.
This commit is contained in:
@@ -26,7 +26,7 @@ from mealie.schema.recipe.recipe import RecipePagination, RecipeSummary, create_
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood
|
||||
from mealie.schema.recipe.recipe_suggestion import RecipeSuggestionQuery, RecipeSuggestionResponseItem
|
||||
from mealie.schema.recipe.recipe_tool import RecipeToolOut
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.schema.response.pagination import OrderByNullPosition, OrderDirection, PaginationQuery
|
||||
from mealie.services.query_filter.builder import QueryFilterBuilder
|
||||
|
||||
from ..db.models._model_base import SqlAlchemyBase
|
||||
@@ -92,6 +92,20 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
||||
)
|
||||
return sa.cast(effective_rating, sa.Float)
|
||||
|
||||
def add_order_attr_to_query(
|
||||
self,
|
||||
query: sa.Select,
|
||||
order_attr: orm.InstrumentedAttribute,
|
||||
order_dir: OrderDirection,
|
||||
order_by_null: OrderByNullPosition | None,
|
||||
) -> sa.Select:
|
||||
# Sort recipe names by their normalized (accent-folded, lowercased) form so that
|
||||
# accented and umlaut characters sort next to their base letter (e.g. "Über" near
|
||||
# "U") instead of after "Z" by raw code point. See GH #6853.
|
||||
if order_attr is RecipeModel.name:
|
||||
order_attr = RecipeModel.name_normalized
|
||||
return super().add_order_attr_to_query(query, order_attr, order_dir, order_by_null)
|
||||
|
||||
def create(self, document: Recipe) -> Recipe: # type: ignore
|
||||
max_retries = 10
|
||||
original_name: str = document.name # type: ignore
|
||||
|
||||
@@ -647,6 +647,40 @@ def test_order_by_last_made(unique_user: TestUser, h2_user: TestUser):
|
||||
assert [item.id for item in h2_query.items] == [recipe_2.id, recipe_1.id]
|
||||
|
||||
|
||||
def test_order_by_name_is_accent_folded(unique_user: TestUser):
|
||||
# Names chosen so that raw code-point sorting (the previous behavior) would place the
|
||||
# umlaut/accented entries after "Zebra". Accent-folded sorting must instead place them
|
||||
# next to their base letter: "ärtsoppa" near "a", "Über" near "u". See GH #6853.
|
||||
names = ["Zebra", "ärtsoppa", "Apple", "Über"]
|
||||
recipes = [
|
||||
unique_user.repos.recipes.create(Recipe(user_id=unique_user.user_id, group_id=unique_user.group_id, name=name))
|
||||
for name in names
|
||||
]
|
||||
recipe_ids = ", ".join(str(recipe.id) for recipe in recipes)
|
||||
|
||||
ascending = unique_user.repos.recipes.page_all(
|
||||
PaginationQuery(
|
||||
page=1,
|
||||
per_page=-1,
|
||||
order_by="name",
|
||||
order_direction=OrderDirection.asc,
|
||||
query_filter=f"id IN [{recipe_ids}]",
|
||||
)
|
||||
)
|
||||
assert [item.name for item in ascending.items] == ["Apple", "ärtsoppa", "Über", "Zebra"]
|
||||
|
||||
descending = unique_user.repos.recipes.page_all(
|
||||
PaginationQuery(
|
||||
page=1,
|
||||
per_page=-1,
|
||||
order_by="name",
|
||||
order_direction=OrderDirection.desc,
|
||||
query_filter=f"id IN [{recipe_ids}]",
|
||||
)
|
||||
)
|
||||
assert [item.name for item in descending.items] == ["Zebra", "Über", "ärtsoppa", "Apple"]
|
||||
|
||||
|
||||
def test_coalesce_last_made(unique_user: TestUser):
|
||||
dt = datetime.now(UTC)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user