diff --git a/CHANGELOG b/CHANGELOG index b172f16a90..c8c0cfb7a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add NULLS ordering * Add context domain on ir.action.act_window * Allow None limit in action window * Add has_window_functions on Database diff --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst index 556bb2bfee..b8672a323f 100644 --- a/doc/ref/models/models.rst +++ b/doc/ref/models/models.rst @@ -378,7 +378,9 @@ Class attributes are: where the first element of the tuple is a field name of the model and the second is the sort ordering as `ASC` for ascending or `DESC` for - descending. + descending. This second element may contain 'NULLS FIRST' or 'NULLS LAST' + to sort null values before or after non-null values. If neither is + specified the default behavior of the backend is used. In case the field used for the first element is a :class:`fields.Many2One`, it is also possible to use the dotted notation to sort on a specific field @@ -687,14 +689,13 @@ Class methods: sequence_ordered ================ -.. method:: sequence_ordered([field_name, [field_label, [order, [null_first]]]]) +.. method:: sequence_ordered([field_name, [field_label, [order]]]) Retuns a mixin_ class which defines the order of a :class:`ModelSQL` with an :class:`trytond.model.fields.Integer` field. field_name indicates the name of the field to be created and its default values is `sequence`. field_label defines the label which will be used by the field and defaults to `Sequence`. -Order specifies the order direction and defaults to `ASC` and null first -if the null values should be ordered first and defaults to `True`. +Order specifies the order direction and defaults to `ASC NULLS FIRST`. .. _mixin: http://en.wikipedia.org/wiki/Mixin diff --git a/trytond/backend/mysql/database.py b/trytond/backend/mysql/database.py index bb0af25fcc..98f0e6cc7d 100644 --- a/trytond/backend/mysql/database.py +++ b/trytond/backend/mysql/database.py @@ -75,7 +75,9 @@ class Database(DatabaseInterface): _list_cache = None _list_cache_timestamp = None - flavor = Flavor(max_limit=18446744073709551610, function_mapping=MAPPING) + flavor = Flavor( + max_limit=18446744073709551610, function_mapping=MAPPING, + null_ordering=False) def connect(self): return self diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py index 4650966ab4..be002ce9bc 100644 --- a/trytond/backend/sqlite/database.py +++ b/trytond/backend/sqlite/database.py @@ -221,7 +221,8 @@ class Database(DatabaseInterface): _local = threading.local() _conn = None - flavor = Flavor(paramstyle='qmark', function_mapping=MAPPING) + flavor = Flavor( + paramstyle='qmark', function_mapping=MAPPING, null_ordering=False) IN_MAX = 200 def __new__(cls, name=':memory:'): diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index f1378f8f16..fe0886bf1b 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.py @@ -4,7 +4,8 @@ import datetime from itertools import islice, izip, chain, ifilter from collections import OrderedDict -from sql import Table, Column, Literal, Desc, Asc, Expression, Null +from sql import (Table, Column, Literal, Desc, Asc, Expression, Null, + NullsFirst, NullsLast) from sql.functions import CurrentTimestamp, Extract from sql.conditionals import Coalesce from sql.operators import Or, And, Operator @@ -1149,14 +1150,25 @@ class ModelSQL(ModelStorage): 'DESC': Desc, 'ASC': Asc, } + null_ordering_types = { + 'NULLS FIRST': NullsFirst, + 'NULLS LAST': NullsLast, + None: lambda _: _ + } if order is None or order is False: order = cls._order for oexpr, otype in order: fname, _, extra_expr = oexpr.partition('.') field = cls._fields[fname] - Order = order_types[otype.upper()] + otype = otype.upper() + try: + otype, null_ordering = otype.split(' ', 1) + except ValueError: + null_ordering = None + Order = order_types[otype] + NullOrdering = null_ordering_types[null_ordering] forder = field.convert_order(oexpr, tables, cls) - order_by.extend((Order(o) for o in forder)) + order_by.extend((NullOrdering(Order(o)) for o in forder)) # construct a clause for the rules : domain = Rule.domain_get(cls.__name__, mode='read') diff --git a/trytond/model/order.py b/trytond/model/order.py index d5a59b26a6..0a99ec8d17 100644 --- a/trytond/model/order.py +++ b/trytond/model/order.py @@ -1,14 +1,10 @@ # This file is part of Tryton. The COPYRIGHT file at the toplevel of this # repository contains the full copyright notices and license terms. -from sql import Null, Column -from sql.operators import Equal, NotEqual -from sql.conditionals import Case - from trytond.model import fields def sequence_ordered(field_name='sequence', field_label='Sequence', - order='ASC', null_first=True): + order='ASC NULLS FIRST'): "Returns a mixin to order the model by order fields" class SequenceOrderedMixin(object): @@ -20,14 +16,4 @@ def sequence_ordered(field_name='sequence', field_label='Sequence', cls._order = [(field_name, order)] + cls._order setattr(SequenceOrderedMixin, field_name, fields.Integer(field_label)) - - @classmethod - def order_function(cls, tables): - table, _ = tables[None] - operator = Equal - if not null_first: - operator = NotEqual - field = Column(table, field_name) - return [Case((operator(field, Null), 0), else_=1), field] - setattr(SequenceOrderedMixin, 'order_%s' % field_name, order_function) return SequenceOrderedMixin diff --git a/trytond/tests/__init__.py b/trytond/tests/__init__.py index f5d9fb2a55..8771f73772 100644 --- a/trytond/tests/__init__.py +++ b/trytond/tests/__init__.py @@ -178,6 +178,7 @@ def register(): TestHistoryLine, FieldContextChild, FieldContextParent, + NullOrder, module='tests', type_='model') Pool.register( TestWizard, diff --git a/trytond/tests/model.py b/trytond/tests/model.py index 69174e6def..3eab4ddb4b 100644 --- a/trytond/tests/model.py +++ b/trytond/tests/model.py @@ -13,6 +13,7 @@ __all__ = [ 'Union', 'UnionUnion', 'Model4UnionTree1', 'Model4UnionTree2', 'UnionTree', 'SequenceOrderedModel', + 'NullOrder', ] @@ -161,3 +162,9 @@ class UnionTree(UnionMixin, ModelSQL): class SequenceOrderedModel(sequence_ordered(), ModelSQL): 'Sequence Ordered Model' __name__ = 'test.order.sequence' + + +class NullOrder(ModelSQL): + "Null Order" + __name__ = 'test.null_order' + integer = fields.Integer('Integer') diff --git a/trytond/tests/test_modelsql.py b/trytond/tests/test_modelsql.py index 22337f275b..cf19a2067b 100644 --- a/trytond/tests/test_modelsql.py +++ b/trytond/tests/test_modelsql.py @@ -133,6 +133,32 @@ class ModelSQLTestCase(unittest.TestCase): TargetModel.name.string, TargetModel.__doc__) self.assertEqual(err.message, msg) + @with_transaction() + def test_null_ordering(self): + 'Test NULL ordering' + pool = Pool() + NullOrder = pool.get('test.null_order') + + NullOrder.create([{ + 'integer': 1, + }, { + 'integer': 3, + }, { + 'integer': None, + }]) + integers = NullOrder.search([], order=[('integer', 'ASC NULLS FIRST')]) + self.assertListEqual([i.integer for i in integers], [None, 1, 3]) + + integers = NullOrder.search( + [], order=[('integer', 'DESC NULLS FIRST')]) + self.assertListEqual([i.integer for i in integers], [None, 3, 1]) + + integers = NullOrder.search([], order=[('integer', 'ASC NULLS LAST')]) + self.assertListEqual([i.integer for i in integers], [1, 3, None]) + + integers = NullOrder.search([], order=[('integer', 'DESC NULLS LAST')]) + self.assertListEqual([i.integer for i in integers], [3, 1, None]) + def suite(): return unittest.TestLoader().loadTestsFromTestCase(ModelSQLTestCase)