From 4668149943b6749cc2c1489c1c926c81f1478254 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 30 Oct 2017 17:20:22 -0400 Subject: [PATCH 1/9] Fixes #1649: Correct fitlering on null values (e.g. ?tenant_id=0) for django-filters v1.1.0+ --- netbox/circuits/filters.py | 6 ++-- netbox/dcim/filters.py | 44 +++++++++++++-------------- netbox/ipam/filters.py | 52 ++++++++++++++++---------------- netbox/netbox/settings.py | 4 +++ netbox/tenancy/filters.py | 6 ++-- netbox/utilities/filters.py | 49 ------------------------------ netbox/virtualization/filters.py | 22 +++++++------- requirements.txt | 2 +- 8 files changed, 70 insertions(+), 115 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 8a1b01a89..ea3831455 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -7,7 +7,7 @@ from django.db.models import Q from dcim.models import Site from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter +from utilities.filters import NumericInFilter from .models import Provider, Circuit, CircuitTermination, CircuitType @@ -78,11 +78,11 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Circuit type (slug)', ) - tenant_id = NullableModelMultipleChoiceFilter( + tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = NullableModelMultipleChoiceFilter( + tenant = django_filters.ModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 6e0c6fa3d..6c4095365 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -9,7 +9,7 @@ from django.db.models import Q from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter +from utilities.filters import NullableCharFieldFilter, NumericInFilter from virtualization.models import Cluster from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -21,11 +21,11 @@ from .models import ( class RegionFilter(django_filters.FilterSet): - parent_id = NullableModelMultipleChoiceFilter( + parent_id = django_filters.ModelMultipleChoiceFilter( queryset=Region.objects.all(), label='Parent region (ID)', ) - parent = NullableModelMultipleChoiceFilter( + parent = django_filters.ModelMultipleChoiceFilter( queryset=Region.objects.all(), to_field_name='slug', label='Parent region (slug)', @@ -42,20 +42,20 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): method='search', label='Search', ) - region_id = NullableModelMultipleChoiceFilter( + region_id = django_filters.ModelMultipleChoiceFilter( queryset=Region.objects.all(), label='Region (ID)', ) - region = NullableModelMultipleChoiceFilter( + region = django_filters.ModelMultipleChoiceFilter( queryset=Region.objects.all(), to_field_name='slug', label='Region (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)', @@ -126,31 +126,31 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Site (slug)', ) - group_id = NullableModelMultipleChoiceFilter( + group_id = django_filters.ModelMultipleChoiceFilter( queryset=RackGroup.objects.all(), label='Group (ID)', ) - group = NullableModelMultipleChoiceFilter( + group = django_filters.ModelMultipleChoiceFilter( name='group', queryset=RackGroup.objects.all(), to_field_name='slug', label='Group', ) - tenant_id = NullableModelMultipleChoiceFilter( + tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = NullableModelMultipleChoiceFilter( + tenant = django_filters.ModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', ) - role_id = NullableModelMultipleChoiceFilter( + role_id = django_filters.ModelMultipleChoiceFilter( queryset=RackRole.objects.all(), label='Role (ID)', ) - role = NullableModelMultipleChoiceFilter( + role = django_filters.ModelMultipleChoiceFilter( name='role', queryset=RackRole.objects.all(), to_field_name='slug', @@ -193,12 +193,12 @@ class RackReservationFilter(django_filters.FilterSet): to_field_name='slug', label='Site (slug)', ) - group_id = NullableModelMultipleChoiceFilter( + group_id = django_filters.ModelMultipleChoiceFilter( name='rack__group', queryset=RackGroup.objects.all(), label='Group (ID)', ) - group = NullableModelMultipleChoiceFilter( + group = django_filters.ModelMultipleChoiceFilter( name='rack__group', queryset=RackGroup.objects.all(), to_field_name='slug', @@ -368,21 +368,21 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): 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( name='tenant', 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( name='platform', queryset=Platform.objects.all(), to_field_name='slug', @@ -405,12 +405,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): queryset=RackGroup.objects.all(), label='Rack group (ID)', ) - rack_id = NullableModelMultipleChoiceFilter( + rack_id = django_filters.ModelMultipleChoiceFilter( name='rack', queryset=Rack.objects.all(), label='Rack (ID)', ) - cluster_id = NullableModelMultipleChoiceFilter( + cluster_id = django_filters.ModelMultipleChoiceFilter( queryset=Cluster.objects.all(), label='VM cluster (ID)', ) @@ -595,7 +595,7 @@ class DeviceBayFilter(DeviceComponentFilterSet): class InventoryItemFilter(DeviceComponentFilterSet): - parent_id = NullableModelMultipleChoiceFilter( + parent_id = django_filters.ModelMultipleChoiceFilter( queryset=InventoryItem.objects.all(), label='Parent inventory item (ID)', ) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index b8996c74b..d6ce6b987 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -9,7 +9,7 @@ from django.db.models import Q from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter +from utilities.filters import NumericInFilter from virtualization.models import VirtualMachine from .models import ( Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, @@ -23,11 +23,11 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): method='search', label='Search', ) - tenant_id = NullableModelMultipleChoiceFilter( + tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = NullableModelMultipleChoiceFilter( + tenant = django_filters.ModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', @@ -110,37 +110,37 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): method='filter_mask_length', label='Mask length', ) - vrf_id = NullableModelMultipleChoiceFilter( + vrf_id = django_filters.ModelMultipleChoiceFilter( queryset=VRF.objects.all(), label='VRF', ) - vrf = NullableModelMultipleChoiceFilter( + vrf = django_filters.ModelMultipleChoiceFilter( name='vrf', queryset=VRF.objects.all(), to_field_name='rd', label='VRF (RD)', ) - tenant_id = NullableModelMultipleChoiceFilter( + tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = NullableModelMultipleChoiceFilter( + tenant = django_filters.ModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', ) - site_id = NullableModelMultipleChoiceFilter( + site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', ) - site = NullableModelMultipleChoiceFilter( + site = django_filters.ModelMultipleChoiceFilter( name='site', queryset=Site.objects.all(), to_field_name='slug', label='Site (slug)', ) - vlan_id = NullableModelMultipleChoiceFilter( + vlan_id = django_filters.ModelMultipleChoiceFilter( queryset=VLAN.objects.all(), label='VLAN (ID)', ) @@ -148,11 +148,11 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): name='vlan__vid', label='VLAN number (1-4095)', ) - role_id = NullableModelMultipleChoiceFilter( + role_id = django_filters.ModelMultipleChoiceFilter( queryset=Role.objects.all(), label='Role (ID)', ) - role = NullableModelMultipleChoiceFilter( + role = django_filters.ModelMultipleChoiceFilter( name='role', queryset=Role.objects.all(), to_field_name='slug', @@ -207,21 +207,21 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): method='filter_mask_length', label='Mask length', ) - vrf_id = NullableModelMultipleChoiceFilter( + vrf_id = django_filters.ModelMultipleChoiceFilter( queryset=VRF.objects.all(), label='VRF', ) - vrf = NullableModelMultipleChoiceFilter( + vrf = django_filters.ModelMultipleChoiceFilter( name='vrf', queryset=VRF.objects.all(), to_field_name='rd', label='VRF (RD)', ) - tenant_id = NullableModelMultipleChoiceFilter( + tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = NullableModelMultipleChoiceFilter( + tenant = django_filters.ModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', @@ -290,11 +290,11 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): class VLANGroupFilter(django_filters.FilterSet): - site_id = NullableModelMultipleChoiceFilter( + site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', ) - site = NullableModelMultipleChoiceFilter( + site = django_filters.ModelMultipleChoiceFilter( name='site', queryset=Site.objects.all(), to_field_name='slug', @@ -312,41 +312,41 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): method='search', label='Search', ) - site_id = NullableModelMultipleChoiceFilter( + site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', ) - site = NullableModelMultipleChoiceFilter( + site = django_filters.ModelMultipleChoiceFilter( name='site', queryset=Site.objects.all(), to_field_name='slug', label='Site (slug)', ) - group_id = NullableModelMultipleChoiceFilter( + group_id = django_filters.ModelMultipleChoiceFilter( queryset=VLANGroup.objects.all(), label='Group (ID)', ) - group = NullableModelMultipleChoiceFilter( + group = django_filters.ModelMultipleChoiceFilter( name='group', queryset=VLANGroup.objects.all(), to_field_name='slug', label='Group', ) - tenant_id = NullableModelMultipleChoiceFilter( + tenant_id = django_filters.ModelMultipleChoiceFilter( queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = NullableModelMultipleChoiceFilter( + tenant = django_filters.ModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', ) - role_id = NullableModelMultipleChoiceFilter( + role_id = django_filters.ModelMultipleChoiceFilter( queryset=Role.objects.all(), label='Role (ID)', ) - role = NullableModelMultipleChoiceFilter( + role = django_filters.ModelMultipleChoiceFilter( name='role', queryset=Role.objects.all(), to_field_name='slug', diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index be6311d8b..572d5a40a 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -206,6 +206,10 @@ LOGIN_URL = '/{}login/'.format(BASE_PATH) # Secrets SECRETS_MIN_PUBKEY_SIZE = 2048 +# Django filters +FILTERS_NULL_CHOICE_LABEL = 'None' +FILTERS_NULL_CHOICE_VALUE = '0' # Must be a string + # Django REST framework (API) REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version REST_FRAMEWORK = { diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 630e936e4..275d998e2 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -5,7 +5,7 @@ import django_filters 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 5bd635a46..de671cd0a 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -4,7 +4,6 @@ import django_filters import itertools from django import forms -from django.db.models import Q from django.utils.encoding import force_text @@ -66,51 +65,3 @@ class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField): 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 4ddad4d5b..123cd30af 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -9,7 +9,7 @@ from django.db.models import Q 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/requirements.txt b/requirements.txt index cdda3cf1d..303d2ad47 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 From bbd0761887a6b01736195e79530a40ddf42d3790 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 30 Oct 2017 17:45:05 -0400 Subject: [PATCH 2/9] Fixes #1653: Remove outdated description for DeviceType's is_network_device flag --- netbox/templates/dcim/devicetype.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 9eb3e0a5e..12281734b 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -120,7 +120,7 @@ Network Device
- This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} non-management network interfaces + This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} network interfaces From d08bc7767e02741b60dbe705b089a4229a865f6e Mon Sep 17 00:00:00 2001 From: wanglf Date: Tue, 31 Oct 2017 22:05:30 +0800 Subject: [PATCH 3/9] Fixed missing field 'serial' in function to_csv() --- netbox/dcim/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index e86c702f4..5d2acb09b 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -296,6 +296,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): self.tenant.name if self.tenant else None, self.role.name if self.role else None, self.get_type_display() if self.type else None, + self.serial, self.width, self.u_height, self.desc_units, From b46cc2c1a9969540240a525dae9e2c5db7cb6aae Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Oct 2017 13:52:35 -0400 Subject: [PATCH 4/9] Closes #1666: Allow modifying the owner of a rack reservation --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/forms.py | 13 ++++++++++- .../0049_rackreservation_change_user.py | 22 +++++++++++++++++++ netbox/dcim/models.py | 2 +- netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 10 +++++++++ .../templates/dcim/rackreservation_list.html | 2 +- 7 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 netbox/dcim/migrations/0049_rackreservation_change_user.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index fa0f6dc14..f92c1f64e 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -221,7 +221,7 @@ class WritableRackReservationSerializer(ValidatedModelSerializer): class Meta: model = RackReservation - fields = ['id', 'rack', 'units', 'description'] + fields = ['id', 'rack', 'units', 'user', 'description'] # diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c3a545f7c..c4ec531f3 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -4,6 +4,7 @@ from mptt.forms import TreeNodeChoiceField import re from django import forms +from django.contrib.auth.models import User from django.contrib.postgres.forms.array import SimpleArrayField from django.db.models import Count, Q @@ -376,10 +377,11 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): class RackReservationForm(BootstrapMixin, forms.ModelForm): units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10})) + user = forms.ModelChoiceField(queryset=User.objects.order_by('username')) class Meta: model = RackReservation - fields = ['units', 'description'] + fields = ['units', 'user', 'description'] def __init__(self, *args, **kwargs): @@ -411,6 +413,15 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form): ) +class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField(queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput) + user = forms.ModelChoiceField(queryset=User.objects.order_by('username'), required=False) + description = forms.CharField(max_length=100, required=False) + + class Meta: + nullable_fields = [] + + # # Manufacturers # diff --git a/netbox/dcim/migrations/0049_rackreservation_change_user.py b/netbox/dcim/migrations/0049_rackreservation_change_user.py new file mode 100644 index 000000000..ae9f95246 --- /dev/null +++ b/netbox/dcim/migrations/0049_rackreservation_change_user.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-31 17:32 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0048_rack_serial'), + ] + + operations = [ + migrations.AlterField( + model_name='rackreservation', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 5d2acb09b..daa4c80cd 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -418,7 +418,7 @@ class RackReservation(models.Model): rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE) units = ArrayField(models.PositiveSmallIntegerField()) created = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey(User, editable=False, on_delete=models.PROTECT) + user = models.ForeignKey(User, on_delete=models.PROTECT) description = models.CharField(max_length=100) class Meta: diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 633cf9a8b..2d00f096d 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -45,6 +45,7 @@ urlpatterns = [ # Rack reservations url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'), + url(r'^rack-reservations/edit/$', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'), url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), url(r'^rack-reservations/(?P\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'), url(r'^rack-reservations/(?P\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index e0ab7f1f2..3120197d2 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -426,6 +426,16 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): return obj.rack.get_absolute_url() +class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_rackreservation' + cls = RackReservation + queryset = RackReservation.objects.select_related('rack', 'user') + filter = filters.RackReservationFilter + table = tables.RackReservationTable + form = forms.RackReservationBulkEditForm + default_return_url = 'dcim:rackreservation_list' + + class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackreservation' cls = RackReservation diff --git a/netbox/templates/dcim/rackreservation_list.html b/netbox/templates/dcim/rackreservation_list.html index b93dc0bf1..f1524d328 100644 --- a/netbox/templates/dcim/rackreservation_list.html +++ b/netbox/templates/dcim/rackreservation_list.html @@ -5,7 +5,7 @@

{% block title %}Rack Reservations{% endblock %}

- {% 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' %} From cfa6bee08116211d0d8eebb124911c589d571343 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Oct 2017 13:58:27 -0400 Subject: [PATCH 5/9] Release v2.2.3 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 572d5a40a..af5e6dbaa 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.2.3-dev' +VERSION = '2.2.3' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From b9cd834e9548aac4adf1cab0c6132d0283f71499 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Oct 2017 14:07:19 -0400 Subject: [PATCH 6/9] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index af5e6dbaa..80f3952c0 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.2.3' +VERSION = '2.2.4-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 85c273c8ca454163b0099b077f176ad9f461da32 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Oct 2017 14:47:14 -0400 Subject: [PATCH 7/9] Fixes #1670: Corrected filter names (regression from #1649) --- netbox/circuits/filters.py | 2 +- netbox/dcim/filters.py | 15 +++++++++------ netbox/ipam/filters.py | 24 ++++++++++++------------ netbox/tenancy/filters.py | 2 +- netbox/virtualization/filters.py | 6 +++++- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index ea3831455..e2d5321cd 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -83,7 +83,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Tenant (ID)', ) tenant = django_filters.ModelMultipleChoiceFilter( - name='tenant', + name='tenant__slug', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 6c4095365..6316cad4e 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -26,6 +26,7 @@ class RegionFilter(django_filters.FilterSet): label='Parent region (ID)', ) parent = django_filters.ModelMultipleChoiceFilter( + name='parent__slug', queryset=Region.objects.all(), to_field_name='slug', label='Parent region (slug)', @@ -47,6 +48,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Region (ID)', ) region = django_filters.ModelMultipleChoiceFilter( + name='region__slug', queryset=Region.objects.all(), to_field_name='slug', label='Region (slug)', @@ -56,6 +58,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Tenant (ID)', ) tenant = django_filters.ModelMultipleChoiceFilter( + name='tenant__slug', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', @@ -131,7 +134,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Group (ID)', ) group = django_filters.ModelMultipleChoiceFilter( - name='group', + name='group__slug', queryset=RackGroup.objects.all(), to_field_name='slug', label='Group', @@ -141,7 +144,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Tenant (ID)', ) tenant = django_filters.ModelMultipleChoiceFilter( - name='tenant', + name='tenant__slug', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', @@ -151,7 +154,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Role (ID)', ) role = django_filters.ModelMultipleChoiceFilter( - name='role', + name='role__slug', queryset=RackRole.objects.all(), to_field_name='slug', label='Role (slug)', @@ -199,7 +202,7 @@ class RackReservationFilter(django_filters.FilterSet): label='Group (ID)', ) group = django_filters.ModelMultipleChoiceFilter( - name='rack__group', + name='rack__group__slug', queryset=RackGroup.objects.all(), to_field_name='slug', label='Group', @@ -373,7 +376,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Tenant (ID)', ) tenant = django_filters.ModelMultipleChoiceFilter( - name='tenant', + name='tenant__slug', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', @@ -383,7 +386,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Platform (ID)', ) platform = django_filters.ModelMultipleChoiceFilter( - name='platform', + name='platform__slug', queryset=Platform.objects.all(), to_field_name='slug', label='Platform (slug)', diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index d6ce6b987..23a7bdfa5 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -28,7 +28,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Tenant (ID)', ) tenant = django_filters.ModelMultipleChoiceFilter( - name='tenant', + name='tenant__slug', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', @@ -115,7 +115,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): label='VRF', ) vrf = django_filters.ModelMultipleChoiceFilter( - name='vrf', + name='vrf__rd', queryset=VRF.objects.all(), to_field_name='rd', label='VRF (RD)', @@ -125,7 +125,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Tenant (ID)', ) tenant = django_filters.ModelMultipleChoiceFilter( - name='tenant', + name='tenant__slug', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', @@ -135,7 +135,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Site (ID)', ) site = django_filters.ModelMultipleChoiceFilter( - name='site', + name='site__slug', queryset=Site.objects.all(), to_field_name='slug', label='Site (slug)', @@ -153,7 +153,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Role (ID)', ) role = django_filters.ModelMultipleChoiceFilter( - name='role', + name='role__slug', queryset=Role.objects.all(), to_field_name='slug', label='Role (slug)', @@ -212,7 +212,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): label='VRF', ) vrf = django_filters.ModelMultipleChoiceFilter( - name='vrf', + name='vrf__rd', queryset=VRF.objects.all(), to_field_name='rd', label='VRF (RD)', @@ -222,7 +222,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Tenant (ID)', ) tenant = django_filters.ModelMultipleChoiceFilter( - name='tenant', + name='tenant__slug', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', @@ -295,7 +295,7 @@ class VLANGroupFilter(django_filters.FilterSet): label='Site (ID)', ) site = django_filters.ModelMultipleChoiceFilter( - name='site', + name='site__slug', queryset=Site.objects.all(), to_field_name='slug', label='Site (slug)', @@ -317,7 +317,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Site (ID)', ) site = django_filters.ModelMultipleChoiceFilter( - name='site', + name='site__slug', queryset=Site.objects.all(), to_field_name='slug', label='Site (slug)', @@ -327,7 +327,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Group (ID)', ) group = django_filters.ModelMultipleChoiceFilter( - name='group', + name='group__slug', queryset=VLANGroup.objects.all(), to_field_name='slug', label='Group', @@ -337,7 +337,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Tenant (ID)', ) tenant = django_filters.ModelMultipleChoiceFilter( - name='tenant', + name='tenant__slug', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', @@ -347,7 +347,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Role (ID)', ) role = django_filters.ModelMultipleChoiceFilter( - name='role', + name='role__slug', queryset=Role.objects.all(), to_field_name='slug', label='Role (slug)', diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 275d998e2..090a0f9ee 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -27,7 +27,7 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet): label='Group (ID)', ) group = django_filters.ModelMultipleChoiceFilter( - name='group', + name='group__slug', queryset=TenantGroup.objects.all(), to_field_name='slug', label='Group (slug)', diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 123cd30af..c661bc973 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -25,6 +25,7 @@ class ClusterFilter(CustomFieldFilterSet): label='Parent group (ID)', ) group = django_filters.ModelMultipleChoiceFilter( + name='group__slug', queryset=ClusterGroup.objects.all(), to_field_name='slug', label='Parent group (slug)', @@ -78,7 +79,7 @@ class VirtualMachineFilter(CustomFieldFilterSet): label='Cluster group (ID)', ) cluster_group = django_filters.ModelMultipleChoiceFilter( - name='cluster__group', + name='cluster__group__slug', queryset=ClusterGroup.objects.all(), to_field_name='slug', label='Cluster group (slug)', @@ -92,6 +93,7 @@ class VirtualMachineFilter(CustomFieldFilterSet): label='Role (ID)', ) role = django_filters.ModelMultipleChoiceFilter( + name='role__slug', queryset=DeviceRole.objects.all(), to_field_name='slug', label='Role (slug)', @@ -101,6 +103,7 @@ class VirtualMachineFilter(CustomFieldFilterSet): label='Tenant (ID)', ) tenant = django_filters.ModelMultipleChoiceFilter( + name='tenant__slug', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', @@ -110,6 +113,7 @@ class VirtualMachineFilter(CustomFieldFilterSet): label='Platform (ID)', ) platform = django_filters.ModelMultipleChoiceFilter( + name='platform__slug', queryset=Platform.objects.all(), to_field_name='slug', label='Platform (slug)', From 76ebd2d34fe3bb8ec7be8c8de1df8bdf0e629183 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Oct 2017 15:17:35 -0400 Subject: [PATCH 8/9] Release v2.2.4 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 80f3952c0..64ea556bc 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.2.4-dev' +VERSION = '2.2.4' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From e56fc4b1ee88491328416d5bf68f177dea02fbb8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Oct 2017 15:25:59 -0400 Subject: [PATCH 9/9] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 64ea556bc..0283731fe 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.2.4' +VERSION = '2.2.5-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))