Skip to content

Commit 66f11dc

Browse files
albertodonatolyschoening
authored andcommitted
Use sort_attribute as default sort for instances, support DESC sorting (biosustain#152)
* contrib/alchemy: Use sort_attribute as default sort for instances, support DESC sorting * accept Meta.sort_attribute as a tuple
1 parent 2e1a394 commit 66f11dc

File tree

4 files changed

+109
-7
lines changed

4 files changed

+109
-7
lines changed

docs/resources.rst

-4
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ for the resource.
1919

2020
:class:`ModelResource` is written for create, read, update, delete actions on collections of items matching the resource schema.
2121

22-
A data store connection is maintained by a :class:`manager.Manager` instance.
23-
The manager class can be specified in ``Meta.manager``; if no manager is specified, ``Api.default_manager`` is used.
24-
Managers are configured through attributes in ``Meta``. Most managers expect a *model* to be defined under ``Meta.model``.
25-
2622
.. autoclass:: ModelResource
2723
:members:
2824

flask_potion/contrib/alchemy/manager.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ def _init_model(self, resource, model, meta):
4545
self.id_attribute = mapper.primary_key[0].name
4646

4747
self.id_field = self._get_field_from_column_type(self.id_column, self.id_attribute, io="r")
48-
self.default_sort_expression = self.id_column.asc()
48+
self.default_sort_expression = self._get_sort_expression(
49+
model, meta, self.id_column)
4950

5051
fs = resource.schema
5152
if meta.include_id:
@@ -86,6 +87,14 @@ def _init_model(self, resource, model, meta):
8687
fs.required.add(name)
8788
fs.set(name, self._get_field_from_column_type(column, name, io=io))
8889

90+
def _get_sort_expression(self, model, meta, id_column):
91+
if meta.sort_attribute is None:
92+
return id_column.asc()
93+
94+
attr_name, reverse = meta.sort_attribute
95+
attr = getattr(model, attr_name)
96+
return attr.desc() if reverse else attr.asc()
97+
8998
def _get_field_from_column_type(self, column, attribute, io="rw"):
9099
args = ()
91100
kwargs = {}
@@ -195,7 +204,12 @@ def _query_order_by(self, query, sort=None):
195204
if isinstance(field, fields.ToOne):
196205
target_alias = aliased(field.target.meta.model)
197206
query = query.outerjoin(target_alias, column).reset_joinpoint()
198-
column = getattr(target_alias, field.target.meta.sort_attribute or field.target.manager.id_attribute)
207+
sort_attribute = None
208+
if field.target.meta.sort_attribute:
209+
sort_attribute, _ = field.target.meta.sort_attribute
210+
column = getattr(
211+
target_alias,
212+
sort_attribute or field.target.manager.id_attribute)
199213

200214
order_clauses.append(column.desc() if reverse else column.asc())
201215

flask_potion/resource.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,28 @@ def __new__(mcs, name, bases, members):
212212
if 'model' in changes or 'model' in meta and 'manager' in changes:
213213
if meta.manager is not None:
214214
class_.manager = meta.manager(class_, meta.model)
215+
216+
sort_attribute = meta.get('sort_attribute')
217+
if sort_attribute is not None and isinstance(sort_attribute, str):
218+
meta.sort_attribute = sort_attribute, False
219+
215220
return class_
216221

217222

218223
class ModelResource(six.with_metaclass(ModelResourceMeta, Resource)):
219224
"""
225+
:class:`Meta` class attributes:
226+
227+
===================== ============================== ==============================================================================
228+
Attribute name Default Description
229+
===================== ============================== ==============================================================================
230+
manager ``Api.default_manager`` A data store connection is maintained by a :class:`manager.Manager` instance.
231+
Managers are configured through attributes in ``Meta``. Most managers expect
232+
a *model* to be defined under ``Meta.model``.
233+
sort_attribute None The field used to sort the list in the `instances` endpoint. Can be the
234+
field name as ``string`` or a ``tuple`` with the field name and a boolean
235+
for ``reverse`` (defaults to ``False``).
236+
===================== ============================== ==============================================================================
220237
221238
.. method:: create
222239
@@ -322,4 +339,4 @@ class Meta:
322339
RefKey(),
323340
IDKey()
324341
)
325-
natural_key = None
342+
natural_key = None

tests/contrib/alchemy/test_manager_sqlalchemy.py

+75
Original file line numberDiff line numberDiff line change
@@ -448,3 +448,78 @@ class Meta:
448448
"$id": 1,
449449
"username": "foo"
450450
}, response.json)
451+
452+
453+
class SQLAlchemySortTestCase(BaseTestCase):
454+
455+
def setUp(self):
456+
super(SQLAlchemySortTestCase, self).setUp()
457+
self.app.config['SQLALCHEMY_ENGINE'] = 'sqlite://'
458+
self.api = Api(self.app)
459+
self.sa = sa = SQLAlchemy(
460+
self.app, session_options={"autoflush": False})
461+
462+
class Type(sa.Model):
463+
id = sa.Column(sa.Integer, primary_key=True)
464+
name = sa.Column(sa.String(60), nullable=False)
465+
466+
class Machine(sa.Model):
467+
id = sa.Column(sa.Integer, primary_key=True)
468+
name = sa.Column(sa.String(60), nullable=False)
469+
470+
type_id = sa.Column(sa.Integer, sa.ForeignKey(Type.id))
471+
type = sa.relationship(Type, foreign_keys=[type_id])
472+
473+
sa.create_all()
474+
475+
class MachineResource(ModelResource):
476+
class Meta:
477+
model = Machine
478+
479+
class Schema:
480+
type = fields.ToOne('type')
481+
482+
class TypeResource(ModelResource):
483+
class Meta:
484+
model = Type
485+
sort_attribute = ('name', True)
486+
487+
self.MachineResource = MachineResource
488+
self.TypeResource = TypeResource
489+
490+
self.api.add_resource(MachineResource)
491+
self.api.add_resource(TypeResource)
492+
493+
def test_default_sorting_with_desc(self):
494+
self.client.post('/type', data={"name": "aaa"})
495+
self.client.post('/type', data={"name": "ccc"})
496+
self.client.post('/type', data={"name": "bbb"})
497+
response = self.client.get('/type')
498+
self.assert200(response)
499+
self.assertJSONEqual(
500+
[{'$uri': '/type/2', 'name': 'ccc'},
501+
{'$uri': '/type/3', 'name': 'bbb'},
502+
{'$uri': '/type/1', 'name': 'aaa'}],
503+
response.json)
504+
505+
def test_sort_by_related_field(self):
506+
response = self.client.post('/type', data={"name": "aaa"})
507+
self.assert200(response)
508+
aaa_uri = response.json["$uri"]
509+
response = self.client.post('/type', data={"name": "bbb"})
510+
self.assert200(response)
511+
bbb_uri = response.json["$uri"]
512+
self.client.post(
513+
'/machine', data={"name": "foo", "type": {"$ref": aaa_uri}})
514+
self.assert200(response)
515+
self.client.post(
516+
'/machine', data={"name": "bar", "type": {"$ref": bbb_uri}})
517+
self.assert200(response)
518+
response = self.client.get('/machine?sort={"type": true}')
519+
self.assert200(response)
520+
type_uris = [entry['type']['$ref'] for entry in response.json]
521+
self.assertTrue(type_uris, [bbb_uri, aaa_uri])
522+
response = self.client.get('/machine?sort={"type": false}')
523+
self.assert200(response)
524+
type_uris = [entry['type']['$ref'] for entry in response.json]
525+
self.assertTrue(type_uris, [bbb_uri, aaa_uri])

0 commit comments

Comments
 (0)