* Resolve conflict with virtualization filters.

This commit is contained in:
dansheps 2019-03-05 08:18:04 -06:00
commit 3bb1cbcdb0
10 changed files with 66 additions and 61 deletions

View File

@ -2,7 +2,13 @@ v2.5.8 (FUTURE)
## Bug Fixes ## Bug Fixes
* [#2705](https://github.com/digitalocean/netbox/issues/2705) - Fix endpoint grouping in API docs
* [#2781](https://github.com/digitalocean/netbox/issues/2781) - Fix filtering of sites/devices/VMs by multiple regions
* [#2923](https://github.com/digitalocean/netbox/issues/2923) - Provider filter form's site field should be blank by default * [#2923](https://github.com/digitalocean/netbox/issues/2923) - Provider filter form's site field should be blank by default
* [#2938](https://github.com/digitalocean/netbox/issues/2938) - Enforce deterministic ordering of device components returned by API
* [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint
* [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function
* [#2954](https://github.com/digitalocean/netbox/issues/2954) - Remove trailing slashes to fix root/template paths on Windows
--- ---

View File

@ -496,11 +496,11 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = Interface.objects.select_related( queryset = Interface.objects.select_related(
'device', '_connected_interface', '_connected_circuittermination' 'device', '_connected_interface__device'
).filter( ).filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair # Avoid duplicate connections by only selecting the lower PK in a connected pair
Q(_connected_interface__isnull=False, pk__lt=F('_connected_interface')) | _connected_interface__isnull=False,
Q(_connected_circuittermination__isnull=False) pk__lt=F('_connected_interface')
) )
serializer_class = serializers.InterfaceConnectionSerializer serializer_class = serializers.InterfaceConnectionSerializer
filterset_class = filters.InterfaceConnectionFilter filterset_class = filters.InterfaceConnectionFilter

View File

@ -1,6 +1,5 @@
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q from django.db.models import Q
from netaddr import EUI from netaddr import EUI
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
@ -8,7 +7,9 @@ from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.filters import TenancyFilterSet from tenancy.filters import TenancyFilterSet
from utilities.constants import COLOR_CHOICES from utilities.constants import COLOR_CHOICES
from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter from utilities.filters import (
NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
)
from virtualization.models import Cluster from virtualization.models import Cluster
from .constants import * from .constants import *
from .models import ( from .models import (
@ -49,14 +50,15 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSe
choices=SITE_STATUS_CHOICES, choices=SITE_STATUS_CHOICES,
null_value=None null_value=None
) )
region_id = django_filters.NumberFilter( region_id = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='pk', field_name='region__in',
label='Region (ID)', label='Region (ID)',
) )
region = django_filters.CharFilter( region = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='slug', field_name='region__in',
to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
tag = TagFilter() tag = TagFilter()
@ -85,16 +87,6 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSe
pass pass
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def filter_region(self, queryset, name, value):
try:
region = Region.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
return queryset.filter(
Q(region=region) |
Q(region__in=region.get_descendants())
)
class RackGroupFilter(NameSlugSearchFilterSet): class RackGroupFilter(NameSlugSearchFilterSet):
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
@ -473,14 +465,15 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
) )
name = NullableCharFieldFilter() name = NullableCharFieldFilter()
asset_tag = NullableCharFieldFilter() asset_tag = NullableCharFieldFilter()
region_id = django_filters.NumberFilter( region_id = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='pk', field_name='site__region__in',
label='Region (ID)', label='Region (ID)',
) )
region = django_filters.CharFilter( region = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='slug', field_name='site__region__in',
to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
@ -579,16 +572,6 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
Q(comments__icontains=value) Q(comments__icontains=value)
).distinct() ).distinct()
def filter_region(self, queryset, name, value):
try:
region = Region.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
return queryset.filter(
Q(site__region=region) |
Q(site__region__in=region.get_descendants())
)
def _mac_address(self, queryset, name, value): def _mac_address(self, queryset, name, value):
value = value.strip() value = value.strip()
if not value: if not value:

View File

@ -27,7 +27,7 @@ class DeviceComponentManager(Manager):
select={ select={
'name_padded': sql.format(table_name, table_name), 'name_padded': sql.format(table_name, table_name),
} }
).order_by('name_padded') ).order_by('name_padded', 'pk')
class InterfaceQuerySet(QuerySet): class InterfaceQuerySet(QuerySet):

View File

@ -29,7 +29,11 @@ def cache_changed_object(instance, **kwargs):
def _record_object_deleted(request, instance, **kwargs): def _record_object_deleted(request, instance, **kwargs):
# Record that the object was deleted. # Force resolution of request.user in case it's still a SimpleLazyObject. This seems to happen
# occasionally during tests, but haven't been able to determine why.
assert request.user.is_authenticated
# Record that the object was deleted
if hasattr(instance, 'log_change'): if hasattr(instance, 'log_change'):
instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE) instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)

View File

@ -197,7 +197,7 @@ ROOT_URLCONF = 'netbox.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR + '/templates/'], 'DIRS': [BASE_DIR + '/templates'],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -223,7 +223,7 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
STATIC_ROOT = BASE_DIR + '/static/' STATIC_ROOT = BASE_DIR + '/static'
STATIC_URL = '/{}static/'.format(BASE_PATH) STATIC_URL = '/{}static/'.format(BASE_PATH)
STATICFILES_DIRS = ( STATICFILES_DIRS = (
os.path.join(BASE_DIR, "project-static"), os.path.join(BASE_DIR, "project-static"),

View File

@ -267,6 +267,7 @@ class SearchView(View):
class APIRootView(APIView): class APIRootView(APIView):
_ignore_model_permissions = True _ignore_model_permissions = True
exclude_from_schema = True exclude_from_schema = True
swagger_schema = None
def get_view_name(self): def get_view_name(self):
return "API Root" return "API Root"

View File

@ -36,13 +36,14 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Tenant model = Tenant
fields = ['name'] fields = ['name', 'slug']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
return queryset.filter( return queryset.filter(
Q(name__icontains=value) | Q(name__icontains=value) |
Q(slug__icontains=value) |
Q(description__icontains=value) | Q(description__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
) )

View File

@ -4,6 +4,15 @@ from django.db.models import Q
from taggit.models import Tag from taggit.models import Tag
class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
"""
Filters for a set of Models, including all descendant models within a Tree. Example: [<Region: R1>,<Region: R2>]
"""
def filter(self, qs, value):
value = [node.get_descendants(include_self=True) for node in value]
return super().filter(qs, value)
class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter): class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
""" """
Filters for a set of numeric values. Example: id__in=100,200,300 Filters for a set of numeric values. Example: id__in=100,200,300

View File

@ -6,8 +6,8 @@ from netaddr.core import AddrFormatError
from dcim.models import DeviceRole, Interface, Platform, Region, Site from dcim.models import DeviceRole, Interface, Platform, Region, Site
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.filters import TenancyFilterSet from tenancy.models import Tenant
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
from .constants import VM_STATUS_CHOICES from .constants import VM_STATUS_CHOICES
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -80,7 +80,7 @@ class ClusterFilter(CustomFieldFilterSet):
) )
class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet): class VirtualMachineFilter(CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -119,14 +119,15 @@ class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet):
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
label='Cluster (ID)', label='Cluster (ID)',
) )
region_id = django_filters.NumberFilter( region_id = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='pk', field_name='cluster__site__region__in',
label='Region (ID)', label='Region (ID)',
) )
region = django_filters.CharFilter( region = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='slug', field_name='cluster__site__region__in',
to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
@ -150,6 +151,16 @@ class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet):
to_field_name='slug', to_field_name='slug',
label='Role (slug)', label='Role (slug)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
platform_id = django_filters.ModelMultipleChoiceFilter( platform_id = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
label='Platform (ID)', label='Platform (ID)',
@ -174,16 +185,6 @@ class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet):
Q(comments__icontains=value) Q(comments__icontains=value)
) )
def filter_region(self, queryset, name, value):
try:
region = Region.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
return queryset.filter(
Q(cluster__site__region=region) |
Q(cluster__site__region__in=region.get_descendants())
)
class InterfaceFilter(django_filters.FilterSet): class InterfaceFilter(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(