From ed0344916424ff0f4c8b756c900e16864eb7e6e9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 10 Aug 2016 11:52:27 -0400 Subject: [PATCH] Closes #241: Introduced rack roles --- netbox/dcim/admin.py | 12 +++- netbox/dcim/api/serializers.py | 28 ++++++++-- netbox/dcim/api/urls.py | 4 ++ netbox/dcim/api/views.py | 22 +++++++- netbox/dcim/filters.py | 13 ++++- netbox/dcim/forms.py | 58 ++++++++++++++++++-- netbox/dcim/migrations/0017_rack_add_role.py | 33 +++++++++++ netbox/dcim/models.py | 34 +++++++++++- netbox/dcim/tables.py | 27 ++++++++- netbox/dcim/tests/test_apis.py | 3 + netbox/dcim/urls.py | 6 ++ netbox/dcim/views.py | 38 +++++++++++-- netbox/templates/_base.html | 5 ++ netbox/templates/dcim/rackrole_list.html | 21 +++++++ 14 files changed, 280 insertions(+), 24 deletions(-) create mode 100644 netbox/dcim/migrations/0017_rack_add_role.py create mode 100644 netbox/templates/dcim/rackrole_list.html diff --git a/netbox/dcim/admin.py b/netbox/dcim/admin.py index 95a84be00..001a685c7 100644 --- a/netbox/dcim/admin.py +++ b/netbox/dcim/admin.py @@ -4,7 +4,7 @@ from django.db.models import Count from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform, - PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site, + PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site, ) @@ -24,9 +24,17 @@ class RackGroupAdmin(admin.ModelAdmin): } +@admin.register(RackRole) +class RackRoleAdmin(admin.ModelAdmin): + list_display = ['name', 'slug', 'color'] + prepopulated_fields = { + 'slug': ['name'], + } + + @admin.register(Rack) class RackAdmin(admin.ModelAdmin): - list_display = ['name', 'facility_id', 'site', 'type', 'width', 'u_height'] + list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height'] # diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d40488301..a0a2776f1 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -4,7 +4,7 @@ from ipam.models import IPAddress from dcim.models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, - PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site, + PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site, ) from tenancy.api.serializers import TenantNestedSerializer @@ -46,6 +46,23 @@ class RackGroupNestedSerializer(RackGroupSerializer): fields = ['id', 'name', 'slug'] +# +# Rack roles +# + +class RackRoleSerializer(serializers.ModelSerializer): + + class Meta: + model = RackRole + fields = ['id', 'name', 'slug', 'color'] + + +class RackRoleNestedSerializer(RackRoleSerializer): + + class Meta(RackRoleSerializer.Meta): + fields = ['id', 'name', 'slug'] + + # # Racks # @@ -55,11 +72,12 @@ class RackSerializer(serializers.ModelSerializer): site = SiteNestedSerializer() group = RackGroupNestedSerializer() tenant = TenantNestedSerializer() + role = RackRoleNestedSerializer() class Meta: model = Rack - fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'type', 'width', 'u_height', - 'comments'] + fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', + 'u_height', 'comments'] class RackNestedSerializer(RackSerializer): @@ -73,8 +91,8 @@ class RackDetailSerializer(RackSerializer): rear_units = serializers.SerializerMethodField() class Meta(RackSerializer.Meta): - fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'type', 'width', 'u_height', - 'comments', 'front_units', 'rear_units'] + fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', + 'u_height', 'comments', 'front_units', 'rear_units'] def get_front_units(self, obj): units = obj.get_rack_units(face=RACK_FACE_FRONT) diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 024a06f67..23787f4b4 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -18,6 +18,10 @@ urlpatterns = [ url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'), url(r'^rack-groups/(?P\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'), + # Rack roles + url(r'^rack-roles/$', RackRoleListView.as_view(), name='rackrole_list'), + url(r'^rack-roles/(?P\d+)/$', RackRoleDetailView.as_view(), name='rackrole_detail'), + # Racks url(r'^racks/$', RackListView.as_view(), name='rack_list'), url(r'^racks/(?P\d+)/$', RackDetailView.as_view(), name='rack_detail'), diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 02dbec2c3..5bde03a6e 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404 from dcim.models import ( ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface, - InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site, + InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site, ) from dcim import filters from .exceptions import MissingFilterException @@ -60,6 +60,26 @@ class RackGroupDetailView(generics.RetrieveAPIView): serializer_class = serializers.RackGroupSerializer +# +# Rack roles +# + +class RackRoleListView(generics.ListAPIView): + """ + List all rack roles + """ + queryset = RackRole.objects.all() + serializer_class = serializers.RackRoleSerializer + + +class RackRoleDetailView(generics.RetrieveAPIView): + """ + Retrieve a single rack role + """ + queryset = RackRole.objects.all() + serializer_class = serializers.RackRoleSerializer + + # # Racks # diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 1800bf266..b124d53dd 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -4,7 +4,7 @@ from django.db.models import Q from .models import ( ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer, - Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site, + Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site, ) from tenancy.models import Tenant @@ -96,6 +96,17 @@ class RackFilter(django_filters.FilterSet): to_field_name='slug', label='Tenant (slug)', ) + role_id = django_filters.ModelMultipleChoiceFilter( + name='role', + queryset=RackRole.objects.all(), + label='Role (ID)', + ) + role = django_filters.ModelMultipleChoiceFilter( + name='role', + queryset=RackRole.objects.all(), + to_field_name='slug', + label='Role (slug)', + ) class Meta: model = Rack diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index f85bcb327..56e786a26 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -15,8 +15,8 @@ from .models import ( DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, - PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, Site, - STATUS_CHOICES, SUBDEVICE_ROLE_CHILD + PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole, + Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD ) @@ -50,6 +50,30 @@ def bulkedit_platform_choices(): return choices +def bulkedit_rackgroup_choices(): + """ + Include an option to remove the currently assigned group from a rack. + """ + choices = [ + (None, '---------'), + (0, 'None'), + ] + choices += [(r.pk, r) for r in RackGroup.objects.all()] + return choices + + +def bulkedit_rackrole_choices(): + """ + Include an option to remove the currently assigned role from a rack. + """ + choices = [ + (None, '---------'), + (0, 'None'), + ] + choices += [(r.pk, r.name) for r in RackRole.objects.all()] + return choices + + # # Sites # @@ -124,6 +148,18 @@ class RackGroupFilterForm(forms.Form, BootstrapMixin): widget=forms.SelectMultiple(attrs={'size': 8})) +# +# Rack roles +# + +class RackRoleForm(forms.ModelForm, BootstrapMixin): + slug = SlugField() + + class Meta: + model = RackRole + fields = ['name', 'slug', 'color'] + + # # Racks # @@ -136,7 +172,7 @@ class RackForm(forms.ModelForm, BootstrapMixin): class Meta: model = Rack - fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'type', 'width', 'u_height', 'comments'] + fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'comments'] help_texts = { 'site': "The site at which the rack exists", 'name': "Organizational rack name", @@ -166,11 +202,13 @@ class RackFromCSVForm(forms.ModelForm): group_name = forms.CharField(required=False) tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, error_messages={'invalid_choice': 'Tenant not found.'}) + role = forms.ModelChoiceField(RackRole.objects.all(), to_field_name='name', required=False, + error_messages={'invalid_choice': 'Role not found.'}) type = forms.CharField(required=False) class Meta: model = Rack - fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'type', 'width', 'u_height'] + fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height'] def clean(self): @@ -204,9 +242,10 @@ class RackImportForm(BulkImportForm, BootstrapMixin): class RackBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput) - site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) - group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False) + site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site') + group = forms.TypedChoiceField(choices=bulkedit_rackgroup_choices, coerce=int, required=False, label='Group') tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') + role = forms.TypedChoiceField(choices=bulkedit_rackrole_choices, coerce=int, required=False, label='Role') type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type') width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width') u_height = forms.IntegerField(required=False, label='Height (U)') @@ -228,6 +267,11 @@ def rack_tenant_choices(): return [(t.slug, u'{} ({})'.format(t.name, t.rack_count)) for t in tenant_choices] +def rack_role_choices(): + role_choices = RackRole.objects.annotate(rack_count=Count('racks')) + return [(r.slug, u'{} ({})'.format(r.name, r.rack_count)) for r in role_choices] + + class RackFilterForm(forms.Form, BootstrapMixin): site = forms.MultipleChoiceField(required=False, choices=rack_site_choices, widget=forms.SelectMultiple(attrs={'size': 8})) @@ -235,6 +279,8 @@ class RackFilterForm(forms.Form, BootstrapMixin): widget=forms.SelectMultiple(attrs={'size': 8})) tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices, widget=forms.SelectMultiple(attrs={'size': 8})) + role = forms.MultipleChoiceField(required=False, choices=rack_role_choices, + widget=forms.SelectMultiple(attrs={'size': 8})) # diff --git a/netbox/dcim/migrations/0017_rack_add_role.py b/netbox/dcim/migrations/0017_rack_add_role.py new file mode 100644 index 000000000..eb3560b37 --- /dev/null +++ b/netbox/dcim/migrations/0017_rack_add_role.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-08-10 14:58 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0016_module_add_manufacturer'), + ] + + operations = [ + migrations.CreateModel( + name='RackRole', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', models.SlugField(unique=True)), + ('color', models.CharField(choices=[[b'teal', b'Teal'], [b'green', b'Green'], [b'blue', b'Blue'], [b'purple', b'Purple'], [b'yellow', b'Yellow'], [b'orange', b'Orange'], [b'red', b'Red'], [b'light_gray', b'Light Gray'], [b'medium_gray', b'Medium Gray'], [b'dark_gray', b'Dark Gray']], max_length=30)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.AddField( + model_name='rack', + name='role', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index b00a92d83..537d33087 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -61,7 +61,7 @@ COLOR_RED = 'red' COLOR_GRAY1 = 'light_gray' COLOR_GRAY2 = 'medium_gray' COLOR_GRAY3 = 'dark_gray' -DEVICE_ROLE_COLOR_CHOICES = [ +ROLE_COLOR_CHOICES = [ [COLOR_TEAL, 'Teal'], [COLOR_GREEN, 'Green'], [COLOR_BLUE, 'Blue'], @@ -203,6 +203,10 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()): }).order_by(*ordering) +# +# Sites +# + class SiteManager(NaturalOrderByManager): def get_queryset(self): @@ -264,6 +268,10 @@ class Site(CreatedUpdatedModel): return self.circuits.count() +# +# Racks +# + class RackGroup(models.Model): """ Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For @@ -288,6 +296,24 @@ class RackGroup(models.Model): return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk) +class RackRole(models.Model): + """ + Racks can be organized by functional role, similar to Devices. + """ + name = models.CharField(max_length=50, unique=True) + slug = models.SlugField(unique=True) + color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES) + + class Meta: + ordering = ['name'] + + def __unicode__(self): + return self.name + + def get_absolute_url(self): + return "{}?role={}".format(reverse('dcim:rack_list'), self.slug) + + class RackManager(NaturalOrderByManager): def get_queryset(self): @@ -304,6 +330,7 @@ class Rack(CreatedUpdatedModel): site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT) group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL) tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT) + role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT) type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type') width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width', help_text='Rail-to-rail width') @@ -344,6 +371,9 @@ class Rack(CreatedUpdatedModel): self.name, self.facility_id or '', self.tenant.name if self.tenant else '', + self.role.name if self.role else '', + self.get_type_display() if self.type else '', + self.width, str(self.u_height), ]) @@ -651,7 +681,7 @@ class DeviceRole(models.Model): """ name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) - color = models.CharField(max_length=30, choices=DEVICE_ROLE_COLOR_CHOICES) + color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES) class Meta: ordering = ['name'] diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index b7920cb71..e41a83207 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -22,6 +22,12 @@ RACKGROUP_ACTIONS = """ {% endif %} """ +RACKROLE_ACTIONS = """ +{% if perms.dcim.change_rackrole %} + +{% endif %} +""" + DEVICEROLE_ACTIONS = """ {% if perms.dcim.change_devicerole %} @@ -94,6 +100,24 @@ class RackGroupTable(BaseTable): fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions') +# +# Rack roles +# + +class RackRoleTable(BaseTable): + pk = ToggleColumn() + name = tables.LinkColumn(verbose_name='Name') + rack_count = tables.Column(verbose_name='Racks') + color = tables.Column(verbose_name='Color') + slug = tables.Column(verbose_name='Slug') + actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, + verbose_name='') + + class Meta(BaseTable.Meta): + model = RackGroup + fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions') + + # # Racks # @@ -105,6 +129,7 @@ class RackTable(BaseTable): group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') facility_id = tables.Column(verbose_name='Facility ID') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') + role = tables.Column(verbose_name='Role') u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices') u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used') @@ -112,7 +137,7 @@ class RackTable(BaseTable): class Meta(BaseTable.Meta): model = Rack - fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'u_height', 'devices', 'u_consumed', + fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'u_consumed', 'utilization') diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index fb4377ce4..bc9ea6547 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -42,6 +42,7 @@ class SiteTest(APITestCase): 'site', 'group', 'tenant', + 'role', 'type', 'width', 'u_height', @@ -120,6 +121,7 @@ class RackTest(APITestCase): 'site', 'group', 'tenant', + 'role', 'type', 'width', 'u_height', @@ -134,6 +136,7 @@ class RackTest(APITestCase): 'site', 'group', 'tenant', + 'role', 'type', 'width', 'u_height', diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 12aa60d4f..dfeb06467 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -26,6 +26,12 @@ urlpatterns = [ url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), url(r'^rack-groups/(?P\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'), + # Rack roles + url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'), + url(r'^rack-roles/add/$', views.RackRoleEditView.as_view(), name='rackrole_add'), + url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), + url(r'^rack-roles/(?P\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'), + # Racks url(r'^racks/$', views.RackListView.as_view(), name='rack_list'), url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ffffc80a2..8e9e7f3cb 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -26,7 +26,7 @@ from .models import ( CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, - Site, + RackRole, Site, ) @@ -158,6 +158,31 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): default_redirect_url = 'dcim:rackgroup_list' +# +# Rack roles +# + +class RackRoleListView(ObjectListView): + queryset = RackRole.objects.annotate(rack_count=Count('racks')) + table = tables.RackRoleTable + edit_permissions = ['dcim.change_rackrole', 'dcim.delete_rackrole'] + template_name = 'dcim/rackrole_list.html' + + +class RackRoleEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.change_rackrole' + model = RackRole + form_class = forms.RackRoleForm + success_url = 'dcim:rackrole_list' + cancel_url = 'dcim:rackrole_list' + + +class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_rackrole' + cls = RackRole + default_redirect_url = 'dcim:rackrole_list' + + # # Racks # @@ -223,11 +248,12 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - if form.cleaned_data['tenant'] == 0: - fields_to_update['tenant'] = None - elif form.cleaned_data['tenant']: - fields_to_update['tenant'] = form.cleaned_data['tenant'] - for field in ['site', 'group', 'tenant', 'type', 'width', 'u_height', 'comments']: + for field in ['group', 'tenant', 'role']: + if form.cleaned_data[field] == 0: + fields_to_update[field] = None + elif form.cleaned_data[field]: + fields_to_update[field] = form.cleaned_data[field] + for field in ['site', 'type', 'width', 'u_height', 'comments']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index b3b52741b..f0291f250 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -61,6 +61,11 @@ {% if perms.dcim.add_rackgroup %}
  • Add a Rack Group
  • {% endif %} +
  • +
  • Rack Roles
  • + {% if perms.dcim.add_rackrole %} +
  • Add a Rack Role
  • + {% endif %}