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
|
# 0.9.8
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
@ -75,7 +75,7 @@ class UndefinedType: # pragma no cover
|
|||||||
|
|
||||||
Undefined = UndefinedType()
|
Undefined = UndefinedType()
|
||||||
|
|
||||||
__version__ = "0.9.8"
|
__version__ = "0.9.9"
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Integer",
|
"Integer",
|
||||||
"BigInteger",
|
"BigInteger",
|
||||||
|
|||||||
@ -51,8 +51,8 @@ def populate_default_options_values(
|
|||||||
new_model.Meta.model_fields = model_fields
|
new_model.Meta.model_fields = model_fields
|
||||||
if not hasattr(new_model.Meta, "abstract"):
|
if not hasattr(new_model.Meta, "abstract"):
|
||||||
new_model.Meta.abstract = False
|
new_model.Meta.abstract = False
|
||||||
if not hasattr(new_model.Meta, "order_by"):
|
if not hasattr(new_model.Meta, "orders_by"):
|
||||||
new_model.Meta.order_by = []
|
new_model.Meta.orders_by = []
|
||||||
|
|
||||||
if any(
|
if any(
|
||||||
is_field_an_forward_ref(field) for field in new_model.Meta.model_fields.values()
|
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.columns = columns
|
||||||
new_model.Meta.pkname = pkname
|
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
|
# 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
|
return new_model
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -71,7 +71,7 @@ class ModelMeta:
|
|||||||
signals: SignalEmitter
|
signals: SignalEmitter
|
||||||
abstract: bool
|
abstract: bool
|
||||||
requires_ref_update: bool
|
requires_ref_update: bool
|
||||||
order_by: List[str]
|
orders_by: List[str]
|
||||||
|
|
||||||
|
|
||||||
def add_cached_properties(new_model: Type["Model"]) -> None:
|
def add_cached_properties(new_model: Type["Model"]) -> None:
|
||||||
|
|||||||
@ -314,7 +314,7 @@ class SqlJoin:
|
|||||||
self.used_aliases.append(self.next_alias)
|
self.used_aliases.append(self.next_alias)
|
||||||
|
|
||||||
def _set_default_primary_key_order_by(self) -> None:
|
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(
|
clause = ormar.OrderAction(
|
||||||
order_str=order_by, model_cls=self.next_model, alias=self.next_alias,
|
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()
|
self.sorted_orders[clause] = clause.get_text_clause()
|
||||||
|
|
||||||
if not current_table_sorted:
|
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)
|
clause = ormar.OrderAction(order_str=order_by, model_cls=self.model_cls)
|
||||||
self.sorted_orders[clause] = clause.get_text_clause()
|
self.sorted_orders[clause] = clause.get_text_clause()
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ class BaseMeta(ormar.ModelMeta):
|
|||||||
class Author(ormar.Model):
|
class Author(ormar.Model):
|
||||||
class Meta(BaseMeta):
|
class Meta(BaseMeta):
|
||||||
tablename = "authors"
|
tablename = "authors"
|
||||||
order_by = ["-name"]
|
orders_by = ["-name"]
|
||||||
|
|
||||||
id: int = ormar.Integer(primary_key=True)
|
id: int = ormar.Integer(primary_key=True)
|
||||||
name: str = ormar.String(max_length=100)
|
name: str = ormar.String(max_length=100)
|
||||||
@ -28,7 +28,7 @@ class Author(ormar.Model):
|
|||||||
class Book(ormar.Model):
|
class Book(ormar.Model):
|
||||||
class Meta(BaseMeta):
|
class Meta(BaseMeta):
|
||||||
tablename = "books"
|
tablename = "books"
|
||||||
order_by = ["year", "-ranking"]
|
orders_by = ["year", "-ranking"]
|
||||||
|
|
||||||
id: int = ormar.Integer(primary_key=True)
|
id: int = ormar.Integer(primary_key=True)
|
||||||
author: Optional[Author] = ormar.ForeignKey(Author)
|
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
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
import databases
|
import databases
|
||||||
@ -6,7 +6,7 @@ import pytest
|
|||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
import ormar
|
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 ormar import pre_save
|
||||||
from tests.settings import DATABASE_URL
|
from tests.settings import DATABASE_URL
|
||||||
|
|
||||||
@ -95,6 +95,15 @@ def _get_filtered_query(
|
|||||||
return 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(
|
async def _populate_order_on_insert(
|
||||||
sender: Type[Model], instance: Model, from_class: Type[Model], to_class: Type[Model]
|
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
|
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.
|
sender class, instance and have to accept **kwargs even if it's empty as of now.
|
||||||
"""
|
"""
|
||||||
relations = list(instance.extract_related_names())
|
rel_one, rel_two = _get_through_model_relations(sender, instance)
|
||||||
rel_one = sender.Meta.model_fields[relations[0]].to
|
|
||||||
rel_two = sender.Meta.model_fields[relations[1]].to
|
|
||||||
await _populate_order_on_insert(
|
await _populate_order_on_insert(
|
||||||
sender=sender, instance=instance, from_class=rel_one, to_class=rel_two
|
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.
|
update and have to accept **kwargs even if it's empty as of now.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
relations = list(instance.extract_related_names())
|
rel_one, rel_two = _get_through_model_relations(sender, instance)
|
||||||
rel_one = sender.Meta.model_fields[relations[0]].to
|
|
||||||
rel_two = sender.Meta.model_fields[relations[1]].to
|
|
||||||
await _reorder_on_update(
|
await _reorder_on_update(
|
||||||
sender=sender,
|
sender=sender,
|
||||||
instance=instance,
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_ordering_by_through_on_m2m_field():
|
async def test_ordering_by_through_on_m2m_field():
|
||||||
async with database:
|
async with database:
|
||||||
@ -210,7 +255,13 @@ async def test_ordering_by_through_on_m2m_field():
|
|||||||
field_name = (
|
field_name = (
|
||||||
"favoriteAnimals" if isinstance(instance, Human) else "favoriteHumans"
|
"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 [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()
|
alice = await Human(name="Alice").save()
|
||||||
bob = await Human(name="Bob").save()
|
bob = await Human(name="Bob").save()
|
||||||
|
|||||||
@ -27,7 +27,7 @@ class Author(ormar.Model):
|
|||||||
class Book(ormar.Model):
|
class Book(ormar.Model):
|
||||||
class Meta(BaseMeta):
|
class Meta(BaseMeta):
|
||||||
tablename = "books"
|
tablename = "books"
|
||||||
order_by = ["-ranking"]
|
orders_by = ["-ranking"]
|
||||||
|
|
||||||
id: int = ormar.Integer(primary_key=True)
|
id: int = ormar.Integer(primary_key=True)
|
||||||
author: Optional[Author] = ormar.ForeignKey(
|
author: Optional[Author] = ormar.ForeignKey(
|
||||||
|
|||||||
Reference in New Issue
Block a user