diff --git a/netbox/dcim/admin.py b/netbox/dcim/admin.py index c5210987b..95a84be00 100644 --- a/netbox/dcim/admin.py +++ b/netbox/dcim/admin.py @@ -26,7 +26,7 @@ class RackGroupAdmin(admin.ModelAdmin): @admin.register(Rack) class RackAdmin(admin.ModelAdmin): - list_display = ['name', 'facility_id', 'site', 'u_height'] + list_display = ['name', 'facility_id', 'site', 'type', 'width', 'u_height'] # diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 7a6693c37..c36612c49 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -58,7 +58,8 @@ class RackSerializer(serializers.ModelSerializer): class Meta: model = Rack - fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments'] + fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'type', 'width', 'u_height', + 'comments'] class RackNestedSerializer(RackSerializer): @@ -72,8 +73,8 @@ class RackDetailSerializer(RackSerializer): rear_units = serializers.SerializerMethodField() class Meta(RackSerializer.Meta): - fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments', - 'front_units', 'rear_units'] + fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', '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/forms.py b/netbox/dcim/forms.py index 5fa133bf5..15b2dd6be 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -7,7 +7,7 @@ from ipam.models import IPAddress from tenancy.forms import bulkedit_tenant_choices from tenancy.models import Tenant from utilities.forms import ( - APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField, + APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, ) @@ -15,7 +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, RackGroup, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD + PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, Site, + STATUS_CHOICES, SUBDEVICE_ROLE_CHILD ) @@ -135,7 +136,7 @@ class RackForm(forms.ModelForm, BootstrapMixin): class Meta: model = Rack - fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'u_height', 'comments'] + fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'type', 'width', 'u_height', 'comments'] help_texts = { 'site': "The site at which the rack exists", 'name': "Organizational rack name", @@ -165,10 +166,11 @@ 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.'}) + type = forms.CharField(required=False) class Meta: model = Rack - fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'u_height'] + fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'type', 'width', 'u_height'] def clean(self): @@ -182,6 +184,19 @@ class RackFromCSVForm(forms.ModelForm): except RackGroup.DoesNotExist: self.add_error('group_name', "Invalid rack group ({})".format(group)) + def clean_type(self): + rack_type = self.cleaned_data['type'] + if not rack_type: + return None + try: + choices = {v.lower(): k for k, v in RACK_TYPE_CHOICES} + return choices[rack_type.lower()] + except KeyError: + raise forms.ValidationError('Invalid rack type ({}). Valid choices are: {}.'.format( + rack_type, + ', '.join({v: k for k, v in RACK_TYPE_CHOICES}), + )) + class RackImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=RackFromCSVForm) @@ -192,6 +207,8 @@ class RackBulkEditForm(forms.Form, BootstrapMixin): site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False) tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') + 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)') comments = CommentField() diff --git a/netbox/dcim/migrations/0014_rack_add_type_width.py b/netbox/dcim/migrations/0014_rack_add_type_width.py new file mode 100644 index 000000000..c14768c0f --- /dev/null +++ b/netbox/dcim/migrations/0014_rack_add_type_width.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-08-08 21:11 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0013_add_interface_form_factors'), + ] + + operations = [ + migrations.AddField( + model_name='rack', + name='type', + field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'), + ), + migrations.AddField( + model_name='rack', + name='width', + field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 119336569..6d9d0e290 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -16,6 +16,26 @@ from utilities.models import CreatedUpdatedModel from .fields import ASNField, MACAddressField +RACK_TYPE_2POST = 100 +RACK_TYPE_4POST = 200 +RACK_TYPE_CABINET = 300 +RACK_TYPE_WALLFRAME = 1000 +RACK_TYPE_WALLCABINET = 1100 +RACK_TYPE_CHOICES = ( + (RACK_TYPE_2POST, '2-post frame'), + (RACK_TYPE_4POST, '4-post frame'), + (RACK_TYPE_CABINET, '4-post cabinet'), + (RACK_TYPE_WALLFRAME, 'Wall-mounted frame'), + (RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'), +) + +RACK_WIDTH_19IN = 19 +RACK_WIDTH_23IN = 23 +RACK_WIDTH_CHOICES = ( + (RACK_WIDTH_19IN, '19 inches'), + (RACK_WIDTH_23IN, '23 inches'), +) + RACK_FACE_FRONT = 0 RACK_FACE_REAR = 1 RACK_FACE_CHOICES = [ @@ -284,6 +304,9 @@ 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) + 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') u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)') comments = models.TextField(blank=True) diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index 8b0d8ca53..fb4377ce4 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -42,6 +42,8 @@ class SiteTest(APITestCase): 'site', 'group', 'tenant', + 'type', + 'width', 'u_height', 'comments' ] @@ -118,6 +120,8 @@ class RackTest(APITestCase): 'site', 'group', 'tenant', + 'type', + 'width', 'u_height', 'comments' ] @@ -130,6 +134,8 @@ class RackTest(APITestCase): 'site', 'group', 'tenant', + 'type', + 'width', 'u_height', 'comments', 'front_units', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index cc5ea9af3..928c11f51 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -227,7 +227,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView): fields_to_update['tenant'] = None elif form.cleaned_data['tenant']: fields_to_update['tenant'] = form.cleaned_data['tenant'] - for field in ['site', 'group', 'tenant', 'u_height', 'comments']: + for field in ['site', 'group', 'tenant', 'type', 'width', 'u_height', 'comments']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index cf6ebf567..4996a80c2 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -96,6 +96,20 @@ {% endif %} + + Type + + {% if rack.type %} + {{ rack.get_type_display }} + {% else %} + None + {% endif %} + + + + Width + {{ rack.get_width_display }} + Height {{ rack.u_height }}U diff --git a/netbox/templates/dcim/rack_bulk_edit.html b/netbox/templates/dcim/rack_bulk_edit.html index 1085dd1e2..5ffa00582 100644 --- a/netbox/templates/dcim/rack_bulk_edit.html +++ b/netbox/templates/dcim/rack_bulk_edit.html @@ -4,13 +4,24 @@ {% block title %}Rack Bulk Edit{% endblock %} {% block select_objects_table %} + + Name + Site + Group + Tenant + Type + Width + Height + {% for rack in selected_objects %} {{ rack }} - {{ rack.facility_id }} {{ rack.site }} + {{ rack.group }} {{ rack.tenant }} - {{ rack.u_height }} + {{ rack.get_type_display }} + {{ rack.get_width_display }} + {{ rack.u_height }}U {% endfor %} {% endblock %} diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html index ab055e8ee..166c052f6 100644 --- a/netbox/templates/dcim/rack_edit.html +++ b/netbox/templates/dcim/rack_edit.html @@ -10,6 +10,8 @@ {% render_field form.name %} {% render_field form.facility_id %} {% render_field form.tenant %} + {% render_field form.type %} + {% render_field form.width %} {% render_field form.u_height %} diff --git a/netbox/templates/dcim/rack_import.html b/netbox/templates/dcim/rack_import.html index b6e797d39..e088f0809 100644 --- a/netbox/templates/dcim/rack_import.html +++ b/netbox/templates/dcim/rack_import.html @@ -53,6 +53,16 @@ Name of tenant (optional) Pied Piper + + Type + Rack type (optional) + 4-post cabinet + + + Width + Rail-to-rail width (19 or 23 inches) + 19 + Height Height in rack units @@ -61,7 +71,7 @@

Example

-
DC-4,Cage 1400,R101,J12.100,Pied Piper,42
+
DC-4,Cage 1400,R101,J12.100,Pied Piper,4-post cabinet,19,42
{% endblock %} diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 979bdd0ad..f1c942fe6 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -27,6 +27,13 @@ def expand_pattern(string): yield "{}{}{}".format(lead, i, remnant) +def add_blank_choice(choices): + """ + Add a blank choice to the beginning of a choices list. + """ + return ((None, '---------'),) + choices + + # # Widgets #