ugly but working - to refactor

This commit is contained in:
collerek
2021-03-11 17:53:13 +01:00
parent e306eecc2c
commit 0ae340100e
16 changed files with 688 additions and 77 deletions

View File

@ -54,6 +54,8 @@ class BaseField(FieldInfo):
through: Type["Model"] through: Type["Model"]
self_reference: bool = False self_reference: bool = False
self_reference_primary: Optional[str] = None self_reference_primary: Optional[str] = None
orders_by: Optional[List[str]] = None
related_orders_by: Optional[List[str]] = None
encrypt_secret: str encrypt_secret: str
encrypt_backend: EncryptBackends = EncryptBackends.NONE encrypt_backend: EncryptBackends = EncryptBackends.NONE

View File

@ -3,7 +3,7 @@ import sys
import uuid import uuid
from dataclasses import dataclass from dataclasses import dataclass
from random import choices from random import choices
from typing import Any, List, Optional, TYPE_CHECKING, Tuple, Type, Union from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, Union
import sqlalchemy import sqlalchemy
from pydantic import BaseModel, create_model from pydantic import BaseModel, create_model
@ -119,6 +119,35 @@ def populate_fk_params_based_on_to_model(
return __type__, constraints, column_type return __type__, constraints, column_type
def validate_not_allowed_fields(kwargs: Dict) -> None:
"""
Verifies if not allowed parameters are set on relation models.
Usually they are omitted later anyway but this way it's explicitly
notify the user that it's not allowed/ supported.
:raises ModelDefinitionError: if any forbidden field is set
:param kwargs: dict of kwargs to verify passed to relation field
:type kwargs: Dict
"""
default = kwargs.pop("default", None)
encrypt_secret = kwargs.pop("encrypt_secret", None)
encrypt_backend = kwargs.pop("encrypt_backend", None)
encrypt_custom_backend = kwargs.pop("encrypt_custom_backend", None)
not_supported = [
default,
encrypt_secret,
encrypt_backend,
encrypt_custom_backend,
]
if any(x is not None for x in not_supported):
raise ModelDefinitionError(
f"Argument {next((x for x in not_supported if x is not None))} "
f"is not supported "
"on relation fields!"
)
class UniqueColumns(UniqueConstraint): class UniqueColumns(UniqueConstraint):
""" """
Subclass of sqlalchemy.UniqueConstraint. Subclass of sqlalchemy.UniqueConstraint.
@ -184,24 +213,10 @@ def ForeignKey( # noqa CFQ002
owner = kwargs.pop("owner", None) owner = kwargs.pop("owner", None)
self_reference = kwargs.pop("self_reference", False) self_reference = kwargs.pop("self_reference", False)
orders_by = kwargs.pop("orders_by", None)
related_orders_by = kwargs.pop("related_orders_by", None)
default = kwargs.pop("default", None) validate_not_allowed_fields(kwargs)
encrypt_secret = kwargs.pop("encrypt_secret", None)
encrypt_backend = kwargs.pop("encrypt_backend", None)
encrypt_custom_backend = kwargs.pop("encrypt_custom_backend", None)
not_supported = [
default,
encrypt_secret,
encrypt_backend,
encrypt_custom_backend,
]
if any(x is not None for x in not_supported):
raise ModelDefinitionError(
f"Argument {next((x for x in not_supported if x is not None))} "
f"is not supported "
"on relation fields!"
)
if to.__class__ == ForwardRef: if to.__class__ == ForwardRef:
__type__ = to if not nullable else Optional[to] __type__ = to if not nullable else Optional[to]
@ -237,6 +252,8 @@ def ForeignKey( # noqa CFQ002
owner=owner, owner=owner,
self_reference=self_reference, self_reference=self_reference,
is_relation=True, is_relation=True,
orders_by=orders_by,
related_orders_by=related_orders_by,
) )
return type("ForeignKey", (ForeignKeyField, BaseField), namespace) return type("ForeignKey", (ForeignKeyField, BaseField), namespace)

View File

@ -5,7 +5,7 @@ from pydantic.typing import ForwardRef, evaluate_forwardref
import ormar # noqa: I100 import ormar # noqa: I100
from ormar import ModelDefinitionError from ormar import ModelDefinitionError
from ormar.fields import BaseField from ormar.fields import BaseField
from ormar.fields.foreign_key import ForeignKeyField from ormar.fields.foreign_key import ForeignKeyField, validate_not_allowed_fields
if TYPE_CHECKING: # pragma no cover if TYPE_CHECKING: # pragma no cover
from ormar.models import Model from ormar.models import Model
@ -93,26 +93,13 @@ def ManyToMany(
nullable = kwargs.pop("nullable", True) nullable = kwargs.pop("nullable", True)
owner = kwargs.pop("owner", None) owner = kwargs.pop("owner", None)
self_reference = kwargs.pop("self_reference", False) self_reference = kwargs.pop("self_reference", False)
orders_by = kwargs.pop("orders_by", None)
related_orders_by = kwargs.pop("related_orders_by", None)
if through is not None and through.__class__ != ForwardRef: if through is not None and through.__class__ != ForwardRef:
forbid_through_relations(cast(Type["Model"], through)) forbid_through_relations(cast(Type["Model"], through))
default = kwargs.pop("default", None) validate_not_allowed_fields(kwargs)
encrypt_secret = kwargs.pop("encrypt_secret", None)
encrypt_backend = kwargs.pop("encrypt_backend", None)
encrypt_custom_backend = kwargs.pop("encrypt_custom_backend", None)
not_supported = [
default,
encrypt_secret,
encrypt_backend,
encrypt_custom_backend,
]
if any(x is not None for x in not_supported):
raise ModelDefinitionError(
f"Argument {next((x for x in not_supported if x is not None))} "
f"is not supported "
"on relation fields!"
)
if to.__class__ == ForwardRef: if to.__class__ == ForwardRef:
__type__ = to if not nullable else Optional[to] __type__ = to if not nullable else Optional[to]
@ -141,6 +128,8 @@ def ManyToMany(
self_reference=self_reference, self_reference=self_reference,
is_relation=True, is_relation=True,
is_multi=True, is_multi=True,
orders_by=orders_by,
related_orders_by=related_orders_by,
) )
return type("ManyToMany", (ManyToManyField, BaseField), namespace) return type("ManyToMany", (ManyToManyField, BaseField), namespace)

View File

@ -51,6 +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"):
new_model.Meta.order_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

@ -110,6 +110,7 @@ def register_reverse_model_fields(model_field: Type["ForeignKeyField"]) -> None:
owner=model_field.to, owner=model_field.to,
self_reference=model_field.self_reference, self_reference=model_field.self_reference,
self_reference_primary=model_field.self_reference_primary, self_reference_primary=model_field.self_reference_primary,
orders_by=model_field.related_orders_by,
) )
# register foreign keys on through model # register foreign keys on through model
model_field = cast(Type["ManyToManyField"], model_field) model_field = cast(Type["ManyToManyField"], model_field)
@ -123,6 +124,7 @@ def register_reverse_model_fields(model_field: Type["ForeignKeyField"]) -> None:
related_name=model_field.name, related_name=model_field.name,
owner=model_field.to, owner=model_field.to,
self_reference=model_field.self_reference, self_reference=model_field.self_reference,
orders_by=model_field.related_orders_by,
) )

View File

@ -252,6 +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:
# by default we sort by pk name if other option not provided
new_model.Meta.order_by.append(pkname)
return new_model return new_model

View File

@ -71,6 +71,7 @@ class ModelMeta:
signals: SignalEmitter signals: SignalEmitter
abstract: bool abstract: bool
requires_ref_update: bool requires_ref_update: bool
order_by: List[str]
def add_cached_properties(new_model: Type["Model"]) -> None: def add_cached_properties(new_model: Type["Model"]) -> None:

View File

@ -273,7 +273,10 @@ class Model(ModelRow):
return self return self
async def load_all( async def load_all(
self: T, follow: bool = False, exclude: Union[List, str, Set, Dict] = None self: T,
follow: bool = False,
exclude: Union[List, str, Set, Dict] = None,
order_by: Union[List, str] = None,
) -> T: ) -> T:
""" """
Allow to refresh existing Models fields from database. Allow to refresh existing Models fields from database.
@ -291,6 +294,8 @@ class Model(ModelRow):
will load second Model A but will never follow into Model X. will load second Model A but will never follow into Model X.
Nested relations of those kind need to be loaded manually. Nested relations of those kind need to be loaded manually.
:param order_by: columns by which models should be sorted
:type order_by: Union[List, str]
:raises NoMatch: If given pk is not found in database. :raises NoMatch: If given pk is not found in database.
:param exclude: related models to exclude :param exclude: related models to exclude
@ -308,6 +313,8 @@ class Model(ModelRow):
queryset = self.__class__.objects queryset = self.__class__.objects
if exclude: if exclude:
queryset = queryset.exclude_fields(exclude) queryset = queryset.exclude_fields(exclude)
if order_by:
queryset = queryset.order_by(order_by)
instance = await queryset.select_related(relations).get(pk=self.pk) instance = await queryset.select_related(relations).get(pk=self.pk)
self._orm.clear() self._orm.clear()
self.update_from_dict(instance.dict()) self.update_from_dict(instance.dict())

View File

@ -1,22 +1,23 @@
from collections import OrderedDict from collections import OrderedDict
from typing import ( from typing import (
Any, Any,
Dict,
List, List,
Optional, Optional,
TYPE_CHECKING, TYPE_CHECKING,
Tuple, Tuple,
Type, Type, cast,
) )
import sqlalchemy import sqlalchemy
from sqlalchemy import text from sqlalchemy import text
import ormar # noqa I100 import ormar # noqa I100
from ormar.exceptions import RelationshipInstanceError from ormar.exceptions import ModelDefinitionError, RelationshipInstanceError
from ormar.relations import AliasManager from ormar.relations import AliasManager
if TYPE_CHECKING: # pragma no cover if TYPE_CHECKING: # pragma no cover
from ormar import Model from ormar import Model, ManyToManyField
from ormar.queryset import OrderAction from ormar.queryset import OrderAction
from ormar.models.excludable import ExcludableItems from ormar.models.excludable import ExcludableItems
@ -36,14 +37,18 @@ class SqlJoin:
related_models: Any = None, related_models: Any = None,
own_alias: str = "", own_alias: str = "",
source_model: Type["Model"] = None, source_model: Type["Model"] = None,
already_sorted: Dict = None,
) -> None: ) -> None:
self.relation_name = relation_name self.relation_name = relation_name
self.related_models = related_models or [] self.related_models = related_models or []
self.select_from = select_from self.select_from = select_from
self.columns = columns self.columns = columns
self.excludable = excludable self.excludable = excludable
self.order_columns = order_columns self.order_columns = order_columns
self.sorted_orders = sorted_orders self.sorted_orders = sorted_orders
self.already_sorted = already_sorted or dict()
self.main_model = main_model self.main_model = main_model
self.own_alias = own_alias self.own_alias = own_alias
self.used_aliases = used_aliases self.used_aliases = used_aliases
@ -205,6 +210,7 @@ class SqlJoin:
relation_str="__".join([self.relation_str, related_name]), relation_str="__".join([self.relation_str, related_name]),
own_alias=self.next_alias, own_alias=self.next_alias,
source_model=self.source_model or self.main_model, source_model=self.source_model or self.main_model,
already_sorted=self.already_sorted,
) )
( (
self.used_aliases, self.used_aliases,
@ -307,10 +313,9 @@ 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:
clause = ormar.OrderAction( clause = ormar.OrderAction(
order_str=self.next_model.Meta.pkname, order_str=order_by, model_cls=self.next_model, alias=self.next_alias,
model_cls=self.next_model,
alias=self.next_alias,
) )
self.sorted_orders[clause] = clause.get_text_clause() self.sorted_orders[clause] = clause.get_text_clause()
@ -320,18 +325,60 @@ class SqlJoin:
Otherwise by default each table is sorted by a primary key column asc. Otherwise by default each table is sorted by a primary key column asc.
""" """
alias = self.next_alias alias = self.next_alias
if self.order_columns:
current_table_sorted = False current_table_sorted = False
if f"{alias}_{self.next_model.get_name()}" in self.already_sorted:
current_table_sorted = True
if self.order_columns:
for condition in self.order_columns: for condition in self.order_columns:
if condition.check_if_filter_apply( if condition.check_if_filter_apply(
target_model=self.next_model, alias=alias target_model=self.next_model, alias=alias
): ):
current_table_sorted = True current_table_sorted = True
self.sorted_orders[condition] = condition.get_text_clause() self.sorted_orders[condition] = condition.get_text_clause()
if not current_table_sorted and not self.target_field.is_multi: self.already_sorted[
self._set_default_primary_key_order_by() f"{self.next_alias}_{self.next_model.get_name()}"
] = condition
# TODO: refactor into smaller helper functions
if self.target_field.orders_by and not current_table_sorted:
current_table_sorted = True
for order_by in self.target_field.orders_by:
if self.target_field.is_multi and "__" in order_by:
parts = order_by.split("__")
if (
len(parts) > 2
or parts[0] != self.target_field.through.get_name()
):
raise ModelDefinitionError(
"You can order the relation only"
"by related or link table columns!"
)
model = self.target_field.owner
clause = ormar.OrderAction(
order_str=order_by, model_cls=model, alias=alias,
)
elif self.target_field.is_multi:
alias = self.alias_manager.resolve_relation_alias(
from_model=self.target_field.through,
relation_name=cast("ManyToManyField",
self.target_field).default_target_field_name(),
)
model = self.target_field.to
clause = ormar.OrderAction(
order_str=order_by, model_cls=model, alias=alias
)
else:
alias = self.alias_manager.resolve_relation_alias(
from_model=self.target_field.owner,
relation_name=self.target_field.name,
)
model = self.target_field.to
clause = ormar.OrderAction(
order_str=order_by, model_cls=model, alias=alias
)
self.sorted_orders[clause] = clause.get_text_clause()
self.already_sorted[f"{alias}_{model.get_name()}"] = clause
elif not self.target_field.is_multi: if not current_table_sorted and not self.target_field.is_multi:
self._set_default_primary_key_order_by() self._set_default_primary_key_order_by()
def _get_to_and_from_keys(self) -> Tuple[str, str]: def _get_to_and_from_keys(self) -> Tuple[str, str]:

View File

@ -63,14 +63,16 @@ class Query:
That way the subquery with limit and offset only on main model has proper That way the subquery with limit and offset only on main model has proper
sorting applied and correct models are fetched. sorting applied and correct models are fetched.
""" """
current_table_sorted = False
if self.order_columns: if self.order_columns:
for clause in self.order_columns: for clause in self.order_columns:
if clause.is_source_model_order: if clause.is_source_model_order:
current_table_sorted = True
self.sorted_orders[clause] = clause.get_text_clause() self.sorted_orders[clause] = clause.get_text_clause()
else:
clause = ormar.OrderAction( if not current_table_sorted:
order_str=self.model_cls.Meta.pkname, model_cls=self.model_cls for order_by in self.model_cls.Meta.order_by:
) 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()
def _pagination_query_required(self) -> bool: def _pagination_query_required(self) -> bool:

View File

@ -1,5 +1,4 @@
import datetime import datetime
import os
import databases import databases
import pydantic import pydantic

View File

@ -0,0 +1,113 @@
from typing import Optional
import databases
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
order_by = ["-name"]
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
class Meta(BaseMeta):
tablename = "books"
order_by = ["year", "-ranking"]
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
ranking: int = ormar.Integer(nullable=True)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.fixture(autouse=True, scope="function")
async def cleanup():
await Book.objects.delete(each=True)
await Author.objects.delete(each=True)
@pytest.mark.asyncio
async def test_default_orders_is_applied():
async with database:
tolkien = await Author(name="J.R.R. Tolkien").save()
sapkowski = await Author(name="Andrzej Sapkowski").save()
king = await Author(name="Stephen King").save()
lewis = await Author(name="C.S Lewis").save()
authors = await Author.objects.all()
assert authors[0] == king
assert authors[1] == tolkien
assert authors[2] == lewis
assert authors[3] == sapkowski
authors = await Author.objects.order_by("name").all()
assert authors[3] == king
assert authors[2] == tolkien
assert authors[1] == lewis
assert authors[0] == sapkowski
@pytest.mark.asyncio
async def test_default_orders_is_applied_on_related():
async with database:
tolkien = await Author(name="J.R.R. Tolkien").save()
silmarillion = await Book(
author=tolkien, title="The Silmarillion", year=1977
).save()
lotr = await Book(
author=tolkien, title="The Lord of the Rings", year=1955
).save()
hobbit = await Book(author=tolkien, title="The Hobbit", year=1933).save()
await tolkien.books.all()
assert tolkien.books[0] == hobbit
assert tolkien.books[1] == lotr
assert tolkien.books[2] == silmarillion
await tolkien.books.order_by("-title").all()
assert tolkien.books[2] == hobbit
assert tolkien.books[1] == lotr
assert tolkien.books[0] == silmarillion
@pytest.mark.asyncio
async def test_default_orders_is_applied_on_related_two_fields():
async with database:
sanders = await Author(name="Brandon Sanderson").save()
twok = await Book(
author=sanders, title="The Way of Kings", year=2010, ranking=10
).save()
bret = await Author(name="Peter V. Bret").save()
tds = await Book(
author=bret, title="The Desert Spear", year=2010, ranking=9
).save()
books = await Book.objects.all()
assert books[0] == twok
assert books[1] == tds

View File

@ -0,0 +1,149 @@
from typing import List, Optional
from uuid import UUID, uuid4
import databases
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
class Meta(BaseMeta):
tablename = "books"
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(
Author, orders_by=["name"], related_orders_by=["-year"]
)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
ranking: int = ormar.Integer(nullable=True)
class Animal(ormar.Model):
class Meta(BaseMeta):
tablename = "animals"
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.String(max_length=200)
specie: str = ormar.String(max_length=200)
class Human(ormar.Model):
class Meta(BaseMeta):
tablename = "humans"
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
pets: List[Animal] = ormar.ManyToMany(
Animal,
related_name="care_takers",
orders_by=["specie", "-name"],
related_orders_by=["name"],
)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.fixture(autouse=True, scope="function")
async def cleanup():
await Book.objects.delete(each=True)
await Author.objects.delete(each=True)
@pytest.mark.asyncio
async def test_default_orders_is_applied_from_reverse_relation():
async with database:
tolkien = await Author(name="J.R.R. Tolkien").save()
hobbit = await Book(author=tolkien, title="The Hobbit", year=1933).save()
silmarillion = await Book(
author=tolkien, title="The Silmarillion", year=1977
).save()
lotr = await Book(
author=tolkien, title="The Lord of the Rings", year=1955
).save()
tolkien = await Author.objects.select_related("books").get()
assert tolkien.books[2] == hobbit
assert tolkien.books[1] == lotr
assert tolkien.books[0] == silmarillion
@pytest.mark.asyncio
async def test_default_orders_is_applied_from_relation():
async with database:
bret = await Author(name="Peter V. Bret").save()
tds = await Book(
author=bret, title="The Desert Spear", year=2010, ranking=9
).save()
sanders = await Author(name="Brandon Sanderson").save()
twok = await Book(
author=sanders, title="The Way of Kings", year=2010, ranking=10
).save()
books = await Book.objects.order_by("year").select_related("author").all()
assert books[0] == twok
assert books[1] == tds
@pytest.mark.asyncio
async def test_default_orders_is_applied_from_relation_on_m2m():
async with database:
alice = await Human(name="Alice").save()
spot = await Animal(name="Spot", specie="Cat").save()
zkitty = await Animal(name="ZKitty", specie="Cat").save()
noodle = await Animal(name="Noodle", specie="Anaconda").save()
await alice.pets.add(noodle)
await alice.pets.add(spot)
await alice.pets.add(zkitty)
await alice.load_all()
assert alice.pets[0] == noodle
assert alice.pets[1] == zkitty
assert alice.pets[2] == spot
@pytest.mark.asyncio
async def test_default_orders_is_applied_from_reverse_relation_on_m2m():
async with database:
max = await Animal(name="Max", specie="Dog").save()
joe = await Human(name="Joe").save()
zack = await Human(name="Zack").save()
julia = await Human(name="Julia").save()
await max.care_takers.add(joe)
await max.care_takers.add(zack)
await max.care_takers.add(julia)
await max.load_all()
assert max.care_takers[0] == joe
assert max.care_takers[1] == julia
assert max.care_takers[2] == zack

View File

@ -0,0 +1,176 @@
from typing import List
from uuid import UUID, uuid4
import databases
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class Animal(ormar.Model):
class Meta(BaseMeta):
tablename = "animals"
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
# favoriteHumans
class Link(ormar.Model):
class Meta(BaseMeta):
tablename = "link_table"
id: int = ormar.Integer(primary_key=True)
animal_order: int = ormar.Integer(nullable=True)
human_order: int = ormar.Integer(nullable=True)
class Human(ormar.Model):
class Meta(BaseMeta):
tablename = "humans"
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
favoriteAnimals: List[Animal] = ormar.ManyToMany(
Animal,
through=Link,
related_name="favoriteHumans",
orders_by=["link__animal_order"],
related_orders_by=["link__human_order"],
)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.mark.asyncio
async def test_ordering_by_through_on_m2m_field():
async with database:
alice = await Human(name="Alice").save()
bob = await Human(name="Bob").save()
charlie = await Human(name="Charlie").save()
spot = await Animal(name="Spot").save()
kitty = await Animal(name="Kitty").save()
noodle = await Animal(name="Noodle").save()
# you need to add them in order anyway so can provide order explicitly
# if you have a lot of them a list with enumerate might be an option
await alice.favoriteAnimals.add(noodle, animal_order=0, human_order=0)
await alice.favoriteAnimals.add(spot, animal_order=1, human_order=0)
await alice.favoriteAnimals.add(kitty, animal_order=2, human_order=0)
# you dont have to reload queries on queryset clears the existing related
# alice = await alice.reload()
await alice.load_all()
assert [x.name for x in alice.favoriteAnimals] == ["Noodle", "Spot", "Kitty"]
await bob.favoriteAnimals.add(noodle, animal_order=0, human_order=1)
await bob.favoriteAnimals.add(kitty, animal_order=1, human_order=1)
await bob.favoriteAnimals.add(spot, animal_order=2, human_order=1)
await bob.load_all()
assert [x.name for x in bob.favoriteAnimals] == ["Noodle", "Kitty", "Spot"]
await charlie.favoriteAnimals.add(kitty, animal_order=0, human_order=2)
await charlie.favoriteAnimals.add(noodle, animal_order=1, human_order=2)
await charlie.favoriteAnimals.add(spot, animal_order=2, human_order=2)
await charlie.load_all()
assert [x.name for x in charlie.favoriteAnimals] == ["Kitty", "Noodle", "Spot"]
animals = [noodle, kitty, spot]
for animal in animals:
await animal.load_all()
assert [x.name for x in animal.favoriteHumans] == [
"Alice",
"Bob",
"Charlie",
]
zack = await Human(name="Zack").save()
async def reorder_humans(animal, new_ordered_humans):
noodle_links = await Link.objects.filter(animal=animal).all()
for link in noodle_links:
link.human_order = next(
(
i
for i, x in enumerate(new_ordered_humans)
if x.pk == link.human.pk
),
None,
)
await Link.objects.bulk_update(noodle_links, columns=["human_order"])
await noodle.favoriteHumans.add(zack, animal_order=0, human_order=0)
await reorder_humans(noodle, [zack, alice, bob, charlie])
await noodle.load_all()
assert [x.name for x in noodle.favoriteHumans] == [
"Zack",
"Alice",
"Bob",
"Charlie",
]
await zack.load_all()
assert [x.name for x in zack.favoriteAnimals] == ["Noodle"]
humans = noodle.favoriteHumans
humans.insert(1, humans.pop(0))
await reorder_humans(noodle, humans)
await noodle.load_all()
assert [x.name for x in noodle.favoriteHumans] == [
"Alice",
"Zack",
"Bob",
"Charlie",
]
humans.insert(2, humans.pop(1))
await reorder_humans(noodle, humans)
await noodle.load_all()
assert [x.name for x in noodle.favoriteHumans] == [
"Alice",
"Bob",
"Zack",
"Charlie",
]
humans.insert(3, humans.pop(2))
await reorder_humans(noodle, humans)
await noodle.load_all()
assert [x.name for x in noodle.favoriteHumans] == [
"Alice",
"Bob",
"Charlie",
"Zack",
]
await kitty.favoriteHumans.remove(bob)
await kitty.load_all()
assert [x.name for x in kitty.favoriteHumans] == ["Alice", "Charlie"]
bob = await noodle.favoriteHumans.get(pk=bob.pk)
assert bob.link.human_order == 1
await noodle.favoriteHumans.remove(
await noodle.favoriteHumans.filter(link__human_order=2).get()
)
await noodle.load_all()
assert [x.name for x in noodle.favoriteHumans] == ["Alice", "Bob", "Zack"]

View File

@ -107,6 +107,30 @@ async def test_load_all_many_to_many():
assert hq.nicks[1].name == "Bazinga20" assert hq.nicks[1].name == "Bazinga20"
@pytest.mark.asyncio
async def test_load_all_with_order():
async with database:
async with database.transaction(force_rollback=True):
nick1 = await NickName.objects.create(name="Barry", is_lame=False)
nick2 = await NickName.objects.create(name="Joe", is_lame=True)
hq = await HQ.objects.create(name="Main")
await hq.nicks.add(nick1)
await hq.nicks.add(nick2)
hq = await HQ.objects.get(name="Main")
await hq.load_all(order_by="-nicks__name")
assert hq.nicks[0] == nick2
assert hq.nicks[0].name == "Joe"
assert hq.nicks[1] == nick1
assert hq.nicks[1].name == "Barry"
await hq.load_all()
assert hq.nicks[0] == nick1
assert hq.nicks[1] == nick2
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_loading_reversed_relation(): async def test_loading_reversed_relation():
async with database: async with database:

View File

@ -0,0 +1,78 @@
from typing import Optional
import databases
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
class Meta(BaseMeta):
tablename = "books"
order_by = ["-ranking"]
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(
Author, orders_by=["name"], related_orders_by=["-year"]
)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
ranking: int = ormar.Integer(nullable=True)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.fixture(autouse=True, scope="function")
async def cleanup():
await Book.objects.delete(each=True)
await Author.objects.delete(each=True)
@pytest.mark.asyncio
async def test_default_orders_is_applied_from_reverse_relation():
async with database:
tolkien = await Author(name="J.R.R. Tolkien").save()
hobbit = await Book(author=tolkien, title="The Hobbit", year=1933).save()
silmarillion = await Book(
author=tolkien, title="The Silmarillion", year=1977
).save()
lotr = await Book(
author=tolkien, title="The Lord of the Rings", year=1955
).save()
tolkien = await Author.objects.select_related("books").get()
assert tolkien.books[2] == hobbit
assert tolkien.books[1] == lotr
assert tolkien.books[0] == silmarillion
tolkien = (
await Author.objects.select_related("books").order_by("books__title").get()
)
assert tolkien.books[0] == hobbit
assert tolkien.books[1] == lotr
assert tolkien.books[2] == silmarillion