Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add base_ordering to OrderingFilter #1115

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions django_filters/filters.py
Expand Up @@ -669,6 +669,13 @@ class OrderingFilter(BaseCSVFilter, ChoiceFilter):
of {field name: human readable label}. Keep in mind that the key is the
field name, and not the exposed parameter name.

* ``base_ordering`` is an optional argument that allows you to specify
a set of fields that will be given to ``order_by()`` after any provided
ordering options. This is useful if you filter by a non-unique field
but want an unique field to serve as a base. Django guarantees a
consistent order only for querysets with at least one unique ordering
field. Consistent ordering is important for things like pagination.

Additionally, you can just provide your own ``choices`` if you require
explicit control over the exposed options. For example, when you might
want to disable descending sort options.
Expand All @@ -684,12 +691,14 @@ def __init__(self, *args, **kwargs):
"""
``fields`` may be either a mapping or an iterable.
``field_labels`` must be a map of field names to display labels
``base_ordering`` must be an iterable.
"""
fields = kwargs.pop('fields', {})
fields = self.normalize_fields(fields)
field_labels = kwargs.pop('field_labels', {})

self.param_map = {v: k for k, v in fields.items()}
self.base_ordering = kwargs.pop('base_ordering', ())

if 'choices' not in kwargs:
kwargs['choices'] = self.build_choices(fields, field_labels)
Expand All @@ -706,11 +715,29 @@ def get_ordering_value(self, param):

return "-%s" % field_name if descending else field_name

def get_base_ordering(self, ordering):
"""
Get base ordering for order_by call.

Removes any values already in ordering (ascending or descending).
"""
base_ordering = []
for field in self.base_ordering:
descending = field.startswith('-')
field_ascending = field[1:] if descending else field
field_descending = field if descending else ('-%s' % field)
if (field_ascending not in ordering) and (field_descending not in ordering):
base_ordering.append(field)
return base_ordering

def filter(self, qs, value):
if value in EMPTY_VALUES:
return qs

ordering = [self.get_ordering_value(param) for param in value]
if self.base_ordering:
base_ordering = self.get_base_ordering(ordering)
return qs.order_by(*ordering, *base_ordering)
return qs.order_by(*ordering)

@classmethod
Expand Down
18 changes: 18 additions & 0 deletions tests/test_filters.py
Expand Up @@ -1461,6 +1461,24 @@ def test_filtering_skipped_with_none_value(self):
result = f.filter(qs, None)
self.assertEqual(qs, result)

def test_base_ordering(self):
qs = mock.Mock(spec=['order_by'])
f = OrderingFilter(base_ordering=['b'])
f.filter(qs, ['a'])
qs.order_by.assert_called_once_with('a', 'b')

def test_base_ordering_no_duplicate(self):
qs = mock.Mock(spec=['order_by'])
f = OrderingFilter(base_ordering=['b'])
f.filter(qs, ['a', 'b'])
qs.order_by.assert_called_once_with('a', 'b')

def test_base_ordering_no_duplicate_descending(self):
qs = mock.Mock(spec=['order_by'])
f = OrderingFilter(base_ordering=['b'])
f.filter(qs, ['a', '-b'])
qs.order_by.assert_called_once_with('a', '-b')

def test_choices_unaltered(self):
# provided 'choices' should not be altered when 'fields' is present
f = OrderingFilter(
Expand Down