# -*- coding: utf-8 -*-
from django.contrib.admin import ModelAdmin, StackedInline, TabularInline
from ..compat import ADMIN_QUERYSET_METHOD_NAME, admin_validation
from ..exceptions import QueryablePropertyError
from ..managers import QueryablePropertiesQuerySetMixin
from ..utils.internal import QueryPath
from .checks import QueryablePropertiesChecksMixin
from .filters import QueryablePropertyField
__all__ = [
'QueryablePropertiesAdmin',
'QueryablePropertiesAdminMixin',
'QueryablePropertiesStackedInline',
'QueryablePropertiesTabularInline',
]
[docs]
class QueryablePropertiesAdminMixin(object):
"""
A mixin for admin classes including inlines that allows to use queryable
properties in various admin features.
"""
list_select_properties = ()
"""A sequence of queryable property names that should be selected."""
def __init__(self, *args, **kwargs):
super(QueryablePropertiesAdminMixin, self).__init__(*args, **kwargs)
if hasattr(self, 'list_filter') and not hasattr(ModelAdmin, 'get_list_filter'): # pragma: no cover
# In very old Django versions, there was no get_list_filter method,
# therefore the processed queryable property filters must be stored
# directly in the list_filter attribute.
self.list_filter = self.process_queryable_property_filters(self.list_filter)
@classmethod
def validate(cls, model): # pragma: no cover
cls._ensure_queryable_property_checks()
return super(QueryablePropertiesAdminMixin, cls).validate(model)
def check(self, *args, **kwargs):
self._ensure_queryable_property_checks(self)
return super(QueryablePropertiesAdminMixin, self).check(*args, **kwargs)
if getattr(getattr(ModelAdmin, 'check', None), '__self__', None): # pragma: no cover
# In old Django versions, check was a classmethod.
check = classmethod(check)
@classmethod
def _ensure_queryable_property_checks(cls, obj=None):
"""
Make sure that the queryable properties admin check extensions are used
to avoid errors due to Django's default validation, which would treat
queryable property names as invalid.
:param obj: The (optional) model admin instance to ensure the queryable
property checks for. If not provided, they are ensured for
the current class instead.
:type obj: ModelAdmin | None
"""
obj = obj or cls
# Dynamically add a mixin that handles queryable properties into the
# admin's checks/validation class.
for attr_name in ('checks_class', 'validator_class', 'default_validator_class'):
checks_class = getattr(obj, attr_name, None)
if checks_class:
class_name = 'QueryableProperties' + checks_class.__name__
setattr(obj, attr_name, QueryablePropertiesChecksMixin.mix_with_class(checks_class, class_name))
def get_queryset(self, request):
# The base method has different names in different Django versions (see
# comment on the constant definition).
base_method = getattr(super(QueryablePropertiesAdminMixin, self), ADMIN_QUERYSET_METHOD_NAME)
# Make sure to use a queryset with queryable properties features.
queryset = QueryablePropertiesQuerySetMixin.apply_to(base_method(request))
# Apply list_select_properties.
list_select_properties = self.get_list_select_properties(request)
if list_select_properties:
queryset = queryset.select_properties(*list_select_properties)
return queryset
def queryset(self, request): # pragma: no cover
# Same as get_queryset, but for very old Django versions. Simply
# delegate to get_queryset, which is aware of the different methods in
# different versions and therefore calls the correct super methods if
# necessary.
return self.get_queryset(request)
[docs]
def get_list_select_properties(self, request):
"""
Wrapper around the ``list_select_properties`` attribute that allows to
dynamically create the list of queryable property names to select based
on the given request.
:param django.http.HttpRequest request: The request to the admin.
:return: A sequence of queryable property names to select.
:rtype: collections.Sequence[str]
"""
return self.list_select_properties
def get_list_filter(self, request):
list_filter = super(QueryablePropertiesAdminMixin, self).get_list_filter(request)
return self.process_queryable_property_filters(list_filter)
[docs]
def process_queryable_property_filters(self, list_filter):
"""
Process a sequence of list filters to create a new sequence in which
queryable property references are replaced with custom callables that
make them compatible with Django's filter workflow.
:param collections.Sequence list_filter: The list filter sequence.
:return: The processed list filter sequence.
:rtype: list
"""
processed_filters = []
for item in list_filter:
if not callable(item):
if isinstance(item, (tuple, list)):
field_name, filter_class = item
else:
field_name, filter_class = item, None
try:
item = QueryablePropertyField(self, QueryPath(field_name)).get_filter_creator(filter_class)
except QueryablePropertyError:
pass
processed_filters.append(item)
return processed_filters
[docs]
class QueryablePropertiesAdmin(QueryablePropertiesAdminMixin, ModelAdmin):
"""
Base class for admin classes which allows to use queryable properties in
various admin features.
Intended to be used in place of Django's regular ``ModelAdmin`` class.
"""
[docs]
class QueryablePropertiesStackedInline(QueryablePropertiesAdminMixin, StackedInline):
"""
Base class for stacked inline classes which allows to use queryable
properties in various admin features.
Intended to be used in place of Django's regular ``StackedInline`` class.
"""
[docs]
class QueryablePropertiesTabularInline(QueryablePropertiesAdminMixin, TabularInline):
"""
Base class for tabular inline classes which allows to use queryable
properties in various admin features.
Intended to be used in place of Django's regular ``TabularInline`` class.
"""
# In very old django versions, the admin validation happens in one big function
# that cannot really be extended well. Therefore, the Django module will be
# monkeypatched in order to allow the queryable properties validation to take
# effect.
django_validate = getattr(admin_validation, 'validate', None)
django_validate_inline = getattr(admin_validation, 'validate_inline', None)
if django_validate: # pragma: no cover
def validate(cls, model):
if issubclass(cls, QueryablePropertiesAdminMixin):
cls = QueryablePropertiesChecksMixin()._validate_queryable_properties(cls, model)
django_validate(cls, model)
admin_validation.validate = validate
if django_validate_inline: # pragma: no cover
def validate_inline(cls, parent, parent_model):
if issubclass(cls, QueryablePropertiesAdminMixin):
cls = QueryablePropertiesChecksMixin()._validate_queryable_properties(cls, cls.model)
django_validate_inline(cls, parent, parent_model)
admin_validation.validate_inline = validate_inline