218 lines
7.9 KiB
Python
218 lines
7.9 KiB
Python
from typing import TYPE_CHECKING, Type
|
|
|
|
import ormar
|
|
from ormar import ForeignKey, ManyToMany
|
|
from ormar.fields import ManyToManyField
|
|
from ormar.fields.foreign_key import ForeignKeyField
|
|
from ormar.models.helpers.sqlalchemy import adjust_through_many_to_many_model
|
|
from ormar.relations import AliasManager
|
|
|
|
if TYPE_CHECKING: # pragma no cover
|
|
from ormar import Model
|
|
|
|
alias_manager = AliasManager()
|
|
|
|
|
|
def register_relation_on_build(new_model: Type["Model"], field_name: str) -> None:
|
|
"""
|
|
Registers ForeignKey relation in alias_manager to set a table_prefix.
|
|
Registration include also reverse relation side to be able to join both sides.
|
|
|
|
Relation is registered by model name and relation field name to allow for multiple
|
|
relations between two Models that needs to have different
|
|
aliases for proper sql joins.
|
|
|
|
:param new_model: constructed model
|
|
:type new_model: Model class
|
|
:param field_name: name of the related field
|
|
:type field_name: str
|
|
"""
|
|
alias_manager.add_relation_type(new_model, field_name)
|
|
|
|
|
|
def register_many_to_many_relation_on_build(
|
|
new_model: Type["Model"], field: Type[ManyToManyField], field_name: str
|
|
) -> None:
|
|
"""
|
|
Registers connection between through model and both sides of the m2m relation.
|
|
Registration include also reverse relation side to be able to join both sides.
|
|
|
|
Relation is registered by model name and relation field name to allow for multiple
|
|
relations between two Models that needs to have different
|
|
aliases for proper sql joins.
|
|
|
|
By default relation name is a model.name.lower().
|
|
|
|
:param field_name: name of the relation key
|
|
:type field_name: str
|
|
:param new_model: model on which m2m field is declared
|
|
:type new_model: Model class
|
|
:param field: relation field
|
|
:type field: ManyToManyField class
|
|
"""
|
|
alias_manager.add_relation_type(
|
|
field.through, new_model.get_name(), is_multi=True, reverse_name=field_name
|
|
)
|
|
alias_manager.add_relation_type(
|
|
field.through,
|
|
field.to.get_name(),
|
|
is_multi=True,
|
|
reverse_name=field.related_name or new_model.get_name() + "s",
|
|
)
|
|
|
|
|
|
def expand_reverse_relationships(model: Type["Model"]) -> None:
|
|
"""
|
|
Iterates through model_fields of given model and verifies if all reverse
|
|
relation have been populated on related models.
|
|
|
|
If the reverse relation has not been set before it's set here.
|
|
|
|
:param model: model on which relation should be checked and registered
|
|
:type model: Model class
|
|
"""
|
|
for model_field in model.Meta.model_fields.values():
|
|
if issubclass(model_field, ForeignKeyField):
|
|
child_model_name = model_field.related_name or model.get_name() + "s"
|
|
parent_model = model_field.to
|
|
child = model
|
|
if reverse_field_not_already_registered(
|
|
child, child_model_name, parent_model
|
|
):
|
|
register_reverse_model_fields(
|
|
parent_model, child, child_model_name, model_field
|
|
)
|
|
|
|
|
|
def register_reverse_model_fields(
|
|
model: Type["Model"],
|
|
child: Type["Model"],
|
|
related_name: str,
|
|
model_field: Type["ForeignKeyField"],
|
|
) -> None:
|
|
"""
|
|
Registers reverse ForeignKey field on related model.
|
|
By default it's name.lower()+'s' of the model on which relation is defined.
|
|
|
|
But if the related_model name is provided it's registered with that name.
|
|
Autogenerated reverse fields also set related_name to the original field name.
|
|
|
|
:param model: related model on which reverse field should be defined
|
|
:type model: Model class
|
|
:param child: parent model with relation definition
|
|
:type child: Model class
|
|
:param related_name: name by which reverse key should be registered
|
|
:type related_name: str
|
|
:param model_field: original relation ForeignKey field
|
|
:type model_field: relation Field
|
|
"""
|
|
if issubclass(model_field, ManyToManyField):
|
|
model.Meta.model_fields[related_name] = ManyToMany(
|
|
child,
|
|
through=model_field.through,
|
|
name=related_name,
|
|
virtual=True,
|
|
related_name=model_field.name,
|
|
)
|
|
# register foreign keys on through model
|
|
adjust_through_many_to_many_model(model, child, model_field)
|
|
else:
|
|
model.Meta.model_fields[related_name] = ForeignKey(
|
|
child, real_name=related_name, virtual=True, related_name=model_field.name,
|
|
)
|
|
|
|
|
|
def register_relation_in_alias_manager(
|
|
new_model: Type["Model"], field: Type[ForeignKeyField], field_name: str
|
|
) -> None:
|
|
"""
|
|
Registers the relation (and reverse relation) in alias manager.
|
|
The m2m relations require registration of through model between
|
|
actual end models of the relation.
|
|
|
|
Delegates the actual registration to:
|
|
m2m - register_many_to_many_relation_on_build
|
|
fk - register_relation_on_build
|
|
|
|
:param new_model: model on which relation field is declared
|
|
:type new_model: Model class
|
|
:param field: relation field
|
|
:type field: ForeignKey or ManyToManyField class
|
|
:param field_name: name of the relation key
|
|
:type field_name: str
|
|
"""
|
|
if issubclass(field, ManyToManyField):
|
|
register_many_to_many_relation_on_build(
|
|
new_model=new_model, field=field, field_name=field_name
|
|
)
|
|
elif issubclass(field, ForeignKeyField):
|
|
register_relation_on_build(new_model=new_model, field_name=field_name)
|
|
|
|
|
|
def verify_related_name_dont_duplicate(
|
|
child: Type["Model"], parent_model: Type["Model"], related_name: str,
|
|
) -> None:
|
|
"""
|
|
Verifies whether the used related_name (regardless of the fact if user defined or
|
|
auto generated) is already used on related model, but is connected with other model
|
|
than the one that we connect right now.
|
|
|
|
:raises ModelDefinitionError: if name is already used but lead to different related
|
|
model
|
|
:param child: related Model class
|
|
:type child: ormar.models.metaclass.ModelMetaclass
|
|
:param parent_model: parent Model class
|
|
:type parent_model: ormar.models.metaclass.ModelMetaclass
|
|
:param related_name:
|
|
:type related_name:
|
|
:return: None
|
|
:rtype: None
|
|
"""
|
|
if parent_model.Meta.model_fields.get(related_name):
|
|
fk_field = parent_model.Meta.model_fields.get(related_name)
|
|
if not fk_field: # pragma: no cover
|
|
return
|
|
if fk_field.to != child and fk_field.to.Meta != child.Meta:
|
|
raise ormar.ModelDefinitionError(
|
|
f"Relation with related_name "
|
|
f"'{related_name}' "
|
|
f"leading to model "
|
|
f"{parent_model.get_name(lower=False)} "
|
|
f"cannot be used on model "
|
|
f"{child.get_name(lower=False)} "
|
|
f"because it's already used by model "
|
|
f"{fk_field.to.get_name(lower=False)}"
|
|
)
|
|
|
|
|
|
def reverse_field_not_already_registered(
|
|
child: Type["Model"], child_model_name: str, parent_model: Type["Model"]
|
|
) -> bool:
|
|
"""
|
|
Checks if child is already registered in parents pydantic fields.
|
|
|
|
:raises ModelDefinitionError: if related name is already used but lead to different
|
|
related model
|
|
:param child: related Model class
|
|
:type child: ormar.models.metaclass.ModelMetaclass
|
|
:param child_model_name: related_name of the child if provided
|
|
:type child_model_name: str
|
|
:param parent_model: parent Model class
|
|
:type parent_model: ormar.models.metaclass.ModelMetaclass
|
|
:return: result of the check
|
|
:rtype: bool
|
|
"""
|
|
check_result = child_model_name not in parent_model.Meta.model_fields
|
|
check_result2 = child.get_name() not in parent_model.Meta.model_fields
|
|
|
|
if not check_result:
|
|
verify_related_name_dont_duplicate(
|
|
child=child, parent_model=parent_model, related_name=child_model_name
|
|
)
|
|
if not check_result2:
|
|
verify_related_name_dont_duplicate(
|
|
child=child, parent_model=parent_model, related_name=child.get_name()
|
|
)
|
|
|
|
return check_result and check_result2
|