AC-728 Added chain__ filter prefix.

This commit is contained in:
Chris Church 2013-11-26 13:24:45 -05:00
parent f29809a807
commit 112fe089d9
5 changed files with 52 additions and 6 deletions

View File

@ -114,6 +114,7 @@ class FieldLookupBackend(BaseFilterBackend):
# below is (negate, field, value).
and_filters = []
or_filters = []
chain_filters = []
for key, values in request.QUERY_PARAMS.lists():
if key in self.RESERVED_NAMES:
continue
@ -123,11 +124,18 @@ class FieldLookupBackend(BaseFilterBackend):
if key.endswith('__int'):
key = key[:-5]
q_int = True
# Custom or__ filter prefix (or__ can precede not__).
# Custom chain__ and or__ filters, mutually exclusive (both can
# precede not__).
q_chain = False
q_or = False
if key.startswith('or__'):
if key.startswith('chain__'):
key = key[7:]
q_chain = True
elif key.startswith('or__'):
key = key[4:]
q_or = True
# Custom not__ filter prefix.
q_not = False
if key.startswith('not__'):
@ -139,13 +147,15 @@ class FieldLookupBackend(BaseFilterBackend):
if q_int:
value = int(value)
value = self.value_to_python(queryset.model, key, value)
if q_or:
if q_chain:
chain_filters.append((q_not, key, value))
elif q_or:
or_filters.append((q_not, key, value))
else:
and_filters.append((q_not, key, value))
# Now build Q objects for database query filter.
if and_filters or or_filters:
if and_filters or or_filters or chain_filters:
args = []
for n, k, v in and_filters:
if n:
@ -160,8 +170,14 @@ class FieldLookupBackend(BaseFilterBackend):
else:
q |= Q(**{k:v})
args.append(q)
for n,k,v in chain_filters:
if n:
q = ~Q(**{k:v})
else:
q = Q(**{k:v})
queryset = queryset.filter(q)
queryset = queryset.filter(*args)
return queryset
return queryset.distinct()
except (FieldError, FieldDoesNotExist, ValueError), e:
raise ParseError(e.args[0])
except ValidationError, e:

View File

@ -106,6 +106,7 @@ class APIView(views.APIView):
'docstring': type(self).__doc__ or '',
'new_in_13': getattr(self, 'new_in_13', False),
'new_in_14': getattr(self, 'new_in_14', False),
'new_in_15': getattr(self, 'new_in_15', False),
}
def get_description(self, html=False):

View File

@ -83,6 +83,20 @@ with `or__`:
?or__field=value&or__field=othervalue
?or__not__field=value&or__field=othervalue
(_New in AWX 1.5_) The default AND filtering applies all filters simultaneously
to each related object being filtered across database relationships. The chain
filter instead applies filters separately for each related object. To use,
prefix the query string parameter with `chain__`:
?chain__related__field=value&chain__related__field2=othervalue
?chain__not__related__field=value&chain__related__field2=othervalue
If the first query above were written as
`?related__field=value&related__field2=othervalue`, it would return only the
primary objects where the *same* related object satisfied both conditions. As
written using the chain filter, it would return the intersection of primary
objects matching each condition.
Field lookups may also be used for more advanced queries, by appending the
lookup to the field name:

View File

@ -1,2 +1,3 @@
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}
{% if new_in_14 %}> _New in AWX 1.4_{% endif %}
{% if new_in_15 %}> _New in AWX 1.5_{% endif %}

View File

@ -28,10 +28,11 @@ class UsersTest(BaseTest):
def setUp(self):
super(UsersTest, self).setUp()
self.setup_users()
self.organizations = self.make_organizations(self.super_django_user, 1)
self.organizations = self.make_organizations(self.super_django_user, 2)
self.organizations[0].admins.add(self.normal_django_user)
self.organizations[0].users.add(self.other_django_user)
self.organizations[0].users.add(self.normal_django_user)
self.organizations[1].users.add(self.other_django_user)
def test_only_super_user_or_org_admin_can_add_users(self):
url = reverse('api:user_list')
@ -560,6 +561,19 @@ class UsersTest(BaseTest):
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Verify difference between normal AND filter vs. filtering with
# chain__ prefix.
url = '%s?organizations__name__startswith=org0&organizations__name__startswith=org1' % base_url
qs = base_qs.filter(Q(organizations__name__startswith='org0'),
Q(organizations__name__startswith='org1'))
self.assertFalse(qs.count())
self.check_get_list(url, self.super_django_user, qs)
url = '%s?chain__organizations__name__startswith=org0&chain__organizations__name__startswith=org1' % base_url
qs = base_qs.filter(organizations__name__startswith='org0')
qs = qs.filter(organizations__name__startswith='org1')
self.assertTrue(qs.count())
self.check_get_list(url, self.super_django_user, qs)
# Filter by related organization not present.
url = '%s?organizations=None' % base_url
qs = base_qs.filter(organizations=None)