add release docs, change tests

This commit is contained in:
collerek
2021-03-15 13:00:07 +01:00
parent 5c633d32a8
commit 03e6ac6c02
10 changed files with 115 additions and 19 deletions

View File

@ -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

View File

@ -75,7 +75,7 @@ class UndefinedType: # pragma no cover
Undefined = UndefinedType()
__version__ = "0.9.8"
__version__ = "0.9.9"
__all__ = [
"Integer",
"BigInteger",

View File

@ -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()

View File

@ -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

View File

@ -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:

View File

@ -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,
)

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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(