From 66f11dcc62d53f9a8b61e23ffcd7c3a8ce4c431a Mon Sep 17 00:00:00 2001 From: Alberto Donato Date: Fri, 30 Nov 2018 14:12:32 +0200 Subject: [PATCH] Use sort_attribute as default sort for instances, support DESC sorting (#152) * contrib/alchemy: Use sort_attribute as default sort for instances, support DESC sorting * accept Meta.sort_attribute as a tuple --- docs/resources.rst | 4 - flask_potion/contrib/alchemy/manager.py | 18 ++++- flask_potion/resource.py | 19 ++++- .../alchemy/test_manager_sqlalchemy.py | 75 +++++++++++++++++++ 4 files changed, 109 insertions(+), 7 deletions(-) diff --git a/docs/resources.rst b/docs/resources.rst index a59c988..b769794 100644 --- a/docs/resources.rst +++ b/docs/resources.rst @@ -19,10 +19,6 @@ for the resource. :class:`ModelResource` is written for create, read, update, delete actions on collections of items matching the resource schema. -A data store connection is maintained by a :class:`manager.Manager` instance. -The manager class can be specified in ``Meta.manager``; if no manager is specified, ``Api.default_manager`` is used. -Managers are configured through attributes in ``Meta``. Most managers expect a *model* to be defined under ``Meta.model``. - .. autoclass:: ModelResource :members: diff --git a/flask_potion/contrib/alchemy/manager.py b/flask_potion/contrib/alchemy/manager.py index 867e90d..0c09eb8 100644 --- a/flask_potion/contrib/alchemy/manager.py +++ b/flask_potion/contrib/alchemy/manager.py @@ -45,7 +45,8 @@ def _init_model(self, resource, model, meta): self.id_attribute = mapper.primary_key[0].name self.id_field = self._get_field_from_column_type(self.id_column, self.id_attribute, io="r") - self.default_sort_expression = self.id_column.asc() + self.default_sort_expression = self._get_sort_expression( + model, meta, self.id_column) fs = resource.schema if meta.include_id: @@ -86,6 +87,14 @@ def _init_model(self, resource, model, meta): fs.required.add(name) fs.set(name, self._get_field_from_column_type(column, name, io=io)) + def _get_sort_expression(self, model, meta, id_column): + if meta.sort_attribute is None: + return id_column.asc() + + attr_name, reverse = meta.sort_attribute + attr = getattr(model, attr_name) + return attr.desc() if reverse else attr.asc() + def _get_field_from_column_type(self, column, attribute, io="rw"): args = () kwargs = {} @@ -195,7 +204,12 @@ def _query_order_by(self, query, sort=None): if isinstance(field, fields.ToOne): target_alias = aliased(field.target.meta.model) query = query.outerjoin(target_alias, column).reset_joinpoint() - column = getattr(target_alias, field.target.meta.sort_attribute or field.target.manager.id_attribute) + sort_attribute = None + if field.target.meta.sort_attribute: + sort_attribute, _ = field.target.meta.sort_attribute + column = getattr( + target_alias, + sort_attribute or field.target.manager.id_attribute) order_clauses.append(column.desc() if reverse else column.asc()) diff --git a/flask_potion/resource.py b/flask_potion/resource.py index c0cff3d..5ce8f95 100644 --- a/flask_potion/resource.py +++ b/flask_potion/resource.py @@ -212,11 +212,28 @@ def __new__(mcs, name, bases, members): if 'model' in changes or 'model' in meta and 'manager' in changes: if meta.manager is not None: class_.manager = meta.manager(class_, meta.model) + + sort_attribute = meta.get('sort_attribute') + if sort_attribute is not None and isinstance(sort_attribute, str): + meta.sort_attribute = sort_attribute, False + return class_ class ModelResource(six.with_metaclass(ModelResourceMeta, Resource)): """ + :class:`Meta` class attributes: + + ===================== ============================== ============================================================================== + Attribute name Default Description + ===================== ============================== ============================================================================== + manager ``Api.default_manager`` A data store connection is maintained by a :class:`manager.Manager` instance. + Managers are configured through attributes in ``Meta``. Most managers expect + a *model* to be defined under ``Meta.model``. + sort_attribute None The field used to sort the list in the `instances` endpoint. Can be the + field name as ``string`` or a ``tuple`` with the field name and a boolean + for ``reverse`` (defaults to ``False``). + ===================== ============================== ============================================================================== .. method:: create @@ -322,4 +339,4 @@ class Meta: RefKey(), IDKey() ) - natural_key = None \ No newline at end of file + natural_key = None diff --git a/tests/contrib/alchemy/test_manager_sqlalchemy.py b/tests/contrib/alchemy/test_manager_sqlalchemy.py index 57c4287..df55fb0 100644 --- a/tests/contrib/alchemy/test_manager_sqlalchemy.py +++ b/tests/contrib/alchemy/test_manager_sqlalchemy.py @@ -448,3 +448,78 @@ class Meta: "$id": 1, "username": "foo" }, response.json) + + +class SQLAlchemySortTestCase(BaseTestCase): + + def setUp(self): + super(SQLAlchemySortTestCase, self).setUp() + self.app.config['SQLALCHEMY_ENGINE'] = 'sqlite://' + self.api = Api(self.app) + self.sa = sa = SQLAlchemy( + self.app, session_options={"autoflush": False}) + + class Type(sa.Model): + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(60), nullable=False) + + class Machine(sa.Model): + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(60), nullable=False) + + type_id = sa.Column(sa.Integer, sa.ForeignKey(Type.id)) + type = sa.relationship(Type, foreign_keys=[type_id]) + + sa.create_all() + + class MachineResource(ModelResource): + class Meta: + model = Machine + + class Schema: + type = fields.ToOne('type') + + class TypeResource(ModelResource): + class Meta: + model = Type + sort_attribute = ('name', True) + + self.MachineResource = MachineResource + self.TypeResource = TypeResource + + self.api.add_resource(MachineResource) + self.api.add_resource(TypeResource) + + def test_default_sorting_with_desc(self): + self.client.post('/type', data={"name": "aaa"}) + self.client.post('/type', data={"name": "ccc"}) + self.client.post('/type', data={"name": "bbb"}) + response = self.client.get('/type') + self.assert200(response) + self.assertJSONEqual( + [{'$uri': '/type/2', 'name': 'ccc'}, + {'$uri': '/type/3', 'name': 'bbb'}, + {'$uri': '/type/1', 'name': 'aaa'}], + response.json) + + def test_sort_by_related_field(self): + response = self.client.post('/type', data={"name": "aaa"}) + self.assert200(response) + aaa_uri = response.json["$uri"] + response = self.client.post('/type', data={"name": "bbb"}) + self.assert200(response) + bbb_uri = response.json["$uri"] + self.client.post( + '/machine', data={"name": "foo", "type": {"$ref": aaa_uri}}) + self.assert200(response) + self.client.post( + '/machine', data={"name": "bar", "type": {"$ref": bbb_uri}}) + self.assert200(response) + response = self.client.get('/machine?sort={"type": true}') + self.assert200(response) + type_uris = [entry['type']['$ref'] for entry in response.json] + self.assertTrue(type_uris, [bbb_uri, aaa_uri]) + response = self.client.get('/machine?sort={"type": false}') + self.assert200(response) + type_uris = [entry['type']['$ref'] for entry in response.json] + self.assertTrue(type_uris, [bbb_uri, aaa_uri])