Parcourir la source

Closes #241: Introduced rack roles

Jeremy Stretch il y a 8 ans
Parent
commit
ed03449164

+ 10 - 2
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']
 
 
 #

+ 23 - 5
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
 
@@ -47,6 +47,23 @@ class RackGroupNestedSerializer(RackGroupSerializer):
 
 
 #
+# 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)

+ 4 - 0
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<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'),
 
+    # Rack roles
+    url(r'^rack-roles/$', RackRoleListView.as_view(), name='rackrole_list'),
+    url(r'^rack-roles/(?P<pk>\d+)/$', RackRoleDetailView.as_view(), name='rackrole_detail'),
+
     # Racks
     url(r'^racks/$', RackListView.as_view(), name='rack_list'),
     url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),

+ 21 - 1
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
@@ -61,6 +61,26 @@ class RackGroupDetailView(generics.RetrieveAPIView):
 
 
 #
+# 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
 #
 

+ 12 - 1
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

+ 52 - 6
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
 #
@@ -125,6 +149,18 @@ class RackGroupFilterForm(forms.Form, BootstrapMixin):
 
 
 #
+# 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}))
 
 
 #

+ 33 - 0
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'),
+        ),
+    ]

+ 32 - 2
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']

+ 26 - 1
netbox/dcim/tables.py

@@ -22,6 +22,12 @@ RACKGROUP_ACTIONS = """
 {% endif %}
 """
 
+RACKROLE_ACTIONS = """
+{% if perms.dcim.change_rackrole %}
+    <a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
+{% endif %}
+"""
+
 DEVICEROLE_ACTIONS = """
 {% if perms.dcim.change_devicerole %}
     <a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@@ -95,6 +101,24 @@ class RackGroupTable(BaseTable):
 
 
 #
+# 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')
 
 

+ 3 - 0
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',

+ 6 - 0
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<pk>\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<pk>\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'),

+ 32 - 6
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,
 )
 
 
@@ -159,6 +159,31 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 
 #
+# 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]
 

+ 5 - 0
netbox/templates/_base.html

@@ -61,6 +61,11 @@
                             {% if perms.dcim.add_rackgroup %}
                                 <li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
                             {% endif %}
+                            <li class="divider"></li>
+                            <li><a href="{% url 'dcim:rackrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Roles</a></li>
+                            {% if perms.dcim.add_rackrole %}
+                                <li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
+                            {% endif %}
                         </ul>
                     </li>
                     <li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}">

+ 21 - 0
netbox/templates/dcim/rackrole_list.html

@@ -0,0 +1,21 @@
+{% extends '_base.html' %}
+{% load helpers %}
+
+{% block title %}Rack Role{% endblock %}
+
+{% block content %}
+<div class="pull-right">
+    {% if perms.dcim.add_rackrole %}
+        <a href="{% url 'dcim:rackrole_add' %}" class="btn btn-primary">
+            <span class="fa fa-plus" aria-hidden="true"></span>
+            Add a rack role
+        </a>
+    {% endif %}
+</div>
+<h1>Rack Roles</h1>
+<div class="row">
+	<div class="col-md-12">
+        {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackrole_bulk_delete' %}
+    </div>
+</div>
+{% endblock %}