Ordena etiquetas, categorias, cuentas y objetivos ignorando mayusculas
Tag era el unico de los tres modelos con nombre sin ordering en su Meta, asi
que las etiquetas salian en orden de insercion. Al anadirlo aparecio el
problema de fondo: con ordering = ["name"], SQLite ordena por valor binario y
en ASCII todas las mayusculas van antes que todas las minusculas, de modo que
salia AA, MK, ZZ, mk, ms en vez de AA, MK, mk, ms, ZZ.
Importa mas de lo que parece porque en produccion la base de datos es
PostgreSQL, que ordena segun la configuracion regional. El orden pasa a
calcularse en la consulta con Lower("name") en vez de depender de la colacion
del motor, asi que local y NAS coinciden.
Los tres Meta usan ahora ordering = [Lower("name")]. Django admite
expresiones ahi, pero no se propaga solo a todos los sitios: habia tres
sitios con orden explicito que lo pisaban y tambien se corrigen.
- tag_list y account_list tenian .order_by("name")
- goal_list ordenaba igual
- _category_tree reordena los hermanos en memoria, y Python compara por code
point igual que SQLite, asi que el arbol tampoco quedaba bien
Las dos migraciones son AlterModelOptions, sin cambios de esquema, pero hay
que desplegarlas.
Lo que no resuelve: los acentos. Lower() normaliza mayusculas y minusculas,
pero como se ordenan "N", "a" o "u" sigue dependiendo de la colacion de cada
motor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e814bc2d03
commit
f135797d31
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.10 on 2026-09-09 11:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('expenses', '0010_goal_account_goal_include_subcategories_goal_kind_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='tag',
|
||||
options={'ordering': ('name',)},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.2.10 on 2026-09-09 12:59
|
||||
|
||||
import django.db.models.functions.text
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('expenses', '0011_alter_tag_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='account',
|
||||
options={'ordering': [django.db.models.functions.text.Lower('name')]},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='category',
|
||||
options={'ordering': [django.db.models.functions.text.Lower('name')], 'verbose_name_plural': 'categories'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='tag',
|
||||
options={'ordering': [django.db.models.functions.text.Lower('name')]},
|
||||
),
|
||||
]
|
||||
@ -5,7 +5,7 @@ from django.conf import settings
|
||||
from django.db.models import Sum
|
||||
from functools import cached_property
|
||||
from django.utils.text import slugify
|
||||
from django.db.models.functions import ExtractMonth
|
||||
from django.db.models.functions import ExtractMonth, Lower
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
@ -29,7 +29,7 @@ class Category(models.Model):
|
||||
class Meta:
|
||||
unique_together = ("name", "parent", "owner", "slug")
|
||||
verbose_name_plural = "categories"
|
||||
ordering = ["name"]
|
||||
ordering = [Lower("name")]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -71,7 +71,7 @@ class Account(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
ordering = [Lower("name")]
|
||||
|
||||
def current_balance(self):
|
||||
expenses_total = self.expenses.aggregate(total=Sum("amount"))[
|
||||
@ -193,6 +193,7 @@ class Tag(models.Model):
|
||||
|
||||
class Meta:
|
||||
unique_together = ("name", "owner")
|
||||
ordering = [Lower("name")]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@ -21,3 +21,12 @@ def test_tag_list_annotates_expense_usage_count(auth_client, user, account, cate
|
||||
|
||||
counts = {tag.name: tag.expense_count for tag in response.context["tags"]}
|
||||
assert counts == {"Usada": 2, "Sin usar": 0}
|
||||
|
||||
|
||||
def test_tag_ordering_ignores_case(user):
|
||||
for name in ["ZZ", "AA", "mk"]:
|
||||
Tag.objects.create(owner=user, name=name)
|
||||
|
||||
names = list(Tag.objects.filter(owner=user).values_list("name", flat=True))
|
||||
|
||||
assert names == ["AA", "mk", "ZZ"]
|
||||
|
||||
@ -16,7 +16,7 @@ from .forms import (
|
||||
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Sum, Count, ProtectedError
|
||||
from django.db.models.functions import ExtractMonth, ExtractYear, ExtractDay
|
||||
from django.db.models.functions import ExtractMonth, ExtractYear, ExtractDay, Lower
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
@ -64,7 +64,7 @@ def _category_tree(categories):
|
||||
for category in categories:
|
||||
by_parent.setdefault(category.parent_id, []).append(category)
|
||||
for children in by_parent.values():
|
||||
children.sort(key=lambda c: c.name)
|
||||
children.sort(key=lambda c: c.name.lower())
|
||||
|
||||
rows = []
|
||||
|
||||
@ -638,7 +638,7 @@ def tag_list(request):
|
||||
tags = (
|
||||
Tag.objects.filter(owner=request.user)
|
||||
.annotate(expense_count=Count("expenses"))
|
||||
.order_by("name")
|
||||
.order_by(Lower("name"))
|
||||
)
|
||||
|
||||
return render(
|
||||
@ -700,7 +700,7 @@ def tag_delete(request, pk):
|
||||
|
||||
@login_required
|
||||
def account_list(request):
|
||||
accounts = list(Account.objects.filter(owner=request.user).order_by("name"))
|
||||
accounts = list(Account.objects.filter(owner=request.user).order_by(Lower("name")))
|
||||
account_rows, _, _ = _account_balances(accounts)
|
||||
return render(
|
||||
request,
|
||||
@ -1077,7 +1077,7 @@ def goal_list(request):
|
||||
else:
|
||||
selected_kind = ""
|
||||
|
||||
goals = goals.order_by("name")
|
||||
goals = goals.order_by(Lower("name"))
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user