mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-21 11:37:21 -06:00
* Resolve conflict with virtualization filters.
This commit is contained in:
commit
3bb1cbcdb0
@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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"),
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user