Selaa lähdekoodia

Introduce CSVModelChoiceField to provide better validation for CSV model choices

Jeremy Stretch 5 vuotta sitten
vanhempi
commit
70d0a5f665

+ 6 - 6
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',

+ 53 - 53
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,

+ 23 - 22
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',

+ 4 - 4
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',

+ 4 - 4
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',

+ 14 - 1
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

+ 9 - 9
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',