From 34a17d457194d5da5101586d0b965b00897e58ac Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 May 2020 12:18:04 -0400 Subject: [PATCH 01/12] Enable the specifcation of related objects by arbitrary attribute during CSV import --- netbox/utilities/forms.py | 51 +++++++++++++++++++++++++++------------ netbox/utilities/views.py | 22 ++++++++++++++--- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index d95c86527..5bff7ad61 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -405,10 +405,11 @@ class CSVDataField(forms.CharField): """ widget = forms.Textarea - def __init__(self, fields, required_fields=[], *args, **kwargs): + def __init__(self, model, fields, required_fields=None, *args, **kwargs): + self.model = model self.fields = fields - self.required_fields = required_fields + self.required_fields = required_fields or list() super().__init__(*args, **kwargs) @@ -423,31 +424,49 @@ class CSVDataField(forms.CharField): 'in double quotes.' def to_python(self, value): - records = [] reader = csv.reader(StringIO(value)) - # Consume and validate the first line of CSV data as column headers - headers = next(reader) - for f in self.required_fields: - if f not in headers: - raise forms.ValidationError('Required column header "{}" not found.'.format(f)) - for f in headers: - if f not in self.fields: - raise forms.ValidationError('Unexpected column header "{}" found.'.format(f)) + # Consume the first line of CSV data as column headers. Create a dictionary mapping each header to an optional + # "to" field specifying how the related object is being referenced. For example, importing a Device might use a + # `site.slug` header, to indicate the related site is being referenced by its slug. + headers = {} + for header in next(reader): + if '.' in header: + field, to_field = header.split('.', 1) + headers[field] = to_field + else: + headers[header] = None # Parse CSV data for i, row in enumerate(reader, start=1): if row: if len(row) != len(headers): - raise forms.ValidationError( - "Row {}: Expected {} columns but found {}".format(i, len(headers), len(row)) - ) + raise forms.ValidationError(f"Row {i}: Expected {len(headers)} columns but found {len(row)}") row = [col.strip() for col in row] - record = dict(zip(headers, row)) + record = dict(zip(headers.keys(), row)) records.append(record) - return records + return headers, records + + def validate(self, value): + headers, records = value + + # Validate provided column headers + for field, to_field in headers.items(): + if field not in self.fields: + raise forms.ValidationError(f'Unexpected column header "{field}" found.') + if to_field and not hasattr(self.fields[field], 'to_field_name'): + raise forms.ValidationError(f'Column "{field}" is not a related object; cannot use dots') + if to_field and not hasattr(self.fields[field].queryset.model, to_field): + raise forms.ValidationError(f'Invalid related object attribute for column "{field}": {to_field}') + + # Validate required fields + for f in self.required_fields: + if f not in headers: + raise forms.ValidationError(f'Required column header "{f}" not found.') + + return value class CSVChoiceField(forms.ChoiceField): diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 294acb1d1..b1f74a9c6 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -557,11 +557,18 @@ class BulkImportView(GetReturnURLMixin, View): def _import_form(self, *args, **kwargs): - fields = self.model_form().fields.keys() - required_fields = [name for name, field in self.model_form().fields.items() if field.required] + fields = self.model_form().fields + required_fields = [ + name for name, field in self.model_form().fields.items() if field.required + ] class ImportForm(BootstrapMixin, Form): - csv = CSVDataField(fields=fields, required_fields=required_fields, widget=Textarea(attrs=self.widget_attrs)) + csv = CSVDataField( + model=self.model_form.Meta.model, + fields=fields, + required_fields=required_fields, + widget=Textarea(attrs=self.widget_attrs) + ) return ImportForm(*args, **kwargs) @@ -591,8 +598,15 @@ class BulkImportView(GetReturnURLMixin, View): try: # Iterate through CSV data and bind each row to a new model form instance. with transaction.atomic(): - for row, data in enumerate(form.cleaned_data['csv'], start=1): + headers, records = form.cleaned_data['csv'] + for row, data in enumerate(records, start=1): obj_form = self.model_form(data) + + # Modify the model form to accommodate any customized to_field_name properties + for field, to_field in headers.items(): + if to_field is not None: + obj_form.fields[field].to_field_name = to_field + if obj_form.is_valid(): obj = self._save_obj(obj_form, request) new_objs.append(obj) From 61ae4be16a6ebd0e3741d491ebbbf6baf973eacb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 May 2020 13:32:28 -0400 Subject: [PATCH 02/12] Add tests for CSVDataField --- netbox/utilities/forms.py | 27 +++++---- netbox/utilities/tests/test_forms.py | 84 ++++++++++++++++++++++++++++ netbox/utilities/views.py | 9 +-- 3 files changed, 100 insertions(+), 20 deletions(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 5bff7ad61..61ab28ec8 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -405,11 +405,14 @@ class CSVDataField(forms.CharField): """ widget = forms.Textarea - def __init__(self, model, fields, required_fields=None, *args, **kwargs): + def __init__(self, from_form, *args, **kwargs): - self.model = model - self.fields = fields - self.required_fields = required_fields or list() + form = from_form() + self.model = form.Meta.model + self.fields = form.fields + self.required_fields = [ + name for name, field in form.fields.items() if field.required + ] super().__init__(*args, **kwargs) @@ -417,15 +420,16 @@ class CSVDataField(forms.CharField): if not self.label: self.label = '' if not self.initial: - self.initial = ','.join(required_fields) + '\n' + self.initial = ','.join(self.required_fields) + '\n' if not self.help_text: self.help_text = 'Enter the list of column headers followed by one line per record to be imported, using ' \ 'commas to separate values. Multi-line data and values containing commas may be wrapped ' \ 'in double quotes.' def to_python(self, value): + records = [] - reader = csv.reader(StringIO(value)) + reader = csv.reader(StringIO(value.strip())) # Consume the first line of CSV data as column headers. Create a dictionary mapping each header to an optional # "to" field specifying how the related object is being referenced. For example, importing a Device might use a @@ -440,12 +444,11 @@ class CSVDataField(forms.CharField): # Parse CSV data for i, row in enumerate(reader, start=1): - if row: - if len(row) != len(headers): - raise forms.ValidationError(f"Row {i}: Expected {len(headers)} columns but found {len(row)}") - row = [col.strip() for col in row] - record = dict(zip(headers.keys(), row)) - records.append(record) + if len(row) != len(headers): + raise forms.ValidationError(f"Row {i}: Expected {len(headers)} columns but found {len(row)}") + row = [col.strip() for col in row] + record = dict(zip(headers.keys(), row)) + records.append(record) return headers, records diff --git a/netbox/utilities/tests/test_forms.py b/netbox/utilities/tests/test_forms.py index 2d7235505..d6af27b93 100644 --- a/netbox/utilities/tests/test_forms.py +++ b/netbox/utilities/tests/test_forms.py @@ -1,6 +1,8 @@ from django import forms from django.test import TestCase +from ipam.forms import IPAddressCSVForm +from ipam.models import VRF from utilities.forms import * @@ -281,3 +283,85 @@ class ExpandAlphanumeric(TestCase): with self.assertRaises(ValueError): sorted(expand_alphanumeric_pattern('r[a,,b]a')) + + +class CSVDataFieldTest(TestCase): + + def setUp(self): + self.field = CSVDataField(from_form=IPAddressCSVForm) + + def test_clean(self): + input = """ + address,status,vrf + 192.0.2.1/32,Active,Test VRF + """ + output = ( + {'address': None, 'status': None, 'vrf': None}, + [{'address': '192.0.2.1/32', 'status': 'Active', 'vrf': 'Test VRF'}] + ) + self.assertEqual(self.field.clean(input), output) + + def test_clean_invalid_header(self): + input = """ + address,status,vrf,xxx + 192.0.2.1/32,Active,Test VRF,123 + """ + with self.assertRaises(forms.ValidationError): + self.field.clean(input) + + def test_clean_missing_required_header(self): + input = """ + status,vrf + Active,Test VRF + """ + with self.assertRaises(forms.ValidationError): + self.field.clean(input) + + def test_clean_default_to_field(self): + input = """ + address,status,vrf.name + 192.0.2.1/32,Active,Test VRF + """ + output = ( + {'address': None, 'status': None, 'vrf': 'name'}, + [{'address': '192.0.2.1/32', 'status': 'Active', 'vrf': 'Test VRF'}] + ) + self.assertEqual(self.field.clean(input), output) + + def test_clean_pk_to_field(self): + input = """ + address,status,vrf.pk + 192.0.2.1/32,Active,123 + """ + output = ( + {'address': None, 'status': None, 'vrf': 'pk'}, + [{'address': '192.0.2.1/32', 'status': 'Active', 'vrf': '123'}] + ) + self.assertEqual(self.field.clean(input), output) + + def test_clean_custom_to_field(self): + input = """ + address,status,vrf.rd + 192.0.2.1/32,Active,123:456 + """ + output = ( + {'address': None, 'status': None, 'vrf': 'rd'}, + [{'address': '192.0.2.1/32', 'status': 'Active', 'vrf': '123:456'}] + ) + self.assertEqual(self.field.clean(input), output) + + def test_clean_invalid_to_field(self): + input = """ + address,status,vrf.xxx + 192.0.2.1/32,Active,123:456 + """ + with self.assertRaises(forms.ValidationError): + self.field.clean(input) + + def test_clean_to_field_on_non_object(self): + input = """ + address,status.foo,vrf + 192.0.2.1/32,Bar,Test VRF + """ + with self.assertRaises(forms.ValidationError): + self.field.clean(input) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index b1f74a9c6..964d9490c 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -557,16 +557,9 @@ class BulkImportView(GetReturnURLMixin, View): def _import_form(self, *args, **kwargs): - fields = self.model_form().fields - required_fields = [ - name for name, field in self.model_form().fields.items() if field.required - ] - class ImportForm(BootstrapMixin, Form): csv = CSVDataField( - model=self.model_form.Meta.model, - fields=fields, - required_fields=required_fields, + from_form=self.model_form, widget=Textarea(attrs=self.widget_attrs) ) From 4b8ef6b09a5ba62ca7d5c8195ceb52abc6605ab4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 May 2020 13:40:52 -0400 Subject: [PATCH 03/12] Removed FlexibleModelChoiceField --- netbox/dcim/forms.py | 61 +++++++++++++++------------------------ netbox/ipam/forms.py | 26 +++++++---------- netbox/secrets/forms.py | 7 ++--- netbox/utilities/forms.py | 21 -------------- 4 files changed, 37 insertions(+), 78 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 98b321b90..a126a1ea0 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -23,8 +23,8 @@ from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, ExpandableNameField, FlexibleModelChoiceField, form_from_model, JSONField, - SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, + DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SelectWithPK, SmallTextarea, + SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup, VirtualMachine @@ -2046,10 +2046,10 @@ class DeviceCSVForm(BaseDeviceCSVForm): class ChildDeviceCSVForm(BaseDeviceCSVForm): - parent = FlexibleModelChoiceField( + parent = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Name or ID of parent device', + help_text='Parent device', error_messages={ 'invalid_choice': 'Parent device not found.', } @@ -2381,10 +2381,9 @@ class ConsolePortBulkEditForm( class ConsolePortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Name or ID of device', error_messages={ 'invalid_choice': 'Device not found.', } @@ -2485,10 +2484,9 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): class ConsoleServerPortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Name or ID of device', error_messages={ 'invalid_choice': 'Device not found.', } @@ -2585,10 +2583,9 @@ class PowerPortBulkEditForm( class PowerPortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Name or ID of device', error_messages={ 'invalid_choice': 'Device not found.', } @@ -2736,21 +2733,19 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): class PowerOutletCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Name or ID of device', error_messages={ 'invalid_choice': 'Device not found.', } ) - power_port = FlexibleModelChoiceField( + power_port = forms.ModelChoiceField( queryset=PowerPort.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of Power Port', error_messages={ - 'invalid_choice': 'Power Port not found.', + 'invalid_choice': 'Power port not found.', } ) feed_leg = CSVChoiceField( @@ -3058,29 +3053,27 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): class InterfaceCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of device', error_messages={ 'invalid_choice': 'Device not found.', } ) - virtual_machine = FlexibleModelChoiceField( + virtual_machine = forms.ModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of virtual machine', error_messages={ 'invalid_choice': 'Virtual machine not found.', } ) - lag = FlexibleModelChoiceField( + lag = forms.ModelChoiceField( queryset=Interface.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of LAG interface', + help_text='LAG interface', error_messages={ 'invalid_choice': 'LAG interface not found.', } @@ -3271,18 +3264,16 @@ class FrontPortBulkDisconnectForm(ConfirmationForm): class FrontPortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Name or ID of device', error_messages={ 'invalid_choice': 'Device not found.', } ) - rear_port = FlexibleModelChoiceField( + rear_port = forms.ModelChoiceField( queryset=RearPort.objects.all(), to_field_name='name', - help_text='Name or ID of Rear Port', error_messages={ 'invalid_choice': 'Rear Port not found.', } @@ -3409,10 +3400,9 @@ class RearPortBulkDisconnectForm(ConfirmationForm): class RearPortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Name or ID of device', error_messages={ 'invalid_choice': 'Device not found.', } @@ -3517,19 +3507,17 @@ class DeviceBayBulkRenameForm(BulkRenameForm): class DeviceBayCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Name or ID of device', error_messages={ 'invalid_choice': 'Device not found.', } ) - installed_device = FlexibleModelChoiceField( + installed_device = forms.ModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of device', error_messages={ 'invalid_choice': 'Child device not found.', } @@ -3811,10 +3799,10 @@ class CableForm(BootstrapMixin, forms.ModelForm): class CableCSVForm(forms.ModelForm): # Termination A - side_a_device = FlexibleModelChoiceField( + side_a_device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Side A device name or ID', + help_text='Side A device', error_messages={ 'invalid_choice': 'Side A device not found', } @@ -3830,10 +3818,10 @@ class CableCSVForm(forms.ModelForm): ) # Termination B - side_b_device = FlexibleModelChoiceField( + side_b_device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Side B device name or ID', + help_text='Side B device', error_messages={ 'invalid_choice': 'Side B device not found', } @@ -4164,10 +4152,9 @@ class InventoryItemCreateForm(BootstrapMixin, forms.Form): class InventoryItemCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Device name or ID', error_messages={ 'invalid_choice': 'Device not found.', } diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 854843f2e..9a568a2a6 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -11,16 +11,14 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, - DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, - FlexibleModelChoiceField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, - BOOLEAN_WITH_BLANK_CHOICES, + DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, ReturnURLForm, + SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import VirtualMachine -from .constants import * from .choices import * +from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF - PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([ (i, i) for i in range(PREFIX_LENGTH_MIN, PREFIX_LENGTH_MAX + 1) ]) @@ -333,11 +331,10 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class PrefixCSVForm(CustomFieldModelCSVForm): - vrf = FlexibleModelChoiceField( + vrf = forms.ModelChoiceField( queryset=VRF.objects.all(), to_field_name='name', required=False, - help_text='Name of parent VRF (or {ID})', error_messages={ 'invalid_choice': 'VRF not found.', } @@ -737,11 +734,10 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class IPAddressCSVForm(CustomFieldModelCSVForm): - vrf = FlexibleModelChoiceField( + vrf = forms.ModelChoiceField( queryset=VRF.objects.all(), to_field_name='name', required=False, - help_text='Name of parent VRF (or {ID})', error_messages={ 'invalid_choice': 'VRF not found.', } @@ -764,11 +760,11 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): required=False, help_text='Functional role' ) - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of assigned device', + help_text='Assigned device', error_messages={ 'invalid_choice': 'Device not found.', } @@ -777,7 +773,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned virtual machine', + help_text='Assigned virtual machine', error_messages={ 'invalid_choice': 'Virtual machine not found.', } @@ -1299,20 +1295,18 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm): class ServiceCSVForm(CustomFieldModelCSVForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of device', error_messages={ 'invalid_choice': 'Device not found.', } ) - virtual_machine = FlexibleModelChoiceField( + virtual_machine = forms.ModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', - help_text='Name or ID of virtual machine', error_messages={ 'invalid_choice': 'Virtual machine not found.', } diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 03ff8fab8..3243687e5 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -8,8 +8,8 @@ from extras.forms import ( AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, ) from utilities.forms import ( - APISelect, APISelectMultiple, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - FlexibleModelChoiceField, SlugField, StaticSelect2Multiple, TagFilterField, + APISelectMultiple, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, + StaticSelect2Multiple, TagFilterField, ) from .constants import * from .models import Secret, SecretRole, UserKey @@ -120,10 +120,9 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm): class SecretCSVForm(CustomFieldModelCSVForm): - device = FlexibleModelChoiceField( + device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Device name or ID', error_messages={ 'invalid_choice': 'Device not found.', } diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 61ab28ec8..351b845ae 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -552,27 +552,6 @@ class CommentField(forms.CharField): super().__init__(required=required, label=label, help_text=help_text, *args, **kwargs) -class FlexibleModelChoiceField(forms.ModelChoiceField): - """ - Allow a model to be reference by either '{ID}' or the field specified by `to_field_name`. - """ - def to_python(self, value): - if value in self.empty_values: - return None - try: - if not self.to_field_name: - key = 'pk' - elif re.match(r'^\{\d+\}$', value): - key = 'pk' - value = value.strip('{}') - else: - key = self.to_field_name - value = self.queryset.get(**{key: value}) - except (ValueError, TypeError, self.queryset.model.DoesNotExist): - raise forms.ValidationError(self.error_messages['invalid_choice'], code='invalid_choice') - return value - - class SlugField(forms.SlugField): """ Extend the built-in SlugField to automatically populate from a field called `name` unless otherwise specified. From fa630c048cd40956f0831b8097f252249a4cf6f8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 May 2020 14:26:04 -0400 Subject: [PATCH 04/12] Overhaul CSV import template --- .../templates/utilities/obj_bulk_import.html | 134 +++++++++++------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/netbox/templates/utilities/obj_bulk_import.html b/netbox/templates/utilities/obj_bulk_import.html index a476cbd15..ea46dd08b 100644 --- a/netbox/templates/utilities/obj_bulk_import.html +++ b/netbox/templates/utilities/obj_bulk_import.html @@ -3,58 +3,94 @@ {% load form_helpers %} {% block content %} -

{% block title %}{{ obj_type|bettertitle }} Bulk Import{% endblock %}

{% block tabs %}{% endblock %} -
-
- {% if form.non_field_errors %} -
-
Errors
-
- {{ form.non_field_errors }} +
+
+

{% block title %}{{ obj_type|bettertitle }} Bulk Import{% endblock %}

+ {% if form.non_field_errors %} +
+
Errors
+
+ {{ form.non_field_errors }} +
-
- {% endif %} -
- {% csrf_token %} - {% render_form form %} -
-
- - {% if return_url %} - Cancel + {% endif %} + +
+
+ + {% csrf_token %} + {% render_form form %} +
+
+ + {% if return_url %} + Cancel + {% endif %} +
+
+ +
+

+ {% if fields %} +
+
+ CSV Field Options +
+ + + + + + + + {% for name, field in fields.items %} + + + + + + + {% endfor %} +
FieldRequiredDynamicDescription
+ {{ name }} + + {% if field.required %} + + {% endif %} + + {% if field.to_field_name %} + + {% endif %} + + {% if field.help_text %} + {{ field.help_text }}
+ {% elif field.label %} + {{ field.label }}
+ {% endif %} + {% if field.choices %} + Choices: {{ field|example_choices }} + {% elif field|widget_type == 'dateinput' %} + Format: YYYY-MM-DD + {% elif field|widget_type == 'checkboxinput' %} + Specify "true" or "false" + {% endif %} +
+
+

+ Required fields must be specified for all + objects. +

+

+ Dynamic fields may optionally refer to a related object by an + alternative attribute. For example, vrf.rd would identify a VRF by its RD + attribute. +

{% endif %}
- -
-
- {% if fields %} -

CSV Format

- - - - - - - {% for name, field in fields.items %} - - - - - - {% endfor %} -
FieldRequiredDescription
{{ name }}{% if field.required %}{% endif %} - {{ field.help_text|default:field.label }} - {% if field.choices %} -
Choices: {{ field|example_choices }} - {% elif field|widget_type == 'dateinput' %} -
Format: YYYY-MM-DD - {% elif field|widget_type == 'checkboxinput' %} -
Specify "true" or "false" - {% endif %} -
- {% endif %} -
-
+
+
{% endblock %} From 718ff4a743e3bc88b2c7da6e43c1844d1ee577a9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 May 2020 15:40:34 -0400 Subject: [PATCH 05/12] Update help_texts for models, import forms --- netbox/circuits/forms.py | 10 +- netbox/circuits/models.py | 5 +- netbox/dcim/forms.py | 126 ++++++++++-------------- netbox/dcim/models/__init__.py | 30 ++++-- netbox/dcim/models/device_components.py | 19 ++-- netbox/ipam/forms.py | 31 +++--- netbox/ipam/models.py | 8 +- netbox/secrets/forms.py | 6 +- netbox/tenancy/forms.py | 11 +-- netbox/virtualization/forms.py | 22 ++--- 10 files changed, 118 insertions(+), 150 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index c6f0dfdc4..05d34e351 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -55,12 +55,6 @@ class ProviderCSVForm(CustomFieldModelCSVForm): class Meta: model = Provider fields = Provider.csv_headers - help_texts = { - 'name': 'Provider name', - 'asn': '32-bit autonomous system number', - 'portal_url': 'Portal URL', - 'comments': 'Free-form comments', - } class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -195,7 +189,7 @@ class CircuitCSVForm(CustomFieldModelCSVForm): provider = forms.ModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', - help_text='Name of parent provider', + help_text='Assigned provider', error_messages={ 'invalid_choice': 'Provider not found.' } @@ -217,7 +211,7 @@ class CircuitCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', + help_text='Assigned tenant', error_messages={ 'invalid_choice': 'Tenant not found.' } diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index e9e8f8aa1..57d41a994 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -38,7 +38,8 @@ class Provider(ChangeLoggedModel, CustomFieldModel): asn = ASNField( blank=True, null=True, - verbose_name='ASN' + verbose_name='ASN', + help_text='32-bit autonomous system number' ) account = models.CharField( max_length=30, @@ -47,7 +48,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel): ) portal_url = models.URLField( blank=True, - verbose_name='Portal' + verbose_name='Portal URL' ) noc_contact = models.TextField( blank=True, diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index a126a1ea0..94561f9a0 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -5,6 +5,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.forms.array import SimpleArrayField from django.core.exceptions import ObjectDoesNotExist +from django.utils.safestring import mark_safe from mptt.forms import TreeNodeChoiceField from netaddr import EUI from netaddr.core import AddrFormatError @@ -206,10 +207,6 @@ class RegionCSVForm(forms.ModelForm): class Meta: model = Region fields = Region.csv_headers - help_texts = { - 'name': 'Region name', - 'slug': 'URL-friendly slug', - } class RegionFilterForm(BootstrapMixin, forms.Form): @@ -280,7 +277,7 @@ class SiteCSVForm(CustomFieldModelCSVForm): queryset=Region.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned region', + help_text='Assigned region', error_messages={ 'invalid_choice': 'Region not found.', } @@ -289,7 +286,7 @@ class SiteCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', + help_text='Assigned tenant', error_messages={ 'invalid_choice': 'Tenant not found.', } @@ -299,9 +296,9 @@ class SiteCSVForm(CustomFieldModelCSVForm): model = Site fields = Site.csv_headers help_texts = { - 'name': 'Site name', - 'slug': 'URL-friendly slug', - 'asn': '32-bit autonomous system number', + 'time_zone': mark_safe( + 'Time zone (available options)' + ) } @@ -395,7 +392,7 @@ class RackGroupCSVForm(forms.ModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', + help_text='Assigned site', error_messages={ 'invalid_choice': 'Site not found.', } @@ -404,7 +401,7 @@ class RackGroupCSVForm(forms.ModelForm): queryset=RackGroup.objects.all(), required=False, to_field_name='name', - help_text='Name of parent rack group', + help_text='Parent rack group', error_messages={ 'invalid_choice': 'Rack group not found.', } @@ -413,10 +410,6 @@ class RackGroupCSVForm(forms.ModelForm): class Meta: model = RackGroup fields = RackGroup.csv_headers - help_texts = { - 'name': 'Name of rack group', - 'slug': 'URL-friendly slug', - } class RackGroupFilterForm(BootstrapMixin, forms.Form): @@ -475,8 +468,7 @@ class RackRoleCSVForm(forms.ModelForm): model = RackRole fields = RackRole.csv_headers help_texts = { - 'name': 'Name of rack role', - 'color': 'RGB color in hexadecimal (e.g. 00ff00)' + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), } @@ -530,13 +522,11 @@ class RackCSVForm(CustomFieldModelCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', error_messages={ 'invalid_choice': 'Site not found.', } ) group_name = forms.CharField( - help_text='Name of rack group', required=False ) tenant = forms.ModelChoiceField( @@ -580,10 +570,6 @@ class RackCSVForm(CustomFieldModelCSVForm): class Meta: model = Rack fields = Rack.csv_headers - help_texts = { - 'name': 'Rack name', - 'u_height': 'Height in rack units', - } def clean(self): @@ -832,7 +818,7 @@ class RackReservationCSVForm(forms.ModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', + help_text='Parent site', error_messages={ 'invalid_choice': 'Invalid site name.', } @@ -853,7 +839,7 @@ class RackReservationCSVForm(forms.ModelForm): queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', + help_text='Assigned tenant', error_messages={ 'invalid_choice': 'Tenant not found.', } @@ -862,8 +848,6 @@ class RackReservationCSVForm(forms.ModelForm): class Meta: model = RackReservation fields = ('site', 'rack_group', 'rack_name', 'units', 'tenant', 'description') - help_texts = { - } def clean(self): @@ -954,10 +938,6 @@ class ManufacturerCSVForm(forms.ModelForm): class Meta: model = Manufacturer fields = Manufacturer.csv_headers - help_texts = { - 'name': 'Manufacturer name', - 'slug': 'URL-friendly slug', - } # @@ -1675,8 +1655,7 @@ class DeviceRoleCSVForm(forms.ModelForm): model = DeviceRole fields = DeviceRole.csv_headers help_texts = { - 'name': 'Name of device role', - 'color': 'RGB color in hexadecimal (e.g. 00ff00)' + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), } @@ -1709,7 +1688,7 @@ class PlatformCSVForm(forms.ModelForm): queryset=Manufacturer.objects.all(), required=False, to_field_name='name', - help_text='Manufacturer name', + help_text='Limit platform assignments to this manufacturer', error_messages={ 'invalid_choice': 'Manufacturer not found.', } @@ -1718,9 +1697,6 @@ class PlatformCSVForm(forms.ModelForm): class Meta: model = Platform fields = Platform.csv_headers - help_texts = { - 'name': 'Platform name', - } # @@ -1925,7 +1901,7 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): device_role = forms.ModelChoiceField( queryset=DeviceRole.objects.all(), to_field_name='name', - help_text='Name of assigned role', + help_text='Assigned role', error_messages={ 'invalid_choice': 'Invalid device role.', } @@ -1934,7 +1910,7 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', + help_text='Assigned tenant', error_messages={ 'invalid_choice': 'Tenant not found.', } @@ -1954,7 +1930,7 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): queryset=Platform.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned platform', + help_text='Assigned platform', error_messages={ 'invalid_choice': 'Invalid platform.', } @@ -1963,13 +1939,19 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): choices=DeviceStatusChoices, help_text='Operational status' ) + cluster = forms.ModelChoiceField( + queryset=Cluster.objects.all(), + to_field_name='name', + required=False, + help_text='Virtualization cluster', + error_messages={ + 'invalid_choice': 'Invalid cluster name.', + } + ) class Meta: fields = [] model = Device - help_texts = { - 'name': 'Device name', - } def clean(self): @@ -1990,14 +1972,14 @@ class DeviceCSVForm(BaseDeviceCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', + help_text='Assigned site', error_messages={ 'invalid_choice': 'Invalid site name.', } ) rack_group = forms.CharField( required=False, - help_text='Parent rack\'s group (if any)' + help_text='Assigned rack\'s group (if any)' ) rack_name = forms.CharField( required=False, @@ -2008,15 +1990,6 @@ class DeviceCSVForm(BaseDeviceCSVForm): required=False, help_text='Mounted rack face' ) - cluster = forms.ModelChoiceField( - queryset=Cluster.objects.all(), - to_field_name='name', - required=False, - help_text='Virtualization cluster', - error_messages={ - 'invalid_choice': 'Invalid cluster name.', - } - ) class Meta(BaseDeviceCSVForm.Meta): fields = [ @@ -2057,15 +2030,6 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm): device_bay_name = forms.CharField( help_text='Name of device bay', ) - cluster = forms.ModelChoiceField( - queryset=Cluster.objects.all(), - to_field_name='name', - required=False, - help_text='Virtualization cluster', - error_messages={ - 'invalid_choice': 'Invalid cluster name.', - } - ) class Meta(BaseDeviceCSVForm.Meta): fields = [ @@ -2744,6 +2708,7 @@ class PowerOutletCSVForm(forms.ModelForm): queryset=PowerPort.objects.all(), required=False, to_field_name='name', + help_text='Local power port which feeds this outlet', error_messages={ 'invalid_choice': 'Power port not found.', } @@ -2751,6 +2716,7 @@ class PowerOutletCSVForm(forms.ModelForm): feed_leg = CSVChoiceField( choices=PowerOutletFeedLegChoices, required=False, + help_text='Electrical phase (for three-phase circuits)' ) class Meta: @@ -3073,17 +3039,19 @@ class InterfaceCSVForm(forms.ModelForm): queryset=Interface.objects.all(), required=False, to_field_name='name', - help_text='LAG interface', + help_text='Parent LAG interface', error_messages={ 'invalid_choice': 'LAG interface not found.', } ) type = CSVChoiceField( choices=InterfaceTypeChoices, + help_text='Physical medium' ) mode = CSVChoiceField( choices=InterfaceModeChoices, required=False, + help_text='IEEE 802.1Q operational mode (for L2 interfaces)' ) class Meta: @@ -3274,17 +3242,22 @@ class FrontPortCSVForm(forms.ModelForm): rear_port = forms.ModelChoiceField( queryset=RearPort.objects.all(), to_field_name='name', + help_text='Corresponding rear port', error_messages={ 'invalid_choice': 'Rear Port not found.', } ) type = CSVChoiceField( choices=PortTypeChoices, + help_text='Physical medium classification' ) class Meta: model = FrontPort fields = FrontPort.csv_headers + help_texts = { + 'rear_port_position': 'Mapped position on corresponding rear port', + } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -3408,12 +3381,16 @@ class RearPortCSVForm(forms.ModelForm): } ) type = CSVChoiceField( + help_text='Physical medium classification', choices=PortTypeChoices, ) class Meta: model = RearPort fields = RearPort.csv_headers + help_texts = { + 'positions': 'Number of front ports which may be mapped' + } # @@ -3518,6 +3495,7 @@ class DeviceBayCSVForm(forms.ModelForm): queryset=Device.objects.all(), required=False, to_field_name='name', + help_text='Child device installed within this bay', error_messages={ 'invalid_choice': 'Child device not found.', } @@ -3797,7 +3775,6 @@ class CableForm(BootstrapMixin, forms.ModelForm): class CableCSVForm(forms.ModelForm): - # Termination A side_a_device = forms.ModelChoiceField( queryset=Device.objects.all(), @@ -3814,7 +3791,7 @@ class CableCSVForm(forms.ModelForm): help_text='Side A type' ) side_a_name = forms.CharField( - help_text='Side A component' + help_text='Side A component name' ) # Termination B @@ -3833,7 +3810,7 @@ class CableCSVForm(forms.ModelForm): help_text='Side B type' ) side_b_name = forms.CharField( - help_text='Side B component' + help_text='Side B component name' ) # Cable attributes @@ -3845,7 +3822,7 @@ class CableCSVForm(forms.ModelForm): type = CSVChoiceField( choices=CableTypeChoices, required=False, - help_text='Cable type' + help_text='Physical medium classification' ) length_unit = CSVChoiceField( choices=CableLengthUnitChoices, @@ -3860,7 +3837,7 @@ class CableCSVForm(forms.ModelForm): 'status', 'label', 'color', 'length', 'length_unit', ] help_texts = { - 'color': 'RGB color in hexadecimal (e.g. 00ff00)' + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), } # TODO: Merge the clean() methods for either end @@ -4163,7 +4140,6 @@ class InventoryItemCSVForm(forms.ModelForm): queryset=Manufacturer.objects.all(), to_field_name='name', required=False, - help_text='Manufacturer name', error_messages={ 'invalid_choice': 'Invalid manufacturer.', } @@ -4614,7 +4590,7 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', + help_text='Assigned site', error_messages={ 'invalid_choice': 'Site not found.', } @@ -4622,18 +4598,18 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): panel_name = forms.ModelChoiceField( queryset=PowerPanel.objects.all(), to_field_name='name', - help_text='Name of upstream power panel', + help_text='Upstream power panel', error_messages={ 'invalid_choice': 'Power panel not found.', } ) rack_group = forms.CharField( required=False, - help_text="Rack group name (optional)" + help_text="Assigned rack's group name" ) rack_name = forms.CharField( required=False, - help_text="Rack name (optional)" + help_text="Assigned rack name" ) status = CSVChoiceField( choices=PowerFeedStatusChoices, @@ -4648,7 +4624,7 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): supply = CSVChoiceField( choices=PowerFeedSupplyChoices, required=False, - help_text='AC/DC' + help_text='Supply type (AC/DC)' ) phase = CSVChoiceField( choices=PowerFeedPhaseChoices, diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 4b30d20d1..cb7c14fe2 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -179,12 +179,14 @@ class Site(ChangeLoggedModel, CustomFieldModel): ) facility = models.CharField( max_length=50, - blank=True + blank=True, + help_text='Local facility ID or description' ) asn = ASNField( blank=True, null=True, - verbose_name='ASN' + verbose_name='ASN', + help_text='32-bit autonomous system number' ) time_zone = TimeZoneField( blank=True @@ -205,13 +207,15 @@ class Site(ChangeLoggedModel, CustomFieldModel): max_digits=8, decimal_places=6, blank=True, - null=True + null=True, + help_text='GPS coordinate (latitude)' ) longitude = models.DecimalField( max_digits=9, decimal_places=6, blank=True, - null=True + null=True, + help_text='GPS coordinate (longitude)' ) contact_name = models.CharField( max_length=50, @@ -418,7 +422,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): max_length=50, blank=True, null=True, - verbose_name='Facility ID' + verbose_name='Facility ID', + help_text='Locally-assigned identifier' ) site = models.ForeignKey( to='dcim.Site', @@ -430,7 +435,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): on_delete=models.SET_NULL, related_name='racks', blank=True, - null=True + null=True, + help_text='Assigned group' ) tenant = models.ForeignKey( to='tenancy.Tenant', @@ -449,7 +455,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): on_delete=models.PROTECT, related_name='racks', blank=True, - null=True + null=True, + help_text='Functional role' ) serial = models.CharField( max_length=50, @@ -479,7 +486,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): u_height = models.PositiveSmallIntegerField( default=RACK_U_HEIGHT_DEFAULT, verbose_name='Height (U)', - validators=[MinValueValidator(1), MaxValueValidator(100)] + validators=[MinValueValidator(1), MaxValueValidator(100)], + help_text='Height in rack units' ) desc_units = models.BooleanField( default=False, @@ -488,11 +496,13 @@ class Rack(ChangeLoggedModel, CustomFieldModel): ) outer_width = models.PositiveSmallIntegerField( blank=True, - null=True + null=True, + help_text='Outer dimension of rack (width)' ) outer_depth = models.PositiveSmallIntegerField( blank=True, - null=True + null=True, + help_text='Outer dimension of rack (depth)' ) outer_unit = models.CharField( max_length=50, diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 3b61f80ba..4005d41a4 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -239,7 +239,8 @@ class ConsolePort(CableTermination, ComponentModel): type = models.CharField( max_length=50, choices=ConsolePortTypeChoices, - blank=True + blank=True, + help_text='Physical port type' ) connected_endpoint = models.OneToOneField( to='dcim.ConsoleServerPort', @@ -300,7 +301,8 @@ class ConsoleServerPort(CableTermination, ComponentModel): type = models.CharField( max_length=50, choices=ConsolePortTypeChoices, - blank=True + blank=True, + help_text='Physical port type' ) connection_status = models.NullBooleanField( choices=CONNECTION_STATUS_CHOICES, @@ -354,7 +356,8 @@ class PowerPort(CableTermination, ComponentModel): type = models.CharField( max_length=50, choices=PowerPortTypeChoices, - blank=True + blank=True, + help_text='Physical port type' ) maximum_draw = models.PositiveSmallIntegerField( blank=True, @@ -516,7 +519,8 @@ class PowerOutlet(CableTermination, ComponentModel): type = models.CharField( max_length=50, choices=PowerOutletTypeChoices, - blank=True + blank=True, + help_text='Physical port type' ) power_port = models.ForeignKey( to='dcim.PowerPort', @@ -653,7 +657,7 @@ class Interface(CableTermination, ComponentModel): mode = models.CharField( max_length=50, choices=InterfaceModeChoices, - blank=True, + blank=True ) untagged_vlan = models.ForeignKey( to='ipam.VLAN', @@ -1083,7 +1087,8 @@ class InventoryItem(ComponentModel): part_id = models.CharField( max_length=50, verbose_name='Part ID', - blank=True + blank=True, + help_text='Manufacturer-assigned part identifier' ) serial = models.CharField( max_length=50, @@ -1100,7 +1105,7 @@ class InventoryItem(ComponentModel): ) discovered = models.BooleanField( default=False, - verbose_name='Discovered' + help_text='This item was automatically discovered' ) tags = TaggableManager(through=TaggedItem) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 9a568a2a6..7bed22866 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -55,7 +55,7 @@ class VRFCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', + help_text='Assigned tenant', error_messages={ 'invalid_choice': 'Tenant not found.', } @@ -64,9 +64,6 @@ class VRFCSVForm(CustomFieldModelCSVForm): class Meta: model = VRF fields = VRF.csv_headers - help_texts = { - 'name': 'VRF name', - } class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -169,7 +166,7 @@ class AggregateCSVForm(CustomFieldModelCSVForm): rir = forms.ModelChoiceField( queryset=RIR.objects.all(), to_field_name='name', - help_text='Name of parent RIR', + help_text='Assigned RIR', error_messages={ 'invalid_choice': 'RIR not found.', } @@ -251,9 +248,6 @@ class RoleCSVForm(forms.ModelForm): class Meta: model = Role fields = Role.csv_headers - help_texts = { - 'name': 'Role name', - } # @@ -335,6 +329,7 @@ class PrefixCSVForm(CustomFieldModelCSVForm): queryset=VRF.objects.all(), to_field_name='name', required=False, + help_text='Assigned VRF', error_messages={ 'invalid_choice': 'VRF not found.', } @@ -343,7 +338,7 @@ class PrefixCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', + help_text='Assigned tenant', error_messages={ 'invalid_choice': 'Tenant not found.', } @@ -352,7 +347,7 @@ class PrefixCSVForm(CustomFieldModelCSVForm): queryset=Site.objects.all(), required=False, to_field_name='name', - help_text='Name of parent site', + help_text='Assigned site', error_messages={ 'invalid_choice': 'Site not found.', } @@ -738,6 +733,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): queryset=VRF.objects.all(), to_field_name='name', required=False, + help_text='Assigned VRF', error_messages={ 'invalid_choice': 'VRF not found.', } @@ -746,7 +742,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), to_field_name='name', required=False, - help_text='Name of the assigned tenant', + help_text='Assigned tenant', error_messages={ 'invalid_choice': 'Tenant not found.', } @@ -994,7 +990,7 @@ class VLANGroupCSVForm(forms.ModelForm): queryset=Site.objects.all(), required=False, to_field_name='name', - help_text='Name of parent site', + help_text='Assigned site', error_messages={ 'invalid_choice': 'Site not found.', } @@ -1004,9 +1000,6 @@ class VLANGroupCSVForm(forms.ModelForm): class Meta: model = VLANGroup fields = VLANGroup.csv_headers - help_texts = { - 'name': 'Name of VLAN group', - } class VLANGroupFilterForm(BootstrapMixin, forms.Form): @@ -1082,7 +1075,7 @@ class VLANCSVForm(CustomFieldModelCSVForm): queryset=Site.objects.all(), required=False, to_field_name='name', - help_text='Name of parent site', + help_text='Assigned site', error_messages={ 'invalid_choice': 'Site not found.', } @@ -1095,7 +1088,7 @@ class VLANCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), to_field_name='name', required=False, - help_text='Name of assigned tenant', + help_text='Assigned tenant', error_messages={ 'invalid_choice': 'Tenant not found.', } @@ -1299,6 +1292,7 @@ class ServiceCSVForm(CustomFieldModelCSVForm): queryset=Device.objects.all(), required=False, to_field_name='name', + help_text='Required if not assigned to a VM', error_messages={ 'invalid_choice': 'Device not found.', } @@ -1307,6 +1301,7 @@ class ServiceCSVForm(CustomFieldModelCSVForm): queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', + help_text='Required if not assigned to a device', error_messages={ 'invalid_choice': 'Virtual machine not found.', } @@ -1319,8 +1314,6 @@ class ServiceCSVForm(CustomFieldModelCSVForm): class Meta: model = Service fields = Service.csv_headers - help_texts = { - } class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index f6ed7901a..3035c271b 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -50,7 +50,8 @@ class VRF(ChangeLoggedModel, CustomFieldModel): unique=True, blank=True, null=True, - verbose_name='Route distinguisher' + verbose_name='Route distinguisher', + help_text='Unique route distinguisher (as defined in RFC 4364)' ) tenant = models.ForeignKey( to='tenancy.Tenant', @@ -1017,7 +1018,10 @@ class Service(ChangeLoggedModel, CustomFieldModel): choices=ServiceProtocolChoices ) port = models.PositiveIntegerField( - validators=[MinValueValidator(SERVICE_PORT_MIN), MaxValueValidator(SERVICE_PORT_MAX)], + validators=[ + MinValueValidator(SERVICE_PORT_MIN), + MaxValueValidator(SERVICE_PORT_MAX) + ], verbose_name='Port number' ) ipaddresses = models.ManyToManyField( diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 3243687e5..afc9708b1 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -61,9 +61,6 @@ class SecretRoleCSVForm(forms.ModelForm): class Meta: model = SecretRole fields = SecretRole.csv_headers - help_texts = { - 'name': 'Name of secret role', - } # @@ -123,6 +120,7 @@ class SecretCSVForm(CustomFieldModelCSVForm): device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', + help_text='Assigned device', error_messages={ 'invalid_choice': 'Device not found.', } @@ -130,7 +128,7 @@ class SecretCSVForm(CustomFieldModelCSVForm): role = forms.ModelChoiceField( queryset=SecretRole.objects.all(), to_field_name='name', - help_text='Name of assigned role', + help_text='Assigned role', error_messages={ 'invalid_choice': 'Invalid secret role.', } diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 78c872c6a..5f803e816 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -37,7 +37,7 @@ class TenantGroupCSVForm(forms.ModelForm): queryset=TenantGroup.objects.all(), required=False, to_field_name='name', - help_text='Name of parent tenant group', + help_text='Parent group', error_messages={ 'invalid_choice': 'Tenant group not found.', } @@ -47,9 +47,6 @@ class TenantGroupCSVForm(forms.ModelForm): class Meta: model = TenantGroup fields = TenantGroup.csv_headers - help_texts = { - 'name': 'Group name', - } # @@ -80,7 +77,7 @@ class TenantCSVForm(CustomFieldModelForm): queryset=TenantGroup.objects.all(), required=False, to_field_name='name', - help_text='Name of parent group', + help_text='Assigned group', error_messages={ 'invalid_choice': 'Group not found.' } @@ -89,10 +86,6 @@ class TenantCSVForm(CustomFieldModelForm): class Meta: model = Tenant fields = Tenant.csv_headers - help_texts = { - 'name': 'Tenant name', - 'comments': 'Free-form comments' - } class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 9ba5ff032..894783433 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -42,9 +42,6 @@ class ClusterTypeCSVForm(forms.ModelForm): class Meta: model = ClusterType fields = ClusterType.csv_headers - help_texts = { - 'name': 'Name of cluster type', - } # @@ -67,9 +64,6 @@ class ClusterGroupCSVForm(forms.ModelForm): class Meta: model = ClusterGroup fields = ClusterGroup.csv_headers - help_texts = { - 'name': 'Name of cluster group', - } # @@ -104,7 +98,7 @@ class ClusterCSVForm(CustomFieldModelCSVForm): type = forms.ModelChoiceField( queryset=ClusterType.objects.all(), to_field_name='name', - help_text='Name of cluster type', + help_text='Type of cluster', error_messages={ 'invalid_choice': 'Invalid cluster type name.', } @@ -113,7 +107,7 @@ class ClusterCSVForm(CustomFieldModelCSVForm): queryset=ClusterGroup.objects.all(), to_field_name='name', required=False, - help_text='Name of cluster group', + help_text='Assigned cluster group', error_messages={ 'invalid_choice': 'Invalid cluster group name.', } @@ -122,7 +116,7 @@ class ClusterCSVForm(CustomFieldModelCSVForm): queryset=Site.objects.all(), to_field_name='name', required=False, - help_text='Name of assigned site', + help_text='Assigned site', error_messages={ 'invalid_choice': 'Invalid site name.', } @@ -131,7 +125,7 @@ class ClusterCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), to_field_name='name', required=False, - help_text='Name of assigned tenant', + help_text='Assigned tenant', error_messages={ 'invalid_choice': 'Invalid tenant name' } @@ -410,7 +404,7 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): cluster = forms.ModelChoiceField( queryset=Cluster.objects.all(), to_field_name='name', - help_text='Name of parent cluster', + help_text='Assigned cluster', error_messages={ 'invalid_choice': 'Invalid cluster name.', } @@ -421,7 +415,7 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): ), required=False, to_field_name='name', - help_text='Name of functional role', + help_text='Functional role', error_messages={ 'invalid_choice': 'Invalid role name.' } @@ -430,7 +424,7 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', + help_text='Assigned tenant', error_messages={ 'invalid_choice': 'Tenant not found.' } @@ -439,7 +433,7 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): queryset=Platform.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned platform', + help_text='Assigned platform', error_messages={ 'invalid_choice': 'Invalid platform.', } From 4486957b9a14099724a8edb1ec954281a708f27b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 May 2020 16:01:30 -0400 Subject: [PATCH 06/12] Clean up comments --- netbox/utilities/forms.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 351b845ae..c347669ec 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -400,8 +400,11 @@ class TimePicker(forms.TextInput): class CSVDataField(forms.CharField): """ - A CharField (rendered as a Textarea) which accepts CSV-formatted data. It returns a list of dictionaries mapping - column headers to values. Each dictionary represents an individual record. + A CharField (rendered as a Textarea) which accepts CSV-formatted data. It returns data as a two-tuple: The first + item is a dictionary of column headers, mapping field names to the attribute by which they match a related object + (where applicable). The second item is a list of dictionaries, each representing a discrete row of CSV data. + + :param from_form: The form from which the field derives its validation rules. """ widget = forms.Textarea @@ -442,10 +445,12 @@ class CSVDataField(forms.CharField): else: headers[header] = None - # Parse CSV data + # Parse CSV rows into a list of dictionaries mapped from the column headers. for i, row in enumerate(reader, start=1): if len(row) != len(headers): - raise forms.ValidationError(f"Row {i}: Expected {len(headers)} columns but found {len(row)}") + raise forms.ValidationError( + f"Row {i}: Expected {len(headers)} columns but found {len(row)}" + ) row = [col.strip() for col in row] record = dict(zip(headers.keys(), row)) records.append(record) From f9f7c19d810e622d376f544e6c5d1c232e86a1f4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 May 2020 16:01:55 -0400 Subject: [PATCH 07/12] Clean up CSV import table --- netbox/templates/utilities/obj_bulk_import.html | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/netbox/templates/utilities/obj_bulk_import.html b/netbox/templates/utilities/obj_bulk_import.html index ea46dd08b..b735adb39 100644 --- a/netbox/templates/utilities/obj_bulk_import.html +++ b/netbox/templates/utilities/obj_bulk_import.html @@ -43,7 +43,7 @@ Field Required - Dynamic + Accessor Description {% for name, field in fields.items %} @@ -54,11 +54,15 @@ {% if field.required %} + {% else %} + {% endif %} {% if field.to_field_name %} - + {{ field.to_field_name }} + {% else %} + {% endif %} @@ -84,9 +88,8 @@ objects.

- Dynamic fields may optionally refer to a related object by an - alternative attribute. For example, vrf.rd would identify a VRF by its RD - attribute. + Related objects may be referenced by any unique attribute. + For example, vrf.rd would identify a VRF by its route distinguisher.

{% endif %}
From d85d96384238b4daf7a0a04891597d17c3137665 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 May 2020 16:30:21 -0400 Subject: [PATCH 08/12] Remove example choices from CSV import form --- .../templates/utilities/obj_bulk_import.html | 4 +--- netbox/utilities/templatetags/helpers.py | 22 ------------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/netbox/templates/utilities/obj_bulk_import.html b/netbox/templates/utilities/obj_bulk_import.html index b735adb39..4359d49a6 100644 --- a/netbox/templates/utilities/obj_bulk_import.html +++ b/netbox/templates/utilities/obj_bulk_import.html @@ -71,9 +71,7 @@ {% elif field.label %} {{ field.label }}
{% endif %} - {% if field.choices %} - Choices: {{ field|example_choices }} - {% elif field|widget_type == 'dateinput' %} + {% if field|widget_type == 'dateinput' %} Format: YYYY-MM-DD {% elif field|widget_type == 'checkboxinput' %} Specify "true" or "false" diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 466690a4c..8a82fc48b 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -116,28 +116,6 @@ def humanize_speed(speed): return '{} Kbps'.format(speed) -@register.filter() -def example_choices(field, arg=3): - """ - Returns a number (default: 3) of example choices for a ChoiceFiled (useful for CSV import forms). - """ - examples = [] - if hasattr(field, 'queryset'): - choices = [ - (obj.pk, getattr(obj, field.to_field_name)) for obj in field.queryset[:arg + 1] - ] - else: - choices = field.choices - for value, label in unpack_grouped_choices(choices): - if len(examples) == arg: - examples.append('etc.') - break - if not value or not label: - continue - examples.append(label) - return ', '.join(examples) or 'None' - - @register.filter() def tzoffset(value): """ From 839e999a7103173a957dc77e7caf1111e3d1bcfe Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 May 2020 16:15:09 -0400 Subject: [PATCH 09/12] Introduce CSVModelForm for dynamic CSV imports --- netbox/circuits/forms.py | 4 +- netbox/dcim/forms.py | 329 ++++++++++++++++---------------- netbox/dcim/models/__init__.py | 10 +- netbox/dcim/tests/test_views.py | 8 +- netbox/extras/forms.py | 4 +- netbox/ipam/forms.py | 168 ++++++++-------- netbox/ipam/models.py | 6 +- netbox/secrets/forms.py | 6 +- netbox/tenancy/forms.py | 8 +- netbox/utilities/forms.py | 14 ++ netbox/utilities/views.py | 7 +- netbox/virtualization/forms.py | 10 +- 12 files changed, 280 insertions(+), 294 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 05d34e351..50504d99f 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -8,7 +8,7 @@ from extras.forms import ( from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( - APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker, + APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, ) @@ -142,7 +142,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm): ] -class CircuitTypeCSVForm(forms.ModelForm): +class CircuitTypeCSVForm(CSVModelForm): slug = SlugField() class Meta: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 94561f9a0..c84a3bb28 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -23,9 +23,9 @@ 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, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SelectWithPK, SmallTextarea, - SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, + BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelForm, + DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, + SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup, VirtualMachine @@ -193,7 +193,7 @@ class RegionForm(BootstrapMixin, forms.ModelForm): ) -class RegionCSVForm(forms.ModelForm): +class RegionCSVForm(CSVModelForm): parent = forms.ModelChoiceField( queryset=Region.objects.all(), required=False, @@ -388,7 +388,7 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm): ) -class RackGroupCSVForm(forms.ModelForm): +class RackGroupCSVForm(CSVModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -461,7 +461,7 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm): ] -class RackRoleCSVForm(forms.ModelForm): +class RackRoleCSVForm(CSVModelForm): slug = SlugField() class Meta: @@ -526,8 +526,13 @@ class RackCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Site not found.', } ) - group_name = forms.CharField( - required=False + group = forms.ModelChoiceField( + queryset=RackGroup.objects.all(), + required=False, + to_field_name='name', + error_messages={ + 'invalid_choice': 'Rack group not found.', + } ) tenant = forms.ModelChoiceField( queryset=Tenant.objects.all(), @@ -571,33 +576,14 @@ class RackCSVForm(CustomFieldModelCSVForm): model = Rack fields = Rack.csv_headers - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - group_name = self.cleaned_data.get('group_name') - name = self.cleaned_data.get('name') - facility_id = self.cleaned_data.get('facility_id') - - # Validate rack group - if group_name: - try: - self.instance.group = RackGroup.objects.get(site=site, name=group_name) - except RackGroup.DoesNotExist: - raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site)) - - # Validate uniqueness of rack name within group - if Rack.objects.filter(group=self.instance.group, name=name).exists(): - raise forms.ValidationError( - "A rack named {} already exists within group {}".format(name, group_name) - ) - - # Validate uniqueness of facility ID within group - if facility_id and Rack.objects.filter(group=self.instance.group, facility_id=facility_id).exists(): - raise forms.ValidationError( - "A rack with the facility ID {} already exists within group {}".format(facility_id, group_name) - ) + # Limit group queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['group'].queryset = self.fields['group'].queryset.filter(**params) class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -814,21 +800,31 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): return unit_choices -class RackReservationCSVForm(forms.ModelForm): +class RackReservationCSVForm(CSVModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', help_text='Parent site', error_messages={ - 'invalid_choice': 'Invalid site name.', + 'invalid_choice': 'Site not found.', } ) - rack_group = forms.CharField( + rack_group = forms.ModelChoiceField( + queryset=RackGroup.objects.all(), + to_field_name='name', required=False, - help_text="Rack's group (if any)" + help_text="Rack's group (if any)", + error_messages={ + 'invalid_choice': 'Rack group not found.', + } ) - rack_name = forms.CharField( - help_text="Rack name" + rack = forms.ModelChoiceField( + queryset=Rack.objects.all(), + to_field_name='name', + help_text='Rack', + error_messages={ + 'invalid_choice': 'Rack not found.', + } ) units = SimpleArrayField( base_field=forms.IntegerField(), @@ -847,27 +843,23 @@ class RackReservationCSVForm(forms.ModelForm): class Meta: model = RackReservation - fields = ('site', 'rack_group', 'rack_name', 'units', 'tenant', 'description') + fields = ('site', 'rack_group', 'rack', 'units', 'tenant', 'description') - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - rack_group = self.cleaned_data.get('rack_group') - rack_name = self.cleaned_data.get('rack_name') + # Limit rack_group queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params) - # Validate rack - if site and rack_group and rack_name: - try: - self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name) - except Rack.DoesNotExist: - raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group)) - elif site and rack_name: - try: - self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name) - except Rack.DoesNotExist: - raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site)) + # Limit rack queryset by assigned site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"group__{self.fields['rack_group'].to_field_name}": data.get('rack_group'), + } + self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): @@ -933,7 +925,7 @@ class ManufacturerForm(BootstrapMixin, forms.ModelForm): ] -class ManufacturerCSVForm(forms.ModelForm): +class ManufacturerCSVForm(CSVModelForm): class Meta: model = Manufacturer @@ -1648,7 +1640,7 @@ class DeviceRoleForm(BootstrapMixin, forms.ModelForm): ] -class DeviceRoleCSVForm(forms.ModelForm): +class DeviceRoleCSVForm(CSVModelForm): slug = SlugField() class Meta: @@ -1682,7 +1674,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm): } -class PlatformCSVForm(forms.ModelForm): +class PlatformCSVForm(CSVModelForm): slug = SlugField() manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), @@ -1920,11 +1912,16 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): to_field_name='name', help_text='Device type manufacturer', error_messages={ - 'invalid_choice': 'Invalid manufacturer.', + 'invalid_choice': 'Manufacturer not found.', } ) - model_name = forms.CharField( - help_text='Device type model name' + device_type = forms.ModelChoiceField( + queryset=DeviceType.objects.all(), + to_field_name='model', + help_text='Device type model', + error_messages={ + 'invalid_choice': 'Device type not found.', + } ) platform = forms.ModelChoiceField( queryset=Platform.objects.all(), @@ -1953,19 +1950,14 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): fields = [] model = Device - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - manufacturer = self.cleaned_data.get('manufacturer') - model_name = self.cleaned_data.get('model_name') - - # Validate device type - if manufacturer and model_name: - try: - self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name) - except DeviceType.DoesNotExist: - raise forms.ValidationError("Device type {} {} not found".format(manufacturer, model_name)) + # Limit device type queryset by manufacturer + params = {f"manufacturer__{self.fields['manufacturer'].to_field_name}": data.get('manufacturer')} + self.fields['device_type'].queryset = self.fields['device_type'].queryset.filter(**params) class DeviceCSVForm(BaseDeviceCSVForm): @@ -1974,16 +1966,26 @@ class DeviceCSVForm(BaseDeviceCSVForm): to_field_name='name', help_text='Assigned site', error_messages={ - 'invalid_choice': 'Invalid site name.', + 'invalid_choice': 'Site not found.', } ) - rack_group = forms.CharField( + rack_group = forms.ModelChoiceField( + queryset=RackGroup.objects.all(), + to_field_name='name', required=False, - help_text='Assigned rack\'s group (if any)' + help_text="Rack's group (if any)", + error_messages={ + 'invalid_choice': 'Rack group not found.', + } ) - rack_name = forms.CharField( + rack = forms.ModelChoiceField( + queryset=Rack.objects.all(), + to_field_name='name', required=False, - help_text='Name of parent rack' + help_text="Assigned rack", + error_messages={ + 'invalid_choice': 'Rack not found.', + } ) face = CSVChoiceField( choices=DeviceFaceChoices, @@ -1993,29 +1995,25 @@ class DeviceCSVForm(BaseDeviceCSVForm): class Meta(BaseDeviceCSVForm.Meta): fields = [ - 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', - 'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster', 'comments', + 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', + 'site', 'rack_group', 'rack', 'position', 'face', 'cluster', 'comments', ] - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - rack_group = self.cleaned_data.get('rack_group') - rack_name = self.cleaned_data.get('rack_name') + # Limit rack_group queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params) - # Validate rack - if site and rack_group and rack_name: - try: - self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name) - except Rack.DoesNotExist: - raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group)) - elif site and rack_name: - try: - self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name) - except Rack.DoesNotExist: - raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site)) + # Limit rack queryset by assigned site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"group__{self.fields['rack_group'].to_field_name}": data.get('rack_group'), + } + self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) class ChildDeviceCSVForm(BaseDeviceCSVForm): @@ -2027,32 +2025,29 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm): 'invalid_choice': 'Parent device not found.', } ) - device_bay_name = forms.CharField( - help_text='Name of device bay', + device_bay = forms.ModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Device bay in which this device is installed', + error_messages={ + 'invalid_choice': 'Devie bay not found.', + } ) class Meta(BaseDeviceCSVForm.Meta): fields = [ - 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', - 'parent', 'device_bay_name', 'cluster', 'comments', + 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', + 'parent', 'device_bay', 'cluster', 'comments', ] - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - parent = self.cleaned_data.get('parent') - device_bay_name = self.cleaned_data.get('device_bay_name') - - # Validate device bay - if parent and device_bay_name: - try: - self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name) - # Inherit site and rack from parent device - self.instance.site = parent.site - self.instance.rack = parent.rack - except DeviceBay.DoesNotExist: - raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name)) + # Limit device bay queryset by parent device + params = {f"device__{self.fields['parent'].to_field_name}": data.get('parent')} + self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params) class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -2344,7 +2339,7 @@ class ConsolePortBulkEditForm( ) -class ConsolePortCSVForm(forms.ModelForm): +class ConsolePortCSVForm(CSVModelForm): device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2447,7 +2442,7 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): ) -class ConsoleServerPortCSVForm(forms.ModelForm): +class ConsoleServerPortCSVForm(CSVModelForm): device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2546,7 +2541,7 @@ class PowerPortBulkEditForm( ) -class PowerPortCSVForm(forms.ModelForm): +class PowerPortCSVForm(CSVModelForm): device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -2696,7 +2691,7 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): ) -class PowerOutletCSVForm(forms.ModelForm): +class PowerOutletCSVForm(CSVModelForm): device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -3018,7 +3013,7 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): ) -class InterfaceCSVForm(forms.ModelForm): +class InterfaceCSVForm(CSVModelForm): device = forms.ModelChoiceField( queryset=Device.objects.all(), required=False, @@ -3231,7 +3226,7 @@ class FrontPortBulkDisconnectForm(ConfirmationForm): ) -class FrontPortCSVForm(forms.ModelForm): +class FrontPortCSVForm(CSVModelForm): device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -3372,7 +3367,7 @@ class RearPortBulkDisconnectForm(ConfirmationForm): ) -class RearPortCSVForm(forms.ModelForm): +class RearPortCSVForm(CSVModelForm): device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -3483,7 +3478,7 @@ class DeviceBayBulkRenameForm(BulkRenameForm): ) -class DeviceBayCSVForm(forms.ModelForm): +class DeviceBayCSVForm(CSVModelForm): device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -3774,7 +3769,7 @@ class CableForm(BootstrapMixin, forms.ModelForm): } -class CableCSVForm(forms.ModelForm): +class CableCSVForm(CSVModelForm): # Termination A side_a_device = forms.ModelChoiceField( queryset=Device.objects.all(), @@ -4128,7 +4123,7 @@ class InventoryItemCreateForm(BootstrapMixin, forms.Form): ) -class InventoryItemCSVForm(forms.ModelForm): +class InventoryItemCSVForm(CSVModelForm): device = forms.ModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -4439,7 +4434,7 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm): ] -class PowerPanelCSVForm(forms.ModelForm): +class PowerPanelCSVForm(CSVModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -4448,30 +4443,27 @@ class PowerPanelCSVForm(forms.ModelForm): 'invalid_choice': 'Site not found.', } ) - rack_group_name = forms.CharField( + rack_group = forms.ModelChoiceField( + queryset=RackGroup.objects.all(), required=False, - help_text="Rack group name (optional)" + to_field_name='name', + error_messages={ + 'invalid_choice': 'Rack group not found.', + } ) class Meta: model = PowerPanel fields = PowerPanel.csv_headers - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - rack_group_name = self.cleaned_data.get('rack_group_name') - - # Validate rack group - if rack_group_name: - try: - self.instance.rack_group = RackGroup.objects.get(site=site, name=rack_group_name) - except RackGroup.DoesNotExist: - raise forms.ValidationError( - "Rack group {} not found in site {}".format(rack_group_name, site) - ) + # Limit group queryset by assigned site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params) class PowerPanelBulkEditForm(BootstrapMixin, BulkEditForm): @@ -4595,7 +4587,7 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Site not found.', } ) - panel_name = forms.ModelChoiceField( + power_panel = forms.ModelChoiceField( queryset=PowerPanel.objects.all(), to_field_name='name', help_text='Upstream power panel', @@ -4603,13 +4595,23 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Power panel not found.', } ) - rack_group = forms.CharField( + rack_group = forms.ModelChoiceField( + queryset=RackGroup.objects.all(), + to_field_name='name', required=False, - help_text="Assigned rack's group name" + help_text="Rack's group (if any)", + error_messages={ + 'invalid_choice': 'Rack group not found.', + } ) - rack_name = forms.CharField( + rack = forms.ModelChoiceField( + queryset=Rack.objects.all(), + to_field_name='name', required=False, - help_text="Assigned rack name" + help_text='Rack', + error_messages={ + 'invalid_choice': 'Rack not found.', + } ) status = CSVChoiceField( choices=PowerFeedStatusChoices, @@ -4636,32 +4638,25 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): model = PowerFeed fields = PowerFeed.csv_headers - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - panel_name = self.cleaned_data.get('panel_name') - rack_group = self.cleaned_data.get('rack_group') - rack_name = self.cleaned_data.get('rack_name') + # Limit power_panel queryset by site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['power_panel'].queryset = self.fields['power_panel'].queryset.filter(**params) - # Validate power panel - if panel_name: - try: - self.instance.power_panel = PowerPanel.objects.get(site=site, name=panel_name) - except Rack.DoesNotExist: - raise forms.ValidationError( - "Power panel {} not found in site {}".format(panel_name, site) - ) + # Limit rack_group queryset by site + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params) - # Validate rack - if rack_name: - try: - self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name) - except Rack.DoesNotExist: - raise forms.ValidationError( - "Rack {} not found in site {}, group {}".format(rack_name, site, rack_group) - ) + # Limit rack queryset by site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"group__{self.fields['rack_group'].to_field_name}": data.get('rack_group'), + } + self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index cb7c14fe2..ad30ce2e2 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -523,7 +523,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', + 'site', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', ] clone_fields = [ @@ -829,7 +829,7 @@ class RackReservation(ChangeLoggedModel): def clean(self): - if self.units: + if hasattr(self, 'rack') and self.units: # Validate that all specified units exist in the Rack. invalid_units = [u for u in self.units if u not in self.rack.units] @@ -1408,7 +1408,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', + 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments', ] clone_fields = [ @@ -1791,7 +1791,7 @@ class PowerPanel(ChangeLoggedModel): max_length=50 ) - csv_headers = ['site', 'rack_group_name', 'name'] + csv_headers = ['site', 'rack_group', 'name'] class Meta: ordering = ['site', 'name'] @@ -1898,7 +1898,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage', + 'site', 'power_panel', 'rack_group', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', ] clone_fields = [ diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index b1aaf4449..e8a58893b 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -202,7 +202,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - 'site,rack_name,units,description', + 'site,rack,units,description', 'Site 1,Rack 1,"10,11,12",Reservation 1', 'Site 1,Rack 1,"13,14,15",Reservation 2', 'Site 1,Rack 1,"16,17,18",Reservation 3', @@ -947,7 +947,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "device_role,manufacturer,model_name,status,site,name", + "device_role,manufacturer,device_type,status,site,name", "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 4", "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 5", "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6", @@ -1586,7 +1586,7 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "site,rack_group_name,name", + "site,rack_group,name", "Site 1,Rack Group 1,Power Panel 4", "Site 1,Rack Group 1,Power Panel 5", "Site 1,Rack Group 1,Power Panel 6", @@ -1645,7 +1645,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "site,panel_name,name,voltage,amperage,max_utilization", + "site,power_panel,name,voltage,amperage,max_utilization", "Site 1,Power Panel 1,Power Feed 4,120,20,80", "Site 1,Power Panel 1,Power Feed 5,120,20,80", "Site 1,Power Panel 1,Power Feed 6,120,20,80", diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 676d7ceba..384b3563b 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -8,7 +8,7 @@ from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, - CommentField, ContentTypeSelect, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, + ContentTypeSelect, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup @@ -89,7 +89,7 @@ class CustomFieldModelForm(forms.ModelForm): return obj -class CustomFieldModelCSVForm(CustomFieldModelForm): +class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm): def _append_customfield_fields(self): diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 7bed22866..2f2c99ed7 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -1,5 +1,4 @@ from django import forms -from django.core.exceptions import MultipleObjectsReturned from django.core.validators import MaxValueValidator, MinValueValidator from taggit.forms import TagField @@ -11,8 +10,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, - DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, ReturnURLForm, - SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, + CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, + ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import VirtualMachine from .choices import * @@ -115,7 +114,7 @@ class RIRForm(BootstrapMixin, forms.ModelForm): ] -class RIRCSVForm(forms.ModelForm): +class RIRCSVForm(CSVModelForm): slug = SlugField() class Meta: @@ -242,7 +241,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm): ] -class RoleCSVForm(forms.ModelForm): +class RoleCSVForm(CSVModelForm): slug = SlugField() class Meta: @@ -352,13 +351,23 @@ class PrefixCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Site not found.', } ) - vlan_group = forms.CharField( - help_text='Group name of assigned VLAN', - required=False + vlan_group = forms.ModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + to_field_name='name', + help_text="VLAN's group (if any)", + error_messages={ + 'invalid_choice': 'VLAN group not found.', + } ) - vlan_vid = forms.IntegerField( - help_text='Numeric ID of assigned VLAN', - required=False + vlan = forms.ModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + to_field_name='vid', + help_text="Assigned VLAN", + error_messages={ + 'invalid_choice': 'VLAN not found.', + } ) status = CSVChoiceField( choices=PrefixStatusChoices, @@ -378,39 +387,17 @@ class PrefixCSVForm(CustomFieldModelCSVForm): model = Prefix fields = Prefix.csv_headers - def clean(self): + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - super().clean() + if data: - site = self.cleaned_data.get('site') - vlan_group = self.cleaned_data.get('vlan_group') - vlan_vid = self.cleaned_data.get('vlan_vid') - - # Validate VLAN - if vlan_group and vlan_vid: - try: - self.instance.vlan = VLAN.objects.get(site=site, group__name=vlan_group, vid=vlan_vid) - except VLAN.DoesNotExist: - if site: - raise forms.ValidationError("VLAN {} not found in site {} group {}".format( - vlan_vid, site, vlan_group - )) - else: - raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group)) - except MultipleObjectsReturned: - raise forms.ValidationError( - "Multiple VLANs with VID {} found in group {}".format(vlan_vid, vlan_group) - ) - elif vlan_vid: - try: - self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid) - except VLAN.DoesNotExist: - if site: - raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site)) - else: - raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid)) - except MultipleObjectsReturned: - raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid)) + # Limit vlan queryset by assigned site and group + params = { + f"site__{self.fields['site'].to_field_name}": data.get('site'), + f"group__{self.fields['vlan_group'].to_field_name}": data.get('vlan_group'), + } + self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params) class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -760,7 +747,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): queryset=Device.objects.all(), required=False, to_field_name='name', - help_text='Assigned device', + help_text='Parent device of assigned interface (if any)', error_messages={ 'invalid_choice': 'Device not found.', } @@ -769,14 +756,19 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', - help_text='Assigned virtual machine', + help_text='Parent VM of assigned interface (if any)', error_messages={ 'invalid_choice': 'Virtual machine not found.', } ) - interface_name = forms.CharField( - help_text='Name of assigned interface', - required=False + interface = forms.ModelChoiceField( + queryset=Interface.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned interface', + error_messages={ + 'invalid_choice': 'Interface not found.', + } ) is_primary = forms.BooleanField( help_text='Make this the primary IP for the assigned device', @@ -787,38 +779,34 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): model = IPAddress fields = IPAddress.csv_headers + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit interface queryset by assigned device or virtual machine + if data.get('device'): + params = { + f"device__{self.fields['device'].to_field_name}": data.get('device') + } + elif data.get('virtual_machine'): + params = { + f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data.get('virtual_machine') + } + else: + params = { + 'device': None, + 'virtual_machine': None, + } + self.fields['interface'].queryset = self.fields['interface'].queryset.filter(**params) + def clean(self): super().clean() device = self.cleaned_data.get('device') virtual_machine = self.cleaned_data.get('virtual_machine') - interface_name = self.cleaned_data.get('interface_name') is_primary = self.cleaned_data.get('is_primary') - # Validate interface - if interface_name and device: - try: - self.instance.interface = Interface.objects.get(device=device, name=interface_name) - except Interface.DoesNotExist: - raise forms.ValidationError("Invalid interface {} for device {}".format( - interface_name, device - )) - elif interface_name and virtual_machine: - try: - self.instance.interface = Interface.objects.get(virtual_machine=virtual_machine, name=interface_name) - except Interface.DoesNotExist: - raise forms.ValidationError("Invalid interface {} for virtual machine {}".format( - interface_name, virtual_machine - )) - elif interface_name: - raise forms.ValidationError("Interface given ({}) but parent device/virtual machine not specified".format( - interface_name - )) - elif device: - raise forms.ValidationError("Device specified ({}) but interface missing".format(device)) - elif virtual_machine: - raise forms.ValidationError("Virtual machine specified ({}) but interface missing".format(virtual_machine)) - # Validate is_primary if is_primary and not device and not virtual_machine: raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP") @@ -985,7 +973,7 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm): ] -class VLANGroupCSVForm(forms.ModelForm): +class VLANGroupCSVForm(CSVModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, @@ -1080,9 +1068,14 @@ class VLANCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Site not found.', } ) - group_name = forms.CharField( - help_text='Name of VLAN group', - required=False + group = forms.ModelChoiceField( + queryset=VLANGroup.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned VLAN group', + error_messages={ + 'invalid_choice': 'VLAN group not found.', + } ) tenant = forms.ModelChoiceField( queryset=Tenant.objects.all(), @@ -1115,25 +1108,14 @@ class VLANCSVForm(CustomFieldModelCSVForm): 'name': 'VLAN name', } - def clean(self): - super().clean() + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) - site = self.cleaned_data.get('site') - group_name = self.cleaned_data.get('group_name') + if data: - # Validate VLAN group - if group_name: - try: - self.instance.group = VLANGroup.objects.get(site=site, name=group_name) - except VLANGroup.DoesNotExist: - if site: - raise forms.ValidationError( - "VLAN group {} not found for site {}".format(group_name, site) - ) - else: - raise forms.ValidationError( - "Global VLAN group {} not found".format(group_name) - ) + # Limit vlan queryset by assigned group + params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} + self.fields['group'].queryset = self.fields['group'].queryset.filter(**params) class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 3035c271b..84720845e 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -365,7 +365,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description', + 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'description', ] clone_fields = [ 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', @@ -636,7 +636,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = [ - 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary', + 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary', 'dns_name', 'description', ] clone_fields = [ @@ -926,7 +926,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) - csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description'] + csv_headers = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description'] clone_fields = [ 'site', 'group', 'tenant', 'status', 'role', 'description', ] diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index afc9708b1..ec21a48ab 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -8,8 +8,8 @@ from extras.forms import ( AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, ) from utilities.forms import ( - APISelectMultiple, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, - StaticSelect2Multiple, TagFilterField, + APISelectMultiple, BootstrapMixin, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + SlugField, StaticSelect2Multiple, TagFilterField, ) from .constants import * from .models import Secret, SecretRole, UserKey @@ -55,7 +55,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm): } -class SecretRoleCSVForm(forms.ModelForm): +class SecretRoleCSVForm(CSVModelForm): slug = SlugField() class Meta: diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 5f803e816..fca8e9924 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -2,10 +2,10 @@ from django import forms from taggit.forms import TagField from extras.forms import ( - AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm, + AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelCSVForm, ) from utilities.forms import ( - APISelect, APISelectMultiple, BootstrapMixin, CommentField, DynamicModelChoiceField, + APISelect, APISelectMultiple, BootstrapMixin, CommentField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, TagFilterField, ) from .models import Tenant, TenantGroup @@ -32,7 +32,7 @@ class TenantGroupForm(BootstrapMixin, forms.ModelForm): ] -class TenantGroupCSVForm(forms.ModelForm): +class TenantGroupCSVForm(CSVModelForm): parent = forms.ModelChoiceField( queryset=TenantGroup.objects.all(), required=False, @@ -71,7 +71,7 @@ class TenantForm(BootstrapMixin, CustomFieldModelForm): ) -class TenantCSVForm(CustomFieldModelForm): +class TenantCSVForm(CustomFieldModelCSVForm): slug = SlugField() group = forms.ModelChoiceField( queryset=TenantGroup.objects.all(), diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index c347669ec..98761252d 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -712,6 +712,20 @@ class BulkEditForm(forms.Form): self.nullable_fields = self.Meta.nullable_fields +class CSVModelForm(forms.ModelForm): + """ + ModelForm used for the import of objects in CSV format. + """ + def __init__(self, *args, headers=None, **kwargs): + super().__init__(*args, **kwargs) + + # Modify the model form to accommodate any customized to_field_name properties + if headers: + for field, to_field in headers.items(): + if to_field is not None: + self.fields[field].to_field_name = to_field + + class ImportForm(BootstrapMixin, forms.Form): """ Generic form for creating an object from JSON/YAML data diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 964d9490c..ec6776c02 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -593,12 +593,7 @@ class BulkImportView(GetReturnURLMixin, View): with transaction.atomic(): headers, records = form.cleaned_data['csv'] for row, data in enumerate(records, start=1): - obj_form = self.model_form(data) - - # Modify the model form to accommodate any customized to_field_name properties - for field, to_field in headers.items(): - if to_field is not None: - obj_form.fields[field].to_field_name = to_field + obj_form = self.model_form(data, headers=headers) if obj_form.is_valid(): obj = self._save_obj(obj_form, request) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 894783433..f3c5d1633 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -14,9 +14,9 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, - CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, - TagFilterField, + CommentField, ConfirmationForm, CSVChoiceField, CSVModelForm, DynamicModelChoiceField, + DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea, + StaticSelect2, StaticSelect2Multiple, TagFilterField, ) from .choices import * from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -36,7 +36,7 @@ class ClusterTypeForm(BootstrapMixin, forms.ModelForm): ] -class ClusterTypeCSVForm(forms.ModelForm): +class ClusterTypeCSVForm(CSVModelForm): slug = SlugField() class Meta: @@ -58,7 +58,7 @@ class ClusterGroupForm(BootstrapMixin, forms.ModelForm): ] -class ClusterGroupCSVForm(forms.ModelForm): +class ClusterGroupCSVForm(CSVModelForm): slug = SlugField() class Meta: From 607744813aa4c8b4e0b9f3915de77efc118cdd48 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 May 2020 16:49:16 -0400 Subject: [PATCH 10/12] Extend tests for CSV import --- netbox/dcim/tests/test_views.py | 34 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index e8a58893b..65f37c1d5 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -184,7 +184,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): site = Site.objects.create(name='Site 1', slug='site-1') - rack = Rack(name='Rack 1', site=site) + rack_group = RackGroup(name='Rack Group 1', slug='rack-group-1', site=site) + rack_group.save() + + rack = Rack(name='Rack 1', site=site, group=rack_group) rack.save() RackReservation.objects.bulk_create([ @@ -202,10 +205,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - 'site,rack,units,description', - 'Site 1,Rack 1,"10,11,12",Reservation 1', - 'Site 1,Rack 1,"13,14,15",Reservation 2', - 'Site 1,Rack 1,"16,17,18",Reservation 3', + 'site,rack_group,rack,units,description', + 'Site 1,Rack Group 1,Rack 1,"10,11,12",Reservation 1', + 'Site 1,Rack Group 1,Rack 1,"13,14,15",Reservation 2', + 'Site 1,Rack Group 1,Rack 1,"16,17,18",Reservation 3', ) cls.bulk_edit_data = { @@ -268,10 +271,10 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "site,name,width,u_height", - "Site 1,Rack 4,19,42", - "Site 1,Rack 5,19,42", - "Site 1,Rack 6,19,42", + "site,group,name,width,u_height", + "Site 1,,Rack 4,19,42", + "Site 1,Rack Group 1,Rack 5,19,42", + "Site 2,Rack Group 2,Rack 6,19,42", ) cls.bulk_edit_data = { @@ -890,8 +893,11 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) Site.objects.bulk_create(sites) + rack_group = RackGroup(site=sites[0], name='Rack Group 1', slug='rack-group-1') + rack_group.save() + racks = ( - Rack(name='Rack 1', site=sites[0]), + Rack(name='Rack 1', site=sites[0], group=rack_group), Rack(name='Rack 2', site=sites[1]), ) Rack.objects.bulk_create(racks) @@ -947,10 +953,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "device_role,manufacturer,device_type,status,site,name", - "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 4", - "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 5", - "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6", + "device_role,manufacturer,device_type,status,name,site,rack_group,rack,position,face", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Device 4,Site 1,Rack Group 1,Rack 1,10,Front", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Device 5,Site 1,Rack Group 1,Rack 1,20,Front", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Device 6,Site 1,Rack Group 1,Rack 1,30,Front", ) cls.bulk_edit_data = { From 70d0a5f66594152628dc381493a82fad766ff333 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 6 May 2020 09:43:10 -0400 Subject: [PATCH 11/12] Introduce CSVModelChoiceField to provide better validation for CSV model choices --- netbox/circuits/forms.py | 12 ++-- netbox/dcim/forms.py | 106 ++++++++++++++++----------------- netbox/ipam/forms.py | 45 +++++++------- netbox/secrets/forms.py | 8 +-- netbox/tenancy/forms.py | 8 +-- netbox/utilities/forms.py | 15 ++++- netbox/virtualization/forms.py | 18 +++--- 7 files changed, 113 insertions(+), 99 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 50504d99f..e95f4abb1 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -8,9 +8,9 @@ from extras.forms import ( from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( - APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelForm, DatePicker, - DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2, - StaticSelect2Multiple, TagFilterField, + APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, + CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, + StaticSelect2, StaticSelect2Multiple, TagFilterField, ) from .choices import CircuitStatusChoices from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -186,7 +186,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class CircuitCSVForm(CustomFieldModelCSVForm): - provider = forms.ModelChoiceField( + provider = CSVModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', help_text='Assigned provider', @@ -194,7 +194,7 @@ class CircuitCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Provider not found.' } ) - type = forms.ModelChoiceField( + type = CSVModelChoiceField( queryset=CircuitType.objects.all(), to_field_name='name', help_text='Type of circuit', @@ -207,7 +207,7 @@ class CircuitCSVForm(CustomFieldModelCSVForm): required=False, help_text='Operational status' ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c84a3bb28..88cf0d07b 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -23,9 +23,9 @@ 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, CSVModelForm, - DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, - SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, + BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, + CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, + JSONField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup, VirtualMachine @@ -194,7 +194,7 @@ class RegionForm(BootstrapMixin, forms.ModelForm): class RegionCSVForm(CSVModelForm): - parent = forms.ModelChoiceField( + parent = CSVModelChoiceField( queryset=Region.objects.all(), required=False, to_field_name='name', @@ -273,7 +273,7 @@ class SiteCSVForm(CustomFieldModelCSVForm): required=False, help_text='Operational status' ) - region = forms.ModelChoiceField( + region = CSVModelChoiceField( queryset=Region.objects.all(), required=False, to_field_name='name', @@ -282,7 +282,7 @@ class SiteCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Region not found.', } ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', @@ -389,7 +389,7 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm): class RackGroupCSVForm(CSVModelForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', help_text='Assigned site', @@ -397,7 +397,7 @@ class RackGroupCSVForm(CSVModelForm): 'invalid_choice': 'Site not found.', } ) - parent = forms.ModelChoiceField( + parent = CSVModelChoiceField( queryset=RackGroup.objects.all(), required=False, to_field_name='name', @@ -519,14 +519,14 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class RackCSVForm(CustomFieldModelCSVForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', error_messages={ 'invalid_choice': 'Site not found.', } ) - group = forms.ModelChoiceField( + group = CSVModelChoiceField( queryset=RackGroup.objects.all(), required=False, to_field_name='name', @@ -534,7 +534,7 @@ class RackCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Rack group not found.', } ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', @@ -548,7 +548,7 @@ class RackCSVForm(CustomFieldModelCSVForm): required=False, help_text='Operational status' ) - role = forms.ModelChoiceField( + role = CSVModelChoiceField( queryset=RackRole.objects.all(), required=False, to_field_name='name', @@ -801,7 +801,7 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): class RackReservationCSVForm(CSVModelForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', help_text='Parent site', @@ -809,7 +809,7 @@ class RackReservationCSVForm(CSVModelForm): 'invalid_choice': 'Site not found.', } ) - rack_group = forms.ModelChoiceField( + rack_group = CSVModelChoiceField( queryset=RackGroup.objects.all(), to_field_name='name', required=False, @@ -818,7 +818,7 @@ class RackReservationCSVForm(CSVModelForm): 'invalid_choice': 'Rack group not found.', } ) - rack = forms.ModelChoiceField( + rack = CSVModelChoiceField( queryset=Rack.objects.all(), to_field_name='name', help_text='Rack', @@ -831,7 +831,7 @@ class RackReservationCSVForm(CSVModelForm): required=True, help_text='Comma-separated list of individual unit numbers' ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', @@ -1676,7 +1676,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm): class PlatformCSVForm(CSVModelForm): slug = SlugField() - manufacturer = forms.ModelChoiceField( + manufacturer = CSVModelChoiceField( queryset=Manufacturer.objects.all(), required=False, to_field_name='name', @@ -1890,7 +1890,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class BaseDeviceCSVForm(CustomFieldModelCSVForm): - device_role = forms.ModelChoiceField( + device_role = CSVModelChoiceField( queryset=DeviceRole.objects.all(), to_field_name='name', help_text='Assigned role', @@ -1898,7 +1898,7 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Invalid device role.', } ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', @@ -1907,7 +1907,7 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Tenant not found.', } ) - manufacturer = forms.ModelChoiceField( + manufacturer = CSVModelChoiceField( queryset=Manufacturer.objects.all(), to_field_name='name', help_text='Device type manufacturer', @@ -1915,7 +1915,7 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Manufacturer not found.', } ) - device_type = forms.ModelChoiceField( + device_type = CSVModelChoiceField( queryset=DeviceType.objects.all(), to_field_name='model', help_text='Device type model', @@ -1923,7 +1923,7 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Device type not found.', } ) - platform = forms.ModelChoiceField( + platform = CSVModelChoiceField( queryset=Platform.objects.all(), required=False, to_field_name='name', @@ -1936,7 +1936,7 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): choices=DeviceStatusChoices, help_text='Operational status' ) - cluster = forms.ModelChoiceField( + cluster = CSVModelChoiceField( queryset=Cluster.objects.all(), to_field_name='name', required=False, @@ -1961,7 +1961,7 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): class DeviceCSVForm(BaseDeviceCSVForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', help_text='Assigned site', @@ -1969,7 +1969,7 @@ class DeviceCSVForm(BaseDeviceCSVForm): 'invalid_choice': 'Site not found.', } ) - rack_group = forms.ModelChoiceField( + rack_group = CSVModelChoiceField( queryset=RackGroup.objects.all(), to_field_name='name', required=False, @@ -1978,7 +1978,7 @@ class DeviceCSVForm(BaseDeviceCSVForm): 'invalid_choice': 'Rack group not found.', } ) - rack = forms.ModelChoiceField( + rack = CSVModelChoiceField( queryset=Rack.objects.all(), to_field_name='name', required=False, @@ -2017,7 +2017,7 @@ class DeviceCSVForm(BaseDeviceCSVForm): class ChildDeviceCSVForm(BaseDeviceCSVForm): - parent = forms.ModelChoiceField( + parent = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', help_text='Parent device', @@ -2025,7 +2025,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm): 'invalid_choice': 'Parent device not found.', } ) - device_bay = forms.ModelChoiceField( + device_bay = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', help_text='Device bay in which this device is installed', @@ -2340,7 +2340,7 @@ class ConsolePortBulkEditForm( class ConsolePortCSVForm(CSVModelForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', error_messages={ @@ -2443,7 +2443,7 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): class ConsoleServerPortCSVForm(CSVModelForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', error_messages={ @@ -2542,7 +2542,7 @@ class PowerPortBulkEditForm( class PowerPortCSVForm(CSVModelForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', error_messages={ @@ -2692,14 +2692,14 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): class PowerOutletCSVForm(CSVModelForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', error_messages={ 'invalid_choice': 'Device not found.', } ) - power_port = forms.ModelChoiceField( + power_port = CSVModelChoiceField( queryset=PowerPort.objects.all(), required=False, to_field_name='name', @@ -3014,7 +3014,7 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): class InterfaceCSVForm(CSVModelForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', @@ -3022,7 +3022,7 @@ class InterfaceCSVForm(CSVModelForm): 'invalid_choice': 'Device not found.', } ) - virtual_machine = forms.ModelChoiceField( + virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', @@ -3030,7 +3030,7 @@ class InterfaceCSVForm(CSVModelForm): 'invalid_choice': 'Virtual machine not found.', } ) - lag = forms.ModelChoiceField( + lag = CSVModelChoiceField( queryset=Interface.objects.all(), required=False, to_field_name='name', @@ -3227,14 +3227,14 @@ class FrontPortBulkDisconnectForm(ConfirmationForm): class FrontPortCSVForm(CSVModelForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', error_messages={ 'invalid_choice': 'Device not found.', } ) - rear_port = forms.ModelChoiceField( + rear_port = CSVModelChoiceField( queryset=RearPort.objects.all(), to_field_name='name', help_text='Corresponding rear port', @@ -3368,7 +3368,7 @@ class RearPortBulkDisconnectForm(ConfirmationForm): class RearPortCSVForm(CSVModelForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', error_messages={ @@ -3479,14 +3479,14 @@ class DeviceBayBulkRenameForm(BulkRenameForm): class DeviceBayCSVForm(CSVModelForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', error_messages={ 'invalid_choice': 'Device not found.', } ) - installed_device = forms.ModelChoiceField( + installed_device = CSVModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', @@ -3771,7 +3771,7 @@ class CableForm(BootstrapMixin, forms.ModelForm): class CableCSVForm(CSVModelForm): # Termination A - side_a_device = forms.ModelChoiceField( + side_a_device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', help_text='Side A device', @@ -3779,7 +3779,7 @@ class CableCSVForm(CSVModelForm): 'invalid_choice': 'Side A device not found', } ) - side_a_type = forms.ModelChoiceField( + side_a_type = CSVModelChoiceField( queryset=ContentType.objects.all(), limit_choices_to=CABLE_TERMINATION_MODELS, to_field_name='model', @@ -3790,7 +3790,7 @@ class CableCSVForm(CSVModelForm): ) # Termination B - side_b_device = forms.ModelChoiceField( + side_b_device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', help_text='Side B device', @@ -3798,7 +3798,7 @@ class CableCSVForm(CSVModelForm): 'invalid_choice': 'Side B device not found', } ) - side_b_type = forms.ModelChoiceField( + side_b_type = CSVModelChoiceField( queryset=ContentType.objects.all(), limit_choices_to=CABLE_TERMINATION_MODELS, to_field_name='model', @@ -4124,14 +4124,14 @@ class InventoryItemCreateForm(BootstrapMixin, forms.Form): class InventoryItemCSVForm(CSVModelForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', error_messages={ 'invalid_choice': 'Device not found.', } ) - manufacturer = forms.ModelChoiceField( + manufacturer = CSVModelChoiceField( queryset=Manufacturer.objects.all(), to_field_name='name', required=False, @@ -4435,7 +4435,7 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm): class PowerPanelCSVForm(CSVModelForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', help_text='Name of parent site', @@ -4443,7 +4443,7 @@ class PowerPanelCSVForm(CSVModelForm): 'invalid_choice': 'Site not found.', } ) - rack_group = forms.ModelChoiceField( + rack_group = CSVModelChoiceField( queryset=RackGroup.objects.all(), required=False, to_field_name='name', @@ -4579,7 +4579,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm): class PowerFeedCSVForm(CustomFieldModelCSVForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', help_text='Assigned site', @@ -4587,7 +4587,7 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Site not found.', } ) - power_panel = forms.ModelChoiceField( + power_panel = CSVModelChoiceField( queryset=PowerPanel.objects.all(), to_field_name='name', help_text='Upstream power panel', @@ -4595,7 +4595,7 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Power panel not found.', } ) - rack_group = forms.ModelChoiceField( + rack_group = CSVModelChoiceField( queryset=RackGroup.objects.all(), to_field_name='name', required=False, @@ -4604,7 +4604,7 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Rack group not found.', } ) - rack = forms.ModelChoiceField( + rack = CSVModelChoiceField( queryset=Rack.objects.all(), to_field_name='name', required=False, diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 2f2c99ed7..790a30f03 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -10,8 +10,9 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, - CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, - ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, + CSVModelChoiceField, CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + ExpandableIPAddressField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, + BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import VirtualMachine from .choices import * @@ -50,7 +51,7 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class VRFCSVForm(CustomFieldModelCSVForm): - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', @@ -162,7 +163,7 @@ class AggregateForm(BootstrapMixin, CustomFieldModelForm): class AggregateCSVForm(CustomFieldModelCSVForm): - rir = forms.ModelChoiceField( + rir = CSVModelChoiceField( queryset=RIR.objects.all(), to_field_name='name', help_text='Assigned RIR', @@ -324,7 +325,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class PrefixCSVForm(CustomFieldModelCSVForm): - vrf = forms.ModelChoiceField( + vrf = CSVModelChoiceField( queryset=VRF.objects.all(), to_field_name='name', required=False, @@ -333,7 +334,7 @@ class PrefixCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'VRF not found.', } ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', @@ -342,7 +343,7 @@ class PrefixCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Tenant not found.', } ) - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), required=False, to_field_name='name', @@ -351,7 +352,7 @@ class PrefixCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Site not found.', } ) - vlan_group = forms.ModelChoiceField( + vlan_group = CSVModelChoiceField( queryset=VLANGroup.objects.all(), required=False, to_field_name='name', @@ -360,7 +361,7 @@ class PrefixCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'VLAN group not found.', } ) - vlan = forms.ModelChoiceField( + vlan = CSVModelChoiceField( queryset=VLAN.objects.all(), required=False, to_field_name='vid', @@ -373,7 +374,7 @@ class PrefixCSVForm(CustomFieldModelCSVForm): choices=PrefixStatusChoices, help_text='Operational status' ) - role = forms.ModelChoiceField( + role = CSVModelChoiceField( queryset=Role.objects.all(), required=False, to_field_name='name', @@ -716,7 +717,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class IPAddressCSVForm(CustomFieldModelCSVForm): - vrf = forms.ModelChoiceField( + vrf = CSVModelChoiceField( queryset=VRF.objects.all(), to_field_name='name', required=False, @@ -725,7 +726,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'VRF not found.', } ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), to_field_name='name', required=False, @@ -743,7 +744,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): required=False, help_text='Functional role' ) - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', @@ -752,7 +753,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Device not found.', } ) - virtual_machine = forms.ModelChoiceField( + virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', @@ -761,7 +762,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Virtual machine not found.', } ) - interface = forms.ModelChoiceField( + interface = CSVModelChoiceField( queryset=Interface.objects.all(), required=False, to_field_name='name', @@ -974,7 +975,7 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm): class VLANGroupCSVForm(CSVModelForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), required=False, to_field_name='name', @@ -1059,7 +1060,7 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class VLANCSVForm(CustomFieldModelCSVForm): - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), required=False, to_field_name='name', @@ -1068,7 +1069,7 @@ class VLANCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Site not found.', } ) - group = forms.ModelChoiceField( + group = CSVModelChoiceField( queryset=VLANGroup.objects.all(), required=False, to_field_name='name', @@ -1077,7 +1078,7 @@ class VLANCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'VLAN group not found.', } ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), to_field_name='name', required=False, @@ -1090,7 +1091,7 @@ class VLANCSVForm(CustomFieldModelCSVForm): choices=VLANStatusChoices, help_text='Operational status' ) - role = forms.ModelChoiceField( + role = CSVModelChoiceField( queryset=Role.objects.all(), required=False, to_field_name='name', @@ -1270,7 +1271,7 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm): class ServiceCSVForm(CustomFieldModelCSVForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), required=False, to_field_name='name', @@ -1279,7 +1280,7 @@ class ServiceCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Device not found.', } ) - virtual_machine = forms.ModelChoiceField( + virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index ec21a48ab..5214250db 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -8,8 +8,8 @@ from extras.forms import ( AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, ) from utilities.forms import ( - APISelectMultiple, BootstrapMixin, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - SlugField, StaticSelect2Multiple, TagFilterField, + APISelectMultiple, BootstrapMixin, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, + DynamicModelMultipleChoiceField, SlugField, StaticSelect2Multiple, TagFilterField, ) from .constants import * from .models import Secret, SecretRole, UserKey @@ -117,7 +117,7 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm): class SecretCSVForm(CustomFieldModelCSVForm): - device = forms.ModelChoiceField( + device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', help_text='Assigned device', @@ -125,7 +125,7 @@ class SecretCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Device not found.', } ) - role = forms.ModelChoiceField( + role = CSVModelChoiceField( queryset=SecretRole.objects.all(), to_field_name='name', help_text='Assigned role', diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index fca8e9924..423f752c5 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -5,8 +5,8 @@ from extras.forms import ( AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelCSVForm, ) from utilities.forms import ( - APISelect, APISelectMultiple, BootstrapMixin, CommentField, CSVModelForm, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, SlugField, TagFilterField, + APISelect, APISelectMultiple, BootstrapMixin, CommentField, CSVModelChoiceField, CSVModelForm, + DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, TagFilterField, ) from .models import Tenant, TenantGroup @@ -33,7 +33,7 @@ class TenantGroupForm(BootstrapMixin, forms.ModelForm): class TenantGroupCSVForm(CSVModelForm): - parent = forms.ModelChoiceField( + parent = CSVModelChoiceField( queryset=TenantGroup.objects.all(), required=False, to_field_name='name', @@ -73,7 +73,7 @@ class TenantForm(BootstrapMixin, CustomFieldModelForm): class TenantCSVForm(CustomFieldModelCSVForm): slug = SlugField() - group = forms.ModelChoiceField( + group = CSVModelChoiceField( queryset=TenantGroup.objects.all(), required=False, to_field_name='name', diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 98761252d..5c841a3bc 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -8,6 +8,7 @@ import yaml from django import forms from django.conf import settings from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput +from django.core.exceptions import MultipleObjectsReturned from django.db.models import Count from django.forms import BoundField from django.forms.models import fields_for_model @@ -481,7 +482,6 @@ class CSVChoiceField(forms.ChoiceField): """ Invert the provided set of choices to take the human-friendly label as input, and return the database value. """ - def __init__(self, choices, *args, **kwargs): super().__init__(choices=choices, *args, **kwargs) self.choices = [(label, label) for value, label in unpack_grouped_choices(choices)] @@ -496,6 +496,19 @@ class CSVChoiceField(forms.ChoiceField): return self.choice_values[value] +class CSVModelChoiceField(forms.ModelChoiceField): + """ + Provides additional validation for model choices entered as CSV data. + """ + def to_python(self, value): + try: + return super().to_python(value) + except MultipleObjectsReturned as e: + raise forms.ValidationError( + f'"{value}" is not a unique value for this field; multiple objects were found' + ) + + class ExpandableNameField(forms.CharField): """ A field which allows for numeric range expansion diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index f3c5d1633..ed757171c 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -14,7 +14,7 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, - CommentField, ConfirmationForm, CSVChoiceField, CSVModelForm, DynamicModelChoiceField, + CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField, ) @@ -95,7 +95,7 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class ClusterCSVForm(CustomFieldModelCSVForm): - type = forms.ModelChoiceField( + type = CSVModelChoiceField( queryset=ClusterType.objects.all(), to_field_name='name', help_text='Type of cluster', @@ -103,7 +103,7 @@ class ClusterCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Invalid cluster type name.', } ) - group = forms.ModelChoiceField( + group = CSVModelChoiceField( queryset=ClusterGroup.objects.all(), to_field_name='name', required=False, @@ -112,7 +112,7 @@ class ClusterCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Invalid cluster group name.', } ) - site = forms.ModelChoiceField( + site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', required=False, @@ -121,7 +121,7 @@ class ClusterCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Invalid site name.', } ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), to_field_name='name', required=False, @@ -401,7 +401,7 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): required=False, help_text='Operational status of device' ) - cluster = forms.ModelChoiceField( + cluster = CSVModelChoiceField( queryset=Cluster.objects.all(), to_field_name='name', help_text='Assigned cluster', @@ -409,7 +409,7 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Invalid cluster name.', } ) - role = forms.ModelChoiceField( + role = CSVModelChoiceField( queryset=DeviceRole.objects.filter( vm_role=True ), @@ -420,7 +420,7 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Invalid role name.' } ) - tenant = forms.ModelChoiceField( + tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', @@ -429,7 +429,7 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): 'invalid_choice': 'Tenant not found.' } ) - platform = forms.ModelChoiceField( + platform = CSVModelChoiceField( queryset=Platform.objects.all(), required=False, to_field_name='name', From 270d61ce1bbe38d00277dfe28e7393cd4d429a8e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 6 May 2020 09:58:12 -0400 Subject: [PATCH 12/12] Remove boilerplate error messages from CSV model choice fields --- netbox/circuits/forms.py | 15 +-- netbox/dcim/forms.py | 230 +++++++-------------------------- netbox/ipam/forms.py | 100 +++----------- netbox/secrets/forms.py | 10 +- netbox/tenancy/forms.py | 10 +- netbox/utilities/forms.py | 4 + netbox/virtualization/forms.py | 40 ++---- 7 files changed, 85 insertions(+), 324 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index e95f4abb1..427dc2e89 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -189,18 +189,12 @@ class CircuitCSVForm(CustomFieldModelCSVForm): provider = CSVModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', - help_text='Assigned provider', - error_messages={ - 'invalid_choice': 'Provider not found.' - } + help_text='Assigned provider' ) type = CSVModelChoiceField( queryset=CircuitType.objects.all(), to_field_name='name', - help_text='Type of circuit', - error_messages={ - 'invalid_choice': 'Invalid circuit type.' - } + help_text='Type of circuit' ) status = CSVChoiceField( choices=CircuitStatusChoices, @@ -211,10 +205,7 @@ class CircuitCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.' - } + help_text='Assigned tenant' ) class Meta: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 88cf0d07b..ef6f222a9 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -198,10 +198,7 @@ class RegionCSVForm(CSVModelForm): queryset=Region.objects.all(), required=False, to_field_name='name', - help_text='Name of parent region', - error_messages={ - 'invalid_choice': 'Region not found.', - } + help_text='Name of parent region' ) class Meta: @@ -277,19 +274,13 @@ class SiteCSVForm(CustomFieldModelCSVForm): queryset=Region.objects.all(), required=False, to_field_name='name', - help_text='Assigned region', - error_messages={ - 'invalid_choice': 'Region not found.', - } + help_text='Assigned region' ) tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) class Meta: @@ -392,10 +383,7 @@ class RackGroupCSVForm(CSVModelForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Assigned site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) parent = CSVModelChoiceField( queryset=RackGroup.objects.all(), @@ -521,27 +509,18 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class RackCSVForm(CustomFieldModelCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), - to_field_name='name', - error_messages={ - 'invalid_choice': 'Site not found.', - } + to_field_name='name' ) group = CSVModelChoiceField( queryset=RackGroup.objects.all(), required=False, - to_field_name='name', - error_messages={ - 'invalid_choice': 'Rack group not found.', - } + to_field_name='name' ) tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Name of assigned tenant' ) status = CSVChoiceField( choices=RackStatusChoices, @@ -552,10 +531,7 @@ class RackCSVForm(CustomFieldModelCSVForm): queryset=RackRole.objects.all(), required=False, to_field_name='name', - help_text='Name of assigned role', - error_messages={ - 'invalid_choice': 'Role not found.', - } + help_text='Name of assigned role' ) type = CSVChoiceField( choices=RackTypeChoices, @@ -804,27 +780,18 @@ class RackReservationCSVForm(CSVModelForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Parent site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Parent site' ) rack_group = CSVModelChoiceField( queryset=RackGroup.objects.all(), to_field_name='name', required=False, - help_text="Rack's group (if any)", - error_messages={ - 'invalid_choice': 'Rack group not found.', - } + help_text="Rack's group (if any)" ) rack = CSVModelChoiceField( queryset=Rack.objects.all(), to_field_name='name', - help_text='Rack', - error_messages={ - 'invalid_choice': 'Rack not found.', - } + help_text='Rack' ) units = SimpleArrayField( base_field=forms.IntegerField(), @@ -835,10 +802,7 @@ class RackReservationCSVForm(CSVModelForm): queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) class Meta: @@ -1680,10 +1644,7 @@ class PlatformCSVForm(CSVModelForm): queryset=Manufacturer.objects.all(), required=False, to_field_name='name', - help_text='Limit platform assignments to this manufacturer', - error_messages={ - 'invalid_choice': 'Manufacturer not found.', - } + help_text='Limit platform assignments to this manufacturer' ) class Meta: @@ -1893,44 +1854,29 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): device_role = CSVModelChoiceField( queryset=DeviceRole.objects.all(), to_field_name='name', - help_text='Assigned role', - error_messages={ - 'invalid_choice': 'Invalid device role.', - } + help_text='Assigned role' ) tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) manufacturer = CSVModelChoiceField( queryset=Manufacturer.objects.all(), to_field_name='name', - help_text='Device type manufacturer', - error_messages={ - 'invalid_choice': 'Manufacturer not found.', - } + help_text='Device type manufacturer' ) device_type = CSVModelChoiceField( queryset=DeviceType.objects.all(), to_field_name='model', - help_text='Device type model', - error_messages={ - 'invalid_choice': 'Device type not found.', - } + help_text='Device type model' ) platform = CSVModelChoiceField( queryset=Platform.objects.all(), required=False, to_field_name='name', - help_text='Assigned platform', - error_messages={ - 'invalid_choice': 'Invalid platform.', - } + help_text='Assigned platform' ) status = CSVChoiceField( choices=DeviceStatusChoices, @@ -1940,10 +1886,7 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): queryset=Cluster.objects.all(), to_field_name='name', required=False, - help_text='Virtualization cluster', - error_messages={ - 'invalid_choice': 'Invalid cluster name.', - } + help_text='Virtualization cluster' ) class Meta: @@ -1964,28 +1907,19 @@ class DeviceCSVForm(BaseDeviceCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Assigned site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) rack_group = CSVModelChoiceField( queryset=RackGroup.objects.all(), to_field_name='name', required=False, - help_text="Rack's group (if any)", - error_messages={ - 'invalid_choice': 'Rack group not found.', - } + help_text="Rack's group (if any)" ) rack = CSVModelChoiceField( queryset=Rack.objects.all(), to_field_name='name', required=False, - help_text="Assigned rack", - error_messages={ - 'invalid_choice': 'Rack not found.', - } + help_text="Assigned rack" ) face = CSVChoiceField( choices=DeviceFaceChoices, @@ -2020,18 +1954,12 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm): parent = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Parent device', - error_messages={ - 'invalid_choice': 'Parent device not found.', - } + help_text='Parent device' ) device_bay = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Device bay in which this device is installed', - error_messages={ - 'invalid_choice': 'Devie bay not found.', - } + help_text='Device bay in which this device is installed' ) class Meta(BaseDeviceCSVForm.Meta): @@ -2342,10 +2270,7 @@ class ConsolePortBulkEditForm( class ConsolePortCSVForm(CSVModelForm): device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) class Meta: @@ -2445,10 +2370,7 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): class ConsoleServerPortCSVForm(CSVModelForm): device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) class Meta: @@ -2544,10 +2466,7 @@ class PowerPortBulkEditForm( class PowerPortCSVForm(CSVModelForm): device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) class Meta: @@ -2694,19 +2613,13 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): class PowerOutletCSVForm(CSVModelForm): device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) power_port = CSVModelChoiceField( queryset=PowerPort.objects.all(), required=False, to_field_name='name', - help_text='Local power port which feeds this outlet', - error_messages={ - 'invalid_choice': 'Power port not found.', - } + help_text='Local power port which feeds this outlet' ) feed_leg = CSVChoiceField( choices=PowerOutletFeedLegChoices, @@ -3017,27 +2930,18 @@ class InterfaceCSVForm(CSVModelForm): device = CSVModelChoiceField( queryset=Device.objects.all(), required=False, - to_field_name='name', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, - to_field_name='name', - error_messages={ - 'invalid_choice': 'Virtual machine not found.', - } + to_field_name='name' ) lag = CSVModelChoiceField( queryset=Interface.objects.all(), required=False, to_field_name='name', - help_text='Parent LAG interface', - error_messages={ - 'invalid_choice': 'LAG interface not found.', - } + help_text='Parent LAG interface' ) type = CSVChoiceField( choices=InterfaceTypeChoices, @@ -3229,18 +3133,12 @@ class FrontPortBulkDisconnectForm(ConfirmationForm): class FrontPortCSVForm(CSVModelForm): device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) rear_port = CSVModelChoiceField( queryset=RearPort.objects.all(), to_field_name='name', - help_text='Corresponding rear port', - error_messages={ - 'invalid_choice': 'Rear Port not found.', - } + help_text='Corresponding rear port' ) type = CSVChoiceField( choices=PortTypeChoices, @@ -3370,10 +3268,7 @@ class RearPortBulkDisconnectForm(ConfirmationForm): class RearPortCSVForm(CSVModelForm): device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) type = CSVChoiceField( help_text='Physical medium classification', @@ -3481,10 +3376,7 @@ class DeviceBayBulkRenameForm(BulkRenameForm): class DeviceBayCSVForm(CSVModelForm): device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) installed_device = CSVModelChoiceField( queryset=Device.objects.all(), @@ -3774,10 +3666,7 @@ class CableCSVForm(CSVModelForm): side_a_device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Side A device', - error_messages={ - 'invalid_choice': 'Side A device not found', - } + help_text='Side A device' ) side_a_type = CSVModelChoiceField( queryset=ContentType.objects.all(), @@ -3793,10 +3682,7 @@ class CableCSVForm(CSVModelForm): side_b_device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Side B device', - error_messages={ - 'invalid_choice': 'Side B device not found', - } + help_text='Side B device' ) side_b_type = CSVModelChoiceField( queryset=ContentType.objects.all(), @@ -4126,18 +4012,12 @@ class InventoryItemCreateForm(BootstrapMixin, forms.Form): class InventoryItemCSVForm(CSVModelForm): device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - error_messages={ - 'invalid_choice': 'Device not found.', - } + to_field_name='name' ) manufacturer = CSVModelChoiceField( queryset=Manufacturer.objects.all(), to_field_name='name', - required=False, - error_messages={ - 'invalid_choice': 'Invalid manufacturer.', - } + required=False ) class Meta: @@ -4438,18 +4318,12 @@ class PowerPanelCSVForm(CSVModelForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Name of parent site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Name of parent site' ) rack_group = CSVModelChoiceField( queryset=RackGroup.objects.all(), required=False, - to_field_name='name', - error_messages={ - 'invalid_choice': 'Rack group not found.', - } + to_field_name='name' ) class Meta: @@ -4582,36 +4456,24 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Assigned site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) power_panel = CSVModelChoiceField( queryset=PowerPanel.objects.all(), to_field_name='name', - help_text='Upstream power panel', - error_messages={ - 'invalid_choice': 'Power panel not found.', - } + help_text='Upstream power panel' ) rack_group = CSVModelChoiceField( queryset=RackGroup.objects.all(), to_field_name='name', required=False, - help_text="Rack's group (if any)", - error_messages={ - 'invalid_choice': 'Rack group not found.', - } + help_text="Rack's group (if any)" ) rack = CSVModelChoiceField( queryset=Rack.objects.all(), to_field_name='name', required=False, - help_text='Rack', - error_messages={ - 'invalid_choice': 'Rack not found.', - } + help_text='Rack' ) status = CSVChoiceField( choices=PowerFeedStatusChoices, diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 790a30f03..7eda1add3 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -55,10 +55,7 @@ class VRFCSVForm(CustomFieldModelCSVForm): queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) class Meta: @@ -166,10 +163,7 @@ class AggregateCSVForm(CustomFieldModelCSVForm): rir = CSVModelChoiceField( queryset=RIR.objects.all(), to_field_name='name', - help_text='Assigned RIR', - error_messages={ - 'invalid_choice': 'RIR not found.', - } + help_text='Assigned RIR' ) class Meta: @@ -329,46 +323,31 @@ class PrefixCSVForm(CustomFieldModelCSVForm): queryset=VRF.objects.all(), to_field_name='name', required=False, - help_text='Assigned VRF', - error_messages={ - 'invalid_choice': 'VRF not found.', - } + help_text='Assigned VRF' ) tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) site = CSVModelChoiceField( queryset=Site.objects.all(), required=False, to_field_name='name', - help_text='Assigned site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) vlan_group = CSVModelChoiceField( queryset=VLANGroup.objects.all(), required=False, to_field_name='name', - help_text="VLAN's group (if any)", - error_messages={ - 'invalid_choice': 'VLAN group not found.', - } + help_text="VLAN's group (if any)" ) vlan = CSVModelChoiceField( queryset=VLAN.objects.all(), required=False, to_field_name='vid', - help_text="Assigned VLAN", - error_messages={ - 'invalid_choice': 'VLAN not found.', - } + help_text="Assigned VLAN" ) status = CSVChoiceField( choices=PrefixStatusChoices, @@ -378,10 +357,7 @@ class PrefixCSVForm(CustomFieldModelCSVForm): queryset=Role.objects.all(), required=False, to_field_name='name', - help_text='Functional role', - error_messages={ - 'invalid_choice': 'Invalid role.', - } + help_text='Functional role' ) class Meta: @@ -721,19 +697,13 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): queryset=VRF.objects.all(), to_field_name='name', required=False, - help_text='Assigned VRF', - error_messages={ - 'invalid_choice': 'VRF not found.', - } + help_text='Assigned VRF' ) tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), to_field_name='name', required=False, - help_text='Assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) status = CSVChoiceField( choices=IPAddressStatusChoices, @@ -748,28 +718,19 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): queryset=Device.objects.all(), required=False, to_field_name='name', - help_text='Parent device of assigned interface (if any)', - error_messages={ - 'invalid_choice': 'Device not found.', - } + help_text='Parent device of assigned interface (if any)' ) virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', - help_text='Parent VM of assigned interface (if any)', - error_messages={ - 'invalid_choice': 'Virtual machine not found.', - } + help_text='Parent VM of assigned interface (if any)' ) interface = CSVModelChoiceField( queryset=Interface.objects.all(), required=False, to_field_name='name', - help_text='Assigned interface', - error_messages={ - 'invalid_choice': 'Interface not found.', - } + help_text='Assigned interface' ) is_primary = forms.BooleanField( help_text='Make this the primary IP for the assigned device', @@ -979,10 +940,7 @@ class VLANGroupCSVForm(CSVModelForm): queryset=Site.objects.all(), required=False, to_field_name='name', - help_text='Assigned site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) slug = SlugField() @@ -1064,28 +1022,19 @@ class VLANCSVForm(CustomFieldModelCSVForm): queryset=Site.objects.all(), required=False, to_field_name='name', - help_text='Assigned site', - error_messages={ - 'invalid_choice': 'Site not found.', - } + help_text='Assigned site' ) group = CSVModelChoiceField( queryset=VLANGroup.objects.all(), required=False, to_field_name='name', - help_text='Assigned VLAN group', - error_messages={ - 'invalid_choice': 'VLAN group not found.', - } + help_text='Assigned VLAN group' ) tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), to_field_name='name', required=False, - help_text='Assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.', - } + help_text='Assigned tenant' ) status = CSVChoiceField( choices=VLANStatusChoices, @@ -1095,10 +1044,7 @@ class VLANCSVForm(CustomFieldModelCSVForm): queryset=Role.objects.all(), required=False, to_field_name='name', - help_text='Functional role', - error_messages={ - 'invalid_choice': 'Invalid role.', - } + help_text='Functional role' ) class Meta: @@ -1275,19 +1221,13 @@ class ServiceCSVForm(CustomFieldModelCSVForm): queryset=Device.objects.all(), required=False, to_field_name='name', - help_text='Required if not assigned to a VM', - error_messages={ - 'invalid_choice': 'Device not found.', - } + help_text='Required if not assigned to a VM' ) virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), required=False, to_field_name='name', - help_text='Required if not assigned to a device', - error_messages={ - 'invalid_choice': 'Virtual machine not found.', - } + help_text='Required if not assigned to a device' ) protocol = CSVChoiceField( choices=ServiceProtocolChoices, diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 5214250db..368a47590 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -120,18 +120,12 @@ class SecretCSVForm(CustomFieldModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', - help_text='Assigned device', - error_messages={ - 'invalid_choice': 'Device not found.', - } + help_text='Assigned device' ) role = CSVModelChoiceField( queryset=SecretRole.objects.all(), to_field_name='name', - help_text='Assigned role', - error_messages={ - 'invalid_choice': 'Invalid secret role.', - } + help_text='Assigned role' ) plaintext = forms.CharField( help_text='Plaintext secret data' diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 423f752c5..700d88b1d 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -37,10 +37,7 @@ class TenantGroupCSVForm(CSVModelForm): queryset=TenantGroup.objects.all(), required=False, to_field_name='name', - help_text='Parent group', - error_messages={ - 'invalid_choice': 'Tenant group not found.', - } + help_text='Parent group' ) slug = SlugField() @@ -77,10 +74,7 @@ class TenantCSVForm(CustomFieldModelCSVForm): queryset=TenantGroup.objects.all(), required=False, to_field_name='name', - help_text='Assigned group', - error_messages={ - 'invalid_choice': 'Group not found.' - } + help_text='Assigned group' ) class Meta: diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 5c841a3bc..79d76e807 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -500,6 +500,10 @@ class CSVModelChoiceField(forms.ModelChoiceField): """ Provides additional validation for model choices entered as CSV data. """ + default_error_messages = { + 'invalid_choice': 'Object not found.', + } + def to_python(self, value): try: return super().to_python(value) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index ed757171c..0983b2432 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -98,37 +98,25 @@ class ClusterCSVForm(CustomFieldModelCSVForm): type = CSVModelChoiceField( queryset=ClusterType.objects.all(), to_field_name='name', - help_text='Type of cluster', - error_messages={ - 'invalid_choice': 'Invalid cluster type name.', - } + help_text='Type of cluster' ) group = CSVModelChoiceField( queryset=ClusterGroup.objects.all(), to_field_name='name', required=False, - help_text='Assigned cluster group', - error_messages={ - 'invalid_choice': 'Invalid cluster group name.', - } + help_text='Assigned cluster group' ) site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', required=False, - help_text='Assigned site', - error_messages={ - 'invalid_choice': 'Invalid site name.', - } + help_text='Assigned site' ) tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), to_field_name='name', required=False, - help_text='Assigned tenant', - error_messages={ - 'invalid_choice': 'Invalid tenant name' - } + help_text='Assigned tenant' ) class Meta: @@ -404,10 +392,7 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): cluster = CSVModelChoiceField( queryset=Cluster.objects.all(), to_field_name='name', - help_text='Assigned cluster', - error_messages={ - 'invalid_choice': 'Invalid cluster name.', - } + help_text='Assigned cluster' ) role = CSVModelChoiceField( queryset=DeviceRole.objects.filter( @@ -415,28 +400,19 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): ), required=False, to_field_name='name', - help_text='Functional role', - error_messages={ - 'invalid_choice': 'Invalid role name.' - } + help_text='Functional role' ) tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Assigned tenant', - error_messages={ - 'invalid_choice': 'Tenant not found.' - } + help_text='Assigned tenant' ) platform = CSVModelChoiceField( queryset=Platform.objects.all(), required=False, to_field_name='name', - help_text='Assigned platform', - error_messages={ - 'invalid_choice': 'Invalid platform.', - } + help_text='Assigned platform' ) class Meta: