Repository: saleor/saleor

Issue:
Translations for "sort by" fields in category page
Possible "sort by" values in category page don't use translations. These labels should be taken from model fields which I think is possible in django-filters.


Discussion:

Resolving patch:
diff --git a/saleor/product/filters.py b/saleor/product/filters.py
--- a/saleor/product/filters.py
+++ b/saleor/product/filters.py
@@ -3,7 +3,7 @@
 
 from django_filters import (FilterSet, MultipleChoiceFilter, RangeFilter,
                             OrderingFilter)
-from django.forms import CheckboxSelectMultiple
+from django.forms import CheckboxSelectMultiple, ValidationError
 from django.utils.translation import pgettext_lazy
 
 from django_prices.models import PriceField
@@ -11,13 +11,9 @@
 from .models import Product, ProductAttribute
 
 
-DEFAULT_SORT = 'name'
-
-
-SORT_BY_FIELDS = [{'value': 'name',
-                   'label': pgettext_lazy('Sort by filter', 'name')},
-                  {'value': 'price',
-                   'label': pgettext_lazy('Sort by filter', 'price')}]
+SORT_BY_FIELDS = {'name': pgettext_lazy('Product list sorting option', 'name'),
+                  'price': pgettext_lazy(
+                      'Product list sorting option', 'price')}
 
 
 class ProductFilter(FilterSet):
@@ -29,10 +25,12 @@ def __init__(self, *args, **kwargs):
         self.filters.update(self._get_product_attributes_filters())
         self.filters.update(self._get_product_variants_attributes_filters())
         self.filters = OrderedDict(sorted(self.filters.items()))
+        self.form.fields['sort_by'].validators.append(self.validate_sort_by)
 
     sort_by = OrderingFilter(
-        label='Sort by',
-        fields=[(field['value'], field['value']) for field in SORT_BY_FIELDS]
+        label=pgettext_lazy('Product list sorting form', 'Sort by'),
+        fields=SORT_BY_FIELDS.keys(),
+        field_labels=SORT_BY_FIELDS
     )
 
     class Meta:
@@ -79,3 +77,24 @@ def _get_product_variants_attributes_filters(self):
 
     def _get_attribute_choices(self, attribute):
         return [(choice.pk, choice.name) for choice in attribute.values.all()]
+
+    def validate_sort_by(self, value):
+        if value.strip('-') not in SORT_BY_FIELDS:
+            raise ValidationError(
+                _('%(value)s is not a valid sorting option'),
+                params={'value': value}
+            )
+
+
+def get_sort_by_choices(filter):
+    return [(choice[0], choice[1].lower()) for choice in
+            filter.filters['sort_by'].field.choices[1::2]]
+
+
+def get_now_sorted_by(filter):
+    sort_by = filter.form.cleaned_data.get('sort_by')
+    if sort_by:
+        sort_by = SORT_BY_FIELDS[sort_by[0].strip('-')]
+    else:
+        sort_by = SORT_BY_FIELDS['name']
+    return sort_by
diff --git a/saleor/product/views.py b/saleor/product/views.py
--- a/saleor/product/views.py
+++ b/saleor/product/views.py
@@ -8,16 +8,17 @@
 from django.shortcuts import get_object_or_404, redirect
 from django.template.response import TemplateResponse
 
-from ..cart.utils import set_cart_cookie
-from ..core.utils import get_paginator_items, serialize_decimal
-from ..settings import PAGINATE_BY
-from .filters import DEFAULT_SORT, ProductFilter, SORT_BY_FIELDS
+from .filters import (get_now_sorted_by, get_sort_by_choices,
+                      ProductFilter)
 from .models import Category
 from .utils import (products_with_details, products_for_cart,
                     handle_cart_form, get_availability,
                     get_product_images, get_variant_picker_data,
                     get_product_attributes_data,
                     product_json_ld, products_with_availability)
+from ..cart.utils import set_cart_cookie
+from ..core.utils import get_paginator_items, serialize_decimal
+from ..settings import PAGINATE_BY
 
 
 def product_details(request, slug, product_id, form=None):
@@ -120,18 +121,18 @@ def category_index(request, path, category_id):
                         category_id=category_id)
     products = (products_with_details(user=request.user)
                 .filter(categories__name=category)
-                .order_by(DEFAULT_SORT))
+                .order_by('name'))
     product_filter = ProductFilter(
         request.GET, queryset=products, category=category)
     products_paginated = get_paginator_items(
         product_filter.qs, PAGINATE_BY, request.GET.get('page'))
     products_and_availability = list(products_with_availability(
         products_paginated, request.discounts, request.currency))
-    sort_by = request.GET.get('sort_by', DEFAULT_SORT)
+    now_sorted_by = get_now_sorted_by(product_filter)
     ctx = {'category': category, 'filter': product_filter,
            'products': products_and_availability,
            'products_paginated': products_paginated,
-           'sort_by_choices': SORT_BY_FIELDS,
-           'sort_by_label': sort_by.strip('-'),
-           'is_descending': sort_by.startswith('-')}
+           'sort_by_choices': get_sort_by_choices(product_filter),
+           'now_sorted_by': now_sorted_by,
+           'is_descending': now_sorted_by.startswith('-')}
     return TemplateResponse(request, 'category/index.html', ctx)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -256,6 +256,25 @@ def product_in_stock(product_class, default_category):
     return product
 
 
+@pytest.fixture
+def product_list(product_class, default_category):
+    product_attr = product_class.product_attributes.first()
+    attr_value = product_attr.values.first()
+    attributes = {smart_text(product_attr.pk): smart_text(attr_value.pk)}
+
+    product_1 = Product.objects.create(
+        name='Test product 1', price=Decimal('10.00'),
+        product_class=product_class, attributes=attributes)
+    product_1.categories.add(default_category)
+
+    product_2 = Product.objects.create(
+        name='Test product 2', price=Decimal('20.00'),
+        product_class=product_class, attributes=attributes)
+    product_2.categories.add(default_category)
+
+    return [product_1, product_2]
+
+
 @pytest.fixture
 def stock_location():
     warehouse_1 = StockLocation.objects.create(name='Warehouse 1')
diff --git a/tests/test_product.py b/tests/test_product.py
--- a/tests/test_product.py
+++ b/tests/test_product.py
@@ -12,7 +12,8 @@
     models, ProductAvailabilityStatus, VariantAvailabilityStatus)
 from saleor.product.utils import (
     get_attributes_display_map, get_availability,
-    get_product_availability_status, get_variant_availability_status)
+    get_product_availability_status, products_with_details,
+    get_variant_availability_status)
 from tests.utils import filter_products_by_attribute
 
 
@@ -373,7 +374,9 @@ def test_variant_availability_status(unavailable_product):
 
 def test_product_filter_before_filtering(
         authorized_client, product_in_stock, default_category):
-    products = models.Product.objects.all()
+    products = (models.Product.objects.all()
+                .filter(categories__name=default_category)
+                .order_by('-price'))
     url = reverse(
         'product:category', kwargs={'path': default_category.slug,
                                     'category_id': default_category.pk})
@@ -383,7 +386,9 @@ def test_product_filter_before_filtering(
 
 def test_product_filter_product_exists(authorized_client, product_in_stock,
                                        default_category):
-    products = models.Product.objects.all()
+    products = (models.Product.objects.all()
+                .filter(categories__name=default_category)
+                .order_by('-price'))
     url = reverse(
         'product:category', kwargs={'path': default_category.slug,
                                     'category_id': default_category.pk})
@@ -399,12 +404,14 @@ def test_product_filter_product_does_not_exists(
                                     'category_id': default_category.pk})
     data = {'price_0': ['20'], 'price_1': ['']}
     response = authorized_client.get(url, data)
-    assert list(response.context['filter'].qs) == []
+    assert not list(response.context['filter'].qs)
 
 
 def test_product_filter_form(authorized_client, product_in_stock,
-                                      default_category):
-    products = models.Product.objects.all()
+                             default_category):
+    products = (models.Product.objects.all()
+                .filter(categories__name=default_category)
+                .order_by('-price'))
     url = reverse(
         'product:category', kwargs={'path': default_category.slug,
                                     'category_id': default_category.pk})
@@ -412,3 +419,26 @@ def test_product_filter_form(authorized_client, product_in_stock,
     assert 'price' in response.context['filter'].form.fields.keys()
     assert 'sort_by' in response.context['filter'].form.fields.keys()
     assert list(response.context['filter'].qs) == list(products)
+
+
+def test_product_filter_sorted_by_price_descending(
+    authorized_client, product_list, default_category):
+    products = (models.Product.objects.all()
+                .filter(categories__name=default_category)
+                .order_by('-price'))
+    url = reverse(
+        'product:category', kwargs={'path': default_category.slug,
+                                    'category_id': default_category.pk})
+    data = {'sort_by': '-price'}
+    response = authorized_client.get(url, data)
+    assert list(response.context['filter'].qs) == list(products)
+
+
+def test_product_filter_sorted_by_wrong_parameter(
+        authorized_client, product_in_stock, default_category):
+    url = reverse(
+        'product:category', kwargs={'path': default_category.slug,
+                                    'category_id': default_category.pk})
+    data = {'sort_by': 'aaa'}
+    response = authorized_client.get(url, data)
+    assert not list(response.context['filter'].qs)