bump version, update docs
This commit is contained in:
@ -154,7 +154,7 @@ assert len(tracks) == 1
|
||||
* `bulk_create(objects: List[Model]) -> None`
|
||||
* `bulk_update(objects: List[Model], columns: List[str] = None) -> None`
|
||||
* `delete(each: bool = False, **kwargs) -> int`
|
||||
* `all(self, **kwargs) -> List[Optional[Model]]`
|
||||
* `all(**kwargs) -> List[Optional[Model]]`
|
||||
* `filter(**kwargs) -> QuerySet`
|
||||
* `exclude(**kwargs) -> QuerySet`
|
||||
* `select_related(related: Union[List, str]) -> QuerySet`
|
||||
|
||||
@ -154,7 +154,7 @@ assert len(tracks) == 1
|
||||
* `bulk_create(objects: List[Model]) -> None`
|
||||
* `bulk_update(objects: List[Model], columns: List[str] = None) -> None`
|
||||
* `delete(each: bool = False, **kwargs) -> int`
|
||||
* `all(self, **kwargs) -> List[Optional[Model]]`
|
||||
* `all(**kwargs) -> List[Optional[Model]]`
|
||||
* `filter(**kwargs) -> QuerySet`
|
||||
* `exclude(**kwargs) -> QuerySet`
|
||||
* `select_related(related: Union[List, str]) -> QuerySet`
|
||||
|
||||
@ -176,7 +176,7 @@ Return number of rows deleted.
|
||||
|
||||
### all
|
||||
|
||||
`all(self, **kwargs) -> List[Optional["Model"]]`
|
||||
`all(**kwargs) -> List[Optional["Model"]]`
|
||||
|
||||
Returns all rows from a database for given model for set filter options.
|
||||
|
||||
|
||||
@ -29,6 +29,83 @@ By default it's child (source) `Model` name + s, like courses in snippet below:
|
||||
--8<-- "../docs_src/fields/docs001.py"
|
||||
```
|
||||
|
||||
Reverse relation exposes API to manage related objects also from parent side.
|
||||
|
||||
##### add
|
||||
|
||||
Adding child model from parent side causes adding related model to currently loaded parent relation,
|
||||
as well as sets child's model foreign key value and updates the model.
|
||||
|
||||
```python
|
||||
department = await Department(name="Science").save()
|
||||
course = Course(name="Math", completed=False) # note - not saved
|
||||
|
||||
await department.courses.add(course)
|
||||
assert course.pk is not None # child model was saved
|
||||
# relation on child model is set and FK column saved in db
|
||||
assert courses.department == department
|
||||
# relation on parent model is also set
|
||||
assert department.courses[0] == course
|
||||
```
|
||||
|
||||
!!!warning
|
||||
If you want to add child model on related model the primary key value for parent model **has to exist in database**.
|
||||
|
||||
Otherwise ormar will raise RelationshipInstanceError as it cannot set child's ForeignKey column value
|
||||
if parent model has no primary key value.
|
||||
|
||||
That means that in example above the department has to be saved before you can call `department.courses.add()`.
|
||||
|
||||
##### remove
|
||||
|
||||
Removal of the related model one by one.
|
||||
|
||||
In reverse relation calling `remove()` does not remove the child model, but instead nulls it ForeignKey value.
|
||||
|
||||
```python
|
||||
# continuing from above
|
||||
await department.courses.remove(course)
|
||||
assert len(department.courses) == 0
|
||||
# course still exists and was saved in remove
|
||||
assert course.pk is not None
|
||||
assert course.department is None
|
||||
|
||||
# to remove child from db
|
||||
await course.delete()
|
||||
```
|
||||
|
||||
But if you want to clear the relation and delete the child at the same time you can issue:
|
||||
|
||||
```python
|
||||
# this will not only clear the relation
|
||||
# but also delete related course from db
|
||||
await department.courses.remove(course, keep_reversed=False)
|
||||
```
|
||||
|
||||
##### clear
|
||||
|
||||
Removal of all related models in one call.
|
||||
|
||||
Like remove by default `clear()` nulls the ForeigKey column on child model (all, not matter if they are loaded or not).
|
||||
|
||||
```python
|
||||
# nulls department column on all courses related to this department
|
||||
await department.courses.clear()
|
||||
```
|
||||
|
||||
If you want to remove the children altogether from the database, set `keep_reversed=False`
|
||||
|
||||
```python
|
||||
# deletes from db all courses related to this department
|
||||
await department.courses.clear(keep_reversed=False)
|
||||
```
|
||||
|
||||
##### QuerysetProxy
|
||||
|
||||
Reverse relation exposes QuerysetProxy API that allows you to query related model like you would issue a normal Query.
|
||||
|
||||
To read which methods of QuerySet are available read below [querysetproxy][querysetproxy]
|
||||
|
||||
#### related_name
|
||||
|
||||
But you can overwrite this name by providing `related_name` parameter like below:
|
||||
@ -94,7 +171,7 @@ Sqlalchemy column and Type are automatically taken from target `Model`.
|
||||
* Sqlalchemy column: class of a target `Model` primary key column
|
||||
* Type (used for pydantic): type of a target `Model`
|
||||
|
||||
####Defining `Models`:
|
||||
####Defining `Models`
|
||||
|
||||
```Python
|
||||
--8<-- "../docs_src/relations/docs002.py"
|
||||
@ -107,7 +184,7 @@ post = await Post.objects.create(title="Hello, M2M", author=guido)
|
||||
news = await Category.objects.create(name="News")
|
||||
```
|
||||
|
||||
#### Adding related models
|
||||
#### add
|
||||
|
||||
```python
|
||||
# Add a category to a post.
|
||||
@ -121,7 +198,110 @@ await news.posts.add(post)
|
||||
|
||||
Otherwise an IntegrityError will be raised by your database driver library.
|
||||
|
||||
#### create()
|
||||
#### remove
|
||||
|
||||
Removal of the related model one by one.
|
||||
|
||||
Removes also the relation in the database.
|
||||
|
||||
```python
|
||||
await news.posts.remove(post)
|
||||
```
|
||||
|
||||
#### clear
|
||||
|
||||
Removal of all related models in one call.
|
||||
|
||||
Removes also the relation in the database.
|
||||
|
||||
```python
|
||||
await news.posts.clear()
|
||||
```
|
||||
|
||||
#### QuerysetProxy
|
||||
|
||||
Reverse relation exposes QuerysetProxy API that allows you to query related model like you would issue a normal Query.
|
||||
|
||||
To read which methods of QuerySet are available read below [querysetproxy][querysetproxy]
|
||||
|
||||
### QuerySetProxy
|
||||
|
||||
When access directly the related `ManyToMany` field as well as `ReverseForeignKey` returns the list of related models.
|
||||
|
||||
But at the same time it exposes subset of QuerySet API, so you can filter, create, select related etc related models directly from parent model.
|
||||
|
||||
!!!note
|
||||
By default exposed QuerySet is already filtered to return only `Models` related to parent `Model`.
|
||||
|
||||
So if you issue `post.categories.all()` you will get all categories related to that post, not all in table.
|
||||
|
||||
!!!note
|
||||
Note that when accessing QuerySet API methods through QuerysetProxy you don't
|
||||
need to use `objects` attribute like in normal queries.
|
||||
|
||||
So note that it's `post.categories.all()` and **not** `post.categories.objects.all()`.
|
||||
|
||||
To learn more about available QuerySet methods visit [queries][queries]
|
||||
|
||||
!!!warning
|
||||
Querying related models from ManyToMany cleans list of related models loaded on parent model:
|
||||
|
||||
Example: `post.categories.first()` will set post.categories to list of 1 related model -> the one returned by first()
|
||||
|
||||
Example 2: if post has 4 categories so `len(post.categories) == 4` calling `post.categories.limit(2).all()`
|
||||
-> will load only 2 children and now `assert len(post.categories) == 2`
|
||||
|
||||
This happens for all QuerysetProxy methods returning data: `get`, `all` and `first` and in `get_or_create` if model already exists.
|
||||
|
||||
Note that value returned by `create` or created in `get_or_create` and `update_or_create`
|
||||
if model does not exist will be added to relation list (not clearing it).
|
||||
|
||||
#### get
|
||||
|
||||
`get(**kwargs): -> Model`
|
||||
|
||||
To grab just one of related models filtered by name you can use `get(**kwargs)` method.
|
||||
|
||||
```python
|
||||
# grab one category
|
||||
assert news == await post.categories.get(name="News")
|
||||
|
||||
# note that method returns the category so you can grab this value
|
||||
# but it also modifies list of related models in place
|
||||
# so regardless of what was previously loaded on parent model
|
||||
# now it has only one value -> just loaded with get() call
|
||||
assert len(post.categories) == 1
|
||||
assert post.categories[0] == news
|
||||
|
||||
```
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [get][get]
|
||||
|
||||
#### all
|
||||
|
||||
`all(**kwargs) -> List[Optional["Model"]]`
|
||||
|
||||
To get a list of related models use `all()` method.
|
||||
|
||||
Note that you can filter the queryset, select related, exclude fields etc. like in normal query.
|
||||
|
||||
```python
|
||||
# with all Queryset methods - filtering, selecting columns, counting etc.
|
||||
await news.posts.filter(title__contains="M2M").all()
|
||||
await Category.objects.filter(posts__author=guido).get()
|
||||
|
||||
# columns models of many to many relation can be prefetched
|
||||
news_posts = await news.posts.select_related("author").all()
|
||||
assert news_posts[0].author == guido
|
||||
```
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [all][all]
|
||||
|
||||
#### create
|
||||
|
||||
`create(**kwargs): -> Model`
|
||||
|
||||
Create related `Model` directly from parent `Model`.
|
||||
|
||||
@ -134,64 +314,117 @@ assert len(await post.categories.all()) == 2
|
||||
# newly created instance already have relation persisted in the database
|
||||
```
|
||||
|
||||
!!!note
|
||||
Note that when accessing QuerySet API methods through ManyToMany relation you don't
|
||||
need to use objects attribute like in normal queries.
|
||||
|
||||
To learn more about available QuerySet methods visit [queries][queries]
|
||||
!!!tip
|
||||
Read more in queries documentation [create][create]
|
||||
|
||||
#### remove()
|
||||
|
||||
Removal of the related model one by one.
|
||||
#### get_or_create
|
||||
|
||||
Removes also the relation in the database.
|
||||
|
||||
```python
|
||||
await news.posts.remove(post)
|
||||
```
|
||||
|
||||
#### clear()
|
||||
|
||||
Removal all related models in one call.
|
||||
|
||||
Removes also the relation in the database.
|
||||
|
||||
```python
|
||||
await news.posts.clear()
|
||||
```
|
||||
|
||||
#### Other queryset methods
|
||||
|
||||
When access directly the related `ManyToMany` field returns the list of related models.
|
||||
|
||||
But at the same time it exposes full QuerySet API, so you can filter, create, select related etc.
|
||||
|
||||
```python
|
||||
# Many to many relation exposes a list of columns models
|
||||
# and an API of the Queryset:
|
||||
assert news == await post.categories.get(name="News")
|
||||
|
||||
# with all Queryset methods - filtering, selecting columns, counting etc.
|
||||
await news.posts.filter(title__contains="M2M").all()
|
||||
await Category.objects.filter(posts__author=guido).get()
|
||||
|
||||
# columns models of many to many relation can be prefetched
|
||||
news_posts = await news.posts.select_related("author").all()
|
||||
assert news_posts[0].author == guido
|
||||
```
|
||||
|
||||
Currently supported methods are:
|
||||
`get_or_create(**kwargs) -> Model`
|
||||
|
||||
!!!tip
|
||||
To learn more about available QuerySet methods visit [queries][queries]
|
||||
Read more in queries documentation [get_or_create][get_or_create]
|
||||
|
||||
##### get()
|
||||
##### all()
|
||||
##### filter()
|
||||
##### select_related()
|
||||
##### limit()
|
||||
##### offset()
|
||||
##### count()
|
||||
##### exists()
|
||||
#### update_or_create
|
||||
|
||||
[queries]: ./queries.md
|
||||
`update_or_create(**kwargs) -> Model`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [update_or_create][update_or_create]
|
||||
|
||||
#### filter
|
||||
|
||||
`filter(**kwargs) -> QuerySet`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [filter][filter]
|
||||
|
||||
#### exclude
|
||||
|
||||
`exclude(**kwargs) -> QuerySet`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [exclude][exclude]
|
||||
|
||||
#### select_related
|
||||
|
||||
`select_related(related: Union[List, str]) -> QuerySet`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [select_related][select_related]
|
||||
|
||||
#### prefetch_related
|
||||
|
||||
`prefetch_related(related: Union[List, str]) -> QuerySet`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [prefetch_related][prefetch_related]
|
||||
|
||||
#### limit
|
||||
|
||||
`limit(limit_count: int) -> QuerySet`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [limit][limit]
|
||||
|
||||
#### offset
|
||||
|
||||
`offset(offset: int) -> QuerySet`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [offset][offset]
|
||||
|
||||
#### count
|
||||
|
||||
`count() -> int`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [count][count]
|
||||
|
||||
#### exists
|
||||
|
||||
`exists() -> bool`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [exists][exists]
|
||||
|
||||
#### fields
|
||||
|
||||
`fields(columns: Union[List, str, set, dict]) -> QuerySet`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [fields][fields]
|
||||
|
||||
#### exclude_fields
|
||||
|
||||
`exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [exclude_fields][exclude_fields]
|
||||
|
||||
#### order_by
|
||||
|
||||
`order_by(columns:Union[List, str]) -> QuerySet`
|
||||
|
||||
!!!tip
|
||||
Read more in queries documentation [order_by][order_by]
|
||||
|
||||
|
||||
[queries]: ./queries.md
|
||||
[querysetproxy]: ./relations.md#querysetproxy-methods
|
||||
[get]: ./queries.md#get
|
||||
[all]: ./queries.md#all
|
||||
[create]: ./queries.md#create
|
||||
[get_or_create]: ./queries.md#get_or_create
|
||||
[update_or_create]: ./queries.md#update_or_create
|
||||
[filter]: ./queries.md#filter
|
||||
[exclude]: ./queries.md#exclude
|
||||
[select_related]: ./queries.md#select_related
|
||||
[prefetch_related]: ./queries.md#prefetch_related
|
||||
[limit]: ./queries.md#limit
|
||||
[offset]: ./queries.md#offset
|
||||
[count]: ./queries.md#count
|
||||
[exists]: ./queries.md#exists
|
||||
[fields]: ./queries.md#fields
|
||||
[exclude_fields]: ./queries.md#exclude_fields
|
||||
[order_by]: ./queries.md#order_by
|
||||
@ -1,3 +1,15 @@
|
||||
# 0.6.0
|
||||
|
||||
* **Breaking:** calling instance.load() when the instance row was deleted from db now raises ormar.NoMatch instead of ValueError
|
||||
* **Breaking:** calling add and remove on ReverseForeignKey relation now updates the child model in db setting/removing fk column
|
||||
* **Breaking:** ReverseForeignKey relation now exposes QuerySetProxy API like ManyToMany relation
|
||||
* **Breaking:** querying related models from ManyToMany cleans list of related models loaded on parent model:
|
||||
* Example: `post.categories.first()` will set post.categories to list of 1 related model -> the one returned by first()
|
||||
* Example 2: if post has 4 categories so `len(post.categories) == 4` calling `post.categories.limit(2).all()` -> will load only 2 children and now `assert len(post.categories) == 2`
|
||||
* Added `get_or_create`, `update_or_create`, `fields`, `exclude_fields`, `exclude`, `prefetch_related` and `order_by` to QuerySetProxy
|
||||
so now you can use those methods directly from relation
|
||||
* Update docs
|
||||
|
||||
# 0.5.5
|
||||
|
||||
* Fix for alembic autogenaration of migration `UUID` columns. It should just produce sqlalchemy `CHAR(32)` or `CHAR(36)`
|
||||
|
||||
@ -29,7 +29,7 @@ class Course(ormar.Model):
|
||||
department: Optional[Department] = ormar.ForeignKey(Department)
|
||||
|
||||
|
||||
department = Department(name="Science")
|
||||
department = await Department(name="Science").save()
|
||||
course = Course(name="Math", completed=False, department=department)
|
||||
|
||||
print(department.courses[0])
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
site_name: ormar
|
||||
site_description: An simple async ORM with fastapi in mind and pydantic validation.
|
||||
site_description: A simple async ORM with fastapi in mind and pydantic validation.
|
||||
nav:
|
||||
- Overview: index.md
|
||||
- Installation: install.md
|
||||
|
||||
@ -30,7 +30,7 @@ class UndefinedType: # pragma no cover
|
||||
|
||||
Undefined = UndefinedType()
|
||||
|
||||
__version__ = "0.5.5"
|
||||
__version__ = "0.6.0"
|
||||
__all__ = [
|
||||
"Integer",
|
||||
"BigInteger",
|
||||
|
||||
@ -15,7 +15,7 @@ from typing import (
|
||||
import sqlalchemy
|
||||
|
||||
import ormar.queryset # noqa I100
|
||||
from ormar.exceptions import ModelPersistenceError
|
||||
from ormar.exceptions import ModelPersistenceError, NoMatch
|
||||
from ormar.fields.many_to_many import ManyToManyField
|
||||
from ormar.models import NewBaseModel # noqa I100
|
||||
from ormar.models.metaclass import ModelMeta
|
||||
@ -286,9 +286,7 @@ class Model(NewBaseModel):
|
||||
expr = self.Meta.table.select().where(self.pk_column == self.pk)
|
||||
row = await self.Meta.database.fetch_one(expr)
|
||||
if not row: # pragma nocover
|
||||
raise ValueError(
|
||||
"Instance was deleted from database and cannot be refreshed"
|
||||
)
|
||||
raise NoMatch("Instance was deleted from database and cannot be refreshed")
|
||||
kwargs = dict(row)
|
||||
kwargs = self.translate_aliases_to_columns(kwargs)
|
||||
self.from_dict(kwargs)
|
||||
|
||||
@ -280,7 +280,9 @@ class QuerySet:
|
||||
return await self.database.fetch_val(expr)
|
||||
|
||||
async def update(self, each: bool = False, **kwargs: Any) -> int:
|
||||
self_fields = self.model.extract_db_own_fields()
|
||||
self_fields = self.model.extract_db_own_fields().union(
|
||||
self.model.extract_related_names()
|
||||
)
|
||||
updates = {k: v for k, v in kwargs.items() if k in self_fields}
|
||||
updates = self.model.translate_columns_to_aliases(updates)
|
||||
if not each and not self.filter_clauses:
|
||||
|
||||
@ -88,7 +88,7 @@ class QuerysetProxy(ormar.QuerySetProtocol):
|
||||
async def count(self) -> int:
|
||||
return await self.queryset.count()
|
||||
|
||||
async def clear(self) -> int:
|
||||
async def clear(self, keep_reversed: bool = True) -> int:
|
||||
if self.type_ == ormar.RelationType.MULTIPLE:
|
||||
queryset = ormar.QuerySet(model_cls=self.relation.through)
|
||||
owner_column = self._owner.get_name()
|
||||
@ -97,10 +97,16 @@ class QuerysetProxy(ormar.QuerySetProtocol):
|
||||
owner_column = self.related_field.name
|
||||
kwargs = {owner_column: self._owner}
|
||||
self._clean_items_on_load()
|
||||
if keep_reversed and self.type_ == ormar.RelationType.REVERSE:
|
||||
update_kwrgs = {f"{owner_column}": None}
|
||||
return await queryset.filter(_exclude=False, **kwargs).update(
|
||||
each=False, **update_kwrgs
|
||||
)
|
||||
return await queryset.delete(**kwargs) # type: ignore
|
||||
|
||||
async def first(self, **kwargs: Any) -> "Model":
|
||||
first = await self.queryset.first(**kwargs)
|
||||
self._clean_items_on_load()
|
||||
self._register_related(first)
|
||||
return first
|
||||
|
||||
|
||||
@ -38,17 +38,20 @@ class RelationProxy(list):
|
||||
and self.queryset_proxy.queryset is not None
|
||||
)
|
||||
|
||||
def _set_queryset(self) -> "QuerySet":
|
||||
related_field = self._owner.resolve_relation_field(
|
||||
self.relation.to, self._owner
|
||||
)
|
||||
pkname = self._owner.get_column_alias(self._owner.Meta.pkname)
|
||||
def _check_if_model_saved(self) -> None:
|
||||
pk_value = self._owner.pk
|
||||
if not pk_value:
|
||||
raise RelationshipInstanceError(
|
||||
"You cannot query relationships from unsaved model."
|
||||
)
|
||||
kwargs = {f"{related_field.get_alias()}__{pkname}": pk_value}
|
||||
|
||||
def _set_queryset(self) -> "QuerySet":
|
||||
related_field = self._owner.resolve_relation_field(
|
||||
self.relation.to, self._owner
|
||||
)
|
||||
pkname = self._owner.get_column_alias(self._owner.Meta.pkname)
|
||||
self._check_if_model_saved()
|
||||
kwargs = {f"{related_field.get_alias()}__{pkname}": self._owner.pk}
|
||||
queryset = (
|
||||
ormar.QuerySet(model_cls=self.relation.to)
|
||||
.select_related(related_field.name)
|
||||
@ -56,7 +59,9 @@ class RelationProxy(list):
|
||||
)
|
||||
return queryset
|
||||
|
||||
async def remove(self, item: "Model") -> None: # type: ignore
|
||||
async def remove( # type: ignore
|
||||
self, item: "Model", keep_reversed: bool = True
|
||||
) -> None:
|
||||
if item not in self:
|
||||
raise NoMatch(
|
||||
f"Object {self._owner.get_name()} has no "
|
||||
@ -74,8 +79,11 @@ class RelationProxy(list):
|
||||
if self.type_ == ormar.RelationType.MULTIPLE:
|
||||
await self.queryset_proxy.delete_through_instance(item)
|
||||
else:
|
||||
setattr(item, rel_name, None)
|
||||
await item.update()
|
||||
if keep_reversed:
|
||||
setattr(item, rel_name, None)
|
||||
await item.update()
|
||||
else:
|
||||
await item.delete()
|
||||
|
||||
async def add(self, item: "Model") -> None:
|
||||
if self.type_ == ormar.RelationType.MULTIPLE:
|
||||
@ -83,6 +91,7 @@ class RelationProxy(list):
|
||||
rel_name = item.resolve_relation_name(item, self._owner)
|
||||
setattr(item, rel_name, self._owner)
|
||||
else:
|
||||
self._check_if_model_saved()
|
||||
related_field = self._owner.resolve_relation_field(
|
||||
self.relation.to, self._owner
|
||||
)
|
||||
|
||||
@ -5,6 +5,7 @@ import pytest
|
||||
import sqlalchemy
|
||||
|
||||
import ormar
|
||||
from ormar import NoMatch
|
||||
from tests.settings import DATABASE_URL
|
||||
|
||||
database = databases.Database(DATABASE_URL, force_rollback=True)
|
||||
@ -190,6 +191,26 @@ async def test_getting():
|
||||
assert len(tracks) == 0
|
||||
assert len(album.tracks) == 0
|
||||
|
||||
still_tracks = await Track.objects.all()
|
||||
assert len(still_tracks) == 4
|
||||
for track in still_tracks:
|
||||
assert track.album is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleaning_related():
|
||||
async with database:
|
||||
async with database.transaction(force_rollback=True):
|
||||
sample_data = await get_sample_data()
|
||||
album = sample_data[0]
|
||||
await album.tracks.clear(keep_reversed=False)
|
||||
tracks = await album.tracks.all()
|
||||
assert len(tracks) == 0
|
||||
assert len(album.tracks) == 0
|
||||
|
||||
no_tracks = await Track.objects.all()
|
||||
assert len(no_tracks) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_loading_related():
|
||||
@ -224,12 +245,14 @@ async def test_adding_removing():
|
||||
track_check = await Track.objects.get(title="Rainbow")
|
||||
assert track_check.album == album
|
||||
|
||||
track_test = await Track.objects.get(title="Rainbow")
|
||||
assert track_test.album == album
|
||||
|
||||
await album.tracks.remove(track_new)
|
||||
assert track_new.album is None
|
||||
assert len(album.tracks) == 3
|
||||
|
||||
track1 = album.tracks[0]
|
||||
await album.tracks.remove(track1, keep_reversed=False)
|
||||
with pytest.raises(NoMatch):
|
||||
await track1.load()
|
||||
|
||||
track_test = await Track.objects.get(title="Rainbow")
|
||||
assert track_test.album is None
|
||||
|
||||
Reference in New Issue
Block a user