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 # 0.9.8
## Features ## Features

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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