2017-06-01 12:18:02 +03:00
|
|
|
"""
|
|
|
|
|
PolymorphicQuerySet support functions
|
2010-02-19 19:12:21 +03:00
|
|
|
"""
|
2024-03-12 23:23:19 +03:00
|
|
|
|
2016-08-05 18:26:13 +03:00
|
|
|
import copy
|
2026-01-12 09:48:39 +03:00
|
|
|
from functools import reduce
|
|
|
|
|
from operator import or_
|
2017-05-19 09:58:34 +03:00
|
|
|
|
2017-11-20 17:18:43 +03:00
|
|
|
from django.apps import apps
|
2010-02-19 19:12:21 +03:00
|
|
|
from django.contrib.contenttypes.models import ContentType
|
2026-01-12 22:35:46 +03:00
|
|
|
from django.core.exceptions import FieldDoesNotExist, FieldError
|
2017-05-19 09:12:33 +03:00
|
|
|
from django.db import models
|
2026-01-12 09:48:39 +03:00
|
|
|
from django.db.models import Q, Subquery
|
2017-05-19 09:12:33 +03:00
|
|
|
from django.db.models.fields.related import ForeignObjectRel, RelatedField
|
2016-05-28 04:35:39 +03:00
|
|
|
from django.db.utils import DEFAULT_DB_ALIAS
|
2015-01-28 02:41:34 +03:00
|
|
|
|
2026-01-12 21:16:50 +03:00
|
|
|
from .utils import _lazy_ctype, _map_queryname_to_class, concrete_descendants
|
2026-01-12 09:48:39 +03:00
|
|
|
|
2010-02-19 19:12:21 +03:00
|
|
|
# These functions implement the additional filter- and Q-object functionality.
|
|
|
|
|
# They form a kind of small framework for easily adding more
|
|
|
|
|
# functionality to filters and Q objects.
|
|
|
|
|
# Probably a more general queryset enhancement class could be made out of them.
|
2019-07-12 18:38:49 +03:00
|
|
|
|
2019-07-15 10:50:03 +03:00
|
|
|
###################################################################################
|
|
|
|
|
# PolymorphicQuerySet support functions
|
|
|
|
|
|
2010-02-19 19:12:21 +03:00
|
|
|
|
2019-07-15 10:50:03 +03:00
|
|
|
def translate_polymorphic_filter_definitions_in_kwargs(
|
|
|
|
|
queryset_model, kwargs, using=DEFAULT_DB_ALIAS
|
|
|
|
|
):
|
2010-02-19 19:12:21 +03:00
|
|
|
"""
|
|
|
|
|
Translate the keyword argument list for PolymorphicQuerySet.filter()
|
|
|
|
|
|
|
|
|
|
Any kwargs with special polymorphic functionality are replaced in the kwargs
|
|
|
|
|
dict with their vanilla django equivalents.
|
|
|
|
|
|
|
|
|
|
For some kwargs a direct replacement is not possible, as a Q object is needed
|
|
|
|
|
instead to implement the required functionality. In these cases the kwarg is
|
|
|
|
|
deleted from the kwargs dict and a Q object is added to the return list.
|
|
|
|
|
|
|
|
|
|
Modifies: kwargs dict
|
|
|
|
|
Returns: a list of non-keyword-arguments (Q objects) to be added to the filter() query.
|
|
|
|
|
"""
|
|
|
|
|
additional_args = []
|
2023-12-20 13:58:00 +03:00
|
|
|
for field_path, val in kwargs.copy().items(): # `copy` so we're not mutating the dict
|
2019-07-15 10:50:03 +03:00
|
|
|
new_expr = _translate_polymorphic_filter_definition(
|
|
|
|
|
queryset_model, field_path, val, using=using
|
|
|
|
|
)
|
2010-02-19 19:12:21 +03:00
|
|
|
|
2021-11-18 14:04:07 +03:00
|
|
|
if isinstance(new_expr, tuple):
|
2010-02-19 19:12:21 +03:00
|
|
|
# replace kwargs element
|
2019-07-15 10:50:03 +03:00
|
|
|
del kwargs[field_path]
|
2010-02-19 19:12:21 +03:00
|
|
|
kwargs[new_expr[0]] = new_expr[1]
|
|
|
|
|
|
|
|
|
|
elif isinstance(new_expr, models.Q):
|
2019-07-15 10:50:03 +03:00
|
|
|
del kwargs[field_path]
|
2010-02-19 19:12:21 +03:00
|
|
|
additional_args.append(new_expr)
|
|
|
|
|
|
|
|
|
|
return additional_args
|
|
|
|
|
|
2011-11-26 10:22:18 +04:00
|
|
|
|
2021-11-18 14:40:39 +03:00
|
|
|
def translate_polymorphic_Q_object(queryset_model, potential_q_object, using=DEFAULT_DB_ALIAS):
|
2010-10-17 06:22:15 +04:00
|
|
|
def tree_node_correct_field_specs(my_model, node):
|
2021-11-18 14:40:39 +03:00
|
|
|
"process all children of this Q node"
|
2023-05-11 02:56:17 +03:00
|
|
|
cpy = copy.copy(node)
|
|
|
|
|
cpy.children = []
|
|
|
|
|
for child in node.children:
|
2021-11-18 14:04:07 +03:00
|
|
|
if isinstance(child, (tuple, list)):
|
2010-10-17 06:22:15 +04:00
|
|
|
# this Q object child is a tuple => a kwarg like Q( instance_of=ModelB )
|
|
|
|
|
key, val = child
|
2019-07-15 10:50:03 +03:00
|
|
|
new_expr = _translate_polymorphic_filter_definition(
|
|
|
|
|
my_model, key, val, using=using
|
|
|
|
|
)
|
2023-05-11 02:56:17 +03:00
|
|
|
cpy.children.append(new_expr or child)
|
2024-01-31 06:30:26 +03:00
|
|
|
elif isinstance(child, models.Q):
|
|
|
|
|
# this Q object child is another Q object, recursively process
|
2023-05-11 02:56:17 +03:00
|
|
|
cpy.children.append(tree_node_correct_field_specs(my_model, child))
|
|
|
|
|
else:
|
|
|
|
|
cpy.children.append(child)
|
|
|
|
|
return cpy
|
2010-10-17 06:22:15 +04:00
|
|
|
|
|
|
|
|
if isinstance(potential_q_object, models.Q):
|
2023-05-11 02:56:17 +03:00
|
|
|
return tree_node_correct_field_specs(queryset_model, potential_q_object)
|
2010-10-17 06:22:15 +04:00
|
|
|
|
|
|
|
|
return potential_q_object
|
|
|
|
|
|
2011-11-26 10:22:18 +04:00
|
|
|
|
2021-11-18 14:40:39 +03:00
|
|
|
def translate_polymorphic_filter_definitions_in_args(queryset_model, args, using=DEFAULT_DB_ALIAS):
|
2010-02-19 19:12:21 +03:00
|
|
|
"""
|
|
|
|
|
Translate the non-keyword argument list for PolymorphicQuerySet.filter()
|
|
|
|
|
|
2016-06-02 08:08:47 +03:00
|
|
|
In the args list, we return all kwargs to Q-objects that contain special
|
2010-02-19 19:12:21 +03:00
|
|
|
polymorphic functionality with their vanilla django equivalents.
|
|
|
|
|
We traverse the Q object tree for this (which is simple).
|
|
|
|
|
|
|
|
|
|
|
2016-06-02 08:08:47 +03:00
|
|
|
Returns: modified Q objects
|
2010-02-19 19:12:21 +03:00
|
|
|
"""
|
2023-05-11 02:56:17 +03:00
|
|
|
|
|
|
|
|
return [translate_polymorphic_Q_object(queryset_model, q, using=using) for q in args]
|
2010-02-19 19:12:21 +03:00
|
|
|
|
|
|
|
|
|
2019-07-15 10:50:03 +03:00
|
|
|
def _translate_polymorphic_filter_definition(
|
|
|
|
|
queryset_model, field_path, field_val, using=DEFAULT_DB_ALIAS
|
|
|
|
|
):
|
2010-02-19 19:12:21 +03:00
|
|
|
"""
|
|
|
|
|
Translate a keyword argument (field_path=field_val), as used for
|
|
|
|
|
PolymorphicQuerySet.filter()-like functions (and Q objects).
|
|
|
|
|
|
|
|
|
|
A kwarg with special polymorphic functionality is translated into
|
|
|
|
|
its vanilla django equivalent, which is returned, either as tuple
|
|
|
|
|
(field_path, field_val) or as Q object.
|
|
|
|
|
|
|
|
|
|
Returns: kwarg tuple or Q object or None (if no change is required)
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# handle instance_of expressions or alternatively,
|
|
|
|
|
# if this is a normal Django filter expression, return None
|
2019-07-15 10:50:03 +03:00
|
|
|
if field_path == "instance_of":
|
2018-08-24 12:16:30 +03:00
|
|
|
return create_instanceof_q(field_val, using=using)
|
2019-07-15 10:50:03 +03:00
|
|
|
elif field_path == "not_instance_of":
|
2018-08-24 12:16:30 +03:00
|
|
|
return create_instanceof_q(field_val, not_instance_of=True, using=using)
|
2019-07-15 10:50:03 +03:00
|
|
|
elif "___" not in field_path:
|
2011-11-26 10:22:18 +04:00
|
|
|
return None # no change
|
2010-02-19 19:12:21 +03:00
|
|
|
|
|
|
|
|
# filter expression contains '___' (i.e. filter for polymorphic field)
|
|
|
|
|
# => get the model class specified in the filter expression
|
|
|
|
|
newpath = translate_polymorphic_field_path(queryset_model, field_path)
|
|
|
|
|
return (newpath, field_val)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def translate_polymorphic_field_path(queryset_model, field_path):
|
|
|
|
|
"""
|
|
|
|
|
Translate a field path from a keyword argument, as used for
|
|
|
|
|
PolymorphicQuerySet.filter()-like functions (and Q objects).
|
|
|
|
|
Supports leading '-' (for order_by args).
|
|
|
|
|
|
|
|
|
|
E.g.: if queryset_model is ModelA, then "ModelC___field3" is translated
|
|
|
|
|
into modela__modelb__modelc__field3.
|
|
|
|
|
Returns: translated path (unchanged, if no translation needed)
|
|
|
|
|
"""
|
2019-07-15 10:50:03 +03:00
|
|
|
classname, sep, pure_field_path = field_path.partition("___")
|
2026-01-12 22:35:46 +03:00
|
|
|
if not sep or not classname:
|
2011-05-19 17:07:25 +04:00
|
|
|
return field_path
|
2010-02-19 19:12:21 +03:00
|
|
|
|
|
|
|
|
negated = False
|
2019-07-15 10:50:03 +03:00
|
|
|
if classname[0] == "-":
|
2010-02-19 19:12:21 +03:00
|
|
|
negated = True
|
2019-07-15 10:50:03 +03:00
|
|
|
classname = classname.lstrip("-")
|
2010-02-19 19:12:21 +03:00
|
|
|
|
2019-07-15 10:50:03 +03:00
|
|
|
if "__" in classname:
|
2010-02-19 19:12:21 +03:00
|
|
|
# the user has app label prepended to class name via __ => use Django's get_model function
|
2019-07-15 10:50:03 +03:00
|
|
|
appname, sep, classname = classname.partition("__")
|
2026-01-12 22:35:46 +03:00
|
|
|
try:
|
|
|
|
|
model = apps.get_model(appname, classname)
|
|
|
|
|
except LookupError as le:
|
|
|
|
|
raise FieldError(f"Model {appname}.{classname} does not exist") from le
|
2010-02-19 19:12:21 +03:00
|
|
|
if not issubclass(model, queryset_model):
|
2026-01-12 22:35:46 +03:00
|
|
|
raise FieldError(
|
|
|
|
|
f"{model._meta.label} is not derived from {queryset_model._meta.label}"
|
2019-07-15 10:50:03 +03:00
|
|
|
)
|
2010-02-19 19:12:21 +03:00
|
|
|
|
|
|
|
|
else:
|
2013-07-22 12:19:13 +04:00
|
|
|
# the user has only given us the class name via ___
|
2010-02-19 19:12:21 +03:00
|
|
|
# => select the model from the sub models of the queryset base model
|
|
|
|
|
|
2013-07-22 12:19:13 +04:00
|
|
|
# Test whether it's actually a regular relation__ _fieldname (the field starting with an _)
|
|
|
|
|
# so no tripple ClassName___field was intended.
|
|
|
|
|
try:
|
2017-05-19 09:12:33 +03:00
|
|
|
# This also retreives M2M relations now (including reverse foreign key relations)
|
|
|
|
|
field = queryset_model._meta.get_field(classname)
|
2015-12-29 16:42:24 +03:00
|
|
|
|
2017-05-19 09:12:33 +03:00
|
|
|
if isinstance(field, (RelatedField, ForeignObjectRel)):
|
2013-07-22 12:19:13 +04:00
|
|
|
# Can also test whether the field exists in the related object to avoid ambiguity between
|
|
|
|
|
# class names and field names, but that never happens when your class names are in CamelCase.
|
|
|
|
|
return field_path # No exception raised, field does exist.
|
2020-08-05 11:39:48 +03:00
|
|
|
except FieldDoesNotExist:
|
2013-07-22 12:19:13 +04:00
|
|
|
pass
|
|
|
|
|
|
2026-01-12 21:16:50 +03:00
|
|
|
model = _map_queryname_to_class(queryset_model, classname)
|
2010-02-19 19:12:21 +03:00
|
|
|
|
|
|
|
|
basepath = _create_base_path(queryset_model, model)
|
|
|
|
|
|
2011-05-19 17:07:25 +04:00
|
|
|
if negated:
|
2019-07-15 10:50:03 +03:00
|
|
|
newpath = "-"
|
2011-05-19 17:07:25 +04:00
|
|
|
else:
|
2019-07-15 10:50:03 +03:00
|
|
|
newpath = ""
|
2011-11-26 10:22:18 +04:00
|
|
|
|
2010-02-19 19:12:21 +03:00
|
|
|
newpath += basepath
|
2011-05-19 17:07:25 +04:00
|
|
|
if basepath:
|
2019-07-15 10:50:03 +03:00
|
|
|
newpath += "__"
|
2010-02-19 19:12:21 +03:00
|
|
|
|
|
|
|
|
newpath += pure_field_path
|
|
|
|
|
return newpath
|
|
|
|
|
|
|
|
|
|
|
2019-07-12 12:37:13 +03:00
|
|
|
def _create_base_path(baseclass, myclass):
|
|
|
|
|
# create new field path for expressions, e.g. for baseclass=ModelA, myclass=ModelC
|
|
|
|
|
# 'modelb__modelc" is returned
|
2019-07-12 12:59:13 +03:00
|
|
|
for b in myclass.__bases__:
|
2019-07-12 12:37:13 +03:00
|
|
|
if b == baseclass:
|
|
|
|
|
return _get_query_related_name(myclass)
|
2019-07-12 12:59:13 +03:00
|
|
|
|
2019-07-12 12:37:13 +03:00
|
|
|
path = _create_base_path(baseclass, b)
|
|
|
|
|
if path:
|
|
|
|
|
if b._meta.abstract or b._meta.proxy:
|
2019-07-12 12:59:13 +03:00
|
|
|
return _get_query_related_name(myclass)
|
|
|
|
|
else:
|
2023-12-20 13:29:23 +03:00
|
|
|
return f"{path}__{_get_query_related_name(myclass)}"
|
2019-07-15 10:50:03 +03:00
|
|
|
return ""
|
2019-07-12 12:37:13 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_query_related_name(myclass):
|
2019-07-12 12:59:13 +03:00
|
|
|
for f in myclass._meta.local_fields:
|
|
|
|
|
if isinstance(f, models.OneToOneField) and f.remote_field.parent_link:
|
|
|
|
|
return f.related_query_name()
|
|
|
|
|
|
|
|
|
|
# Fallback to undetected name,
|
|
|
|
|
# this happens on proxy models (e.g. SubclassSelectorProxyModel)
|
2019-07-12 12:37:13 +03:00
|
|
|
return myclass.__name__.lower()
|
|
|
|
|
|
|
|
|
|
|
2018-08-24 12:16:30 +03:00
|
|
|
def create_instanceof_q(modellist, not_instance_of=False, using=DEFAULT_DB_ALIAS):
|
2010-02-19 19:12:21 +03:00
|
|
|
"""
|
|
|
|
|
Helper function for instance_of / not_instance_of
|
|
|
|
|
Creates and returns a Q object that filters for the models in modellist,
|
|
|
|
|
including all subclasses of these models (as we want to do the same
|
|
|
|
|
as pythons isinstance() ).
|
|
|
|
|
.
|
|
|
|
|
We recursively collect all __subclasses__(), create a Q filter for each,
|
|
|
|
|
and or-combine these Q objects. This could be done much more
|
|
|
|
|
efficiently however (regarding the resulting sql), should an optimization
|
|
|
|
|
be needed.
|
|
|
|
|
"""
|
2011-05-19 17:07:25 +04:00
|
|
|
if not modellist:
|
|
|
|
|
return None
|
2010-02-19 19:12:21 +03:00
|
|
|
|
2018-08-24 12:16:30 +03:00
|
|
|
if not isinstance(modellist, (list, tuple)):
|
|
|
|
|
from .models import PolymorphicModel
|
2019-07-15 10:50:03 +03:00
|
|
|
|
2010-02-19 19:12:21 +03:00
|
|
|
if issubclass(modellist, PolymorphicModel):
|
|
|
|
|
modellist = [modellist]
|
|
|
|
|
else:
|
2018-08-24 12:16:30 +03:00
|
|
|
raise TypeError(
|
2019-07-15 10:50:03 +03:00
|
|
|
"PolymorphicModel: instance_of expects a list of (polymorphic) "
|
|
|
|
|
"models or a single (polymorphic) model"
|
2018-08-24 12:16:30 +03:00
|
|
|
)
|
2010-02-19 19:12:21 +03:00
|
|
|
|
2026-01-12 09:48:39 +03:00
|
|
|
lazy_cts, ct_ids = _get_mro_content_type_ids(modellist, using)
|
|
|
|
|
q = Q()
|
|
|
|
|
if lazy_cts:
|
|
|
|
|
q |= Q(
|
|
|
|
|
polymorphic_ctype__in=Subquery(
|
|
|
|
|
# no need to pass using here
|
|
|
|
|
ContentType.objects.filter(reduce(or_, lazy_cts)).values("pk")
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if ct_ids:
|
|
|
|
|
q |= Q(polymorphic_ctype__in=ct_ids)
|
2011-05-19 17:07:25 +04:00
|
|
|
if not_instance_of:
|
2018-08-24 12:16:30 +03:00
|
|
|
q = ~q
|
|
|
|
|
return q
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_mro_content_type_ids(models, using):
|
2026-01-12 09:48:39 +03:00
|
|
|
lazy = []
|
|
|
|
|
ids = []
|
2018-08-24 12:16:30 +03:00
|
|
|
for model in models:
|
2026-01-12 09:48:39 +03:00
|
|
|
cid = _lazy_ctype(model, using=using)
|
|
|
|
|
ids.append(cid.pk) if isinstance(cid, ContentType) else lazy.append(cid)
|
2026-01-12 21:16:50 +03:00
|
|
|
for descendent in concrete_descendants(model, include_proxy=True):
|
|
|
|
|
cid = _lazy_ctype(descendent, using=using)
|
|
|
|
|
ids.append(cid.pk) if isinstance(cid, ContentType) else lazy.append(cid)
|
2026-01-12 09:48:39 +03:00
|
|
|
return lazy, ids
|