add release docs, change tests
This commit is contained in:
@ -1,3 +1,48 @@
|
||||
# 0.9.9
|
||||
|
||||
## Features
|
||||
* Add possibility to change default ordering of relations and models.
|
||||
* To change model sorting pass `orders_by = [columns]` where `columns: List[str]` to model `Meta` class
|
||||
* To change relation order_by pass `orders_by = [columns]` where `columns: List[str]`
|
||||
* To change reverse relation order_by pass `related_orders_by = [columns]` where `columns: List[str]`
|
||||
* Arguments can be column names or `-{col_name}` to sort descending
|
||||
* In relations you can sort only by directly related model columns
|
||||
or for `ManyToMany` columns also `Through` model columns `"{through_field_name}__{column_name}"`
|
||||
* Order in which order_by clauses are applied is as follows:
|
||||
* Explicitly passed `order_by()` calls in query
|
||||
* Relation passed `orders_by` if exists
|
||||
* Model `Meta` class `orders_by`
|
||||
* Model primary key column asc (fallback, used if none of above provided)
|
||||
* Add 4 new aggregated functions -> `min`, `max`, `sum` and `avg` that are their
|
||||
corresponding sql equivalents.
|
||||
* You can pass one or many column names including related columns.
|
||||
* As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
|
||||
you can have `sum(col1, col2)` and later add 2 returned sums in python)
|
||||
* You cannot `sum` and `avg` non numeric columns
|
||||
* If you aggregate on one column, the single value is directly returned as a result
|
||||
* If you aggregate on multiple columns a dictionary with column: result pairs is returned
|
||||
* Add 4 new signals -> `pre_relation_add`, `post_relation_add`, `pre_relation_remove` and `post_relation_remove`
|
||||
* The newly added signals are emitted for `ManyToMany` relations (both sides)
|
||||
and reverse side of `ForeignKey` relation (same as `QuerysetProxy` is exposed).
|
||||
* Signals recieve following args: `sender: Type[Model]` - sender class,
|
||||
`instance: Model` - instance to which related model is added, `child: Model` - model being added,
|
||||
`relation_name: str` - name of the relation to which child is added,
|
||||
for add signals also `passed_kwargs: Dict` - dict of kwargs passed to `add()`
|
||||
|
||||
## Changes
|
||||
* `Through` models for ManyToMany relations are now instantiated on creation, deletion and update, so you can provide not only
|
||||
autoincrement int as a primary key but any column type with default function provided.
|
||||
* Since `Through` models are now instantiated you can also subscribe to `Through` model
|
||||
pre/post save/update/delete signals
|
||||
* `pre_update` signals receivers now get also passed_args argument which is a
|
||||
dict of values passed to update function if any (else empty dict)
|
||||
|
||||
## Fixes
|
||||
* `pre_update` signal now is sent before the extraction of values so you can modify the passed
|
||||
instance in place and modified fields values will be reflected in database
|
||||
* `bulk_update` now works correctly also with `UUID` primary key column type
|
||||
|
||||
|
||||
# 0.9.8
|
||||
|
||||
## Features
|
||||
|
||||
@ -75,7 +75,7 @@ class UndefinedType: # pragma no cover
|
||||
|
||||
Undefined = UndefinedType()
|
||||
|
||||
__version__ = "0.9.8"
|
||||
__version__ = "0.9.9"
|
||||
__all__ = [
|
||||
"Integer",
|
||||
"BigInteger",
|
||||
|
||||
@ -51,8 +51,8 @@ def populate_default_options_values(
|
||||
new_model.Meta.model_fields = model_fields
|
||||
if not hasattr(new_model.Meta, "abstract"):
|
||||
new_model.Meta.abstract = False
|
||||
if not hasattr(new_model.Meta, "order_by"):
|
||||
new_model.Meta.order_by = []
|
||||
if not hasattr(new_model.Meta, "orders_by"):
|
||||
new_model.Meta.orders_by = []
|
||||
|
||||
if any(
|
||||
is_field_an_forward_ref(field) for field in new_model.Meta.model_fields.values()
|
||||
|
||||
@ -252,9 +252,9 @@ def populate_meta_tablename_columns_and_pk(
|
||||
|
||||
new_model.Meta.columns = columns
|
||||
new_model.Meta.pkname = pkname
|
||||
if not new_model.Meta.order_by:
|
||||
if not new_model.Meta.orders_by:
|
||||
# by default we sort by pk name if other option not provided
|
||||
new_model.Meta.order_by.append(pkname)
|
||||
new_model.Meta.orders_by.append(pkname)
|
||||
return new_model
|
||||
|
||||
|
||||
|
||||
@ -71,7 +71,7 @@ class ModelMeta:
|
||||
signals: SignalEmitter
|
||||
abstract: bool
|
||||
requires_ref_update: bool
|
||||
order_by: List[str]
|
||||
orders_by: List[str]
|
||||
|
||||
|
||||
def add_cached_properties(new_model: Type["Model"]) -> None:
|
||||
|
||||
@ -314,7 +314,7 @@ class SqlJoin:
|
||||
self.used_aliases.append(self.next_alias)
|
||||
|
||||
def _set_default_primary_key_order_by(self) -> None:
|
||||
for order_by in self.next_model.Meta.order_by:
|
||||
for order_by in self.next_model.Meta.orders_by:
|
||||
clause = ormar.OrderAction(
|
||||
order_str=order_by, model_cls=self.next_model, alias=self.next_alias,
|
||||
)
|
||||
|
||||
@ -71,7 +71,7 @@ class Query:
|
||||
self.sorted_orders[clause] = clause.get_text_clause()
|
||||
|
||||
if not current_table_sorted:
|
||||
for order_by in self.model_cls.Meta.order_by:
|
||||
for order_by in self.model_cls.Meta.orders_by:
|
||||
clause = ormar.OrderAction(order_str=order_by, model_cls=self.model_cls)
|
||||
self.sorted_orders[clause] = clause.get_text_clause()
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ class BaseMeta(ormar.ModelMeta):
|
||||
class Author(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "authors"
|
||||
order_by = ["-name"]
|
||||
orders_by = ["-name"]
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
@ -28,7 +28,7 @@ class Author(ormar.Model):
|
||||
class Book(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "books"
|
||||
order_by = ["year", "-ranking"]
|
||||
orders_by = ["year", "-ranking"]
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
author: Optional[Author] = ormar.ForeignKey(Author)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict, List, Type, cast
|
||||
from typing import Any, Dict, List, Tuple, Type, cast
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import databases
|
||||
@ -6,7 +6,7 @@ import pytest
|
||||
import sqlalchemy
|
||||
|
||||
import ormar
|
||||
from ormar import ModelDefinitionError, Model, QuerySet, pre_update
|
||||
from ormar import ModelDefinitionError, Model, QuerySet, pre_relation_remove, pre_update
|
||||
from ormar import pre_save
|
||||
from tests.settings import DATABASE_URL
|
||||
|
||||
@ -95,6 +95,15 @@ def _get_filtered_query(
|
||||
return query
|
||||
|
||||
|
||||
def _get_through_model_relations(
|
||||
sender: Type[Model], instance: Model
|
||||
) -> Tuple[Type[Model], Type[Model]]:
|
||||
relations = list(instance.extract_related_names())
|
||||
rel_one = sender.Meta.model_fields[relations[0]].to
|
||||
rel_two = sender.Meta.model_fields[relations[1]].to
|
||||
return rel_one, rel_two
|
||||
|
||||
|
||||
async def _populate_order_on_insert(
|
||||
sender: Type[Model], instance: Model, from_class: Type[Model], to_class: Type[Model]
|
||||
):
|
||||
@ -161,9 +170,7 @@ async def order_link_on_insert(sender: Type[Model], instance: Model, **kwargs: A
|
||||
by calling save() on a model. Note that signal functions for pre_save signal accepts
|
||||
sender class, instance and have to accept **kwargs even if it's empty as of now.
|
||||
"""
|
||||
relations = list(instance.extract_related_names())
|
||||
rel_one = sender.Meta.model_fields[relations[0]].to
|
||||
rel_two = sender.Meta.model_fields[relations[1]].to
|
||||
rel_one, rel_two = _get_through_model_relations(sender, instance)
|
||||
await _populate_order_on_insert(
|
||||
sender=sender, instance=instance, from_class=rel_one, to_class=rel_two
|
||||
)
|
||||
@ -183,9 +190,7 @@ async def reorder_links_on_update(
|
||||
update and have to accept **kwargs even if it's empty as of now.
|
||||
"""
|
||||
|
||||
relations = list(instance.extract_related_names())
|
||||
rel_one = sender.Meta.model_fields[relations[0]].to
|
||||
rel_two = sender.Meta.model_fields[relations[1]].to
|
||||
rel_one, rel_two = _get_through_model_relations(sender, instance)
|
||||
await _reorder_on_update(
|
||||
sender=sender,
|
||||
instance=instance,
|
||||
@ -202,6 +207,46 @@ async def reorder_links_on_update(
|
||||
)
|
||||
|
||||
|
||||
@pre_relation_remove([Animal, Human])
|
||||
async def reorder_links_on_remove(
|
||||
sender: Type[ormar.Model],
|
||||
instance: ormar.Model,
|
||||
child: ormar.Model,
|
||||
relation_name: str,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Signal receiver registered on Anima and Human models, triggered every time before
|
||||
relation on a model is removed. Note that signal functions for pre_relation_remove
|
||||
signal accepts sender class, instance, child, relation_name and have to accept
|
||||
**kwargs even if it's empty as of now.
|
||||
|
||||
Note that if classes have many relations you need to check if current one is ordered
|
||||
"""
|
||||
through_class = sender.Meta.model_fields[relation_name].through
|
||||
through_instance = getattr(instance, through_class.get_name())
|
||||
if not through_instance:
|
||||
parent_pk = instance.pk
|
||||
child_pk = child.pk
|
||||
filter_kwargs = {f"{sender.get_name()}": parent_pk, child.get_name(): child_pk}
|
||||
through_instance = await through_class.objects.get(**filter_kwargs)
|
||||
rel_one, rel_two = _get_through_model_relations(through_class, through_instance)
|
||||
await _reorder_on_update(
|
||||
sender=through_class,
|
||||
instance=through_instance,
|
||||
from_class=rel_one,
|
||||
to_class=rel_two,
|
||||
passed_args={f"{rel_one.get_name()}_order": 999999},
|
||||
)
|
||||
await _reorder_on_update(
|
||||
sender=through_class,
|
||||
instance=through_instance,
|
||||
from_class=rel_two,
|
||||
to_class=rel_one,
|
||||
passed_args={f"{rel_two.get_name()}_order": 999999},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ordering_by_through_on_m2m_field():
|
||||
async with database:
|
||||
@ -210,7 +255,13 @@ async def test_ordering_by_through_on_m2m_field():
|
||||
field_name = (
|
||||
"favoriteAnimals" if isinstance(instance, Human) else "favoriteHumans"
|
||||
)
|
||||
order_field_name = (
|
||||
"animal_order" if isinstance(instance, Human) else "human_order"
|
||||
)
|
||||
assert [x.name for x in getattr(instance, field_name)] == expected
|
||||
assert [
|
||||
getattr(x.link, order_field_name) for x in getattr(instance, field_name)
|
||||
] == [i for i in range(len(expected))]
|
||||
|
||||
alice = await Human(name="Alice").save()
|
||||
bob = await Human(name="Bob").save()
|
||||
|
||||
@ -27,7 +27,7 @@ class Author(ormar.Model):
|
||||
class Book(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "books"
|
||||
order_by = ["-ranking"]
|
||||
orders_by = ["-ranking"]
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
author: Optional[Author] = ormar.ForeignKey(
|
||||
|
||||
Reference in New Issue
Block a user