1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #241: Introduced rack roles

This commit is contained in:
Jeremy Stretch
2016-08-10 11:52:27 -04:00
parent 47a89999b8
commit ed03449164
14 changed files with 280 additions and 24 deletions

View File

@ -4,7 +4,7 @@ from django.db.models import Count
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform, 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) @admin.register(Rack)
class RackAdmin(admin.ModelAdmin): 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']
# #

View File

@ -4,7 +4,7 @@ from ipam.models import IPAddress
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, 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 from tenancy.api.serializers import TenantNestedSerializer
@ -46,6 +46,23 @@ class RackGroupNestedSerializer(RackGroupSerializer):
fields = ['id', 'name', 'slug'] 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 # Racks
# #
@ -55,11 +72,12 @@ class RackSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer() site = SiteNestedSerializer()
group = RackGroupNestedSerializer() group = RackGroupNestedSerializer()
tenant = TenantNestedSerializer() tenant = TenantNestedSerializer()
role = RackRoleNestedSerializer()
class Meta: class Meta:
model = Rack model = Rack
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'type', 'width', 'u_height', fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
'comments'] 'u_height', 'comments']
class RackNestedSerializer(RackSerializer): class RackNestedSerializer(RackSerializer):
@ -73,8 +91,8 @@ class RackDetailSerializer(RackSerializer):
rear_units = serializers.SerializerMethodField() rear_units = serializers.SerializerMethodField()
class Meta(RackSerializer.Meta): class Meta(RackSerializer.Meta):
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'type', 'width', 'u_height', fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
'comments', 'front_units', 'rear_units'] 'u_height', 'comments', 'front_units', 'rear_units']
def get_front_units(self, obj): def get_front_units(self, obj):
units = obj.get_rack_units(face=RACK_FACE_FRONT) units = obj.get_rack_units(face=RACK_FACE_FRONT)

View File

@ -18,6 +18,10 @@ urlpatterns = [
url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'), url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'),
url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'), 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 # Racks
url(r'^racks/$', RackListView.as_view(), name='rack_list'), url(r'^racks/$', RackListView.as_view(), name='rack_list'),
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'), url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),

View File

@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404
from dcim.models import ( from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface, 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 dcim import filters
from .exceptions import MissingFilterException from .exceptions import MissingFilterException
@ -60,6 +60,26 @@ class RackGroupDetailView(generics.RetrieveAPIView):
serializer_class = serializers.RackGroupSerializer 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 # Racks
# #

View File

@ -4,7 +4,7 @@ from django.db.models import Q
from .models import ( from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer, 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 from tenancy.models import Tenant
@ -96,6 +96,17 @@ class RackFilter(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Tenant (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: class Meta:
model = Rack model = Rack

View File

@ -15,8 +15,8 @@ from .models import (
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, Site, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole,
STATUS_CHOICES, SUBDEVICE_ROLE_CHILD Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
) )
@ -50,6 +50,30 @@ def bulkedit_platform_choices():
return 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 # Sites
# #
@ -124,6 +148,18 @@ class RackGroupFilterForm(forms.Form, BootstrapMixin):
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
#
# Rack roles
#
class RackRoleForm(forms.ModelForm, BootstrapMixin):
slug = SlugField()
class Meta:
model = RackRole
fields = ['name', 'slug', 'color']
# #
# Racks # Racks
# #
@ -136,7 +172,7 @@ class RackForm(forms.ModelForm, BootstrapMixin):
class Meta: class Meta:
model = Rack 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 = { help_texts = {
'site': "The site at which the rack exists", 'site': "The site at which the rack exists",
'name': "Organizational rack name", 'name': "Organizational rack name",
@ -166,11 +202,13 @@ class RackFromCSVForm(forms.ModelForm):
group_name = forms.CharField(required=False) group_name = forms.CharField(required=False)
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'}) 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) type = forms.CharField(required=False)
class Meta: class Meta:
model = Rack 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): def clean(self):
@ -204,9 +242,10 @@ class RackImportForm(BulkImportForm, BootstrapMixin):
class RackBulkEditForm(forms.Form, BootstrapMixin): class RackBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False) 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') 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') 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') width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
u_height = forms.IntegerField(required=False, label='Height (U)') 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] 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): class RackFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices, site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
@ -235,6 +279,8 @@ class RackFilterForm(forms.Form, BootstrapMixin):
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices, tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
role = forms.MultipleChoiceField(required=False, choices=rack_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
# #

View File

@ -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'),
),
]

View File

@ -61,7 +61,7 @@ COLOR_RED = 'red'
COLOR_GRAY1 = 'light_gray' COLOR_GRAY1 = 'light_gray'
COLOR_GRAY2 = 'medium_gray' COLOR_GRAY2 = 'medium_gray'
COLOR_GRAY3 = 'dark_gray' COLOR_GRAY3 = 'dark_gray'
DEVICE_ROLE_COLOR_CHOICES = [ ROLE_COLOR_CHOICES = [
[COLOR_TEAL, 'Teal'], [COLOR_TEAL, 'Teal'],
[COLOR_GREEN, 'Green'], [COLOR_GREEN, 'Green'],
[COLOR_BLUE, 'Blue'], [COLOR_BLUE, 'Blue'],
@ -203,6 +203,10 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
}).order_by(*ordering) }).order_by(*ordering)
#
# Sites
#
class SiteManager(NaturalOrderByManager): class SiteManager(NaturalOrderByManager):
def get_queryset(self): def get_queryset(self):
@ -264,6 +268,10 @@ class Site(CreatedUpdatedModel):
return self.circuits.count() return self.circuits.count()
#
# Racks
#
class RackGroup(models.Model): 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 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) 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): class RackManager(NaturalOrderByManager):
def get_queryset(self): def get_queryset(self):
@ -304,6 +330,7 @@ class Rack(CreatedUpdatedModel):
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT) 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) 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) 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') 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', width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width',
help_text='Rail-to-rail width') help_text='Rail-to-rail width')
@ -344,6 +371,9 @@ class Rack(CreatedUpdatedModel):
self.name, self.name,
self.facility_id or '', self.facility_id or '',
self.tenant.name if self.tenant else '', 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), str(self.u_height),
]) ])
@ -651,7 +681,7 @@ class DeviceRole(models.Model):
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(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: class Meta:
ordering = ['name'] ordering = ['name']

View File

@ -22,6 +22,12 @@ RACKGROUP_ACTIONS = """
{% endif %} {% 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 = """ DEVICEROLE_ACTIONS = """
{% if perms.dcim.change_devicerole %} {% 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> <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>
@ -94,6 +100,24 @@ class RackGroupTable(BaseTable):
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions') 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 # Racks
# #
@ -105,6 +129,7 @@ class RackTable(BaseTable):
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
facility_id = tables.Column(verbose_name='Facility ID') facility_id = tables.Column(verbose_name='Facility ID')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') 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') u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices') devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used') u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used')
@ -112,7 +137,7 @@ class RackTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Rack 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') 'utilization')

View File

@ -42,6 +42,7 @@ class SiteTest(APITestCase):
'site', 'site',
'group', 'group',
'tenant', 'tenant',
'role',
'type', 'type',
'width', 'width',
'u_height', 'u_height',
@ -120,6 +121,7 @@ class RackTest(APITestCase):
'site', 'site',
'group', 'group',
'tenant', 'tenant',
'role',
'type', 'type',
'width', 'width',
'u_height', 'u_height',
@ -134,6 +136,7 @@ class RackTest(APITestCase):
'site', 'site',
'group', 'group',
'tenant', 'tenant',
'role',
'type', 'type',
'width', 'width',
'u_height', 'u_height',

View File

@ -26,6 +26,12 @@ urlpatterns = [
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), 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'), 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 # Racks
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'), url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'), url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),

View File

@ -26,7 +26,7 @@ from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, 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' 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 # Racks
# #
@ -223,11 +248,12 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form): def update_objects(self, pk_list, form):
fields_to_update = {} fields_to_update = {}
if form.cleaned_data['tenant'] == 0: for field in ['group', 'tenant', 'role']:
fields_to_update['tenant'] = None if form.cleaned_data[field] == 0:
elif form.cleaned_data['tenant']: fields_to_update[field] = None
fields_to_update['tenant'] = form.cleaned_data['tenant'] elif form.cleaned_data[field]:
for field in ['site', 'group', 'tenant', 'type', 'width', 'u_height', 'comments']: fields_to_update[field] = form.cleaned_data[field]
for field in ['site', 'type', 'width', 'u_height', 'comments']:
if form.cleaned_data[field]: if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field] fields_to_update[field] = form.cleaned_data[field]

View File

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

View File

@ -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 %}