mirror of
				https://github.com/netbox-community/netbox.git
				synced 2024-05-10 07:54:54 +00:00 
			
		
		
		
	* Resolve conflict with virtualization filters.
This commit is contained in:
		@@ -2,7 +2,13 @@ v2.5.8 (FUTURE)
 | 
			
		||||
 | 
			
		||||
## 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
 | 
			
		||||
* [#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
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -496,11 +496,11 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
 | 
			
		||||
 | 
			
		||||
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
 | 
			
		||||
    queryset = Interface.objects.select_related(
 | 
			
		||||
        'device', '_connected_interface', '_connected_circuittermination'
 | 
			
		||||
        'device', '_connected_interface__device'
 | 
			
		||||
    ).filter(
 | 
			
		||||
        # Avoid duplicate connections by only selecting the lower PK in a connected pair
 | 
			
		||||
        Q(_connected_interface__isnull=False, pk__lt=F('_connected_interface')) |
 | 
			
		||||
        Q(_connected_circuittermination__isnull=False)
 | 
			
		||||
        _connected_interface__isnull=False,
 | 
			
		||||
        pk__lt=F('_connected_interface')
 | 
			
		||||
    )
 | 
			
		||||
    serializer_class = serializers.InterfaceConnectionSerializer
 | 
			
		||||
    filterset_class = filters.InterfaceConnectionFilter
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import django_filters
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from netaddr import EUI
 | 
			
		||||
from netaddr.core import AddrFormatError
 | 
			
		||||
@@ -8,7 +7,9 @@ from netaddr.core import AddrFormatError
 | 
			
		||||
from extras.filters import CustomFieldFilterSet
 | 
			
		||||
from tenancy.filters import TenancyFilterSet
 | 
			
		||||
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 .constants import *
 | 
			
		||||
from .models import (
 | 
			
		||||
@@ -49,14 +50,15 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSe
 | 
			
		||||
        choices=SITE_STATUS_CHOICES,
 | 
			
		||||
        null_value=None
 | 
			
		||||
    )
 | 
			
		||||
    region_id = django_filters.NumberFilter(
 | 
			
		||||
        method='filter_region',
 | 
			
		||||
        field_name='pk',
 | 
			
		||||
    region_id = TreeNodeMultipleChoiceFilter(
 | 
			
		||||
        queryset=Region.objects.all(),
 | 
			
		||||
        field_name='region__in',
 | 
			
		||||
        label='Region (ID)',
 | 
			
		||||
    )
 | 
			
		||||
    region = django_filters.CharFilter(
 | 
			
		||||
        method='filter_region',
 | 
			
		||||
        field_name='slug',
 | 
			
		||||
    region = TreeNodeMultipleChoiceFilter(
 | 
			
		||||
        queryset=Region.objects.all(),
 | 
			
		||||
        field_name='region__in',
 | 
			
		||||
        to_field_name='slug',
 | 
			
		||||
        label='Region (slug)',
 | 
			
		||||
    )
 | 
			
		||||
    tag = TagFilter()
 | 
			
		||||
@@ -85,16 +87,6 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, django_filters.FilterSe
 | 
			
		||||
            pass
 | 
			
		||||
        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):
 | 
			
		||||
    site_id = django_filters.ModelMultipleChoiceFilter(
 | 
			
		||||
@@ -473,14 +465,15 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
 | 
			
		||||
    )
 | 
			
		||||
    name = NullableCharFieldFilter()
 | 
			
		||||
    asset_tag = NullableCharFieldFilter()
 | 
			
		||||
    region_id = django_filters.NumberFilter(
 | 
			
		||||
        method='filter_region',
 | 
			
		||||
        field_name='pk',
 | 
			
		||||
    region_id = TreeNodeMultipleChoiceFilter(
 | 
			
		||||
        queryset=Region.objects.all(),
 | 
			
		||||
        field_name='site__region__in',
 | 
			
		||||
        label='Region (ID)',
 | 
			
		||||
    )
 | 
			
		||||
    region = django_filters.CharFilter(
 | 
			
		||||
        method='filter_region',
 | 
			
		||||
        field_name='slug',
 | 
			
		||||
    region = TreeNodeMultipleChoiceFilter(
 | 
			
		||||
        queryset=Region.objects.all(),
 | 
			
		||||
        field_name='site__region__in',
 | 
			
		||||
        to_field_name='slug',
 | 
			
		||||
        label='Region (slug)',
 | 
			
		||||
    )
 | 
			
		||||
    site_id = django_filters.ModelMultipleChoiceFilter(
 | 
			
		||||
@@ -579,16 +572,6 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
 | 
			
		||||
            Q(comments__icontains=value)
 | 
			
		||||
        ).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):
 | 
			
		||||
        value = value.strip()
 | 
			
		||||
        if not value:
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ class DeviceComponentManager(Manager):
 | 
			
		||||
            select={
 | 
			
		||||
                'name_padded': sql.format(table_name, table_name),
 | 
			
		||||
            }
 | 
			
		||||
        ).order_by('name_padded')
 | 
			
		||||
        ).order_by('name_padded', 'pk')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InterfaceQuerySet(QuerySet):
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,11 @@ def cache_changed_object(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'):
 | 
			
		||||
        instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -197,7 +197,7 @@ ROOT_URLCONF = 'netbox.urls'
 | 
			
		||||
TEMPLATES = [
 | 
			
		||||
    {
 | 
			
		||||
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
 | 
			
		||||
        'DIRS': [BASE_DIR + '/templates/'],
 | 
			
		||||
        'DIRS': [BASE_DIR + '/templates'],
 | 
			
		||||
        'APP_DIRS': True,
 | 
			
		||||
        'OPTIONS': {
 | 
			
		||||
            'context_processors': [
 | 
			
		||||
@@ -223,7 +223,7 @@ USE_I18N = True
 | 
			
		||||
USE_TZ = True
 | 
			
		||||
 | 
			
		||||
# Static files (CSS, JavaScript, Images)
 | 
			
		||||
STATIC_ROOT = BASE_DIR + '/static/'
 | 
			
		||||
STATIC_ROOT = BASE_DIR + '/static'
 | 
			
		||||
STATIC_URL = '/{}static/'.format(BASE_PATH)
 | 
			
		||||
STATICFILES_DIRS = (
 | 
			
		||||
    os.path.join(BASE_DIR, "project-static"),
 | 
			
		||||
 
 | 
			
		||||
@@ -267,6 +267,7 @@ class SearchView(View):
 | 
			
		||||
class APIRootView(APIView):
 | 
			
		||||
    _ignore_model_permissions = True
 | 
			
		||||
    exclude_from_schema = True
 | 
			
		||||
    swagger_schema = None
 | 
			
		||||
 | 
			
		||||
    def get_view_name(self):
 | 
			
		||||
        return "API Root"
 | 
			
		||||
 
 | 
			
		||||
@@ -36,13 +36,14 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Tenant
 | 
			
		||||
        fields = ['name']
 | 
			
		||||
        fields = ['name', 'slug']
 | 
			
		||||
 | 
			
		||||
    def search(self, queryset, name, value):
 | 
			
		||||
        if not value.strip():
 | 
			
		||||
            return queryset
 | 
			
		||||
        return queryset.filter(
 | 
			
		||||
            Q(name__icontains=value) |
 | 
			
		||||
            Q(slug__icontains=value) |
 | 
			
		||||
            Q(description__icontains=value) |
 | 
			
		||||
            Q(comments__icontains=value)
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,15 @@ from django.db.models import Q
 | 
			
		||||
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):
 | 
			
		||||
    """
 | 
			
		||||
    Filters for a set of numeric values. Example: id__in=100,200,300
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,8 @@ from netaddr.core import AddrFormatError
 | 
			
		||||
 | 
			
		||||
from dcim.models import DeviceRole, Interface, Platform, Region, Site
 | 
			
		||||
from extras.filters import CustomFieldFilterSet
 | 
			
		||||
from tenancy.filters import TenancyFilterSet
 | 
			
		||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
 | 
			
		||||
from tenancy.models import Tenant
 | 
			
		||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
 | 
			
		||||
from .constants import VM_STATUS_CHOICES
 | 
			
		||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 | 
			
		||||
 | 
			
		||||
@@ -80,7 +80,7 @@ class ClusterFilter(CustomFieldFilterSet):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet):
 | 
			
		||||
class VirtualMachineFilter(CustomFieldFilterSet):
 | 
			
		||||
    id__in = NumericInFilter(
 | 
			
		||||
        field_name='id',
 | 
			
		||||
        lookup_expr='in'
 | 
			
		||||
@@ -119,14 +119,15 @@ class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet):
 | 
			
		||||
        queryset=Cluster.objects.all(),
 | 
			
		||||
        label='Cluster (ID)',
 | 
			
		||||
    )
 | 
			
		||||
    region_id = django_filters.NumberFilter(
 | 
			
		||||
        method='filter_region',
 | 
			
		||||
        field_name='pk',
 | 
			
		||||
    region_id = TreeNodeMultipleChoiceFilter(
 | 
			
		||||
        queryset=Region.objects.all(),
 | 
			
		||||
        field_name='cluster__site__region__in',
 | 
			
		||||
        label='Region (ID)',
 | 
			
		||||
    )
 | 
			
		||||
    region = django_filters.CharFilter(
 | 
			
		||||
        method='filter_region',
 | 
			
		||||
        field_name='slug',
 | 
			
		||||
    region = TreeNodeMultipleChoiceFilter(
 | 
			
		||||
        queryset=Region.objects.all(),
 | 
			
		||||
        field_name='cluster__site__region__in',
 | 
			
		||||
        to_field_name='slug',
 | 
			
		||||
        label='Region (slug)',
 | 
			
		||||
    )
 | 
			
		||||
    site_id = django_filters.ModelMultipleChoiceFilter(
 | 
			
		||||
@@ -150,6 +151,16 @@ class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet):
 | 
			
		||||
        to_field_name='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(
 | 
			
		||||
        queryset=Platform.objects.all(),
 | 
			
		||||
        label='Platform (ID)',
 | 
			
		||||
@@ -174,16 +185,6 @@ class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet):
 | 
			
		||||
            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):
 | 
			
		||||
    q = django_filters.CharFilter(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user