From 03e6ac6c02e3ecabe22e7ceaba1f1da53901d154 Mon Sep 17 00:00:00 2001 From: collerek Date: Mon, 15 Mar 2021 13:00:07 +0100 Subject: [PATCH] add release docs, change tests --- docs/releases.md | 45 +++++++++++++ ormar/__init__.py | 2 +- ormar/models/helpers/models.py | 4 +- ormar/models/helpers/sqlalchemy.py | 4 +- ormar/models/metaclass.py | 2 +- ormar/queryset/join.py | 2 +- ormar/queryset/query.py | 2 +- tests/test_default_model_order.py | 4 +- tests/test_default_through_relation_order.py | 67 +++++++++++++++++--- tests/test_proper_order_of_sorting_apply.py | 2 +- 10 files changed, 115 insertions(+), 19 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index e1795c5..2fb0371 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -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 diff --git a/ormar/__init__.py b/ormar/__init__.py index d9225a4..357c296 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -75,7 +75,7 @@ class UndefinedType: # pragma no cover Undefined = UndefinedType() -__version__ = "0.9.8" +__version__ = "0.9.9" __all__ = [ "Integer", "BigInteger", diff --git a/ormar/models/helpers/models.py b/ormar/models/helpers/models.py index 0e6abab..4ebe8dc 100644 --- a/ormar/models/helpers/models.py +++ b/ormar/models/helpers/models.py @@ -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() diff --git a/ormar/models/helpers/sqlalchemy.py b/ormar/models/helpers/sqlalchemy.py index b846a5d..905af75 100644 --- a/ormar/models/helpers/sqlalchemy.py +++ b/ormar/models/helpers/sqlalchemy.py @@ -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 diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 3d14b52..116a592 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -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: diff --git a/ormar/queryset/join.py b/ormar/queryset/join.py index 1828961..241f60f 100644 --- a/ormar/queryset/join.py +++ b/ormar/queryset/join.py @@ -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, ) diff --git a/ormar/queryset/query.py b/ormar/queryset/query.py index 983d4b0..422b345 100644 --- a/ormar/queryset/query.py +++ b/ormar/queryset/query.py @@ -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() diff --git a/tests/test_default_model_order.py b/tests/test_default_model_order.py index c854bbd..28732ef 100644 --- a/tests/test_default_model_order.py +++ b/tests/test_default_model_order.py @@ -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) diff --git a/tests/test_default_through_relation_order.py b/tests/test_default_through_relation_order.py index 3bdf749..f695426 100644 --- a/tests/test_default_through_relation_order.py +++ b/tests/test_default_through_relation_order.py @@ -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() diff --git a/tests/test_proper_order_of_sorting_apply.py b/tests/test_proper_order_of_sorting_apply.py index a02f6be..057c611 100644 --- a/tests/test_proper_order_of_sorting_apply.py +++ b/tests/test_proper_order_of_sorting_apply.py @@ -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(