|
@@ -2,19 +2,19 @@ import django_filters
|
|
|
from copy import deepcopy
|
|
from copy import deepcopy
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
from django.db import models
|
|
from django.db import models
|
|
|
|
|
+from django_filters.exceptions import FieldLookupError
|
|
|
from django_filters.utils import get_model_field, resolve_field
|
|
from django_filters.utils import get_model_field, resolve_field
|
|
|
|
|
|
|
|
-from dcim.forms import MACAddressField
|
|
|
|
|
from extras.choices import CustomFieldFilterLogicChoices
|
|
from extras.choices import CustomFieldFilterLogicChoices
|
|
|
-from extras.filters import CustomFieldFilter, TagFilter
|
|
|
|
|
|
|
+from extras.filters import TagFilter
|
|
|
from extras.models import CustomField
|
|
from extras.models import CustomField
|
|
|
from utilities.constants import (
|
|
from utilities.constants import (
|
|
|
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
|
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
|
|
FILTER_NUMERIC_BASED_LOOKUP_MAP
|
|
FILTER_NUMERIC_BASED_LOOKUP_MAP
|
|
|
)
|
|
)
|
|
|
|
|
+from utilities.forms import MACAddressField
|
|
|
from utilities import filters
|
|
from utilities import filters
|
|
|
|
|
|
|
|
-
|
|
|
|
|
__all__ = (
|
|
__all__ = (
|
|
|
'BaseFilterSet',
|
|
'BaseFilterSet',
|
|
|
'ChangeLoggedModelFilterSet',
|
|
'ChangeLoggedModelFilterSet',
|
|
@@ -84,6 +84,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
def _get_filter_lookup_dict(existing_filter):
|
|
def _get_filter_lookup_dict(existing_filter):
|
|
|
# Choose the lookup expression map based on the filter type
|
|
# Choose the lookup expression map based on the filter type
|
|
|
if isinstance(existing_filter, (
|
|
if isinstance(existing_filter, (
|
|
|
|
|
+ django_filters.NumberFilter,
|
|
|
filters.MultiValueDateFilter,
|
|
filters.MultiValueDateFilter,
|
|
|
filters.MultiValueDateTimeFilter,
|
|
filters.MultiValueDateTimeFilter,
|
|
|
filters.MultiValueNumberFilter,
|
|
filters.MultiValueNumberFilter,
|
|
@@ -115,6 +116,63 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
|
|
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
+ @classmethod
|
|
|
|
|
+ def get_additional_lookups(cls, existing_filter_name, existing_filter):
|
|
|
|
|
+ new_filters = {}
|
|
|
|
|
+
|
|
|
|
|
+ # Skip nonstandard lookup expressions
|
|
|
|
|
+ if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
|
|
|
|
|
+ return {}
|
|
|
|
|
+
|
|
|
|
|
+ # Choose the lookup expression map based on the filter type
|
|
|
|
|
+ lookup_map = cls._get_filter_lookup_dict(existing_filter)
|
|
|
|
|
+ if lookup_map is None:
|
|
|
|
|
+ # Do not augment this filter type with more lookup expressions
|
|
|
|
|
+ return {}
|
|
|
|
|
+
|
|
|
|
|
+ # Get properties of the existing filter for later use
|
|
|
|
|
+ field_name = existing_filter.field_name
|
|
|
|
|
+ field = get_model_field(cls._meta.model, field_name)
|
|
|
|
|
+
|
|
|
|
|
+ # Create new filters for each lookup expression in the map
|
|
|
|
|
+ for lookup_name, lookup_expr in lookup_map.items():
|
|
|
|
|
+ new_filter_name = f'{existing_filter_name}__{lookup_name}'
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ if existing_filter_name in cls.declared_filters:
|
|
|
|
|
+ # The filter field has been explicitly defined on the filterset class so we must manually
|
|
|
|
|
+ # create the new filter with the same type because there is no guarantee the defined type
|
|
|
|
|
+ # is the same as the default type for the field
|
|
|
|
|
+ resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
|
|
|
|
+ new_filter = type(existing_filter)(
|
|
|
|
|
+ field_name=field_name,
|
|
|
|
|
+ lookup_expr=lookup_expr,
|
|
|
|
|
+ label=existing_filter.label,
|
|
|
|
|
+ exclude=existing_filter.exclude,
|
|
|
|
|
+ distinct=existing_filter.distinct,
|
|
|
|
|
+ **existing_filter.extra
|
|
|
|
|
+ )
|
|
|
|
|
+ elif hasattr(existing_filter, 'custom_field'):
|
|
|
|
|
+ # Filter is for a custom field
|
|
|
|
|
+ custom_field = existing_filter.custom_field
|
|
|
|
|
+ new_filter = custom_field.to_filter(lookup_expr=lookup_expr)
|
|
|
|
|
+ else:
|
|
|
|
|
+ # The filter field is listed in Meta.fields so we can safely rely on default behaviour
|
|
|
|
|
+ # Will raise FieldLookupError if the lookup is invalid
|
|
|
|
|
+ new_filter = cls.filter_for_field(field, field_name, lookup_expr)
|
|
|
|
|
+ except FieldLookupError:
|
|
|
|
|
+ # The filter could not be created because the lookup expression is not supported on the field
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ if lookup_name.startswith('n'):
|
|
|
|
|
+ # This is a negation filter which requires a queryset.exclude() clause
|
|
|
|
|
+ # Of course setting the negation of the existing filter's exclude attribute handles both cases
|
|
|
|
|
+ new_filter.exclude = not existing_filter.exclude
|
|
|
|
|
+
|
|
|
|
|
+ new_filters[new_filter_name] = new_filter
|
|
|
|
|
+
|
|
|
|
|
+ return new_filters
|
|
|
|
|
+
|
|
|
@classmethod
|
|
@classmethod
|
|
|
def get_filters(cls):
|
|
def get_filters(cls):
|
|
|
"""
|
|
"""
|
|
@@ -125,59 +183,12 @@ class BaseFilterSet(django_filters.FilterSet):
|
|
|
"""
|
|
"""
|
|
|
filters = super().get_filters()
|
|
filters = super().get_filters()
|
|
|
|
|
|
|
|
- new_filters = {}
|
|
|
|
|
|
|
+ additional_filters = {}
|
|
|
for existing_filter_name, existing_filter in filters.items():
|
|
for existing_filter_name, existing_filter in filters.items():
|
|
|
- # Loop over existing filters to extract metadata by which to create new filters
|
|
|
|
|
-
|
|
|
|
|
- # If the filter makes use of a custom filter method or lookup expression skip it
|
|
|
|
|
- # as we cannot sanely handle these cases in a generic mannor
|
|
|
|
|
- if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
|
|
|
|
|
- continue
|
|
|
|
|
|
|
+ additional_filters.update(cls.get_additional_lookups(existing_filter_name, existing_filter))
|
|
|
|
|
|
|
|
- # Choose the lookup expression map based on the filter type
|
|
|
|
|
- lookup_map = cls._get_filter_lookup_dict(existing_filter)
|
|
|
|
|
- if lookup_map is None:
|
|
|
|
|
- # Do not augment this filter type with more lookup expressions
|
|
|
|
|
- continue
|
|
|
|
|
|
|
+ filters.update(additional_filters)
|
|
|
|
|
|
|
|
- # Get properties of the existing filter for later use
|
|
|
|
|
- field_name = existing_filter.field_name
|
|
|
|
|
- field = get_model_field(cls._meta.model, field_name)
|
|
|
|
|
-
|
|
|
|
|
- # Create new filters for each lookup expression in the map
|
|
|
|
|
- for lookup_name, lookup_expr in lookup_map.items():
|
|
|
|
|
- new_filter_name = '{}__{}'.format(existing_filter_name, lookup_name)
|
|
|
|
|
-
|
|
|
|
|
- try:
|
|
|
|
|
- if existing_filter_name in cls.declared_filters:
|
|
|
|
|
- # The filter field has been explicity defined on the filterset class so we must manually
|
|
|
|
|
- # create the new filter with the same type because there is no guarantee the defined type
|
|
|
|
|
- # is the same as the default type for the field
|
|
|
|
|
- resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
|
|
|
|
- new_filter = type(existing_filter)(
|
|
|
|
|
- field_name=field_name,
|
|
|
|
|
- lookup_expr=lookup_expr,
|
|
|
|
|
- label=existing_filter.label,
|
|
|
|
|
- exclude=existing_filter.exclude,
|
|
|
|
|
- distinct=existing_filter.distinct,
|
|
|
|
|
- **existing_filter.extra
|
|
|
|
|
- )
|
|
|
|
|
- else:
|
|
|
|
|
- # The filter field is listed in Meta.fields so we can safely rely on default behaviour
|
|
|
|
|
- # Will raise FieldLookupError if the lookup is invalid
|
|
|
|
|
- new_filter = cls.filter_for_field(field, field_name, lookup_expr)
|
|
|
|
|
- except django_filters.exceptions.FieldLookupError:
|
|
|
|
|
- # The filter could not be created because the lookup expression is not supported on the field
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- if lookup_name.startswith('n'):
|
|
|
|
|
- # This is a negation filter which requires a queryset.exclude() clause
|
|
|
|
|
- # Of course setting the negation of the existing filter's exclude attribute handles both cases
|
|
|
|
|
- new_filter.exclude = not existing_filter.exclude
|
|
|
|
|
-
|
|
|
|
|
- new_filters[new_filter_name] = new_filter
|
|
|
|
|
-
|
|
|
|
|
- filters.update(new_filters)
|
|
|
|
|
return filters
|
|
return filters
|
|
|
|
|
|
|
|
|
|
|
|
@@ -213,8 +224,19 @@ class PrimaryModelFilterSet(ChangeLoggedModelFilterSet):
|
|
|
).exclude(
|
|
).exclude(
|
|
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
|
|
)
|
|
)
|
|
|
- for cf in custom_fields:
|
|
|
|
|
- self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ custom_field_filters = {}
|
|
|
|
|
+ for custom_field in custom_fields:
|
|
|
|
|
+ filter_name = f'cf_{custom_field.name}'
|
|
|
|
|
+ filter_instance = custom_field.to_filter()
|
|
|
|
|
+ if filter_instance:
|
|
|
|
|
+ custom_field_filters[filter_name] = filter_instance
|
|
|
|
|
+
|
|
|
|
|
+ # Add relevant additional lookups
|
|
|
|
|
+ additional_lookups = self.get_additional_lookups(filter_name, filter_instance)
|
|
|
|
|
+ custom_field_filters.update(additional_lookups)
|
|
|
|
|
+
|
|
|
|
|
+ self.filters.update(custom_field_filters)
|
|
|
|
|
|
|
|
|
|
|
|
|
class OrganizationalModelFilterSet(PrimaryModelFilterSet):
|
|
class OrganizationalModelFilterSet(PrimaryModelFilterSet):
|