- {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackreservation_bulk_delete' %}
+ {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rackreservation_bulk_edit' bulk_delete_url='dcim:rackreservation_bulk_delete' %}
{% include 'inc/search_panel.html' %}
diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py
index 630e936e4b6..275d998e22a 100644
--- a/netbox/tenancy/filters.py
+++ b/netbox/tenancy/filters.py
@@ -5,7 +5,7 @@
from django.db.models import Q
from extras.filters import CustomFieldFilterSet
-from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
+from utilities.filters import NumericInFilter
from .models import Tenant, TenantGroup
@@ -22,11 +22,11 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search',
label='Search',
)
- group_id = NullableModelMultipleChoiceFilter(
+ group_id = django_filters.ModelMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
label='Group (ID)',
)
- group = NullableModelMultipleChoiceFilter(
+ group = django_filters.ModelMultipleChoiceFilter(
name='group',
queryset=TenantGroup.objects.all(),
to_field_name='slug',
diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py
index 5bd635a4627..de671cd0af7 100644
--- a/netbox/utilities/filters.py
+++ b/netbox/utilities/filters.py
@@ -4,7 +4,6 @@
import itertools
from django import forms
-from django.db.models import Q
from django.utils.encoding import force_text
@@ -66,51 +65,3 @@ def clean(self, value):
stripped_value = value
super(NullableModelMultipleChoiceField, self).clean(stripped_value)
return value
-
-
-class NullableModelMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
- """
- This class extends ModelMultipleChoiceFilter to accept an additional value which implies "is null". The default
- queryset filter argument is:
-
- .filter(fieldname=value)
-
- When filtering by the value representing "is null" ('0' by default) the argument is modified to:
-
- .filter(fieldname__isnull=True)
- """
- field_class = NullableModelMultipleChoiceField
-
- def __init__(self, *args, **kwargs):
- self.null_value = kwargs.get('null_value', 0)
- super(NullableModelMultipleChoiceFilter, self).__init__(*args, **kwargs)
-
- def filter(self, qs, value):
- value = value or () # Make sure we have an iterable
-
- if self.is_noop(qs, value):
- return qs
-
- # Even though not a noop, no point filtering if empty
- if not value:
- return qs
-
- q = Q()
- for v in set(value):
- # Filtering by "is null"
- if v == force_text(self.null_value):
- arg = {'{}__isnull'.format(self.name): True}
- # Filtering by a related field (e.g. slug)
- elif self.field.to_field_name is not None:
- arg = {'{}__{}'.format(self.name, self.field.to_field_name): v}
- # Filtering by primary key (default)
- else:
- arg = {self.name: v}
- if self.conjoined:
- qs = self.get_method(qs)(**arg)
- else:
- q |= Q(**arg)
- if self.distinct:
- return self.get_method(qs)(q).distinct()
-
- return self.get_method(qs)(q)
diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py
index 4ddad4d5ba3..123cd30af37 100644
--- a/netbox/virtualization/filters.py
+++ b/netbox/virtualization/filters.py
@@ -9,7 +9,7 @@
from dcim.models import DeviceRole, Interface, Platform, Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
-from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
+from utilities.filters import NumericInFilter
from .constants import STATUS_CHOICES
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -20,11 +20,11 @@ class ClusterFilter(CustomFieldFilterSet):
method='search',
label='Search',
)
- group_id = NullableModelMultipleChoiceFilter(
+ group_id = django_filters.ModelMultipleChoiceFilter(
queryset=ClusterGroup.objects.all(),
label='Parent group (ID)',
)
- group = NullableModelMultipleChoiceFilter(
+ group = django_filters.ModelMultipleChoiceFilter(
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
label='Parent group (slug)',
@@ -72,12 +72,12 @@ class VirtualMachineFilter(CustomFieldFilterSet):
status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES
)
- cluster_group_id = NullableModelMultipleChoiceFilter(
+ cluster_group_id = django_filters.ModelMultipleChoiceFilter(
name='cluster__group',
queryset=ClusterGroup.objects.all(),
label='Cluster group (ID)',
)
- cluster_group = NullableModelMultipleChoiceFilter(
+ cluster_group = django_filters.ModelMultipleChoiceFilter(
name='cluster__group',
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
@@ -87,29 +87,29 @@ class VirtualMachineFilter(CustomFieldFilterSet):
queryset=Cluster.objects.all(),
label='Cluster (ID)',
)
- role_id = NullableModelMultipleChoiceFilter(
+ role_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceRole.objects.all(),
label='Role (ID)',
)
- role = NullableModelMultipleChoiceFilter(
+ role = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
- tenant_id = NullableModelMultipleChoiceFilter(
+ tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
- tenant = NullableModelMultipleChoiceFilter(
+ tenant = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
- platform_id = NullableModelMultipleChoiceFilter(
+ platform_id = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
label='Platform (ID)',
)
- platform = NullableModelMultipleChoiceFilter(
+ platform = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
to_field_name='slug',
label='Platform (slug)',
diff --git a/netbox/virtualization/fixtures/initial_data.json b/netbox/virtualization/fixtures/initial_data.json
new file mode 100644
index 00000000000..6f57baffa82
--- /dev/null
+++ b/netbox/virtualization/fixtures/initial_data.json
@@ -0,0 +1,91 @@
+[
+{
+ "model": "virtualization.clustertype",
+ "pk": 1,
+ "fields": {
+ "name": "Public Cloud",
+ "slug": "public-cloud"
+ }
+},
+{
+ "model": "virtualization.clustertype",
+ "pk": 2,
+ "fields": {
+ "name": "vSphere",
+ "slug": "vsphere"
+ }
+},
+{
+ "model": "virtualization.clustertype",
+ "pk": 3,
+ "fields": {
+ "name": "Hyper-V",
+ "slug": "hyper-v"
+ }
+},
+{
+ "model": "virtualization.clustertype",
+ "pk": 4,
+ "fields": {
+ "name": "libvirt",
+ "slug": "libvirt"
+ }
+},
+{
+ "model": "virtualization.clustertype",
+ "pk": 5,
+ "fields": {
+ "name": "LXD",
+ "slug": "lxd"
+ }
+},
+{
+ "model": "virtualization.clustertype",
+ "pk": 6,
+ "fields": {
+ "name": "Docker",
+ "slug": "docker"
+ }
+},
+{
+ "model": "virtualization.clustergroup",
+ "pk": 1,
+ "fields": {
+ "name": "VM Host",
+ "slug": "vm-host"
+ }
+},
+{
+ "model": "virtualization.cluster",
+ "pk": 1,
+ "fields": {
+ "name": "Digital Ocean",
+ "type": 1,
+ "group": 1,
+ "created": "2016-08-01",
+ "last_updated": "2016-08-01T15:22:42.289Z"
+ }
+},
+{
+ "model": "virtualization.cluster",
+ "pk": 2,
+ "fields": {
+ "name": "Amazon EC2",
+ "type": 1,
+ "group": 1,
+ "created": "2016-08-01",
+ "last_updated": "2016-08-01T15:22:42.289Z"
+ }
+},
+{
+ "model": "virtualization.cluster",
+ "pk": 3,
+ "fields": {
+ "name": "Microsoft Azure",
+ "type": 1,
+ "group": 1,
+ "created": "2016-08-01",
+ "last_updated": "2016-08-01T15:22:42.289Z"
+ }
+}
+]
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index ad49b7dec14..00f59f8d4ae 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -314,6 +314,7 @@ class VirtualMachineBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'virtualization.delete_virtualmachine'
cls = VirtualMachine
queryset = VirtualMachine.objects.select_related('cluster', 'tenant')
+ filter = filters.VirtualMachineFilter
table = tables.VirtualMachineTable
default_return_url = 'virtualization:virtualmachine_list'
diff --git a/requirements.txt b/requirements.txt
index cdda3cf1d04..303d2ad47f7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
Django>=1.11,<2.0
django-cors-headers>=2.1
django-debug-toolbar>=1.8
-django-filter>=1.0.4
+django-filter>=1.1.0
django-mptt==0.8.7
django-rest-swagger>=2.1.0
django-tables2>=1.10.0