mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Merge pull request #4723 from jsenecal/4615_interface_label
Closes: #4615 Physical labels
This commit is contained in:
commit
9b48a26aef
@ -249,7 +249,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -262,7 +262,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type']
|
||||
|
||||
|
||||
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -275,7 +275,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw']
|
||||
|
||||
|
||||
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -296,7 +296,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg']
|
||||
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -305,7 +305,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'mgmt_only']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'mgmt_only']
|
||||
|
||||
|
||||
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -332,7 +332,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'device_type', 'name']
|
||||
fields = ['id', 'device_type', 'name', 'label']
|
||||
|
||||
|
||||
#
|
||||
@ -447,7 +447,7 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
|
||||
'id', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
|
||||
'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
@ -465,7 +465,7 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
|
||||
'id', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
|
||||
'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
@ -495,7 +495,7 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
|
||||
'id', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
@ -513,7 +513,7 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
|
||||
'id', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
@ -537,7 +537,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||
'id', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan',
|
||||
'tagged_vlans', 'tags', 'count_ipaddresses',
|
||||
]
|
||||
@ -604,7 +604,7 @@ class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'device', 'name', 'description', 'installed_device', 'tags']
|
||||
fields = ['id', 'device', 'name', 'label', 'description', 'installed_device', 'tags']
|
||||
|
||||
|
||||
#
|
||||
|
@ -22,10 +22,10 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||
BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField,
|
||||
CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model,
|
||||
JSONField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
BOOLEAN_WITH_BLANK_CHOICES, BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField,
|
||||
CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
|
||||
form_from_model, JSONField, LabeledComponentForm, SelectWithPK, SmallTextarea, SlugField, StaticSelect2,
|
||||
StaticSelect2Multiple, TagFilterField,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||
from .choices import *
|
||||
@ -1037,20 +1037,17 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type',
|
||||
'device_type', 'name', 'label', 'type',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class ConsolePortTemplateCreateForm(LabeledComponentForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all()
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
widget=StaticSelect2()
|
||||
@ -1077,20 +1074,17 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type',
|
||||
'device_type', 'name', 'label', 'type',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class ConsoleServerPortTemplateCreateForm(LabeledComponentForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all()
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
widget=StaticSelect2()
|
||||
@ -1117,20 +1111,17 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw',
|
||||
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class PowerPortTemplateCreateForm(LabeledComponentForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all()
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerPortTypeChoices),
|
||||
required=False
|
||||
@ -1177,7 +1168,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'power_port', 'feed_leg',
|
||||
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
@ -1194,13 +1185,10 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class PowerOutletTemplateCreateForm(LabeledComponentForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all()
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletTypeChoices),
|
||||
required=False
|
||||
@ -1252,7 +1240,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'mgmt_only',
|
||||
'device_type', 'name', 'label', 'type', 'mgmt_only',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
@ -1260,13 +1248,10 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class InterfaceTemplateCreateForm(LabeledComponentForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all()
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
widget=StaticSelect2()
|
||||
@ -1509,7 +1494,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type',
|
||||
'device_type', 'name', 'label', 'type',
|
||||
]
|
||||
|
||||
|
||||
@ -1518,7 +1503,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type',
|
||||
'device_type', 'name', 'label', 'type',
|
||||
]
|
||||
|
||||
|
||||
@ -1527,7 +1512,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw',
|
||||
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
||||
]
|
||||
|
||||
|
||||
@ -1541,7 +1526,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'power_port', 'feed_leg',
|
||||
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||
]
|
||||
|
||||
|
||||
@ -1553,7 +1538,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'mgmt_only',
|
||||
'device_type', 'name', 'label', 'type', 'mgmt_only',
|
||||
]
|
||||
|
||||
|
||||
@ -2196,14 +2181,11 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
# Bulk device component creation
|
||||
#
|
||||
|
||||
class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
|
||||
class DeviceBulkAddComponentForm(LabeledComponentForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
|
||||
def clean_tags(self):
|
||||
# Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we
|
||||
@ -2234,20 +2216,17 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'device', 'name', 'type', 'description', 'tags',
|
||||
'device', 'name', 'label', 'type', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class ConsolePortCreateForm(BootstrapMixin, forms.Form):
|
||||
class ConsolePortCreateForm(LabeledComponentForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.prefetch_related('device_type__manufacturer')
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
required=False,
|
||||
@ -2327,13 +2306,10 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
|
||||
class ConsoleServerPortCreateForm(LabeledComponentForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.prefetch_related('device_type__manufacturer')
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||
required=False,
|
||||
@ -2427,13 +2403,10 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerPortCreateForm(BootstrapMixin, forms.Form):
|
||||
class PowerPortCreateForm(LabeledComponentForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.prefetch_related('device_type__manufacturer')
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerPortTypeChoices),
|
||||
required=False,
|
||||
@ -2536,13 +2509,10 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletCreateForm(BootstrapMixin, forms.Form):
|
||||
class PowerOutletCreateForm(LabeledComponentForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.prefetch_related('device_type__manufacturer')
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletTypeChoices),
|
||||
required=False,
|
||||
@ -2726,7 +2696,7 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'device', 'name', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description',
|
||||
'device', 'name', 'label', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description',
|
||||
'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
@ -2761,13 +2731,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
|
||||
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
|
||||
|
||||
|
||||
class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
|
||||
class InterfaceCreateForm(InterfaceCommonForm, LabeledComponentForm):
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.prefetch_related('device_type__manufacturer')
|
||||
)
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices,
|
||||
widget=StaticSelect2(),
|
||||
|
73
netbox/dcim/migrations/0107_component_labels.py
Normal file
73
netbox/dcim/migrations/0107_component_labels.py
Normal file
@ -0,0 +1,73 @@
|
||||
# Generated by Django 3.0.7 on 2020-06-04 20:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0106_role_default_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interfacetemplate',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebay',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebaytemplate',
|
||||
name='label',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
]
|
@ -32,6 +32,11 @@ class ComponentTemplateModel(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
if self.label:
|
||||
return f"{self.name} ({self.label})"
|
||||
return self.name
|
||||
|
||||
def instantiate(self, device):
|
||||
"""
|
||||
Instantiate a new component on the specified Device.
|
||||
@ -71,6 +76,11 @@ class ConsolePortTemplate(ComponentTemplateModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
@ -81,9 +91,6 @@ class ConsolePortTemplate(ComponentTemplateModel):
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def instantiate(self, device):
|
||||
return ConsolePort(
|
||||
device=device,
|
||||
@ -109,6 +116,11 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
@ -119,9 +131,6 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def instantiate(self, device):
|
||||
return ConsoleServerPort(
|
||||
device=device,
|
||||
@ -147,6 +156,11 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerPortTypeChoices,
|
||||
@ -169,9 +183,6 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def instantiate(self, device):
|
||||
return PowerPort(
|
||||
device=device,
|
||||
@ -199,6 +210,11 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerOutletTypeChoices,
|
||||
@ -222,9 +238,6 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate power port assignment
|
||||
@ -265,6 +278,11 @@ class InterfaceTemplate(ComponentTemplateModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfaceTypeChoices
|
||||
@ -278,9 +296,6 @@ class InterfaceTemplate(ComponentTemplateModel):
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def instantiate(self, device):
|
||||
return Interface(
|
||||
device=device,
|
||||
@ -420,14 +435,16 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def instantiate(self, device):
|
||||
return DeviceBay(
|
||||
device=device,
|
||||
|
@ -47,6 +47,11 @@ class ComponentModel(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
if self.label:
|
||||
return f"{self.name} ({self.label})"
|
||||
return self.name
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the parent Device/VM
|
||||
try:
|
||||
@ -234,6 +239,11 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
@ -264,9 +274,6 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
@ -301,6 +308,11 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
@ -319,9 +331,6 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
@ -356,6 +365,11 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerPortTypeChoices,
|
||||
@ -400,9 +414,6 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
@ -519,6 +530,11 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerOutletTypeChoices,
|
||||
@ -550,9 +566,6 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
@ -608,6 +621,11 @@ class Interface(CableTermination, ComponentModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
_connected_interface = models.OneToOneField(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -688,9 +706,6 @@ class Interface(CableTermination, ComponentModel):
|
||||
ordering = ('device', CollateAsChar('_name'))
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:interface', kwargs={'pk': self.pk})
|
||||
|
||||
@ -997,6 +1012,11 @@ class DeviceBay(ComponentModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
help_text="Physical label"
|
||||
)
|
||||
installed_device = models.OneToOneField(
|
||||
to='dcim.Device',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -1013,6 +1033,8 @@ class DeviceBay(ComponentModel):
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
if self.label:
|
||||
return '{} - {} ({})'.format(self.device.name, self.name, self.label)
|
||||
return '{} - {}'.format(self.device.name, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
|
@ -116,3 +116,45 @@ class DeviceTestCase(TestCase):
|
||||
|
||||
# Check that the initial value for the cluster group is set automatically when assigning the cluster
|
||||
self.assertEqual(test.initial['cluster_group'], cluster.group.pk)
|
||||
|
||||
|
||||
class LabelTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site = Site.objects.create(name='Site 2', slug='site-2')
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 2', slug='manufacturer-2')
|
||||
cls.device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', u_height=1
|
||||
)
|
||||
device_role = DeviceRole.objects.create(
|
||||
name='Device Role 2', slug='device-role-2', color='ffff00'
|
||||
)
|
||||
cls.device = Device.objects.create(
|
||||
name='Device 2', device_type=cls.device_type, device_role=device_role, site=site
|
||||
)
|
||||
|
||||
def test_interface_label_count_valid(self):
|
||||
"""Test that a `label` can be generated for each generated `name` from `name_pattern` on InterfaceCreateForm"""
|
||||
interface_data = {
|
||||
'device': self.device.pk,
|
||||
'name_pattern': 'eth[0-9]',
|
||||
'label_pattern': 'Interface[0-9]',
|
||||
'type': InterfaceTypeChoices.TYPE_100ME_FIXED,
|
||||
}
|
||||
form = InterfaceCreateForm(interface_data)
|
||||
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
def test_interface_label_count_mismatch(self):
|
||||
"""Test that a `label` cannot be generated for each generated `name` from `name_pattern` due to invalid `label_pattern` on InterfaceCreateForm"""
|
||||
bad_interface_data = {
|
||||
'device': self.device.pk,
|
||||
'name_pattern': 'eth[0-9]',
|
||||
'label_pattern': 'Interface[0-1]',
|
||||
'type': InterfaceTypeChoices.TYPE_100ME_FIXED,
|
||||
}
|
||||
form = InterfaceCreateForm(bad_interface_data)
|
||||
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn('label_pattern', form.errors)
|
||||
|
@ -710,6 +710,8 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Interface Template [4-6]',
|
||||
# Test that a label can be applied to each generated interface templates
|
||||
'label_pattern': 'Interface Template Label [3-5]',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'mgmt_only': True,
|
||||
}
|
||||
@ -1010,6 +1012,8 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Console Port [4-6]',
|
||||
# Test that a label can be applied to each generated console ports
|
||||
'label_pattern': 'Serial[3-5]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
|
@ -58,6 +58,10 @@
|
||||
<td>Name</td>
|
||||
<td>{{ interface.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ interface.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ interface.get_type_display }}</td>
|
||||
|
@ -6,6 +6,7 @@
|
||||
<div class="panel-heading"><strong>Interface</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.name %}
|
||||
{% render_field form.label %}
|
||||
{% render_field form.type %}
|
||||
{% render_field form.enabled %}
|
||||
{% render_field form.lag %}
|
||||
|
@ -530,6 +530,8 @@ class ExpandableNameField(forms.CharField):
|
||||
"""
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
return list()
|
||||
if re.search(ALPHANUMERIC_EXPANSION_PATTERN, value):
|
||||
return list(expand_alphanumeric_pattern(value))
|
||||
return [value]
|
||||
@ -802,6 +804,31 @@ class ImportForm(BootstrapMixin, forms.Form):
|
||||
})
|
||||
|
||||
|
||||
class LabeledComponentForm(BootstrapMixin, forms.Form):
|
||||
"""
|
||||
Base form for adding label pattern validation to `Create` forms
|
||||
"""
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
label_pattern = ExpandableNameField(
|
||||
label='Label',
|
||||
required=False
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate that the number of components being created from both the name_pattern and label_pattern are equal
|
||||
name_pattern_count = len(self.cleaned_data['name_pattern'])
|
||||
label_pattern_count = len(self.cleaned_data['label_pattern'])
|
||||
if label_pattern_count and name_pattern_count != label_pattern_count:
|
||||
raise forms.ValidationError({
|
||||
'label_pattern': 'The provided name pattern will create {} components, however {} labels will '
|
||||
'be generated. These counts must match.'.format(
|
||||
name_pattern_count, label_pattern_count)
|
||||
}, code='label_pattern_mismatch')
|
||||
|
||||
|
||||
class TableConfigForm(BootstrapMixin, forms.Form):
|
||||
"""
|
||||
Form for configuring user's table preferences.
|
||||
|
@ -1088,10 +1088,13 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
||||
new_components = []
|
||||
data = deepcopy(request.POST)
|
||||
|
||||
for i, name in enumerate(form.cleaned_data['name_pattern']):
|
||||
|
||||
names = form.cleaned_data['name_pattern']
|
||||
labels = form.cleaned_data.get('label_pattern')
|
||||
for i, name in enumerate(names):
|
||||
label = labels[i] if labels else None
|
||||
# Initialize the individual component form
|
||||
data['name'] = name
|
||||
data['label'] = label
|
||||
if hasattr(form, 'get_iterative_data'):
|
||||
data.update(form.get_iterative_data(i))
|
||||
component_form = self.model_form(data)
|
||||
@ -1100,9 +1103,11 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
||||
new_components.append(component_form)
|
||||
else:
|
||||
for field, errors in component_form.errors.as_data().items():
|
||||
# Assign errors on the child form's name field to name_pattern on the parent form
|
||||
# Assign errors on the child form's name/label field to name_pattern/label_pattern on the parent form
|
||||
if field == 'name':
|
||||
field = 'name_pattern'
|
||||
elif field == 'label':
|
||||
field = 'label_pattern'
|
||||
for e in errors:
|
||||
form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
|
||||
|
||||
@ -1187,10 +1192,14 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
|
||||
for obj in data['pk']:
|
||||
|
||||
names = data['name_pattern']
|
||||
for name in names:
|
||||
labels = data['label_pattern']
|
||||
for i, name in enumerate(names):
|
||||
label = labels[i] if labels else None
|
||||
|
||||
component_data = {
|
||||
self.parent_field: obj.pk,
|
||||
'name': name,
|
||||
'label': label
|
||||
}
|
||||
component_data.update(data)
|
||||
component_form = self.model_form(component_data)
|
||||
|
Loading…
Reference in New Issue
Block a user