Просмотр исходного кода

Introduce CSVModelForm for dynamic CSV imports

Jeremy Stretch 5 лет назад
Родитель
Сommit
839e999a71

+ 2 - 2
netbox/circuits/forms.py

@@ -8,7 +8,7 @@ from extras.forms import (
 from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import (
 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,
     DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
     StaticSelect2Multiple, TagFilterField,
     StaticSelect2Multiple, TagFilterField,
 )
 )
@@ -142,7 +142,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
         ]
         ]
 
 
 
 
-class CircuitTypeCSVForm(forms.ModelForm):
+class CircuitTypeCSVForm(CSVModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:

+ 162 - 167
netbox/dcim/forms.py

@@ -23,9 +23,9 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
 from utilities.forms import (
     APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
     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,
     BOOLEAN_WITH_BLANK_CHOICES,
 )
 )
 from virtualization.models import Cluster, ClusterGroup, VirtualMachine
 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(
     parent = forms.ModelChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         required=False,
         required=False,
@@ -388,7 +388,7 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm):
         )
         )
 
 
 
 
-class RackGroupCSVForm(forms.ModelForm):
+class RackGroupCSVForm(CSVModelForm):
     site = forms.ModelChoiceField(
     site = forms.ModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='name',
         to_field_name='name',
@@ -461,7 +461,7 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm):
         ]
         ]
 
 
 
 
-class RackRoleCSVForm(forms.ModelForm):
+class RackRoleCSVForm(CSVModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
@@ -526,8 +526,13 @@ class RackCSVForm(CustomFieldModelCSVForm):
             'invalid_choice': 'Site not found.',
             '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(
     tenant = forms.ModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
@@ -571,33 +576,14 @@ class RackCSVForm(CustomFieldModelCSVForm):
         model = Rack
         model = Rack
         fields = Rack.csv_headers
         fields = Rack.csv_headers
 
 
-    def clean(self):
-
-        super().clean()
-
-        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))
+    def __init__(self, data=None, *args, **kwargs):
+        super().__init__(data, *args, **kwargs)
 
 
-            # 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)
-                )
+        if data:
 
 
-            # 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):
 class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
@@ -814,21 +800,31 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
         return unit_choices
         return unit_choices
 
 
 
 
-class RackReservationCSVForm(forms.ModelForm):
+class RackReservationCSVForm(CSVModelForm):
     site = forms.ModelChoiceField(
     site = forms.ModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='name',
         to_field_name='name',
         help_text='Parent site',
         help_text='Parent site',
         error_messages={
         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,
         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(
     units = SimpleArrayField(
         base_field=forms.IntegerField(),
         base_field=forms.IntegerField(),
@@ -847,27 +843,23 @@ class RackReservationCSVForm(forms.ModelForm):
 
 
     class Meta:
     class Meta:
         model = RackReservation
         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):
 class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
@@ -933,7 +925,7 @@ class ManufacturerForm(BootstrapMixin, forms.ModelForm):
         ]
         ]
 
 
 
 
-class ManufacturerCSVForm(forms.ModelForm):
+class ManufacturerCSVForm(CSVModelForm):
 
 
     class Meta:
     class Meta:
         model = Manufacturer
         model = Manufacturer
@@ -1648,7 +1640,7 @@ class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
         ]
         ]
 
 
 
 
-class DeviceRoleCSVForm(forms.ModelForm):
+class DeviceRoleCSVForm(CSVModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
@@ -1682,7 +1674,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
         }
         }
 
 
 
 
-class PlatformCSVForm(forms.ModelForm):
+class PlatformCSVForm(CSVModelForm):
     slug = SlugField()
     slug = SlugField()
     manufacturer = forms.ModelChoiceField(
     manufacturer = forms.ModelChoiceField(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -1920,11 +1912,16 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm):
         to_field_name='name',
         to_field_name='name',
         help_text='Device type manufacturer',
         help_text='Device type manufacturer',
         error_messages={
         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(
     platform = forms.ModelChoiceField(
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
@@ -1953,19 +1950,14 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm):
         fields = []
         fields = []
         model = Device
         model = Device
 
 
-    def clean(self):
-
-        super().clean()
+    def __init__(self, data=None, *args, **kwargs):
+        super().__init__(data, *args, **kwargs)
 
 
-        manufacturer = self.cleaned_data.get('manufacturer')
-        model_name = self.cleaned_data.get('model_name')
+        if data:
 
 
-        # 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):
 class DeviceCSVForm(BaseDeviceCSVForm):
@@ -1974,16 +1966,26 @@ class DeviceCSVForm(BaseDeviceCSVForm):
         to_field_name='name',
         to_field_name='name',
         help_text='Assigned site',
         help_text='Assigned site',
         error_messages={
         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,
         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,
         required=False,
-        help_text='Name of parent rack'
+        help_text="Assigned rack",
+        error_messages={
+            'invalid_choice': 'Rack not found.',
+        }
     )
     )
     face = CSVChoiceField(
     face = CSVChoiceField(
         choices=DeviceFaceChoices,
         choices=DeviceFaceChoices,
@@ -1993,29 +1995,25 @@ class DeviceCSVForm(BaseDeviceCSVForm):
 
 
     class Meta(BaseDeviceCSVForm.Meta):
     class Meta(BaseDeviceCSVForm.Meta):
         fields = [
         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):
 class ChildDeviceCSVForm(BaseDeviceCSVForm):
@@ -2027,32 +2025,29 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
             'invalid_choice': 'Parent device not found.',
             '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):
     class Meta(BaseDeviceCSVForm.Meta):
         fields = [
         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):
-
-        super().clean()
+    def __init__(self, data=None, *args, **kwargs):
+        super().__init__(data, *args, **kwargs)
 
 
-        parent = self.cleaned_data.get('parent')
-        device_bay_name = self.cleaned_data.get('device_bay_name')
+        if data:
 
 
-        # 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):
 class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
@@ -2344,7 +2339,7 @@ class ConsolePortBulkEditForm(
         )
         )
 
 
 
 
-class ConsolePortCSVForm(forms.ModelForm):
+class ConsolePortCSVForm(CSVModelForm):
     device = forms.ModelChoiceField(
     device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         to_field_name='name',
         to_field_name='name',
@@ -2447,7 +2442,7 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
     )
     )
 
 
 
 
-class ConsoleServerPortCSVForm(forms.ModelForm):
+class ConsoleServerPortCSVForm(CSVModelForm):
     device = forms.ModelChoiceField(
     device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         to_field_name='name',
         to_field_name='name',
@@ -2546,7 +2541,7 @@ class PowerPortBulkEditForm(
         )
         )
 
 
 
 
-class PowerPortCSVForm(forms.ModelForm):
+class PowerPortCSVForm(CSVModelForm):
     device = forms.ModelChoiceField(
     device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         to_field_name='name',
         to_field_name='name',
@@ -2696,7 +2691,7 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm):
     )
     )
 
 
 
 
-class PowerOutletCSVForm(forms.ModelForm):
+class PowerOutletCSVForm(CSVModelForm):
     device = forms.ModelChoiceField(
     device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         to_field_name='name',
         to_field_name='name',
@@ -3018,7 +3013,7 @@ class InterfaceBulkDisconnectForm(ConfirmationForm):
     )
     )
 
 
 
 
-class InterfaceCSVForm(forms.ModelForm):
+class InterfaceCSVForm(CSVModelForm):
     device = forms.ModelChoiceField(
     device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         required=False,
         required=False,
@@ -3231,7 +3226,7 @@ class FrontPortBulkDisconnectForm(ConfirmationForm):
     )
     )
 
 
 
 
-class FrontPortCSVForm(forms.ModelForm):
+class FrontPortCSVForm(CSVModelForm):
     device = forms.ModelChoiceField(
     device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         to_field_name='name',
         to_field_name='name',
@@ -3372,7 +3367,7 @@ class RearPortBulkDisconnectForm(ConfirmationForm):
     )
     )
 
 
 
 
-class RearPortCSVForm(forms.ModelForm):
+class RearPortCSVForm(CSVModelForm):
     device = forms.ModelChoiceField(
     device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         to_field_name='name',
         to_field_name='name',
@@ -3483,7 +3478,7 @@ class DeviceBayBulkRenameForm(BulkRenameForm):
     )
     )
 
 
 
 
-class DeviceBayCSVForm(forms.ModelForm):
+class DeviceBayCSVForm(CSVModelForm):
     device = forms.ModelChoiceField(
     device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         to_field_name='name',
         to_field_name='name',
@@ -3774,7 +3769,7 @@ class CableForm(BootstrapMixin, forms.ModelForm):
         }
         }
 
 
 
 
-class CableCSVForm(forms.ModelForm):
+class CableCSVForm(CSVModelForm):
     # Termination A
     # Termination A
     side_a_device = forms.ModelChoiceField(
     side_a_device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -4128,7 +4123,7 @@ class InventoryItemCreateForm(BootstrapMixin, forms.Form):
     )
     )
 
 
 
 
-class InventoryItemCSVForm(forms.ModelForm):
+class InventoryItemCSVForm(CSVModelForm):
     device = forms.ModelChoiceField(
     device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         to_field_name='name',
         to_field_name='name',
@@ -4439,7 +4434,7 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm):
         ]
         ]
 
 
 
 
-class PowerPanelCSVForm(forms.ModelForm):
+class PowerPanelCSVForm(CSVModelForm):
     site = forms.ModelChoiceField(
     site = forms.ModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='name',
         to_field_name='name',
@@ -4448,30 +4443,27 @@ class PowerPanelCSVForm(forms.ModelForm):
             'invalid_choice': 'Site not found.',
             'invalid_choice': 'Site not found.',
         }
         }
     )
     )
-    rack_group_name = forms.CharField(
+    rack_group = forms.ModelChoiceField(
+        queryset=RackGroup.objects.all(),
         required=False,
         required=False,
-        help_text="Rack group name (optional)"
+        to_field_name='name',
+        error_messages={
+            'invalid_choice': 'Rack group not found.',
+        }
     )
     )
 
 
     class Meta:
     class Meta:
         model = PowerPanel
         model = PowerPanel
         fields = PowerPanel.csv_headers
         fields = PowerPanel.csv_headers
 
 
-    def clean(self):
-
-        super().clean()
+    def __init__(self, data=None, *args, **kwargs):
+        super().__init__(data, *args, **kwargs)
 
 
-        site = self.cleaned_data.get('site')
-        rack_group_name = self.cleaned_data.get('rack_group_name')
+        if data:
 
 
-        # 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):
 class PowerPanelBulkEditForm(BootstrapMixin, BulkEditForm):
@@ -4595,7 +4587,7 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm):
             'invalid_choice': 'Site not found.',
             'invalid_choice': 'Site not found.',
         }
         }
     )
     )
-    panel_name = forms.ModelChoiceField(
+    power_panel = forms.ModelChoiceField(
         queryset=PowerPanel.objects.all(),
         queryset=PowerPanel.objects.all(),
         to_field_name='name',
         to_field_name='name',
         help_text='Upstream power panel',
         help_text='Upstream power panel',
@@ -4603,13 +4595,23 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm):
             'invalid_choice': 'Power panel not found.',
             'invalid_choice': 'Power panel not found.',
         }
         }
     )
     )
-    rack_group = forms.CharField(
+    rack_group = forms.ModelChoiceField(
+        queryset=RackGroup.objects.all(),
+        to_field_name='name',
         required=False,
         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,
         required=False,
-        help_text="Assigned rack name"
+        help_text='Rack',
+        error_messages={
+            'invalid_choice': 'Rack not found.',
+        }
     )
     )
     status = CSVChoiceField(
     status = CSVChoiceField(
         choices=PowerFeedStatusChoices,
         choices=PowerFeedStatusChoices,
@@ -4636,32 +4638,25 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm):
         model = PowerFeed
         model = PowerFeed
         fields = PowerFeed.csv_headers
         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):
 class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):

+ 5 - 5
netbox/dcim/models/__init__.py

@@ -523,7 +523,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     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',
         'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
     ]
     ]
     clone_fields = [
     clone_fields = [
@@ -829,7 +829,7 @@ class RackReservation(ChangeLoggedModel):
 
 
     def clean(self):
     def clean(self):
 
 
-        if self.units:
+        if hasattr(self, 'rack') and self.units:
 
 
             # Validate that all specified units exist in the Rack.
             # Validate that all specified units exist in the Rack.
             invalid_units = [u for u in self.units if u not in self.rack.units]
             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)
     tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     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',
         'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
     ]
     ]
     clone_fields = [
     clone_fields = [
@@ -1791,7 +1791,7 @@ class PowerPanel(ChangeLoggedModel):
         max_length=50
         max_length=50
     )
     )
 
 
-    csv_headers = ['site', 'rack_group_name', 'name']
+    csv_headers = ['site', 'rack_group', 'name']
 
 
     class Meta:
     class Meta:
         ordering = ['site', 'name']
         ordering = ['site', 'name']
@@ -1898,7 +1898,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     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',
         'amperage', 'max_utilization', 'comments',
     ]
     ]
     clone_fields = [
     clone_fields = [

+ 4 - 4
netbox/dcim/tests/test_views.py

@@ -202,7 +202,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
         }
 
 
         cls.csv_data = (
         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,"10,11,12",Reservation 1',
             'Site 1,Rack 1,"13,14,15",Reservation 2',
             'Site 1,Rack 1,"13,14,15",Reservation 2',
             'Site 1,Rack 1,"16,17,18",Reservation 3',
             'Site 1,Rack 1,"16,17,18",Reservation 3',
@@ -947,7 +947,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
         }
 
 
         cls.csv_data = (
         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 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 5",
             "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6",
             "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6",
@@ -1586,7 +1586,7 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
         }
 
 
         cls.csv_data = (
         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 4",
             "Site 1,Rack Group 1,Power Panel 5",
             "Site 1,Rack Group 1,Power Panel 5",
             "Site 1,Rack Group 1,Power Panel 6",
             "Site 1,Rack Group 1,Power Panel 6",
@@ -1645,7 +1645,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
         }
 
 
         cls.csv_data = (
         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 4,120,20,80",
             "Site 1,Power Panel 1,Power Feed 5,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",
             "Site 1,Power Panel 1,Power Feed 6,120,20,80",

+ 2 - 2
netbox/extras/forms.py

@@ -8,7 +8,7 @@ from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
     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,
     StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
 )
 )
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
@@ -89,7 +89,7 @@ class CustomFieldModelForm(forms.ModelForm):
         return obj
         return obj
 
 
 
 
-class CustomFieldModelCSVForm(CustomFieldModelForm):
+class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
 
 
     def _append_customfield_fields(self):
     def _append_customfield_fields(self):
 
 

+ 76 - 94
netbox/ipam/forms.py

@@ -1,5 +1,4 @@
 from django import forms
 from django import forms
-from django.core.exceptions import MultipleObjectsReturned
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
 from taggit.forms import TagField
 from taggit.forms import TagField
 
 
@@ -11,8 +10,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField,
     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 virtualization.models import VirtualMachine
 from .choices import *
 from .choices import *
@@ -115,7 +114,7 @@ class RIRForm(BootstrapMixin, forms.ModelForm):
         ]
         ]
 
 
 
 
-class RIRCSVForm(forms.ModelForm):
+class RIRCSVForm(CSVModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
@@ -242,7 +241,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
         ]
         ]
 
 
 
 
-class RoleCSVForm(forms.ModelForm):
+class RoleCSVForm(CSVModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
@@ -352,13 +351,23 @@ class PrefixCSVForm(CustomFieldModelCSVForm):
             'invalid_choice': 'Site not found.',
             '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(
     status = CSVChoiceField(
         choices=PrefixStatusChoices,
         choices=PrefixStatusChoices,
@@ -378,39 +387,17 @@ class PrefixCSVForm(CustomFieldModelCSVForm):
         model = Prefix
         model = Prefix
         fields = Prefix.csv_headers
         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):
 class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
@@ -760,7 +747,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         required=False,
         required=False,
         to_field_name='name',
         to_field_name='name',
-        help_text='Assigned device',
+        help_text='Parent device of assigned interface (if any)',
         error_messages={
         error_messages={
             'invalid_choice': 'Device not found.',
             'invalid_choice': 'Device not found.',
         }
         }
@@ -769,14 +756,19 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
         queryset=VirtualMachine.objects.all(),
         queryset=VirtualMachine.objects.all(),
         required=False,
         required=False,
         to_field_name='name',
         to_field_name='name',
-        help_text='Assigned virtual machine',
+        help_text='Parent VM of assigned interface (if any)',
         error_messages={
         error_messages={
             'invalid_choice': 'Virtual machine not found.',
             '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(
     is_primary = forms.BooleanField(
         help_text='Make this the primary IP for the assigned device',
         help_text='Make this the primary IP for the assigned device',
@@ -787,38 +779,34 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
         model = IPAddress
         model = IPAddress
         fields = IPAddress.csv_headers
         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):
     def clean(self):
         super().clean()
         super().clean()
 
 
         device = self.cleaned_data.get('device')
         device = self.cleaned_data.get('device')
         virtual_machine = self.cleaned_data.get('virtual_machine')
         virtual_machine = self.cleaned_data.get('virtual_machine')
-        interface_name = self.cleaned_data.get('interface_name')
         is_primary = self.cleaned_data.get('is_primary')
         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
         # Validate is_primary
         if is_primary and not device and not virtual_machine:
         if is_primary and not device and not virtual_machine:
             raise forms.ValidationError("No device or virtual machine specified; cannot set as primary IP")
             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(
     site = forms.ModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
@@ -1080,9 +1068,14 @@ class VLANCSVForm(CustomFieldModelCSVForm):
             'invalid_choice': 'Site not found.',
             '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(
     tenant = forms.ModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
@@ -1115,25 +1108,14 @@ class VLANCSVForm(CustomFieldModelCSVForm):
             'name': 'VLAN name',
             'name': 'VLAN name',
         }
         }
 
 
-    def clean(self):
-        super().clean()
+    def __init__(self, data=None, *args, **kwargs):
+        super().__init__(data, *args, **kwargs)
+
+        if data:
 
 
-        site = self.cleaned_data.get('site')
-        group_name = self.cleaned_data.get('group_name')
-
-        # 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):
 class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):

+ 3 - 3
netbox/ipam/models.py

@@ -365,7 +365,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     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 = [
     clone_fields = [
         'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
         'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
@@ -636,7 +636,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     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',
         'dns_name', 'description',
     ]
     ]
     clone_fields = [
     clone_fields = [
@@ -926,7 +926,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
 
 
     tags = TaggableManager(through=TaggedItem)
     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 = [
     clone_fields = [
         'site', 'group', 'tenant', 'status', 'role', 'description',
         'site', 'group', 'tenant', 'status', 'role', 'description',
     ]
     ]

+ 3 - 3
netbox/secrets/forms.py

@@ -8,8 +8,8 @@ from extras.forms import (
     AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
     AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
 )
 )
 from utilities.forms import (
 from utilities.forms import (
-    APISelectMultiple, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
-    StaticSelect2Multiple, TagFilterField,
+    APISelectMultiple, BootstrapMixin, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
+    SlugField, StaticSelect2Multiple, TagFilterField,
 )
 )
 from .constants import *
 from .constants import *
 from .models import Secret, SecretRole, UserKey
 from .models import Secret, SecretRole, UserKey
@@ -55,7 +55,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm):
         }
         }
 
 
 
 
-class SecretRoleCSVForm(forms.ModelForm):
+class SecretRoleCSVForm(CSVModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:

+ 4 - 4
netbox/tenancy/forms.py

@@ -2,10 +2,10 @@ from django import forms
 from taggit.forms import TagField
 from taggit.forms import TagField
 
 
 from extras.forms import (
 from extras.forms import (
-    AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm,
+    AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelCSVForm,
 )
 )
 from utilities.forms import (
 from utilities.forms import (
-    APISelect, APISelectMultiple, BootstrapMixin, CommentField, DynamicModelChoiceField,
+    APISelect, APISelectMultiple, BootstrapMixin, CommentField, CSVModelForm, DynamicModelChoiceField,
     DynamicModelMultipleChoiceField, SlugField, TagFilterField,
     DynamicModelMultipleChoiceField, SlugField, TagFilterField,
 )
 )
 from .models import Tenant, TenantGroup
 from .models import Tenant, TenantGroup
@@ -32,7 +32,7 @@ class TenantGroupForm(BootstrapMixin, forms.ModelForm):
         ]
         ]
 
 
 
 
-class TenantGroupCSVForm(forms.ModelForm):
+class TenantGroupCSVForm(CSVModelForm):
     parent = forms.ModelChoiceField(
     parent = forms.ModelChoiceField(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
         required=False,
         required=False,
@@ -71,7 +71,7 @@ class TenantForm(BootstrapMixin, CustomFieldModelForm):
         )
         )
 
 
 
 
-class TenantCSVForm(CustomFieldModelForm):
+class TenantCSVForm(CustomFieldModelCSVForm):
     slug = SlugField()
     slug = SlugField()
     group = forms.ModelChoiceField(
     group = forms.ModelChoiceField(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),

+ 14 - 0
netbox/utilities/forms.py

@@ -712,6 +712,20 @@ class BulkEditForm(forms.Form):
             self.nullable_fields = self.Meta.nullable_fields
             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):
 class ImportForm(BootstrapMixin, forms.Form):
     """
     """
     Generic form for creating an object from JSON/YAML data
     Generic form for creating an object from JSON/YAML data

+ 1 - 6
netbox/utilities/views.py

@@ -593,12 +593,7 @@ class BulkImportView(GetReturnURLMixin, View):
                 with transaction.atomic():
                 with transaction.atomic():
                     headers, records = form.cleaned_data['csv']
                     headers, records = form.cleaned_data['csv']
                     for row, data in enumerate(records, start=1):
                     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():
                         if obj_form.is_valid():
                             obj = self._save_obj(obj_form, request)
                             obj = self._save_obj(obj_form, request)

+ 5 - 5
netbox/virtualization/forms.py

@@ -14,9 +14,9 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
     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 .choices import *
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 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()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
@@ -58,7 +58,7 @@ class ClusterGroupForm(BootstrapMixin, forms.ModelForm):
         ]
         ]
 
 
 
 
-class ClusterGroupCSVForm(forms.ModelForm):
+class ClusterGroupCSVForm(CSVModelForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta: