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:
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user