Просмотр исходного кода

* Resolve conflict with virtualization filters.

dansheps 7 лет назад
Родитель
Сommit
3bb1cbcdb0

+ 6 - 0
CHANGELOG.md

@@ -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
 
 ---
 

+ 3 - 3
netbox/dcim/api/views.py

@@ -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

+ 17 - 34
netbox/dcim/filters.py

@@ -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:

+ 1 - 1
netbox/dcim/managers.py

@@ -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):

+ 5 - 1
netbox/extras/middleware.py

@@ -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)
 

+ 2 - 2
netbox/netbox/settings.py

@@ -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"),

+ 1 - 0
netbox/netbox/views.py

@@ -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"

+ 2 - 1
netbox/tenancy/filters.py

@@ -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)
         )

+ 9 - 0
netbox/utilities/filters.py

@@ -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

+ 20 - 19
netbox/virtualization/filters.py

@@ -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(