Skip to content
This repository has been archived by the owner on Mar 28, 2019. It is now read-only.

Commit

Permalink
Merge pull request #432 from mozilla-services/rl68-exclude-values
Browse files Browse the repository at this point in the history
Let people exclude values with filters. (refs mozilla-services/#68).
  • Loading branch information
almet committed Sep 2, 2015
2 parents e215aa0 + 0bbc450 commit 4a8ba6b
Show file tree
Hide file tree
Showing 8 changed files with 29 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This document describes changes between each past release.
**Protocol**

- Collection records can now be filtered using multiple values (``?in_status=1,2,3``) (fixes #39)
- Collection records can now be filtered excluding multiple values (``?exclude_status=1,2,3``) (fixes mozilla-services/readinglist#68)


2.4.3 (2015-08-26)
Expand Down
4 changes: 2 additions & 2 deletions cliquet/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,7 @@ def _extract_filters(self, queryparams=None):
)
continue

m = re.match(r'^(min|max|not|lt|gt|in)_(\w+)$', param)
m = re.match(r'^(min|max|not|lt|gt|in|exclude)_(\w+)$', param)
if m:
keyword, field = m.groups()
operator = getattr(COMPARISON, keyword.upper())
Expand All @@ -955,7 +955,7 @@ def _extract_filters(self, queryparams=None):
}
raise_invalid(self.request, **error_details)

if operator is COMPARISON.IN:
if operator in (COMPARISON.IN, COMPARISON.EXCLUDE):
value = set([native_value(v) for v in paramvalue.split(',')])

filters.append(Filter(field, value, operator))
Expand Down
3 changes: 2 additions & 1 deletion cliquet/storage/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,15 @@ def apply_filters(self, records, filters):
COMPARISON.MIN: operator.ge,
COMPARISON.GT: operator.gt,
COMPARISON.IN: operator.contains,
COMPARISON.EXCLUDE: lambda x, y: not operator.contains(x, y),
}

for record in records:
matches = True
for f in filters:
left = record.get(f.field)
right = f.value
if f.operator == COMPARISON.IN:
if f.operator in (COMPARISON.IN, COMPARISON.EXCLUDE):
right = left
left = f.value
matches = matches and operators[f.operator](left, right)
Expand Down
3 changes: 2 additions & 1 deletion cliquet/storage/postgresql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ def _format_conditions(self, filters, id_field, modified_field,
COMPARISON.EQ: '=',
COMPARISON.NOT: '<>',
COMPARISON.IN: 'IN',
COMPARISON.EXCLUDE: 'NOT IN',
}

conditions = []
Expand All @@ -641,7 +642,7 @@ def _format_conditions(self, filters, id_field, modified_field,
# If field is missing, we default to ''.
sql_field = "coalesce(data->>%%(%s)s, '')" % field_holder

if filtr.operator != COMPARISON.IN:
if filtr.operator not in (COMPARISON.IN, COMPARISON.EXCLUDE):
# For the IN operator, let psycopg escape the values list.
# Otherwise JSON-ify the native value (e.g. True -> 'true')
if not isinstance(filtr.value, six.string_types):
Expand Down
6 changes: 6 additions & 0 deletions cliquet/tests/resource/test_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,9 @@ def test_in_values(self):
result = self.resource.collection_get()
values = [item['status'] for item in result['data']]
self.assertEqual(sorted(values), [0, 0, 1, 1])

def test_exclude_values(self):
self.resource.request.GET = {'exclude_status': '0'}
result = self.resource.collection_get()
values = [item['status'] for item in result['data']]
self.assertEqual(sorted(values), [1, 1, 2, 2])
8 changes: 8 additions & 0 deletions cliquet/tests/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,14 @@ def test_get_all_can_filter_with_list_of_values(self):
**self.storage_kw)
self.assertEqual(len(records), 2)

def test_get_all_can_filter_with_list_of_excluded_values(self):
for l in ['a', 'b', 'c']:
self.create_record({'code': l})
filters = [Filter('code', ('a', 'b'), utils.COMPARISON.EXCLUDE)]
records, _ = self.storage.get_all(filters=filters,
**self.storage_kw)
self.assertEqual(len(records), 1)

def test_get_all_handle_a_pagination_rules(self):
for x in range(10):
record = dict(self.record)
Expand Down
3 changes: 2 additions & 1 deletion cliquet/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ def Enum(**enums):
NOT='!=',
EQ='==',
GT='>',
IN='in'
IN='in',
EXCLUDE='exclude',
)


Expand Down
6 changes: 6 additions & 0 deletions cliquet_docs/api/resource.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ Prefix attribute name with ``not_``:

* ``/collection?not_field=0``

**Exclude multiple values**

Prefix attribute name with ``exclude_``:

* ``/collection?exclude_field=0,1``

.. note::

Will return an error if a field is unknown.
Expand Down

0 comments on commit 4a8ba6b

Please sign in to comment.