diff --git a/catalog/expressions.py b/catalog/expressions.py new file mode 100644 index 0000000..62fe43a --- /dev/null +++ b/catalog/expressions.py @@ -0,0 +1,8 @@ +from django.db.models import Func + + +class Substring(Func): + function = 'substring' + arity = 2 + + diff --git a/catalog/models.py b/catalog/models.py index 76e1994..36065b9 100644 --- a/catalog/models.py +++ b/catalog/models.py @@ -13,6 +13,8 @@ from django.utils.translation import ugettext_lazy as _ from unidecode import unidecode +from catalog.expressions import Substring + SLUG_MAX_LENGTH = 50 @@ -225,27 +227,30 @@ def __str__(self): class TagQuerySet(models.QuerySet): + # @todo #273:30m Apply new order_by_alphanumeric for SE/STB. + + # @todo #273:60m Create an index for order_by_alphanumeric query. + def order_by_alphanumeric(self): + """Sort the Tag by name's alphabetic chars and then by numeric chars.""" + return self.annotate( + tag_name=Substring(models.F('name'), models.Value('[a-zA-Zа-яА-Я]+')), + tag_value=models.functions.Cast( + Substring(models.F('name'), models.Value('[0-9]+\.?[0-9]*')), + models.FloatField(), + )).order_by('tag_name', 'tag_value') def filter_by_products(self, products: Iterable[AbstractProduct]): - ordering = settings.TAGS_ORDER - distinct = [order.lstrip('-') for order in ordering] - return ( self .filter(products__in=products) - .order_by(*ordering) - .distinct(*distinct, 'id') + .distinct() ) def exclude_by_products(self, products: Iterable[AbstractProduct]): - ordering = settings.TAGS_ORDER - distinct = [order.lstrip('-') for order in ordering] - return ( self .exclude(products__in=products) - .order_by(*ordering) - .distinct(*distinct, 'id') + .distinct() ) def get_group_tags_pairs(self) -> List[Tuple[TagGroup, List['Tag']]]: @@ -378,11 +383,8 @@ def parsed(self, raw: str): class TagManager(models.Manager.from_queryset(TagQuerySet)): - def get_queryset(self): - return ( - super().get_queryset() - .order_by(*settings.TAGS_ORDER) - ) + def order_by_alphanumeric(self): + return self.get_queryset().order_by_alphanumeric() def get_group_tags_pairs(self): return self.get_queryset().get_group_tags_pairs() diff --git a/tests/catalog/test_models.py b/tests/catalog/test_models.py index 9778044..7fc6104 100644 --- a/tests/catalog/test_models.py +++ b/tests/catalog/test_models.py @@ -1,5 +1,6 @@ """Defines tests for models in Catalog app.""" import unittest +from random import shuffle from django.db import DataError from django.test import TestCase @@ -289,3 +290,17 @@ def test_long_name(self): self.assertLessEqual(len(tag.slug), catalog.models.SLUG_MAX_LENGTH) except DataError as e: self.assertTrue(False, f'Tag has too long name. {e}') + + def test_order_by_alphanumeric(self): + ordered_tags = [ + catalog_models.MockTag(name=name) + for name in [ + 'a', 'b', '1 A', '2.1 A', '1.2 В', '1.2 В', '1.6 В', '5 В', '12 В', + ] + ] + + # reverse just in case + catalog_models.MockTag.objects.bulk_create(shuffle(ordered_tags)) + + for i, tag in enumerate(catalog_models.MockTag.objects.order_by_alphanumeric()): + self.assertEqual(tag, ordered_tags[i]) diff --git a/tests/test_settings.py b/tests/test_settings.py index 84e86e5..580140f 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -124,8 +124,6 @@ TAGS_TITLE_DELIMITER = ' или ' TAG_GROUPS_TITLE_DELIMITER = ' и ' -TAGS_ORDER = ['group__position', 'group__name', 'position', 'name'] - # random string to append to doubled slugs SLUG_HASH_SIZE = 5