192 lines
6.6 KiB
Python
192 lines
6.6 KiB
Python
from typing import Any, Optional, TYPE_CHECKING
|
|
|
|
import ormar
|
|
from ormar.exceptions import NoMatch, RelationshipInstanceError
|
|
from ormar.relations.querysetproxy import QuerysetProxy
|
|
|
|
if TYPE_CHECKING: # pragma no cover
|
|
from ormar import Model, RelationType
|
|
from ormar.relations import Relation
|
|
from ormar.queryset import QuerySet
|
|
|
|
|
|
class RelationProxy(list):
|
|
"""
|
|
Proxy of the Relation that is a list with special methods.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
relation: "Relation",
|
|
type_: "RelationType",
|
|
field_name: str,
|
|
data_: Any = None,
|
|
) -> None:
|
|
super().__init__(data_ or ())
|
|
self.relation: "Relation" = relation
|
|
self.type_: "RelationType" = type_
|
|
self.field_name = field_name
|
|
self._owner: "Model" = self.relation.manager.owner
|
|
self.queryset_proxy: QuerysetProxy = QuerysetProxy(
|
|
relation=self.relation, type_=type_
|
|
)
|
|
self._related_field_name: Optional[str] = None
|
|
|
|
@property
|
|
def related_field_name(self) -> str:
|
|
"""
|
|
On first access calculates the name of the related field, later stored in
|
|
_related_field_name property.
|
|
|
|
:return: name of the related field
|
|
:rtype: str
|
|
"""
|
|
if self._related_field_name:
|
|
return self._related_field_name
|
|
owner_field = self._owner.Meta.model_fields[self.field_name]
|
|
self._related_field_name = owner_field.get_related_name()
|
|
|
|
return self._related_field_name
|
|
|
|
def __getattribute__(self, item: str) -> Any:
|
|
"""
|
|
Since some QuerySetProxy methods overwrite builtin list methods we
|
|
catch calls to them and delegate it to QuerySetProxy instead.
|
|
|
|
:param item: name of attribute
|
|
:type item: str
|
|
:return: value of attribute
|
|
:rtype: Any
|
|
"""
|
|
if item in ["count", "clear"]:
|
|
self._initialize_queryset()
|
|
return getattr(self.queryset_proxy, item)
|
|
return super().__getattribute__(item)
|
|
|
|
def __getattr__(self, item: str) -> Any:
|
|
"""
|
|
Delegates calls for non existing attributes to QuerySetProxy.
|
|
|
|
:param item: name of attribute/method
|
|
:type item: str
|
|
:return: method from QuerySetProxy if exists
|
|
:rtype: method
|
|
"""
|
|
self._initialize_queryset()
|
|
return getattr(self.queryset_proxy, item)
|
|
|
|
def _clear(self) -> None:
|
|
super().clear()
|
|
|
|
def _initialize_queryset(self) -> None:
|
|
"""
|
|
Initializes the QuerySetProxy if not yet initialized.
|
|
"""
|
|
if not self._check_if_queryset_is_initialized():
|
|
self.queryset_proxy.queryset = self._set_queryset()
|
|
|
|
def _check_if_queryset_is_initialized(self) -> bool:
|
|
"""
|
|
Checks if the QuerySetProxy is already set and ready.
|
|
:return: result of the check
|
|
:rtype: bool
|
|
"""
|
|
return (
|
|
hasattr(self.queryset_proxy, "queryset")
|
|
and self.queryset_proxy.queryset is not None
|
|
)
|
|
|
|
def _check_if_model_saved(self) -> None:
|
|
"""
|
|
Verifies if the parent model of the relation has been already saved.
|
|
Otherwise QuerySetProxy cannot filter by parent primary key.
|
|
"""
|
|
pk_value = self._owner.pk
|
|
if not pk_value:
|
|
raise RelationshipInstanceError(
|
|
"You cannot query relationships from unsaved model."
|
|
)
|
|
|
|
def _set_queryset(self) -> "QuerySet":
|
|
"""
|
|
Creates new QuerySet with relation model and pre filters it with currents
|
|
parent model primary key, so all queries by definition are already related
|
|
to the parent model only, without need for user to filter them.
|
|
|
|
:return: initialized QuerySet
|
|
:rtype: QuerySet
|
|
"""
|
|
related_field_name = self.related_field_name
|
|
related_field = self.relation.to.Meta.model_fields[related_field_name]
|
|
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, proxy_source_model=self._owner.__class__
|
|
)
|
|
.select_related(related_field.name)
|
|
.filter(**kwargs)
|
|
)
|
|
return queryset
|
|
|
|
async def remove( # type: ignore
|
|
self, item: "Model", keep_reversed: bool = True
|
|
) -> None:
|
|
"""
|
|
Removes the related from relation with parent.
|
|
|
|
Through models are automatically deleted for m2m relations.
|
|
|
|
For reverse FK relations keep_reversed flag marks if the reversed models
|
|
should be kept or deleted from the database too (False means that models
|
|
will be deleted, and not only removed from relation).
|
|
|
|
:param item: child to remove from relation
|
|
:type item: Model
|
|
:param keep_reversed: flag if the reversed model should be kept or deleted too
|
|
:type keep_reversed: bool
|
|
"""
|
|
if item not in self:
|
|
raise NoMatch(
|
|
f"Object {self._owner.get_name()} has no "
|
|
f"{item.get_name()} with given primary key!"
|
|
)
|
|
super().remove(item)
|
|
relation_name = self.related_field_name
|
|
relation = item._orm._get(relation_name)
|
|
if relation is None: # pragma nocover
|
|
raise ValueError(
|
|
f"{self._owner.get_name()} does not have relation {relation_name}"
|
|
)
|
|
relation.remove(self._owner)
|
|
self.relation.remove(item)
|
|
if self.type_ == ormar.RelationType.MULTIPLE:
|
|
await self.queryset_proxy.delete_through_instance(item)
|
|
else:
|
|
if keep_reversed:
|
|
setattr(item, relation_name, None)
|
|
await item.update()
|
|
else:
|
|
await item.delete()
|
|
|
|
async def add(self, item: "Model", **kwargs: Any) -> None:
|
|
"""
|
|
Adds child model to relation.
|
|
|
|
For ManyToMany relations through instance is automatically created.
|
|
|
|
:param kwargs: dict of additional keyword arguments for through instance
|
|
:type kwargs: Any
|
|
:param item: child to add to relation
|
|
:type item: Model
|
|
"""
|
|
relation_name = self.related_field_name
|
|
self._check_if_model_saved()
|
|
if self.type_ == ormar.RelationType.MULTIPLE:
|
|
await self.queryset_proxy.create_through_instance(item, **kwargs)
|
|
setattr(item, relation_name, self._owner)
|
|
else:
|
|
setattr(item, relation_name, self._owner)
|
|
await item.update()
|