1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into develop-2.8

This commit is contained in:
Jeremy Stretch
2020-03-06 11:34:01 -05:00
27 changed files with 1295 additions and 1263 deletions

View File

@@ -28,12 +28,47 @@ COLOR_CHOICES = (
('ffffff', 'White'),
)
#
# Filter lookup expressions
#
FILTER_CHAR_BASED_LOOKUP_MAP = dict(
n='exact',
ic='icontains',
nic='icontains',
iew='iendswith',
niew='iendswith',
isw='istartswith',
nisw='istartswith',
ie='iexact',
nie='iexact'
)
FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(
n='exact',
lte='lte',
lt='lt',
gte='gte',
gt='gt'
)
FILTER_NEGATION_LOOKUP_MAP = dict(
n='exact'
)
FILTER_TREENODE_NEGATION_LOOKUP_MAP = dict(
n='in'
)
# Keys for PostgreSQL advisory locks. These are arbitrary bigints used by
# the advisory_lock contextmanager. When a lock is acquired,
# one of these keys will be used to identify said lock.
#
# When adding a new key, pick something arbitrary and unique so
# that it is easily searchable in query logs.
ADVISORY_LOCK_KEYS = {
'available-prefixes': 100100,
'available-ips': 100200,

View File

@@ -1,3 +1,4 @@
from django.contrib.postgres.fields import JSONField
from drf_yasg import openapi
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, FilterInspector, SwaggerAutoSchema
from drf_yasg.utils import get_serializer_ref_name
@@ -75,26 +76,28 @@ class CustomChoiceFieldInspector(FieldInspector):
SwaggerType, _ = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, ChoiceField):
value_schema = openapi.Schema(type=openapi.TYPE_STRING)
choices = field._choices
choice_value = list(choices.keys())
choice_label = list(choices.values())
value_schema = openapi.Schema(type=openapi.TYPE_STRING, enum=choice_value)
choices = list(field._choices.keys())
if set([None] + choices) == {None, True, False}:
if set([None] + choice_value) == {None, True, False}:
# DeviceType.subdevice_role, Device.face and InterfaceConnection.connection_status all need to be
# differentiated since they each have subtly different values in their choice keys.
# - subdevice_role and connection_status are booleans, although subdevice_role includes None
# - face is an integer set {0, 1} which is easily confused with {False, True}
schema_type = openapi.TYPE_STRING
if all(type(x) == bool for x in [c for c in choices if c is not None]):
if all(type(x) == bool for x in [c for c in choice_value if c is not None]):
schema_type = openapi.TYPE_BOOLEAN
value_schema = openapi.Schema(type=schema_type)
value_schema = openapi.Schema(type=schema_type, enum=choice_value)
value_schema['x-nullable'] = True
if isinstance(choices[0], int):
if isinstance(choice_value[0], int):
# Change value_schema for IPAddressFamilyChoices, RackWidthChoices
value_schema = openapi.Schema(type=openapi.TYPE_INTEGER)
value_schema = openapi.Schema(type=openapi.TYPE_INTEGER, enum=choice_value)
schema = SwaggerType(type=openapi.TYPE_OBJECT, required=["label", "value"], properties={
"label": openapi.Schema(type=openapi.TYPE_STRING),
"label": openapi.Schema(type=openapi.TYPE_STRING, enum=choice_label),
"value": value_schema
})
@@ -119,6 +122,15 @@ class NullableBooleanFieldInspector(FieldInspector):
return result
class JSONFieldInspector(FieldInspector):
"""Required because by default, Swagger sees a JSONField as a string and not dict
"""
def process_result(self, result, method_name, obj, **kwargs):
if isinstance(result, openapi.Schema) and isinstance(obj, JSONField):
result.type = 'dict'
return result
class IdInFilterInspector(FilterInspector):
def process_result(self, result, method_name, obj, **kwargs):
if isinstance(result, list):

View File

@@ -1,9 +1,16 @@
import django_filters
from copy import deepcopy
from dcim.forms import MACAddressField
from django import forms
from django.conf import settings
from django.db import models
from django_filters.utils import get_model_field, resolve_field
from extras.models import Tag
from utilities.constants import (
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
FILTER_NUMERIC_BASED_LOOKUP_MAP
)
def multivalue_field_factory(field_class):
@@ -111,6 +118,165 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter):
# FilterSets
#
class BaseFilterSet(django_filters.FilterSet):
"""
A base filterset which provides common functionaly to all NetBox filtersets
"""
FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
FILTER_DEFAULTS.update({
models.AutoField: {
'filter_class': MultiValueNumberFilter
},
models.CharField: {
'filter_class': MultiValueCharFilter
},
models.DateField: {
'filter_class': MultiValueDateFilter
},
models.DateTimeField: {
'filter_class': MultiValueDateTimeFilter
},
models.DecimalField: {
'filter_class': MultiValueNumberFilter
},
models.EmailField: {
'filter_class': MultiValueCharFilter
},
models.FloatField: {
'filter_class': MultiValueNumberFilter
},
models.IntegerField: {
'filter_class': MultiValueNumberFilter
},
models.PositiveIntegerField: {
'filter_class': MultiValueNumberFilter
},
models.PositiveSmallIntegerField: {
'filter_class': MultiValueNumberFilter
},
models.SlugField: {
'filter_class': MultiValueCharFilter
},
models.SmallIntegerField: {
'filter_class': MultiValueNumberFilter
},
models.TimeField: {
'filter_class': MultiValueTimeFilter
},
models.URLField: {
'filter_class': MultiValueCharFilter
},
MACAddressField: {
'filter_class': MultiValueMACAddressFilter
},
})
@staticmethod
def _get_filter_lookup_dict(existing_filter):
# Choose the lookup expression map based on the filter type
if isinstance(existing_filter, (
MultiValueDateFilter,
MultiValueDateTimeFilter,
MultiValueNumberFilter,
MultiValueTimeFilter
)):
lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
elif isinstance(existing_filter, (
TreeNodeMultipleChoiceFilter,
)):
# TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP
elif isinstance(existing_filter, (
django_filters.ModelChoiceFilter,
django_filters.ModelMultipleChoiceFilter,
TagFilter
)) or existing_filter.extra.get('choices'):
# These filter types support only negation
lookup_map = FILTER_NEGATION_LOOKUP_MAP
elif isinstance(existing_filter, (
django_filters.filters.CharFilter,
django_filters.MultipleChoiceFilter,
MultiValueCharFilter,
MultiValueMACAddressFilter
)):
lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP
else:
lookup_map = None
return lookup_map
@classmethod
def get_filters(cls):
"""
Override filter generation to support dynamic lookup expressions for certain filter types.
For specific filter types, new filters are created based on defined lookup expressions in
the form `<field_name>__<lookup_expr>`
"""
# TODO: once 3.6 is the minimum required version of python, change this to a bare super() call
# We have to do it this way in py3.5 becuase of django_filters.FilterSet's use of a metaclass
filters = super(django_filters.FilterSet, cls).get_filters()
new_filters = {}
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
# 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
# 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
class NameSlugSearchFilterSet(django_filters.FilterSet):
"""
A base class for adding the search method to models which only expose the `name` and `slug` fields
@@ -127,54 +293,3 @@ class NameSlugSearchFilterSet(django_filters.FilterSet):
models.Q(name__icontains=value) |
models.Q(slug__icontains=value)
)
#
# Update default filters
#
FILTER_DEFAULTS = django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS
FILTER_DEFAULTS.update({
models.AutoField: {
'filter_class': MultiValueNumberFilter
},
models.CharField: {
'filter_class': MultiValueCharFilter
},
models.DateField: {
'filter_class': MultiValueDateFilter
},
models.DateTimeField: {
'filter_class': MultiValueDateTimeFilter
},
models.DecimalField: {
'filter_class': MultiValueNumberFilter
},
models.EmailField: {
'filter_class': MultiValueCharFilter
},
models.FloatField: {
'filter_class': MultiValueNumberFilter
},
models.IntegerField: {
'filter_class': MultiValueNumberFilter
},
models.PositiveIntegerField: {
'filter_class': MultiValueNumberFilter
},
models.PositiveSmallIntegerField: {
'filter_class': MultiValueNumberFilter
},
models.SlugField: {
'filter_class': MultiValueCharFilter
},
models.SmallIntegerField: {
'filter_class': MultiValueNumberFilter
},
models.TimeField: {
'filter_class': MultiValueTimeFilter
},
models.URLField: {
'filter_class': MultiValueCharFilter
},
})

View File

@@ -1,9 +1,21 @@
from django.conf import settings
from django.test import TestCase
import django_filters
from django.conf import settings
from django.db import models
from django.test import TestCase
from mptt.fields import TreeForeignKey
from taggit.managers import TaggableManager
from dcim.models import Region, Site
from utilities.filters import TreeNodeMultipleChoiceFilter
from dcim.choices import *
from dcim.fields import MACAddressField
from dcim.filters import DeviceFilterSet, SiteFilterSet
from dcim.models import (
Device, DeviceRole, DeviceType, Interface, Manufacturer, Platform, Rack, Region, Site
)
from extras.models import TaggedItem
from utilities.filters import (
BaseFilterSet, MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter,
MultiValueNumberFilter, MultiValueTimeFilter, TagFilter, TreeNodeMultipleChoiceFilter,
)
class TreeNodeMultipleChoiceFilterTest(TestCase):
@@ -60,3 +72,447 @@ class TreeNodeMultipleChoiceFilterTest(TestCase):
self.assertEqual(qs.count(), 2)
self.assertEqual(qs[0], self.site1)
self.assertEqual(qs[1], self.site3)
class DummyModel(models.Model):
"""
Dummy model used by BaseFilterSetTest for filter validation. Should never appear in a schema migration.
"""
charfield = models.CharField(
max_length=10
)
choicefield = models.IntegerField(
choices=(('A', 1), ('B', 2), ('C', 3))
)
datefield = models.DateField()
datetimefield = models.DateTimeField()
integerfield = models.IntegerField()
macaddressfield = MACAddressField()
timefield = models.TimeField()
treeforeignkeyfield = TreeForeignKey(
to='self',
on_delete=models.CASCADE
)
tags = TaggableManager(through=TaggedItem)
class BaseFilterSetTest(TestCase):
"""
Ensure that a BaseFilterSet automatically creates the expected set of filters for each filter type.
"""
class DummyFilterSet(BaseFilterSet):
charfield = django_filters.CharFilter()
macaddressfield = MACAddressFilter()
modelchoicefield = django_filters.ModelChoiceFilter(
field_name='integerfield', # We're pretending this is a ForeignKey field
queryset=Site.objects.all()
)
modelmultiplechoicefield = django_filters.ModelMultipleChoiceFilter(
field_name='integerfield', # We're pretending this is a ForeignKey field
queryset=Site.objects.all()
)
multiplechoicefield = django_filters.MultipleChoiceFilter(
field_name='choicefield'
)
multivaluecharfield = MultiValueCharFilter(
field_name='charfield'
)
tagfield = TagFilter()
treeforeignkeyfield = TreeNodeMultipleChoiceFilter(
queryset=DummyModel.objects.all()
)
class Meta:
model = DummyModel
fields = (
'charfield',
'choicefield',
'datefield',
'datetimefield',
'integerfield',
'macaddressfield',
'modelchoicefield',
'modelmultiplechoicefield',
'multiplechoicefield',
'tagfield',
'timefield',
'treeforeignkeyfield',
)
@classmethod
def setUpTestData(cls):
cls.filters = cls.DummyFilterSet().filters
def test_char_filter(self):
self.assertIsInstance(self.filters['charfield'], django_filters.CharFilter)
self.assertEqual(self.filters['charfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['charfield'].exclude, False)
self.assertEqual(self.filters['charfield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['charfield__n'].exclude, True)
self.assertEqual(self.filters['charfield__ie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['charfield__ie'].exclude, False)
self.assertEqual(self.filters['charfield__nie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['charfield__nie'].exclude, True)
self.assertEqual(self.filters['charfield__ic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['charfield__ic'].exclude, False)
self.assertEqual(self.filters['charfield__nic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['charfield__nic'].exclude, True)
self.assertEqual(self.filters['charfield__isw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['charfield__isw'].exclude, False)
self.assertEqual(self.filters['charfield__nisw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['charfield__nisw'].exclude, True)
self.assertEqual(self.filters['charfield__iew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['charfield__iew'].exclude, False)
self.assertEqual(self.filters['charfield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['charfield__niew'].exclude, True)
def test_mac_address_filter(self):
self.assertIsInstance(self.filters['macaddressfield'], MACAddressFilter)
self.assertEqual(self.filters['macaddressfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['macaddressfield'].exclude, False)
self.assertEqual(self.filters['macaddressfield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['macaddressfield__n'].exclude, True)
self.assertEqual(self.filters['macaddressfield__ie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['macaddressfield__ie'].exclude, False)
self.assertEqual(self.filters['macaddressfield__nie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['macaddressfield__nie'].exclude, True)
self.assertEqual(self.filters['macaddressfield__ic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['macaddressfield__ic'].exclude, False)
self.assertEqual(self.filters['macaddressfield__nic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['macaddressfield__nic'].exclude, True)
self.assertEqual(self.filters['macaddressfield__isw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['macaddressfield__isw'].exclude, False)
self.assertEqual(self.filters['macaddressfield__nisw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['macaddressfield__nisw'].exclude, True)
self.assertEqual(self.filters['macaddressfield__iew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['macaddressfield__iew'].exclude, False)
self.assertEqual(self.filters['macaddressfield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['macaddressfield__niew'].exclude, True)
def test_model_choice_filter(self):
self.assertIsInstance(self.filters['modelchoicefield'], django_filters.ModelChoiceFilter)
self.assertEqual(self.filters['modelchoicefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['modelchoicefield'].exclude, False)
self.assertEqual(self.filters['modelchoicefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['modelchoicefield__n'].exclude, True)
def test_model_multiple_choice_filter(self):
self.assertIsInstance(self.filters['modelmultiplechoicefield'], django_filters.ModelMultipleChoiceFilter)
self.assertEqual(self.filters['modelmultiplechoicefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['modelmultiplechoicefield'].exclude, False)
self.assertEqual(self.filters['modelmultiplechoicefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['modelmultiplechoicefield__n'].exclude, True)
def test_multi_value_char_filter(self):
self.assertIsInstance(self.filters['multivaluecharfield'], MultiValueCharFilter)
self.assertEqual(self.filters['multivaluecharfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['multivaluecharfield'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['multivaluecharfield__n'].exclude, True)
self.assertEqual(self.filters['multivaluecharfield__ie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['multivaluecharfield__ie'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__nie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['multivaluecharfield__nie'].exclude, True)
self.assertEqual(self.filters['multivaluecharfield__ic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['multivaluecharfield__ic'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__nic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['multivaluecharfield__nic'].exclude, True)
self.assertEqual(self.filters['multivaluecharfield__isw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['multivaluecharfield__isw'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__nisw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['multivaluecharfield__nisw'].exclude, True)
self.assertEqual(self.filters['multivaluecharfield__iew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['multivaluecharfield__iew'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['multivaluecharfield__niew'].exclude, True)
def test_multi_value_date_filter(self):
self.assertIsInstance(self.filters['datefield'], MultiValueDateFilter)
self.assertEqual(self.filters['datefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['datefield'].exclude, False)
self.assertEqual(self.filters['datefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['datefield__n'].exclude, True)
self.assertEqual(self.filters['datefield__lt'].lookup_expr, 'lt')
self.assertEqual(self.filters['datefield__lt'].exclude, False)
self.assertEqual(self.filters['datefield__lte'].lookup_expr, 'lte')
self.assertEqual(self.filters['datefield__lte'].exclude, False)
self.assertEqual(self.filters['datefield__gt'].lookup_expr, 'gt')
self.assertEqual(self.filters['datefield__gt'].exclude, False)
self.assertEqual(self.filters['datefield__gte'].lookup_expr, 'gte')
self.assertEqual(self.filters['datefield__gte'].exclude, False)
def test_multi_value_datetime_filter(self):
self.assertIsInstance(self.filters['datetimefield'], MultiValueDateTimeFilter)
self.assertEqual(self.filters['datetimefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['datetimefield'].exclude, False)
self.assertEqual(self.filters['datetimefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['datetimefield__n'].exclude, True)
self.assertEqual(self.filters['datetimefield__lt'].lookup_expr, 'lt')
self.assertEqual(self.filters['datetimefield__lt'].exclude, False)
self.assertEqual(self.filters['datetimefield__lte'].lookup_expr, 'lte')
self.assertEqual(self.filters['datetimefield__lte'].exclude, False)
self.assertEqual(self.filters['datetimefield__gt'].lookup_expr, 'gt')
self.assertEqual(self.filters['datetimefield__gt'].exclude, False)
self.assertEqual(self.filters['datetimefield__gte'].lookup_expr, 'gte')
self.assertEqual(self.filters['datetimefield__gte'].exclude, False)
def test_multi_value_number_filter(self):
self.assertIsInstance(self.filters['integerfield'], MultiValueNumberFilter)
self.assertEqual(self.filters['integerfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['integerfield'].exclude, False)
self.assertEqual(self.filters['integerfield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['integerfield__n'].exclude, True)
self.assertEqual(self.filters['integerfield__lt'].lookup_expr, 'lt')
self.assertEqual(self.filters['integerfield__lt'].exclude, False)
self.assertEqual(self.filters['integerfield__lte'].lookup_expr, 'lte')
self.assertEqual(self.filters['integerfield__lte'].exclude, False)
self.assertEqual(self.filters['integerfield__gt'].lookup_expr, 'gt')
self.assertEqual(self.filters['integerfield__gt'].exclude, False)
self.assertEqual(self.filters['integerfield__gte'].lookup_expr, 'gte')
self.assertEqual(self.filters['integerfield__gte'].exclude, False)
def test_multi_value_time_filter(self):
self.assertIsInstance(self.filters['timefield'], MultiValueTimeFilter)
self.assertEqual(self.filters['timefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['timefield'].exclude, False)
self.assertEqual(self.filters['timefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['timefield__n'].exclude, True)
self.assertEqual(self.filters['timefield__lt'].lookup_expr, 'lt')
self.assertEqual(self.filters['timefield__lt'].exclude, False)
self.assertEqual(self.filters['timefield__lte'].lookup_expr, 'lte')
self.assertEqual(self.filters['timefield__lte'].exclude, False)
self.assertEqual(self.filters['timefield__gt'].lookup_expr, 'gt')
self.assertEqual(self.filters['timefield__gt'].exclude, False)
self.assertEqual(self.filters['timefield__gte'].lookup_expr, 'gte')
self.assertEqual(self.filters['timefield__gte'].exclude, False)
def test_multiple_choice_filter(self):
self.assertIsInstance(self.filters['multiplechoicefield'], django_filters.MultipleChoiceFilter)
self.assertEqual(self.filters['multiplechoicefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['multiplechoicefield'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['multiplechoicefield__n'].exclude, True)
self.assertEqual(self.filters['multiplechoicefield__ie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['multiplechoicefield__ie'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__nie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['multiplechoicefield__nie'].exclude, True)
self.assertEqual(self.filters['multiplechoicefield__ic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['multiplechoicefield__ic'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__nic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['multiplechoicefield__nic'].exclude, True)
self.assertEqual(self.filters['multiplechoicefield__isw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['multiplechoicefield__isw'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__nisw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['multiplechoicefield__nisw'].exclude, True)
self.assertEqual(self.filters['multiplechoicefield__iew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['multiplechoicefield__iew'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['multiplechoicefield__niew'].exclude, True)
def test_tag_filter(self):
self.assertIsInstance(self.filters['tagfield'], TagFilter)
self.assertEqual(self.filters['tagfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['tagfield'].exclude, False)
self.assertEqual(self.filters['tagfield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['tagfield__n'].exclude, True)
def test_tree_node_multiple_choice_filter(self):
self.assertIsInstance(self.filters['treeforeignkeyfield'], TreeNodeMultipleChoiceFilter)
# TODO: lookup_expr different for negation?
self.assertEqual(self.filters['treeforeignkeyfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['treeforeignkeyfield'].exclude, False)
self.assertEqual(self.filters['treeforeignkeyfield__n'].lookup_expr, 'in')
self.assertEqual(self.filters['treeforeignkeyfield__n'].exclude, True)
class DynamicFilterLookupExpressionTest(TestCase):
"""
Validate function of automatically generated filters using the Device model as an example.
"""
device_queryset = Device.objects.all()
device_filterset = DeviceFilterSet
site_queryset = Site.objects.all()
site_filterset = SiteFilterSet
@classmethod
def setUpTestData(cls):
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
)
Manufacturer.objects.bulk_create(manufacturers)
device_types = (
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', is_full_depth=True),
DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', is_full_depth=True),
DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', is_full_depth=False),
)
DeviceType.objects.bulk_create(device_types)
device_roles = (
DeviceRole(name='Device Role 1', slug='device-role-1'),
DeviceRole(name='Device Role 2', slug='device-role-2'),
DeviceRole(name='Device Role 3', slug='device-role-3'),
)
DeviceRole.objects.bulk_create(device_roles)
platforms = (
Platform(name='Platform 1', slug='platform-1'),
Platform(name='Platform 2', slug='platform-2'),
Platform(name='Platform 3', slug='platform-3'),
)
Platform.objects.bulk_create(platforms)
regions = (
Region(name='Region 1', slug='region-1'),
Region(name='Region 2', slug='region-2'),
Region(name='Region 3', slug='region-3'),
)
for region in regions:
region.save()
sites = (
Site(name='Site 1', slug='abc-site-1', region=regions[0], asn=65001),
Site(name='Site 2', slug='def-site-2', region=regions[1], asn=65101),
Site(name='Site 3', slug='ghi-site-3', region=regions[2], asn=65201),
)
Site.objects.bulk_create(sites)
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, local_context_data={"foo": 123}),
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED),
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED),
)
Device.objects.bulk_create(devices)
interfaces = (
Interface(device=devices[0], name='Interface 1', mac_address='00-00-00-00-00-01'),
Interface(device=devices[0], name='Interface 2', mac_address='aa-00-00-00-00-01'),
Interface(device=devices[1], name='Interface 3', mac_address='00-00-00-00-00-02'),
Interface(device=devices[1], name='Interface 4', mac_address='bb-00-00-00-00-02'),
Interface(device=devices[2], name='Interface 5', mac_address='00-00-00-00-00-03'),
Interface(device=devices[2], name='Interface 6', mac_address='cc-00-00-00-00-03'),
)
Interface.objects.bulk_create(interfaces)
def test_site_name_negation(self):
params = {'name__n': ['Site 1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_slug_icontains(self):
params = {'slug__ic': ['-1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
def test_site_slug_icontains_negation(self):
params = {'slug__nic': ['-1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_slug_startswith(self):
params = {'slug__isw': ['abc']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
def test_site_slug_startswith_negation(self):
params = {'slug__nisw': ['abc']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_slug_endswith(self):
params = {'slug__iew': ['-1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
def test_site_slug_endswith_negation(self):
params = {'slug__niew': ['-1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_asn_lt(self):
params = {'asn__lt': [65101]}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
def test_site_asn_lte(self):
params = {'asn__lte': [65101]}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_asn_gt(self):
params = {'asn__lt': [65101]}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
def test_site_asn_gte(self):
params = {'asn__gte': [65101]}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_region_negation(self):
params = {'region__n': ['region-1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_region_id_negation(self):
params = {'region_id__n': [Region.objects.first().pk]}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_device_name_eq(self):
params = {'name': ['Device 1']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
def test_device_name_negation(self):
params = {'name__n': ['Device 1']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_name_startswith(self):
params = {'name__isw': ['Device']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 3)
def test_device_name_startswith_negation(self):
params = {'name__nisw': ['Device 1']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_name_endswith(self):
params = {'name__iew': [' 1']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
def test_device_name_endswith_negation(self):
params = {'name__niew': [' 1']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_name_icontains(self):
params = {'name__ic': [' 2']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
def test_device_name_icontains_negation(self):
params = {'name__nic': [' ']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 0)
def test_device_mac_address_negation(self):
params = {'mac_address__n': ['00-00-00-00-00-01', 'aa-00-00-00-00-01']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_mac_address_startswith(self):
params = {'mac_address__isw': ['aa:']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
def test_device_mac_address_startswith_negation(self):
params = {'mac_address__nisw': ['aa:']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_mac_address_endswith(self):
params = {'mac_address__iew': [':02']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
def test_device_mac_address_endswith_negation(self):
params = {'mac_address__niew': [':02']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_mac_address_icontains(self):
params = {'mac_address__ic': ['aa:', 'bb']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_mac_address_icontains_negation(self):
params = {'mac_address__nic': ['aa:', 'bb']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)