Skip to content

Commit

Permalink
Default ordering option (#288)
Browse files Browse the repository at this point in the history
  • Loading branch information
abondar authored Feb 10, 2020
1 parent 4cbca0b commit f277a70
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Changelog
=========
0.15.11
-------
- Added ``ordering`` option for model ``Meta`` class to apply default ordering

0.15.10
-------
- Bumped requirements to cater for newer feature use (#282)
Expand Down
11 changes: 11 additions & 0 deletions docs/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,17 @@ The ``Meta`` class
indexes=(("field_a", "field_b"), )
indexes=(("field_a", "field_b"), ("field_c", "field_d", "field_e")
.. attribute:: ordering
:annotation: = None

Specify ``ordering`` to set up default ordering for given model.
It should be iterable of strings formatted in same way as ``.order_by(...)`` receives.
If query is built with ``GROUP_BY`` clause using ``.annotate(...)`` default ordering is not applied.

.. code-block:: python3
ordering = ["name", "-score"]
``ForeignKeyField``
-------------------

Expand Down
46 changes: 43 additions & 3 deletions tests/test_order_by.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from tests.testmodels import Event, Tournament
from tests.testmodels import (
DefaultOrdered,
DefaultOrderedDesc,
DefaultOrderedInvalid,
Event,
FKToDefaultOrdered,
Tournament,
)
from tortoise.contrib import test
from tortoise.exceptions import FieldError
from tortoise.functions import Count
from tortoise.exceptions import ConfigurationError, FieldError
from tortoise.functions import Count, Sum


class TestOrderBy(test.TestCase):
Expand Down Expand Up @@ -74,3 +81,36 @@ async def test_order_by_aggregation_reversed(self):
"-events_count"
)
self.assertEqual([t.name for t in tournaments], ["1", "2"])


class TestDefaultOrdering(test.TestCase):
async def test_default_order(self):
await DefaultOrdered.create(one="2", second=1)
await DefaultOrdered.create(one="1", second=1)

instance_list = await DefaultOrdered.all()
self.assertEqual([i.one for i in instance_list], ["1", "2"])

async def test_default_order_desc(self):
await DefaultOrderedDesc.create(one="1", second=1)
await DefaultOrderedDesc.create(one="2", second=1)

instance_list = await DefaultOrderedDesc.all()
self.assertEqual([i.one for i in instance_list], ["2", "1"])

async def test_default_order_invalid(self):
await DefaultOrderedInvalid.create(one="1", second=1)
await DefaultOrderedInvalid.create(one="2", second=1)

with self.assertRaises(ConfigurationError):
await DefaultOrderedInvalid.all()

async def test_default_order_annotated_query(self):
instance = await DefaultOrdered.create(one="2", second=1)
await FKToDefaultOrdered.create(link=instance, value=10)
await DefaultOrdered.create(one="1", second=1)

queryset = DefaultOrdered.all().annotate(res=Sum("related__value"))
queryset._make_query()
query = queryset.query.get_sql()
self.assertTrue("order by" not in query.lower())
29 changes: 29 additions & 0 deletions tests/testmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,32 @@ class DoubleFK(Model):
name = fields.CharField(max_length=50)
left = fields.ForeignKeyField("models.DoubleFK", null=True, related_name="left_rel")
right = fields.ForeignKeyField("models.DoubleFK", null=True, related_name="right_rel")


class DefaultOrdered(Model):
one = fields.TextField()
second = fields.IntField()

class Meta:
ordering = ["one", "second"]


class FKToDefaultOrdered(Model):
link = fields.ForeignKeyField("models.DefaultOrdered", related_name="related")
value = fields.IntField()


class DefaultOrderedDesc(Model):
one = fields.TextField()
second = fields.IntField()

class Meta:
ordering = ["-one"]


class DefaultOrderedInvalid(Model):
one = fields.TextField()
second = fields.IntField()

class Meta:
ordering = ["one", "third"]
2 changes: 1 addition & 1 deletion tortoise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,4 +739,4 @@ async def do_stuff():
loop.run_until_complete(Tortoise.close_connections())


__version__ = "0.15.10"
__version__ = "0.15.11"
2 changes: 1 addition & 1 deletion tortoise/fields/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def __init__(
self.model_field_name = ""
self.description = description
# TODO: consider making this not be set from constructor
self.model: "Model" = model # type: ignore
self.model: Type["Model"] = model # type: ignore
# TODO: consider moving this to RelationalField
self.reference = reference

Expand Down
38 changes: 34 additions & 4 deletions tortoise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ def get_together(meta: "Model.Meta", together: str) -> Tuple[Tuple[str, ...], ..
return _together


def prepare_default_ordering(meta: "Model.Meta") -> Tuple[Tuple[str, str], ...]:
ordering_list = getattr(meta, "ordering", ())

parsed_ordering = tuple(
QuerySet._resolve_ordering_string(ordering) for ordering in ordering_list
)

return parsed_ordering


def _fk_setter(self: "Model", value: "Optional[Model]", _key: str, relation_field: str) -> None:
setattr(self, relation_field, value.pk if value else None)
setattr(self, _key, value)
Expand Down Expand Up @@ -128,6 +138,8 @@ class MetaInfo:
"db_native_fields",
"db_default_fields",
"db_complex_fields",
"_default_ordering",
"_ordering_validated",
)

def __init__(self, meta: "Model.Meta") -> None:
Expand All @@ -136,6 +148,8 @@ def __init__(self, meta: "Model.Meta") -> None:
self.app: Optional[str] = getattr(meta, "app", None)
self.unique_together: Tuple[Tuple[str, ...], ...] = get_together(meta, "unique_together")
self.indexes: Tuple[Tuple[str, ...], ...] = get_together(meta, "indexes")
self._default_ordering: Tuple[Tuple[str, str], ...] = prepare_default_ordering(meta)
self._ordering_validated: bool = False
self.fields: Set[str] = set()
self.db_fields: Set[str] = set()
self.m2m_fields: Set[str] = set()
Expand All @@ -156,7 +170,7 @@ def __init__(self, meta: "Model.Meta") -> None:
self.basetable: Table = Table("")
self.pk_attr: str = getattr(meta, "pk_attr", "")
self.generated_db_fields: Tuple[str] = None # type: ignore
self._model: "Model" = None # type: ignore
self._model: Type["Model"] = None # type: ignore
self.table_description: str = getattr(meta, "table_description", "")
self.pk: Field = None # type: ignore
self.db_pk_field: str = ""
Expand Down Expand Up @@ -193,6 +207,16 @@ def db(self) -> BaseDBAsyncClient:
except KeyError:
raise ConfigurationError("No DB associated to model")

@property
def ordering(self) -> Tuple[Tuple[str, str], ...]:
if not self._ordering_validated:
unknown_fields = set(f for f, _ in self._default_ordering) - self.fields
raise ConfigurationError(
f"Unknown fields {','.join(unknown_fields)} in "
f"default ordering for model {self._model.__name__}"
)
return self._default_ordering

def get_filter(self, key: str) -> dict:
return self.filters[key]

Expand Down Expand Up @@ -230,6 +254,12 @@ def finalise_fields(self) -> None:
generated_fields.append(field.source_field or field.model_field_name)
self.generated_db_fields = tuple(generated_fields) # type: ignore

self._ordering_validated = True
for field_name, _ in self._default_ordering:
if field_name.split("__")[0] not in self.fields:
self._ordering_validated = False
break

def _generate_lazy_fk_m2m_fields(self) -> None:
# Create lazy FK fields on model.
for key in self.fk_fields:
Expand Down Expand Up @@ -477,7 +507,7 @@ def __search_for_field_attributes(base: Type, attrs: dict) -> None:
if not fields_map:
meta.abstract = True

new_class: "Model" = super().__new__(mcs, name, bases, attrs) # type: ignore
new_class: Type["Model"] = super().__new__(mcs, name, bases, attrs)
for field in meta.fields_map.values():
field.model = new_class

Expand Down Expand Up @@ -682,7 +712,7 @@ async def create(cls: Type[MODEL], **kwargs: Any) -> MODEL:

@classmethod
async def bulk_create(
cls: Type[MODEL], objects: List[MODEL], using_db: Optional[BaseDBAsyncClient] = None
cls: Type[MODEL], objects: List[MODEL], using_db: Optional[BaseDBAsyncClient] = None,
) -> None:
"""
Bulk insert operation:
Expand Down Expand Up @@ -786,7 +816,7 @@ def get_or_none(cls: Type[MODEL], *args: Q, **kwargs: Any) -> QuerySetSingle[Opt

@classmethod
async def fetch_for_list(
cls, instance_list: "List[Model]", *args: Any, using_db: Optional[BaseDBAsyncClient] = None
cls, instance_list: "List[Model]", *args: Any, using_db: Optional[BaseDBAsyncClient] = None,
) -> None:
"""
Fetches related models for provided list of Model objects.
Expand Down
25 changes: 18 additions & 7 deletions tortoise/queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Dict,
Generator,
Generic,
Iterable,
List,
Optional,
Set,
Expand Down Expand Up @@ -101,11 +102,22 @@ def _join_table_by_field(
self._joined_tables.append(join[0])
return joins[-1][0]

@staticmethod
def _resolve_ordering_string(ordering: str) -> Tuple[str, str]:
order_type = Order.asc
if ordering[0] == "-":
field_name = ordering[1:]
order_type = Order.desc
else:
field_name = ordering

return field_name, order_type

def resolve_ordering(
self,
model: "Type[Model]",
table: Table,
orderings: List[Tuple[str, str]],
orderings: Iterable[Tuple[str, str]],
annotations: Dict[str, Any],
) -> None:
"""
Expand All @@ -117,6 +129,10 @@ def resolve_ordering(
:param orderings: What columns/order to order by
:param annotations: Annotations that may be ordered on
"""
# Do not apply default ordering for annotated queries to not mess them up
if not orderings and self.model._meta.ordering and not annotations:
orderings = self.model._meta.ordering

for ordering in orderings:
field_name = ordering[0]
if field_name in model._meta.fetch_fields:
Expand Down Expand Up @@ -271,12 +287,7 @@ def order_by(self, *orderings: str) -> "QuerySet[MODEL]":
queryset = self._clone()
new_ordering = []
for ordering in orderings:
order_type = Order.asc
if ordering[0] == "-":
field_name = ordering[1:]
order_type = Order.desc
else:
field_name = ordering
field_name, order_type = self._resolve_ordering_string(ordering)

if not (
field_name.split("__")[0] in self.model._meta.fields
Expand Down

0 comments on commit f277a70

Please sign in to comment.