Skip to content

Commit 33be0f5

Browse files
authored
Merge pull request #63 from cloudblue/feature/LITE-27342
LITE-27342 Added support for `MAX_ORDERING_LENGTH_IN_QUERY` and `ALLOWED_ORDERING_PERMUTATIONS_IN_QUERY`
2 parents c18b7a1 + 415247a commit 33be0f5

File tree

4 files changed

+134
-1
lines changed

4 files changed

+134
-1
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class ModelFilterClass(RQLFilterClass):
5656
DISTINCT - Boolean flag, that specifies if queryset must always be DISTINCT
5757
SELECT - Boolean flag, that specifies if Filter Class supports select operations and queryset optimizations
5858
OPENAPI_SPECIFICATION - Python class that renders OpenAPI specification
59+
MAX_ORDERING_LENGTH_IN_QUERY - Integer max allowed number of provided ordering filters in query ordering expression
60+
ALLOWED_ORDERING_PERMUTATIONS_IN_QUERY - Set of tuples of strings to specify a set of allowed ordering permutations
5961
6062
Filters can be set in two ways:
6163
1) string (default settings are calculated from ORM)

dj_rql/filter_cls.py

+52-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66
from collections import defaultdict
77
from datetime import datetime
8+
from itertools import chain
89
from typing import Set
910
from uuid import uuid4
1011

@@ -58,6 +59,13 @@ class RQLFilterClass:
5859
"""A list or tuple of filters definitions."""
5960

6061
EXTENDED_SEARCH_ORM_ROUTES = ()
62+
"""List of additional Django ORM fields for search."""
63+
64+
MAX_ORDERING_LENGTH_IN_QUERY = 5
65+
"""Max allowed number of provided ordering filters in query ordering expression."""
66+
67+
ALLOWED_ORDERING_PERMUTATIONS_IN_QUERY = None
68+
"""Property to specify a set of allowed ordering permutations (default `None`)."""
6169

6270
DISTINCT = False
6371
"""If True, a `SELECT DISTINCT` will always be executed (default `False`)."""
@@ -107,6 +115,16 @@ def _validate_init(self):
107115
e = 'Extended search ORM routes must be iterable.'
108116
assert isinstance(self.EXTENDED_SEARCH_ORM_ROUTES, iterable_types), e
109117

118+
e = 'Max ordering length must be integer.'
119+
assert isinstance(self.MAX_ORDERING_LENGTH_IN_QUERY, int), e
120+
121+
perms = self.ALLOWED_ORDERING_PERMUTATIONS_IN_QUERY
122+
if perms:
123+
e = 'Allowed ordering permutations must be a set of tuples of string filter names.'
124+
assert isinstance(perms, set), e
125+
assert all(isinstance(t, tuple) for t in perms), e
126+
assert all(isinstance(s, str) for s in chain.from_iterable(perms)), e
127+
110128
def _get_init_filters(self):
111129
return self.FILTERS
112130

@@ -120,8 +138,10 @@ def _default_init(self, filters):
120138
self.select_tree = {}
121139
self.default_exclusions = set()
122140
self.annotations = {}
141+
self.allowed_ordering_permutations = None
123142

124143
self._build_filters(filters)
144+
self._validate_and_store_allowed_ordering_permutations()
125145
self._extend_annotations()
126146

127147
def _init_from_class(self, instance):
@@ -132,6 +152,7 @@ def _init_from_class(self, instance):
132152
'select_tree',
133153
'default_exclusions',
134154
'annotations',
155+
'allowed_ordering_permutations',
135156
)
136157
for attr in copied_attributes:
137158
setattr(self, attr, getattr(instance, attr))
@@ -542,19 +563,29 @@ def __apply_field_optimizations(self, qs, data, node):
542563
def _apply_ordering(self, qs, properties):
543564
if len(properties) == 0:
544565
return qs
545-
elif len(properties) > 1:
566+
567+
if len(properties) > 1:
546568
raise RQLFilterParsingError(details={
547569
'error': 'Bad ordering filter: query can contain only one ordering operation.',
548570
})
549571

572+
if len(properties[0]) > self.MAX_ORDERING_LENGTH_IN_QUERY:
573+
raise RQLFilterParsingError(details={
574+
'error': 'Bad ordering filter: max allowed number is {n}.'.format(
575+
n=self.MAX_ORDERING_LENGTH_IN_QUERY,
576+
),
577+
})
578+
550579
ordering_fields = []
580+
perm = []
551581
for prop in properties[0]:
552582
filter_name, sign = self._get_filter_name_with_sign_for_ordering(prop)
553583
if filter_name not in self.ordering_filters:
554584
raise RQLFilterParsingError(details={
555585
'error': 'Bad ordering filter: {0}.'.format(filter_name),
556586
})
557587

588+
perm.append('{0}{1}'.format(sign, filter_name))
558589
filters = self.filters[filter_name]
559590
if not isinstance(filters, list):
560591
filters = [filters]
@@ -565,6 +596,12 @@ def _apply_ordering(self, qs, properties):
565596
ordering_name = self._get_filter_ordering_name(filter_item, filter_name)
566597
ordering_fields.append('{0}{1}'.format(sign, ordering_name))
567598

599+
perms = self.allowed_ordering_permutations
600+
if perms and tuple(perm) not in perms:
601+
raise RQLFilterParsingError(details={
602+
'error': 'Bad ordering filter: permutation not allowed.',
603+
})
604+
568605
return qs.order_by(*ordering_fields)
569606

570607
@staticmethod
@@ -766,6 +803,20 @@ def _extend_annotations(self):
766803

767804
self.annotations.update(dict(extended_annotations))
768805

806+
def _validate_and_store_allowed_ordering_permutations(self):
807+
perms = self.ALLOWED_ORDERING_PERMUTATIONS_IN_QUERY
808+
if perms:
809+
for s in chain.from_iterable(perms):
810+
filter_name = s[1:] if s and s[0] in ('+', '-') else s
811+
812+
e = 'Wrong configuration of allowed ordering permutations: {n}.'.format(n=s)
813+
assert filter_name in self.ordering_filters, e
814+
815+
self.allowed_ordering_permutations = {
816+
tuple(s[1:] if s[0] == '+' else s for s in t)
817+
for t in perms
818+
}
819+
769820
@classmethod
770821
def _is_field_supported(cls, field):
771822
return isinstance(field, SUPPORTED_FIELD_TYPES)

tests/test_filter_cls/test_apply_filters.py

+33
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,31 @@ def test_bad_ordering_filter():
352352
assert e.value.details['error'] == 'Bad ordering filter: id.'
353353

354354

355+
def test_too_much_ordering_filters():
356+
with pytest.raises(RQLFilterParsingError) as e:
357+
apply_filters('ordering(id,id,id,id,id,id)')
358+
359+
assert e.value.details['error'] == 'Bad ordering filter: max allowed number is 5.'
360+
361+
362+
@pytest.mark.django_db
363+
@pytest.mark.parametrize('perms, q', (
364+
({('author.email',)}, '-published.at'),
365+
({('-author.email',)}, 'author.email'),
366+
({('d_id', '-d_id')}, 'd_id,author.email'),
367+
({('d_id', 'author.email')}, 'd_id'),
368+
({('d_id', 'author.email'), ('author.email', '-published.at')}, 'author.email,published.at'),
369+
))
370+
def test_bad_ordering_permutation(perms, q):
371+
class CustomCls(BooksFilterClass):
372+
ALLOWED_ORDERING_PERMUTATIONS_IN_QUERY = perms
373+
374+
with pytest.raises(RQLFilterParsingError) as e:
375+
list(CustomCls(book_qs).apply_filters(f'ordering({q})'))
376+
377+
assert e.value.details['error'] == 'Bad ordering filter: permutation not allowed.'
378+
379+
355380
@pytest.mark.django_db
356381
def test_search():
357382
title = 'book'
@@ -402,6 +427,13 @@ def build_q_for_custom_filter(self, *args, **kwargs):
402427
@pytest.mark.django_db
403428
def test_custom_filter_ordering(generate_books):
404429
class CustomCls(BooksFilterClass):
430+
ALLOWED_ORDERING_PERMUTATIONS_IN_QUERY = {
431+
('author.email', 'published.at'),
432+
('ordering_filter',),
433+
('-ordering_filter',),
434+
('-ordering_filter', 'author.email'),
435+
}
436+
405437
def build_name_for_custom_ordering(self, filter_name):
406438
return 'id'
407439

@@ -412,6 +444,7 @@ def assert_ordering(self, filter_name, expected):
412444

413445
CustomCls(book_qs).assert_ordering('ordering_filter', [books[0], books[1]])
414446
CustomCls(book_qs).assert_ordering('-ordering_filter', [books[1], books[0]])
447+
CustomCls(book_qs).assert_ordering('-ordering_filter,author.email', [books[1], books[0]])
415448

416449

417450
@pytest.mark.django_db

tests/test_filter_cls/test_initialization.py

+47
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,53 @@ class Cls(BooksFilterClass):
126126
assert str(e.value) == 'Extended search ORM routes must be iterable.'
127127

128128

129+
@pytest.mark.parametrize('v', ('5', None, 1.23, []))
130+
def test_wrong_ordering_length_setup(v):
131+
class Cls(BooksFilterClass):
132+
MAX_ORDERING_LENGTH_IN_QUERY = v
133+
134+
with pytest.raises(AssertionError) as e:
135+
Cls(empty_qs)
136+
assert str(e.value) == 'Max ordering length must be integer.'
137+
138+
139+
@pytest.mark.parametrize('v', (
140+
5,
141+
[['name']],
142+
{'name'},
143+
{(5,)},
144+
{('x',), ('y', None)},
145+
))
146+
def test_wrong_ordering_permutations_setup(v):
147+
class Cls(BooksFilterClass):
148+
ALLOWED_ORDERING_PERMUTATIONS_IN_QUERY = v
149+
150+
with pytest.raises(AssertionError) as e:
151+
Cls(empty_qs)
152+
153+
expected = 'Allowed ordering permutations must be a set of tuples of string filter names.'
154+
assert str(e.value) == expected
155+
156+
157+
@pytest.mark.parametrize('v, expected', (
158+
({('x',)}, 'Wrong configuration of allowed ordering permutations: x.'),
159+
({('-d_id', '+')}, 'Wrong configuration of allowed ordering permutations: +.'),
160+
({('',)}, 'Wrong configuration of allowed ordering permutations: .'),
161+
(
162+
{('author.email', '+published.at'), ('fsm', '-title')},
163+
'Wrong configuration of allowed ordering permutations: -title.',
164+
),
165+
))
166+
def test_wrong_ordering_permutations_filter_name_provided(v, expected):
167+
class Cls(BooksFilterClass):
168+
ALLOWED_ORDERING_PERMUTATIONS_IN_QUERY = v
169+
170+
with pytest.raises(AssertionError) as e:
171+
Cls(empty_qs)
172+
173+
assert str(e.value) == expected
174+
175+
129176
@pytest.mark.parametrize('filters', [{}, set()])
130177
def test_wrong_filters_type(filters):
131178
class Cls(RQLFilterClass):

0 commit comments

Comments
 (0)