From cde1db443673036d407a6ff9ea68eb585d0924b7 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Thu, 4 Jun 2020 16:44:25 -0400 Subject: [PATCH 01/21] Add `label` to interface models --- .../dcim/migrations/0107_interface_label.py | 23 +++++++++++++++++++ .../dcim/models/device_component_templates.py | 5 ++++ netbox/dcim/models/device_components.py | 5 ++++ 3 files changed, 33 insertions(+) create mode 100644 netbox/dcim/migrations/0107_interface_label.py diff --git a/netbox/dcim/migrations/0107_interface_label.py b/netbox/dcim/migrations/0107_interface_label.py new file mode 100644 index 000000000..48970d25d --- /dev/null +++ b/netbox/dcim/migrations/0107_interface_label.py @@ -0,0 +1,23 @@ +# 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), + ), + ] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 164d37d77..1a9e7bbf9 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -263,6 +263,11 @@ class InterfaceTemplate(ComponentTemplateModel): max_length=100, blank=True ) + label = models.CharField( + max_length=64, + blank=True, + help_text="The physical label for this interface" + ) type = models.CharField( max_length=50, choices=InterfaceTypeChoices diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 4005d41a4..e5c2e5936 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -605,6 +605,11 @@ class Interface(CableTermination, ComponentModel): max_length=100, blank=True ) + label = models.CharField( + max_length=64, + blank=True, + help_text="The physical label for this interface" + ) _connected_interface = models.OneToOneField( to='self', on_delete=models.SET_NULL, From e9f8640ee680e6eacf49a1ad0523bde1055dd86c Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Thu, 4 Jun 2020 16:50:51 -0400 Subject: [PATCH 02/21] Add `label` to Interface serializers --- netbox/dcim/api/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 9ac58dc3a..e8bc57d1d 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -304,7 +304,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): @@ -536,7 +536,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', ] From f8851121abd26eaa2a761c04375d95f76b666acb Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Thu, 4 Jun 2020 17:11:27 -0400 Subject: [PATCH 03/21] Add the `label` to the string representation --- netbox/dcim/models/device_component_templates.py | 2 ++ netbox/dcim/models/device_components.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 1a9e7bbf9..f61c3f057 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -282,6 +282,8 @@ class InterfaceTemplate(ComponentTemplateModel): unique_together = ('device_type', 'name') def __str__(self): + if self.label: + return f"{self.name} ({self.label})" return self.name def instantiate(self, device): diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index e5c2e5936..9d9e79196 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -691,6 +691,8 @@ class Interface(CableTermination, ComponentModel): unique_together = ('device', 'name') def __str__(self): + if self.label: + return f"{self.name} ({self.label})" return self.name def get_absolute_url(self): From a06d74472da1c4ffbcb8e3fc23b9b8f62af079f0 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Fri, 5 Jun 2020 10:32:59 -0400 Subject: [PATCH 04/21] Add `label` to *ports models --- netbox/dcim/migrations/0108_port_label.py | 53 +++++++++++++++++++ .../dcim/models/device_component_templates.py | 20 +++++++ netbox/dcim/models/device_components.py | 20 +++++++ 3 files changed, 93 insertions(+) create mode 100644 netbox/dcim/migrations/0108_port_label.py diff --git a/netbox/dcim/migrations/0108_port_label.py b/netbox/dcim/migrations/0108_port_label.py new file mode 100644 index 000000000..af0aa1962 --- /dev/null +++ b/netbox/dcim/migrations/0108_port_label.py @@ -0,0 +1,53 @@ +# Generated by Django 3.0.7 on 2020-06-05 14:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0107_interface_label'), + ] + + operations = [ + 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), + ), + ] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index f61c3f057..b7f94a450 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -69,6 +69,11 @@ class ConsolePortTemplate(ComponentTemplateModel): max_length=100, blank=True ) + label = models.CharField( + max_length=64, + blank=True, + help_text="The physical label for this console port" + ) type = models.CharField( max_length=50, choices=ConsolePortTypeChoices, @@ -107,6 +112,11 @@ class ConsoleServerPortTemplate(ComponentTemplateModel): max_length=100, blank=True ) + label = models.CharField( + max_length=64, + blank=True, + help_text="The physical label for this console server port" + ) type = models.CharField( max_length=50, choices=ConsolePortTypeChoices, @@ -145,6 +155,11 @@ class PowerPortTemplate(ComponentTemplateModel): max_length=100, blank=True ) + label = models.CharField( + max_length=64, + blank=True, + help_text="The physical label for this power supply port" + ) type = models.CharField( max_length=50, choices=PowerPortTypeChoices, @@ -197,6 +212,11 @@ class PowerOutletTemplate(ComponentTemplateModel): max_length=100, blank=True ) + label = models.CharField( + max_length=64, + blank=True, + help_text="The physical label for this power outlet" + ) type = models.CharField( max_length=50, choices=PowerOutletTypeChoices, diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 9d9e79196..860e1bd65 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -231,6 +231,11 @@ class ConsolePort(CableTermination, ComponentModel): name = models.CharField( max_length=50 ) + label = models.CharField( + max_length=64, + blank=True, + help_text="The physical label for this console port" + ) _name = NaturalOrderingField( target_field='name', max_length=100, @@ -298,6 +303,11 @@ class ConsoleServerPort(CableTermination, ComponentModel): max_length=100, blank=True ) + label = models.CharField( + max_length=64, + blank=True, + help_text="The physical label for this console server port" + ) type = models.CharField( max_length=50, choices=ConsolePortTypeChoices, @@ -353,6 +363,11 @@ class PowerPort(CableTermination, ComponentModel): max_length=100, blank=True ) + label = models.CharField( + max_length=64, + blank=True, + help_text="The physical label for this power supply port" + ) type = models.CharField( max_length=50, choices=PowerPortTypeChoices, @@ -516,6 +531,11 @@ class PowerOutlet(CableTermination, ComponentModel): max_length=100, blank=True ) + label = models.CharField( + max_length=64, + blank=True, + help_text="The physical label for this power outlet" + ) type = models.CharField( max_length=50, choices=PowerOutletTypeChoices, From 1fae9aff0c90d5217309263388ed02925435d4f2 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Fri, 5 Jun 2020 10:42:13 -0400 Subject: [PATCH 05/21] Add `label` to *port serializers --- netbox/dcim/api/serializers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index e8bc57d1d..a2f576aca 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -248,7 +248,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer): class Meta: model = ConsolePortTemplate - fields = ['id', 'device_type', 'name', 'type'] + fields = ['id', 'device_type', 'name', 'label', 'type'] class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): @@ -261,7 +261,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): class Meta: model = ConsoleServerPortTemplate - fields = ['id', 'device_type', 'name', 'type'] + fields = ['id', 'device_type', 'name', 'label', 'type'] class PowerPortTemplateSerializer(ValidatedModelSerializer): @@ -274,7 +274,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): @@ -295,7 +295,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): @@ -446,7 +446,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', ] @@ -464,7 +464,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', ] @@ -494,7 +494,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', ] @@ -512,7 +512,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', ] From e21cbf2a0667e2d014740a1bfef06a056c238a40 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Fri, 5 Jun 2020 11:01:39 -0400 Subject: [PATCH 06/21] Add the `label` to the string representation --- netbox/dcim/models/device_component_templates.py | 8 ++++++++ netbox/dcim/models/device_components.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index b7f94a450..acb5f0d46 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -85,6 +85,8 @@ class ConsolePortTemplate(ComponentTemplateModel): unique_together = ('device_type', 'name') def __str__(self): + if self.label: + return f"{self.name} ({self.label})" return self.name def instantiate(self, device): @@ -128,6 +130,8 @@ class ConsoleServerPortTemplate(ComponentTemplateModel): unique_together = ('device_type', 'name') def __str__(self): + if self.label: + return f"{self.name} ({self.label})" return self.name def instantiate(self, device): @@ -183,6 +187,8 @@ class PowerPortTemplate(ComponentTemplateModel): unique_together = ('device_type', 'name') def __str__(self): + if self.label: + return f"{self.name} ({self.label})" return self.name def instantiate(self, device): @@ -241,6 +247,8 @@ class PowerOutletTemplate(ComponentTemplateModel): unique_together = ('device_type', 'name') def __str__(self): + if self.label: + return f"{self.name} ({self.label})" return self.name def clean(self): diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 860e1bd65..b8331365d 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -267,6 +267,8 @@ class ConsolePort(CableTermination, ComponentModel): unique_together = ('device', 'name') def __str__(self): + if self.label: + return f"{self.name} ({self.label})" return self.name def get_absolute_url(self): @@ -327,6 +329,8 @@ class ConsoleServerPort(CableTermination, ComponentModel): unique_together = ('device', 'name') def __str__(self): + if self.label: + return f"{self.name} ({self.label})" return self.name def get_absolute_url(self): @@ -413,6 +417,8 @@ class PowerPort(CableTermination, ComponentModel): unique_together = ('device', 'name') def __str__(self): + if self.label: + return f"{self.name} ({self.label})" return self.name def get_absolute_url(self): @@ -568,6 +574,8 @@ class PowerOutlet(CableTermination, ComponentModel): unique_together = ('device', 'name') def __str__(self): + if self.label: + return f"{self.name} ({self.label})" return self.name def get_absolute_url(self): From d65cead212dc781a6dbbe35ea15eea7b75023da9 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Fri, 5 Jun 2020 12:34:09 -0400 Subject: [PATCH 07/21] Return an empty list if value is None --- netbox/utilities/forms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 979b6ac32..3d8dbe33f 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -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] From 286a3e6ca296609201d3f02892a3d50c338b9f78 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Fri, 5 Jun 2020 13:59:59 -0400 Subject: [PATCH 08/21] Add `label` to forms, views and templates --- netbox/dcim/forms.py | 200 ++++++++++++++++++++-- netbox/templates/dcim/interface.html | 4 + netbox/templates/dcim/interface_edit.html | 1 + netbox/utilities/views.py | 19 +- 4 files changed, 207 insertions(+), 17 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 94cf51fcd..0e9d9763e 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1032,7 +1032,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePortTemplate fields = [ - 'device_type', 'name', 'type', + 'device_type', 'name', 'label', 'type', ] widgets = { 'device_type': forms.HiddenInput(), @@ -1046,11 +1046,27 @@ class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1072,7 +1088,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsoleServerPortTemplate fields = [ - 'device_type', 'name', 'type', + 'device_type', 'name', 'label', 'type', ] widgets = { 'device_type': forms.HiddenInput(), @@ -1086,11 +1102,27 @@ class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1112,7 +1144,7 @@ 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(), @@ -1126,6 +1158,10 @@ class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), required=False @@ -1141,6 +1177,18 @@ class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form): help_text="Allocated power draw (watts)" ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1172,7 +1220,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(), @@ -1196,6 +1244,10 @@ class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), required=False @@ -1221,6 +1273,18 @@ class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form): device_type=device_type ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1247,7 +1311,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(), @@ -1262,6 +1326,10 @@ class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=InterfaceTypeChoices, widget=StaticSelect2() @@ -1271,6 +1339,18 @@ class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form): label='Management only' ) + def clean(self): + + # Validate that the number of ports 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 {} interfaces, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1504,7 +1584,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm): class Meta: model = ConsolePortTemplate fields = [ - 'device_type', 'name', 'type', + 'device_type', 'name', 'label', 'type', ] @@ -1513,7 +1593,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): class Meta: model = ConsoleServerPortTemplate fields = [ - 'device_type', 'name', 'type', + 'device_type', 'name', 'label', 'type', ] @@ -1522,7 +1602,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', ] @@ -1536,7 +1616,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', ] @@ -1548,7 +1628,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm): class Meta: model = InterfaceTemplate fields = [ - 'device_type', 'name', 'type', 'mgmt_only', + 'device_type', 'name', 'label', 'type', 'mgmt_only', ] @@ -2199,12 +2279,28 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) def clean_tags(self): # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we # must first convert the list of tags to a string. return ','.join(self.cleaned_data.get('tags')) + def clean(self): + + # Validate that the number of ports 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 {} {}}, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, self.type, label_pattern_count) + }) + # # Console ports @@ -2229,7 +2325,7 @@ 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(), @@ -2243,6 +2339,10 @@ class ConsolePortCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, @@ -2256,6 +2356,18 @@ class ConsolePortCreateForm(BootstrapMixin, forms.Form): required=False ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class ConsolePortBulkCreateForm( form_from_model(ConsolePort, ['type', 'description', 'tags']), @@ -2329,6 +2441,10 @@ class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, @@ -2342,6 +2458,18 @@ class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form): required=False ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class ConsoleServerPortBulkCreateForm( form_from_model(ConsoleServerPort, ['type', 'description', 'tags']), @@ -2429,6 +2557,10 @@ class PowerPortCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), required=False, @@ -2451,6 +2583,17 @@ class PowerPortCreateForm(BootstrapMixin, forms.Form): tags = TagField( required=False ) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) class PowerPortBulkCreateForm( @@ -2538,6 +2681,10 @@ class PowerOutletCreateForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), required=False, @@ -2568,6 +2715,18 @@ class PowerOutletCreateForm(BootstrapMixin, forms.Form): ) self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) + def clean(self): + + # Validate that the number of ports 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 {} ports, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + class PowerOutletBulkCreateForm( form_from_model(PowerOutlet, ['type', 'feed_leg', 'description', 'tags']), @@ -2721,7 +2880,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 = { @@ -2763,6 +2922,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form): name_pattern = ExpandableNameField( label='Name' ) + label_pattern = ExpandableNameField( + label='Label', + required=False + ) type = forms.ChoiceField( choices=InterfaceTypeChoices, widget=StaticSelect2(), @@ -2843,6 +3006,19 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form): self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk) self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk) + def clean(self): + + # Validate that the number of ports 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 {} interfaces, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, label_pattern_count) + }) + + class InterfaceBulkCreateForm( form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'description', 'tags']), diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 9d94e0639..d35504368 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -58,6 +58,10 @@ Name {{ interface.name }} + + Label + {{ interface.label }} + Type {{ interface.get_type_display }} diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index a80b7c592..eaffe2bca 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -6,6 +6,7 @@
Interface
{% render_field form.name %} + {% render_field form.label %} {% render_field form.type %} {% render_field form.enabled %} {% render_field form.lag %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 4b5993c5f..462e45819 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -919,21 +919,26 @@ class ComponentCreateView(GetReturnURLMixin, 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 pos, name in enumerate(names): + label = labels[pos] 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)) + data.update(form.get_iterative_data(pos)) component_form = self.model_form(data) if component_form.is_valid(): 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' + if field == 'label': + field = 'label_pattern' for e in errors: form.add_error(field, '{}: {}'.format(name, ', '.join(e))) @@ -1003,10 +1008,14 @@ class BulkComponentCreateView(GetReturnURLMixin, View): for obj in data['pk']: names = data['name_pattern'] - for name in names: + labels = data['label_pattern'] + for pos, name in enumerate(names): + label = labels[pos] 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) From 892c0e3d8be75e7d7e31c97b7bd01bdd5f8e5c0f Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Mon, 8 Jun 2020 17:00:07 -0400 Subject: [PATCH 09/21] Leftover fix --- netbox/dcim/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 0e9d9763e..736e129a3 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2296,9 +2296,9 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form): 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 {} {}}, however {} labels will ' + 'label_pattern': 'The provided name pattern will create {} ports, however {} labels will ' 'be generated. These counts must match.'.format( - name_pattern_count, self.type, label_pattern_count) + name_pattern_count, label_pattern_count) }) From 90bc1cd95162a2193ef1a470a85cd7b74a4e0634 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Mon, 8 Jun 2020 20:04:31 -0400 Subject: [PATCH 10/21] Test forms and views with labels --- netbox/dcim/tests/test_forms.py | 68 +++++++++++++++++++++++++++++++++ netbox/dcim/tests/test_views.py | 4 ++ 2 files changed, 72 insertions(+) diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 29e741560..116d9affc 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -116,3 +116,71 @@ 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): + good_interface_data = { + 'device': self.device.pk, + 'name_pattern': 'eth[0-9]', + # Test that a label CAN be applied to each generated interfaces + 'label_pattern': 'Interface[0-9]', + 'type': InterfaceTypeChoices.TYPE_100ME_FIXED, + } + form = InterfaceCreateForm(good_interface_data) + + print(form.errors) + self.assertTrue(form.is_valid()) + + def test_interface_label_count_mismatch(self): + bad_interface_data = { + 'device': self.device.pk, + 'name_pattern': 'eth[0-9]', + # Test that a label CANNOT be applied to each generated interfaces + '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) + + def test_console_port_template_label_count_valid(self): + bad_console_port_template_data = { + 'device_type': self.device_type, + 'name_pattern': 'Console Port Template[4-6]', + # Test that a label CANNOT be applied to each generated console port templates + 'label_pattern': 'Serial[4-6]', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + form = ConsolePortTemplateCreateForm(bad_console_port_template_data) + + self.assertTrue(form.is_valid()) + + def test_console_port_template_label_count_mismatch(self): + bad_console_port_template_data = { + 'device_type': self.device_type, + 'name_pattern': 'Console Port Template[4-6]', + # Test that a label CANNOT be applied to each generated console port templates + 'label_pattern': 'Serial[0-1]', + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + form = ConsolePortTemplateCreateForm(bad_console_port_template_data) + + self.assertFalse(form.is_valid()) + self.assertIn('label_pattern', form.errors) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 7ee5d7845..65179bf0c 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -699,6 +699,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, } @@ -995,6 +997,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', From 4301c06d177439356d4f4da207f2af1128984293 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Mon, 8 Jun 2020 23:07:12 -0400 Subject: [PATCH 11/21] Refactorization into LabeledComponentForm --- netbox/dcim/forms.py | 242 +++----------------------------------- netbox/utilities/forms.py | 27 +++++ 2 files changed, 44 insertions(+), 225 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 736e129a3..ca706d6f2 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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 * @@ -1039,34 +1039,15 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): } -class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form): +class ConsolePortTemplateCreateForm(LabeledComponentForm): device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all() ) - name_pattern = ExpandableNameField( - label='Name' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() ) - def clean(self): - - # Validate that the number of ports 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 {} ports, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) - class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1095,34 +1076,15 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form): +class ConsoleServerPortTemplateCreateForm(LabeledComponentForm): device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all() ) - name_pattern = ExpandableNameField( - label='Name' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), widget=StaticSelect2() ) - def clean(self): - - # Validate that the number of ports 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 {} ports, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) - class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1151,17 +1113,10 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form): +class PowerPortTemplateCreateForm(LabeledComponentForm): device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all() ) - name_pattern = ExpandableNameField( - label='Name' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), required=False @@ -1177,18 +1132,6 @@ class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form): help_text="Allocated power draw (watts)" ) - def clean(self): - - # Validate that the number of ports 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 {} ports, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) - class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1237,17 +1180,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' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), required=False @@ -1273,18 +1209,6 @@ class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form): device_type=device_type ) - def clean(self): - - # Validate that the number of ports 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 {} ports, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) - class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -1319,17 +1243,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' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) type = forms.ChoiceField( choices=InterfaceTypeChoices, widget=StaticSelect2() @@ -1339,18 +1256,6 @@ class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form): label='Management only' ) - def clean(self): - - # Validate that the number of ports 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 {} interfaces, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) - class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -2271,36 +2176,17 @@ 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' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) def clean_tags(self): # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we # must first convert the list of tags to a string. return ','.join(self.cleaned_data.get('tags')) - def clean(self): - - # Validate that the number of ports 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 {} ports, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) - # # Console ports @@ -2332,17 +2218,10 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm): } -class ConsolePortCreateForm(BootstrapMixin, forms.Form): +class ConsolePortCreateForm(LabeledComponentForm): device = DynamicModelChoiceField( queryset=Device.objects.prefetch_related('device_type__manufacturer') ) - name_pattern = ExpandableNameField( - label='Name' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, @@ -2356,18 +2235,6 @@ class ConsolePortCreateForm(BootstrapMixin, forms.Form): required=False ) - def clean(self): - - # Validate that the number of ports 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 {} ports, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) - class ConsolePortBulkCreateForm( form_from_model(ConsolePort, ['type', 'description', 'tags']), @@ -2434,17 +2301,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' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) type = forms.ChoiceField( choices=add_blank_choice(ConsolePortTypeChoices), required=False, @@ -2458,18 +2318,6 @@ class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form): required=False ) - def clean(self): - - # Validate that the number of ports 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 {} ports, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) - class ConsoleServerPortBulkCreateForm( form_from_model(ConsoleServerPort, ['type', 'description', 'tags']), @@ -2550,17 +2398,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' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) type = forms.ChoiceField( choices=add_blank_choice(PowerPortTypeChoices), required=False, @@ -2583,17 +2424,6 @@ class PowerPortCreateForm(BootstrapMixin, forms.Form): tags = TagField( required=False ) - def clean(self): - - # Validate that the number of ports 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 {} ports, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) class PowerPortBulkCreateForm( @@ -2674,17 +2504,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' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) type = forms.ChoiceField( choices=add_blank_choice(PowerOutletTypeChoices), required=False, @@ -2715,18 +2538,6 @@ class PowerOutletCreateForm(BootstrapMixin, forms.Form): ) self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) - def clean(self): - - # Validate that the number of ports 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 {} ports, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) - class PowerOutletBulkCreateForm( form_from_model(PowerOutlet, ['type', 'feed_leg', 'description', 'tags']), @@ -2915,17 +2726,11 @@ 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): + component_type = 'interface' device = DynamicModelChoiceField( queryset=Device.objects.prefetch_related('device_type__manufacturer') ) - name_pattern = ExpandableNameField( - label='Name' - ) - label_pattern = ExpandableNameField( - label='Label', - required=False - ) type = forms.ChoiceField( choices=InterfaceTypeChoices, widget=StaticSelect2(), @@ -3006,25 +2811,12 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form): self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk) self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk) - def clean(self): - - # Validate that the number of ports 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 {} interfaces, however {} labels will ' - 'be generated. These counts must match.'.format( - name_pattern_count, label_pattern_count) - }) - - class InterfaceBulkCreateForm( form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'description', 'tags']), DeviceBulkAddComponentForm ): - pass + component_type = 'interface' class InterfaceBulkEditForm( diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 3d8dbe33f..8e80168ab 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -804,6 +804,33 @@ class ImportForm(BootstrapMixin, forms.Form): }) +class LabeledComponentForm(BootstrapMixin, forms.Form): + """ + Base form for adding label pattern validation to `Create` forms + """ + component_type = 'port' + + 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 {} {}s, however {} labels will ' + 'be generated. These counts must match.'.format( + name_pattern_count, self.component_type, label_pattern_count) + }, code='label_pattern_mismatch') + + class TableConfigForm(BootstrapMixin, forms.Form): """ Form for configuring user's table preferences. From e0037c7f7045c264559db6a1fe73da151b5d3938 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Mon, 8 Jun 2020 23:07:53 -0400 Subject: [PATCH 12/21] pycodestyle --- netbox/dcim/api/serializers.py | 14 +++++++------- netbox/dcim/tests/test_forms.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index a2f576aca..ffa027af6 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -261,7 +261,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): class Meta: model = ConsoleServerPortTemplate - fields = ['id', 'device_type', 'name', 'label', 'type'] + fields = ['id', 'device_type', 'name', 'label', 'type'] class PowerPortTemplateSerializer(ValidatedModelSerializer): @@ -274,7 +274,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer): class Meta: model = PowerPortTemplate - fields = ['id', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw'] + fields = ['id', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw'] class PowerOutletTemplateSerializer(ValidatedModelSerializer): @@ -295,7 +295,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer): class Meta: model = PowerOutletTemplate - fields = ['id', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg'] + fields = ['id', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg'] class InterfaceTemplateSerializer(ValidatedModelSerializer): @@ -446,7 +446,7 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer) class Meta: model = ConsoleServerPort fields = [ - 'id', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', + 'id', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'tags', ] @@ -464,7 +464,7 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class Meta: model = ConsolePort fields = [ - 'id', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', + 'id', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'tags', ] @@ -494,7 +494,7 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): class Meta: model = PowerOutlet fields = [ - 'id', 'device', 'name', 'label', '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', ] @@ -512,7 +512,7 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class Meta: model = PowerPort fields = [ - 'id', 'device', 'name', 'label', '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', ] diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 116d9affc..1fc18c3d3 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -133,7 +133,7 @@ class LabelTestCase(TestCase): 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): good_interface_data = { 'device': self.device.pk, @@ -143,7 +143,7 @@ class LabelTestCase(TestCase): 'type': InterfaceTypeChoices.TYPE_100ME_FIXED, } form = InterfaceCreateForm(good_interface_data) - + print(form.errors) self.assertTrue(form.is_valid()) @@ -159,7 +159,7 @@ class LabelTestCase(TestCase): self.assertFalse(form.is_valid()) self.assertIn('label_pattern', form.errors) - + def test_console_port_template_label_count_valid(self): bad_console_port_template_data = { 'device_type': self.device_type, From 5cdaaed311d605b91278fd3404a67fe12b7d0067 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 10 Jun 2020 14:04:55 -0400 Subject: [PATCH 13/21] Use a single migration for `labels` --- ...port_label.py => 0107_component_labels.py} | 14 +++++++++-- .../dcim/migrations/0107_interface_label.py | 23 ------------------- 2 files changed, 12 insertions(+), 25 deletions(-) rename netbox/dcim/migrations/{0108_port_label.py => 0107_component_labels.py} (77%) delete mode 100644 netbox/dcim/migrations/0107_interface_label.py diff --git a/netbox/dcim/migrations/0108_port_label.py b/netbox/dcim/migrations/0107_component_labels.py similarity index 77% rename from netbox/dcim/migrations/0108_port_label.py rename to netbox/dcim/migrations/0107_component_labels.py index af0aa1962..9aa496134 100644 --- a/netbox/dcim/migrations/0108_port_label.py +++ b/netbox/dcim/migrations/0107_component_labels.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.7 on 2020-06-05 14:32 +# Generated by Django 3.0.7 on 2020-06-04 20:37 from django.db import migrations, models @@ -6,10 +6,20 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('dcim', '0107_interface_label'), + ('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', diff --git a/netbox/dcim/migrations/0107_interface_label.py b/netbox/dcim/migrations/0107_interface_label.py deleted file mode 100644 index 48970d25d..000000000 --- a/netbox/dcim/migrations/0107_interface_label.py +++ /dev/null @@ -1,23 +0,0 @@ -# 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), - ), - ] From f041c762ac4f33153de34e32fc41485185dd9207 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 10 Jun 2020 14:59:23 -0400 Subject: [PATCH 14/21] Prevent the table cell from rendering empty for `interface.label` Co-authored-by: Jeremy Stretch --- netbox/templates/dcim/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index d35504368..5714c8940 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -60,7 +60,7 @@ Label - {{ interface.label }} + {{ interface.label|placeholder }} Type From 3b54d6f8e505211cd209a2332f12ff369b049b9c Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 10 Jun 2020 15:11:47 -0400 Subject: [PATCH 15/21] No need to reference the model itself --- netbox/dcim/models/device_component_templates.py | 10 +++++----- netbox/dcim/models/device_components.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index acb5f0d46..3530d77de 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -72,7 +72,7 @@ class ConsolePortTemplate(ComponentTemplateModel): label = models.CharField( max_length=64, blank=True, - help_text="The physical label for this console port" + help_text="Physical label" ) type = models.CharField( max_length=50, @@ -117,7 +117,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel): label = models.CharField( max_length=64, blank=True, - help_text="The physical label for this console server port" + help_text="Physical label" ) type = models.CharField( max_length=50, @@ -162,7 +162,7 @@ class PowerPortTemplate(ComponentTemplateModel): label = models.CharField( max_length=64, blank=True, - help_text="The physical label for this power supply port" + help_text="Physical label" ) type = models.CharField( max_length=50, @@ -221,7 +221,7 @@ class PowerOutletTemplate(ComponentTemplateModel): label = models.CharField( max_length=64, blank=True, - help_text="The physical label for this power outlet" + help_text="Physical label" ) type = models.CharField( max_length=50, @@ -294,7 +294,7 @@ class InterfaceTemplate(ComponentTemplateModel): label = models.CharField( max_length=64, blank=True, - help_text="The physical label for this interface" + help_text="Physical label" ) type = models.CharField( max_length=50, diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index b8331365d..5a79519a2 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -234,7 +234,7 @@ class ConsolePort(CableTermination, ComponentModel): label = models.CharField( max_length=64, blank=True, - help_text="The physical label for this console port" + help_text="Physical label" ) _name = NaturalOrderingField( target_field='name', @@ -308,7 +308,7 @@ class ConsoleServerPort(CableTermination, ComponentModel): label = models.CharField( max_length=64, blank=True, - help_text="The physical label for this console server port" + help_text="Physical label" ) type = models.CharField( max_length=50, @@ -370,7 +370,7 @@ class PowerPort(CableTermination, ComponentModel): label = models.CharField( max_length=64, blank=True, - help_text="The physical label for this power supply port" + help_text="Physical label" ) type = models.CharField( max_length=50, @@ -540,7 +540,7 @@ class PowerOutlet(CableTermination, ComponentModel): label = models.CharField( max_length=64, blank=True, - help_text="The physical label for this power outlet" + help_text="Physical label" ) type = models.CharField( max_length=50, @@ -636,7 +636,7 @@ class Interface(CableTermination, ComponentModel): label = models.CharField( max_length=64, blank=True, - help_text="The physical label for this interface" + help_text="Physical label" ) _connected_interface = models.OneToOneField( to='self', From 81d08ac50bd4425c728bcb4de41922738f657014 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 10 Jun 2020 16:41:52 -0400 Subject: [PATCH 16/21] Stay consistent with codebase: i vs pos --- netbox/utilities/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 462e45819..1fe407dad 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -921,13 +921,13 @@ class ComponentCreateView(GetReturnURLMixin, View): names = form.cleaned_data['name_pattern'] labels = form.cleaned_data.get('label_pattern') - for pos, name in enumerate(names): - label = labels[pos] if labels else None + 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(pos)) + data.update(form.get_iterative_data(i)) component_form = self.model_form(data) if component_form.is_valid(): @@ -1009,8 +1009,8 @@ class BulkComponentCreateView(GetReturnURLMixin, View): names = data['name_pattern'] labels = data['label_pattern'] - for pos, name in enumerate(names): - label = labels[pos] if labels else None + for i, name in enumerate(names): + label = labels[i] if labels else None component_data = { self.parent_field: obj.pk, From e3a8638471a0fd7ea33788e4bd047ead31751c81 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 10 Jun 2020 22:04:45 -0400 Subject: [PATCH 17/21] Move `__str__()` to the abstract class --- .../dcim/models/device_component_templates.py | 30 ++++--------------- netbox/dcim/models/device_components.py | 30 ++++--------------- 2 files changed, 10 insertions(+), 50 deletions(-) diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 3530d77de..53080d8f6 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -30,6 +30,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. @@ -84,11 +89,6 @@ class ConsolePortTemplate(ComponentTemplateModel): ordering = ('device_type', '_name') unique_together = ('device_type', 'name') - def __str__(self): - if self.label: - return f"{self.name} ({self.label})" - return self.name - def instantiate(self, device): return ConsolePort( device=device, @@ -129,11 +129,6 @@ class ConsoleServerPortTemplate(ComponentTemplateModel): ordering = ('device_type', '_name') unique_together = ('device_type', 'name') - def __str__(self): - if self.label: - return f"{self.name} ({self.label})" - return self.name - def instantiate(self, device): return ConsoleServerPort( device=device, @@ -186,11 +181,6 @@ class PowerPortTemplate(ComponentTemplateModel): ordering = ('device_type', '_name') unique_together = ('device_type', 'name') - def __str__(self): - if self.label: - return f"{self.name} ({self.label})" - return self.name - def instantiate(self, device): return PowerPort( device=device, @@ -246,11 +236,6 @@ class PowerOutletTemplate(ComponentTemplateModel): ordering = ('device_type', '_name') unique_together = ('device_type', 'name') - def __str__(self): - if self.label: - return f"{self.name} ({self.label})" - return self.name - def clean(self): # Validate power port assignment @@ -309,11 +294,6 @@ class InterfaceTemplate(ComponentTemplateModel): ordering = ('device_type', '_name') unique_together = ('device_type', 'name') - def __str__(self): - if self.label: - return f"{self.name} ({self.label})" - return self.name - def instantiate(self, device): return Interface( device=device, diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 5a79519a2..022b2733d 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -44,6 +44,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: @@ -266,11 +271,6 @@ class ConsolePort(CableTermination, ComponentModel): ordering = ('device', '_name') unique_together = ('device', 'name') - def __str__(self): - if self.label: - return f"{self.name} ({self.label})" - return self.name - def get_absolute_url(self): return self.device.get_absolute_url() @@ -328,11 +328,6 @@ class ConsoleServerPort(CableTermination, ComponentModel): ordering = ('device', '_name') unique_together = ('device', 'name') - def __str__(self): - if self.label: - return f"{self.name} ({self.label})" - return self.name - def get_absolute_url(self): return self.device.get_absolute_url() @@ -416,11 +411,6 @@ class PowerPort(CableTermination, ComponentModel): ordering = ('device', '_name') unique_together = ('device', 'name') - def __str__(self): - if self.label: - return f"{self.name} ({self.label})" - return self.name - def get_absolute_url(self): return self.device.get_absolute_url() @@ -573,11 +563,6 @@ class PowerOutlet(CableTermination, ComponentModel): ordering = ('device', '_name') unique_together = ('device', 'name') - def __str__(self): - if self.label: - return f"{self.name} ({self.label})" - return self.name - def get_absolute_url(self): return self.device.get_absolute_url() @@ -718,11 +703,6 @@ class Interface(CableTermination, ComponentModel): ordering = ('device', CollateAsChar('_name')) unique_together = ('device', 'name') - def __str__(self): - if self.label: - return f"{self.name} ({self.label})" - return self.name - def get_absolute_url(self): return reverse('dcim:interface', kwargs={'pk': self.pk}) From cf81a8979f13b0481156bc8f9b0037d02ffa82e5 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 10 Jun 2020 22:10:45 -0400 Subject: [PATCH 18/21] Avoid `component_type` on subclasses --- netbox/dcim/forms.py | 3 +-- netbox/utilities/forms.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index ca706d6f2..81f8775ec 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2727,7 +2727,6 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): class InterfaceCreateForm(InterfaceCommonForm, LabeledComponentForm): - component_type = 'interface' device = DynamicModelChoiceField( queryset=Device.objects.prefetch_related('device_type__manufacturer') ) @@ -2816,7 +2815,7 @@ class InterfaceBulkCreateForm( form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'description', 'tags']), DeviceBulkAddComponentForm ): - component_type = 'interface' + pass class InterfaceBulkEditForm( diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 8e80168ab..1dfa3d608 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -808,8 +808,6 @@ class LabeledComponentForm(BootstrapMixin, forms.Form): """ Base form for adding label pattern validation to `Create` forms """ - component_type = 'port' - name_pattern = ExpandableNameField( label='Name' ) @@ -825,9 +823,9 @@ class LabeledComponentForm(BootstrapMixin, forms.Form): 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 {} {}s, however {} labels will ' + 'label_pattern': 'The provided name pattern will create {} components, however {} labels will ' 'be generated. These counts must match.'.format( - name_pattern_count, self.component_type, label_pattern_count) + name_pattern_count, label_pattern_count) }, code='label_pattern_mismatch') From 25cbab2ea4b5c8a9b79ed67759bf9ad155aad595 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 10 Jun 2020 22:16:46 -0400 Subject: [PATCH 19/21] Avoid checking `field` twice on all iterations Yields a small performance improvement Co-authored-by: Jeremy Stretch --- netbox/utilities/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 1fe407dad..ee39707e8 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -937,7 +937,7 @@ class ComponentCreateView(GetReturnURLMixin, View): # 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' - if field == 'label': + elif field == 'label': field = 'label_pattern' for e in errors: form.add_error(field, '{}: {}'.format(name, ', '.join(e))) From c1eea166c9ed76dfafdce87feefd1857ea3ae5dc Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 10 Jun 2020 22:58:52 -0400 Subject: [PATCH 20/21] Fix tests for LabeledComponentForm * Added docstring * Removed redundant tests since refactoration --- netbox/dcim/tests/test_forms.py | 34 ++++----------------------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 1fc18c3d3..aadc2cbfc 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -135,23 +135,22 @@ class LabelTestCase(TestCase): ) def test_interface_label_count_valid(self): - good_interface_data = { + """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]', - # Test that a label CAN be applied to each generated interfaces 'label_pattern': 'Interface[0-9]', 'type': InterfaceTypeChoices.TYPE_100ME_FIXED, } - form = InterfaceCreateForm(good_interface_data) + form = InterfaceCreateForm(interface_data) - print(form.errors) 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]', - # Test that a label CANNOT be applied to each generated interfaces 'label_pattern': 'Interface[0-1]', 'type': InterfaceTypeChoices.TYPE_100ME_FIXED, } @@ -159,28 +158,3 @@ class LabelTestCase(TestCase): self.assertFalse(form.is_valid()) self.assertIn('label_pattern', form.errors) - - def test_console_port_template_label_count_valid(self): - bad_console_port_template_data = { - 'device_type': self.device_type, - 'name_pattern': 'Console Port Template[4-6]', - # Test that a label CANNOT be applied to each generated console port templates - 'label_pattern': 'Serial[4-6]', - 'type': ConsolePortTypeChoices.TYPE_RJ45, - } - form = ConsolePortTemplateCreateForm(bad_console_port_template_data) - - self.assertTrue(form.is_valid()) - - def test_console_port_template_label_count_mismatch(self): - bad_console_port_template_data = { - 'device_type': self.device_type, - 'name_pattern': 'Console Port Template[4-6]', - # Test that a label CANNOT be applied to each generated console port templates - 'label_pattern': 'Serial[0-1]', - 'type': ConsolePortTypeChoices.TYPE_RJ45, - } - form = ConsolePortTemplateCreateForm(bad_console_port_template_data) - - self.assertFalse(form.is_valid()) - self.assertIn('label_pattern', form.errors) From a37d06064a9a2200fa6007d4000c68dd8fe81bec Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Thu, 11 Jun 2020 10:19:53 -0400 Subject: [PATCH 21/21] Add `label` to DeviceBay models and serializers --- netbox/dcim/api/serializers.py | 4 ++-- netbox/dcim/migrations/0107_component_labels.py | 10 ++++++++++ netbox/dcim/models/device_component_templates.py | 8 +++++--- netbox/dcim/models/device_components.py | 7 +++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index ffa027af6..3c360c4a1 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -331,7 +331,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer): class Meta: model = DeviceBayTemplate - fields = ['id', 'device_type', 'name'] + fields = ['id', 'device_type', 'name', 'label'] # @@ -603,7 +603,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'] # diff --git a/netbox/dcim/migrations/0107_component_labels.py b/netbox/dcim/migrations/0107_component_labels.py index 9aa496134..8e5ab8156 100644 --- a/netbox/dcim/migrations/0107_component_labels.py +++ b/netbox/dcim/migrations/0107_component_labels.py @@ -60,4 +60,14 @@ class Migration(migrations.Migration): 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), + ), ] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 53080d8f6..f966b0616 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -433,14 +433,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, diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 022b2733d..a542b0197 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1009,6 +1009,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, @@ -1025,6 +1030,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):