|
|
@@ -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, RackRole,
|
|
|
+ Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -49,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
|
|
|
#
|
|
|
@@ -123,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
|
|
|
#
|
|
|
@@ -135,7 +172,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', 'role', 'type', 'width', 'u_height', 'comments']
|
|
|
help_texts = {
|
|
|
'site': "The site at which the rack exists",
|
|
|
'name': "Organizational rack name",
|
|
|
@@ -165,10 +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', 'u_height']
|
|
|
+ fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height']
|
|
|
|
|
|
def clean(self):
|
|
|
|
|
|
@@ -182,6 +222,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)
|
|
|
@@ -189,9 +242,12 @@ 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)')
|
|
|
comments = CommentField()
|
|
|
|
|
|
@@ -211,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}))
|
|
|
@@ -218,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}))
|
|
|
|
|
|
|
|
|
#
|
|
|
@@ -402,7 +465,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|
|
self.fields['primary_ip6'].widget.attrs['readonly'] = True
|
|
|
|
|
|
# Limit rack choices
|
|
|
- if self.is_bound:
|
|
|
+ if self.is_bound and self.data.get('site'):
|
|
|
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
|
|
|
elif self.initial.get('site'):
|
|
|
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
|
|
@@ -443,6 +506,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|
|
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
|
|
self.fields['site'].disabled = True
|
|
|
self.fields['rack'].disabled = True
|
|
|
+ self.initial['site'] = self.instance.parent_bay.device.rack.site_id
|
|
|
+ self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
|
|
|
|
|
|
|
|
class BaseDeviceFromCSVForm(forms.ModelForm):
|
|
|
@@ -1254,4 +1319,4 @@ class ModuleForm(forms.ModelForm, BootstrapMixin):
|
|
|
|
|
|
class Meta:
|
|
|
model = Module
|
|
|
- fields = ['name', 'part_id', 'serial']
|
|
|
+ fields = ['name', 'manufacturer', 'part_id', 'serial']
|