Skip to content

Commit 8e32477

Browse files
authored
Merge pull request #631 from jfinkels/case-insensitive-sorting
Allows client to request case-insensitive sorting
2 parents 2a878e9 + a676e4d commit 8e32477

File tree

8 files changed

+74
-15
lines changed

8 files changed

+74
-15
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Not yet released.
4848
- :issue:`599`: fixes `unicode` bug using :func:`!urlparse.urljoin` with the
4949
`future`_ library in resource serialization.
5050
- :issue:`625`: adds schema metadata to root endpoint.
51+
- :issue:`626`: allows the client to request case-insensitive sorting.
5152

5253
.. _future: http://python-future.org/
5354

docs/sorting.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Sorting
44
Clients can sort according to the sorting protocol described in the `Sorting
55
<http://jsonapi.org/format/#fetching-sorting>`__ section of the JSON API
66
specification. Sorting by a nullable attribute will cause resources with null
7-
attributes to appear first.
7+
attributes to appear first. The client can request case-insensitive sorting by
8+
setting the query parameter ``ignorecase=1``.
89

910
Clients can also request grouping by using the ``group`` query parameter. For
1011
example, if your database has two people with name ``'foo'`` and two people

flask_restless/search/drivers.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030

3131
def search_relationship(session, instance, relation, filters=None, sort=None,
32-
group_by=None):
32+
group_by=None, ignorecase=False):
3333
"""Returns a filtered, sorted, and grouped SQLAlchemy query
3434
restricted to those objects related to a given instance.
3535
@@ -40,8 +40,8 @@ def search_relationship(session, instance, relation, filters=None, sort=None,
4040
4141
` `relation` is a string naming a to-many relationship of `instance`.
4242
43-
`filters`, `sort`, and `group_by` are identical to the corresponding
44-
arguments of :func:`.search`.
43+
`filters`, `sort`, `group_by`, and `ignorecase` are identical to the
44+
corresponding arguments of :func:`.search`.
4545
4646
"""
4747
model = get_model(instance)
@@ -60,11 +60,12 @@ def search_relationship(session, instance, relation, filters=None, sort=None,
6060
query = query.filter(primary_key_value(related_model).in_(primary_keys))
6161

6262
return search(session, related_model, filters=filters, sort=sort,
63-
group_by=group_by, _initial_query=query)
63+
group_by=group_by, ignorecase=ignorecase,
64+
_initial_query=query)
6465

6566

6667
def search(session, model, filters=None, sort=None, group_by=None,
67-
_initial_query=None):
68+
ignorecase=False, _initial_query=None):
6869
"""Returns a filtered, sorted, and grouped SQLAlchemy query.
6970
7071
`session` is the SQLAlchemy session in which to create the query.
@@ -80,7 +81,9 @@ def search(session, model, filters=None, sort=None, group_by=None,
8081
`sort` is a list of pairs of the form ``(direction, fieldname)``,
8182
where ``direction`` is either '+' or '-' and ``fieldname`` is a
8283
string representing an attribute of the model or a dot-separated
83-
relationship path (for example, 'owner.name').
84+
relationship path (for example, 'owner.name'). If `ignorecase` is
85+
True, the sorting will be case-insensitive (so 'a' will precede 'B'
86+
instead of the default behavior in which 'B' precedes 'a').
8487
8588
`group_by` is a list of dot-separated relationship paths on which to
8689
group the query results.
@@ -113,11 +116,15 @@ def search(session, model, filters=None, sort=None, group_by=None,
113116
field_name, field_name_in_relation = field_name.split('.')
114117
relation_model = aliased(get_related_model(model, field_name))
115118
field = getattr(relation_model, field_name_in_relation)
119+
if ignorecase:
120+
field = field.collate('NOCASE')
116121
direction = getattr(field, direction_name)
117122
query = query.join(relation_model)
118123
query = query.order_by(direction())
119124
else:
120125
field = getattr(model, field_name)
126+
if ignorecase:
127+
field = field.collate('NOCASE')
121128
direction = getattr(field, direction_name)
122129
query = query.order_by(direction())
123130
else:

flask_restless/views/base.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@
111111
#: request.
112112
SORT_PARAM = 'sort'
113113

114+
#: The query parameter key that indicates whether sorting is case-insensitive.
115+
IGNORECASE_PARAM = 'ignorecase'
116+
114117
#: The query parameter key that identifies grouping fields in a
115118
#: :http:method:`get` request.
116119
GROUP_PARAM = 'group'
@@ -1205,6 +1208,7 @@ def collection_parameters(self, resource_id=None, relation_name=None):
12051208
for value in sort.split(',')]
12061209
else:
12071210
sort = []
1211+
ignorecase = bool(int(request.args.get(IGNORECASE_PARAM, '0')))
12081212

12091213
# Determine grouping options.
12101214
group_by = request.args.get(GROUP_PARAM)
@@ -1219,7 +1223,7 @@ def collection_parameters(self, resource_id=None, relation_name=None):
12191223
except ValueError:
12201224
raise SingleKeyError('failed to extract Boolean from parameter')
12211225

1222-
return filters, sort, group_by, single
1226+
return filters, sort, group_by, single, ignorecase
12231227

12241228

12251229
class APIBase(ModelView):
@@ -1601,7 +1605,7 @@ def _get_resource_helper(self, resource, primary_resource=None,
16011605

16021606
def _get_collection_helper(self, resource=None, relation_name=None,
16031607
filters=None, sort=None, group_by=None,
1604-
single=False):
1608+
ignorecase=False, single=False):
16051609
if (resource is None) ^ (relation_name is None):
16061610
raise ValueError('resource and relation must be both None or both'
16071611
' not None')
@@ -1614,7 +1618,7 @@ def _get_collection_helper(self, resource=None, relation_name=None,
16141618
search_ = partial(search, self.session, self.model)
16151619
try:
16161620
search_items = search_(filters=filters, sort=sort,
1617-
group_by=group_by)
1621+
group_by=group_by, ignorecase=ignorecase)
16181622
except (FilterParsingError, FilterCreationError) as exception:
16191623
detail = 'invalid filter object: {0}'.format(str(exception))
16201624
return error_response(400, cause=exception, detail=detail)

flask_restless/views/function.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ def get(self):
124124

125125
# Get the filtering, sorting, and grouping parameters.
126126
try:
127-
filters, sort, group_by, single = self.collection_parameters()
127+
filters, sort, group_by, single, ignorecase = \
128+
self.collection_parameters()
128129
except (TypeError, ValueError, OverflowError) as exception:
129130
detail = 'Unable to decode filter objects as JSON list'
130131
return error_response(400, cause=exception, detail=detail)

flask_restless/views/relationships.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def get(self, resource_id, relation_name):
9595
return error_response(404, detail=detail)
9696
if is_like_list(primary_resource, relation_name):
9797
try:
98-
filters, sort, group_by, single = \
98+
filters, sort, group_by, single, ignorecase = \
9999
self.collection_parameters(resource_id=resource_id,
100100
relation_name=relation_name)
101101
except (TypeError, ValueError, OverflowError) as exception:

flask_restless/views/resources.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def _get_relation(self, resource_id, relation_name):
226226
227227
"""
228228
try:
229-
filters, sort, group_by, single = \
229+
filters, sort, group_by, single, ignorecase = \
230230
self.collection_parameters(resource_id=resource_id,
231231
relation_name=relation_name)
232232
except (TypeError, ValueError, OverflowError) as exception:
@@ -283,6 +283,7 @@ def _get_relation(self, resource_id, relation_name):
283283
relation_name=relation_name,
284284
filters=filters, sort=sort,
285285
group_by=group_by,
286+
ignorecase=ignorecase,
286287
single=single)
287288
else:
288289
resource = getattr(primary_resource, relation_name)
@@ -353,7 +354,8 @@ def _get_collection(self):
353354
354355
"""
355356
try:
356-
filters, sort, group_by, single = self.collection_parameters()
357+
filters, sort, group_by, single, ignorecase = \
358+
self.collection_parameters()
357359
except (TypeError, ValueError, OverflowError) as exception:
358360
detail = 'Unable to decode filter objects as JSON list'
359361
return error_response(400, cause=exception, detail=detail)
@@ -366,7 +368,8 @@ def _get_collection(self):
366368
single=single)
367369

368370
return self._get_collection_helper(filters=filters, sort=sort,
369-
group_by=group_by, single=single)
371+
group_by=group_by, single=single,
372+
ignorecase=ignorecase)
370373

371374
def get(self, resource_id, relation_name, related_resource_id):
372375
"""Returns the JSON document representing a resource or a collection of

tests/test_fetching.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,48 @@ def test_sorting_hybrid_expression(self):
376376
articles = document['data']
377377
self.assertEqual(['1', '2'], list(map(itemgetter('id'), articles)))
378378

379+
def test_case_insensitive_sorting(self):
380+
"""Test for case-insensitive sorting.
381+
382+
For more information, see GitHub issue #626.
383+
384+
"""
385+
person1 = self.Person(id=1, name=u'B')
386+
person2 = self.Person(id=2, name=u'a')
387+
self.session.add_all([person1, person2])
388+
self.session.commit()
389+
query_string = {'sort': 'name', 'ignorecase': 1}
390+
response = self.app.get('/api/person', query_string=query_string)
391+
# The ASCII character code for the uppercase letter 'B' comes
392+
# before the ASCII character code for the lowercase letter 'a',
393+
# but in case-insensitive sorting, the 'a' should precede the
394+
# 'B'.
395+
document = loads(response.data)
396+
person1, person2 = document['data']
397+
self.assertEqual(person1['id'], u'2')
398+
self.assertEqual(person1['attributes']['name'], u'a')
399+
self.assertEqual(person2['id'], u'1')
400+
self.assertEqual(person2['attributes']['name'], u'B')
401+
402+
def test_case_insensitive_sorting_relationship_attributes(self):
403+
"""Test for case-insensitive sorting on relationship attributes."""
404+
person1 = self.Person(id=1, name=u'B')
405+
person2 = self.Person(id=2, name=u'a')
406+
article1 = self.Article(id=1, author=person1)
407+
article2 = self.Article(id=2, author=person2)
408+
self.session.add_all([article1, article2, person1, person2])
409+
self.session.commit()
410+
query_string = {'sort': 'author.name', 'ignorecase': 1}
411+
response = self.app.get('/api/article', query_string=query_string)
412+
# The ASCII character code for the uppercase letter 'B' comes
413+
# before the ASCII character code for the lowercase letter 'a',
414+
# but in case-insensitive sorting, the 'a' should precede the
415+
# 'B'.
416+
document = loads(response.data)
417+
article1, article2 = document['data']
418+
self.assertEqual(article1['id'], u'2')
419+
self.assertEqual(article2['id'], u'1')
420+
379421

380422
class TestFetchResource(ManagerTestBase):
381423

0 commit comments

Comments
 (0)