فهرست منبع

Use DynamicModelChoiceField for all fields using APISelect

Jeremy Stretch 6 سال پیش
والد
کامیت
7177fcfa61
8فایلهای تغییر یافته به همراه363 افزوده شده و 246 حذف شده
  1. 17 10
      netbox/circuits/forms.py
  2. 139 105
      netbox/dcim/forms.py
  3. 52 29
      netbox/extras/forms.py
  4. 66 39
      netbox/ipam/forms.py
  5. 9 8
      netbox/secrets/forms.py
  6. 10 8
      netbox/tenancy/forms.py
  7. 12 8
      netbox/utilities/forms.py
  8. 58 39
      netbox/virtualization/forms.py

+ 17 - 10
netbox/circuits/forms.py

@@ -9,7 +9,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant
 from utilities.forms import (
     APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker,
-    DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
+    DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
+    StaticSelect2Multiple, TagFilterField,
 )
 from .choices import CircuitStatusChoices
 from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -165,6 +166,18 @@ class CircuitTypeCSVForm(forms.ModelForm):
 #
 
 class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
+    provider = DynamicModelChoiceField(
+        queryset=Provider.objects.all(),
+        widget=APISelect(
+            api_url="/api/circuits/providers/"
+        )
+    )
+    type = DynamicModelChoiceField(
+        queryset=CircuitType.objects.all(),
+        widget=APISelect(
+            api_url="/api/circuits/circuit-types/"
+        )
+    )
     comments = CommentField()
     tags = TagField(
         required=False
@@ -181,12 +194,6 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             'commit_rate': "Committed rate",
         }
         widgets = {
-            'provider': APISelect(
-                api_url="/api/circuits/providers/"
-            ),
-            'type': APISelect(
-                api_url="/api/circuits/circuit-types/"
-            ),
             'status': StaticSelect2(),
             'install_date': DatePicker(),
         }
@@ -236,14 +243,14 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
         queryset=Circuit.objects.all(),
         widget=forms.MultipleHiddenInput
     )
-    type = forms.ModelChoiceField(
+    type = DynamicModelChoiceField(
         queryset=CircuitType.objects.all(),
         required=False,
         widget=APISelect(
             api_url="/api/circuits/circuit-types/"
         )
     )
-    provider = forms.ModelChoiceField(
+    provider = DynamicModelChoiceField(
         queryset=Provider.objects.all(),
         required=False,
         widget=APISelect(
@@ -256,7 +263,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
         initial='',
         widget=StaticSelect2()
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(

+ 139 - 105
netbox/dcim/forms.py

@@ -325,7 +325,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         required=False,
         widget=StaticSelect2()
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(
@@ -383,18 +383,20 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
 #
 
 class RackGroupForm(BootstrapMixin, forms.ModelForm):
+    site = DynamicModelChoiceField(
+        queryset=Site.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/sites/"
+        )
+    )
     slug = SlugField()
 
     class Meta:
         model = RackGroup
-        fields = [
+        fields = (
             'site', 'name', 'slug',
-        ]
-        widgets = {
-            'site': APISelect(
-                api_url="/api/dcim/sites/"
-            )
-        }
+        )
 
 
 class RackGroupCSVForm(forms.ModelForm):
@@ -471,6 +473,15 @@ class RackRoleCSVForm(forms.ModelForm):
 #
 
 class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
+    site = DynamicModelChoiceField(
+        queryset=Site.objects.all(),
+        widget=APISelect(
+            api_url="/api/dcim/sites/",
+            filter_for={
+                'group': 'site_id',
+            }
+        )
+    )
     group = DynamicModelChoiceField(
         queryset=RackGroup.objects.all(),
         required=False,
@@ -478,6 +489,13 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             api_url='/api/dcim/rack-groups/',
         )
     )
+    role = DynamicModelChoiceField(
+        queryset=RackRole.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url='/api/dcim/rack-roles/',
+        )
+    )
     comments = CommentField()
     tags = TagField(
         required=False
@@ -496,16 +514,7 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             'u_height': "Height in rack units",
         }
         widgets = {
-            'site': APISelect(
-                api_url="/api/dcim/sites/",
-                filter_for={
-                    'group': 'site_id',
-                }
-            ),
             'status': StaticSelect2(),
-            'role': APISelect(
-                api_url="/api/dcim/rack-roles/"
-            ),
             'type': StaticSelect2(),
             'width': StaticSelect2(),
             'outer_unit': StaticSelect2(),
@@ -605,7 +614,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         queryset=Rack.objects.all(),
         widget=forms.MultipleHiddenInput
     )
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
         widget=APISelect(
@@ -615,14 +624,14 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
             }
         )
     )
-    group = forms.ModelChoiceField(
+    group = DynamicModelChoiceField(
         queryset=RackGroup.objects.all(),
         required=False,
         widget=APISelect(
             api_url="/api/dcim/rack-groups",
         )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(
@@ -635,7 +644,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         initial='',
         widget=StaticSelect2()
     )
-    role = forms.ModelChoiceField(
+    role = DynamicModelChoiceField(
         queryset=RackRole.objects.all(),
         required=False,
         widget=APISelect(
@@ -833,7 +842,7 @@ class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
         required=False,
         widget=StaticSelect2()
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(
@@ -905,6 +914,12 @@ class ManufacturerCSVForm(forms.ModelForm):
 #
 
 class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
+    manufacturer = DynamicModelChoiceField(
+        queryset=Manufacturer.objects.all(),
+        widget=APISelect(
+            api_url="/api/dcim/manufacturers/",
+        )
+    )
     slug = SlugField(
         slug_source='model'
     )
@@ -920,9 +935,6 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
             'tags',
         ]
         widgets = {
-            'manufacturer': APISelect(
-                api_url="/api/dcim/manufacturers/"
-            ),
             'subdevice_role': StaticSelect2()
         }
 
@@ -945,11 +957,11 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkE
         queryset=DeviceType.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    manufacturer = forms.ModelChoiceField(
+    manufacturer = DynamicModelChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
         widget=APISelect(
-            api_url="/api/dcim/manufactureres"
+            api_url="/api/dcim/manufacturers"
         )
     )
     u_height = forms.IntegerField(
@@ -1048,7 +1060,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
 
 
 class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
-    device_type = forms.ModelChoiceField(
+    device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         widget=APISelect(
             api_url='/api/dcim/device-types/'
@@ -1091,7 +1103,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
 
 
 class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form):
-    device_type = forms.ModelChoiceField(
+    device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         widget=APISelect(
             api_url='/api/dcim/device-types/'
@@ -1134,7 +1146,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
 
 
 class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form):
-    device_type = forms.ModelChoiceField(
+    device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         widget=APISelect(
             api_url='/api/dcim/device-types/'
@@ -1207,7 +1219,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
 
 
 class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form):
-    device_type = forms.ModelChoiceField(
+    device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         widget=APISelect(
             api_url='/api/dcim/device-types/'
@@ -1276,7 +1288,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
 
 
 class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form):
-    device_type = forms.ModelChoiceField(
+    device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         widget=APISelect(
             api_url='/api/dcim/device-types/'
@@ -1339,7 +1351,7 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
 
 
 class FrontPortTemplateCreateForm(BootstrapMixin, forms.Form):
-    device_type = forms.ModelChoiceField(
+    device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         widget=APISelect(
             api_url='/api/dcim/device-types/'
@@ -1433,7 +1445,7 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
 
 
 class RearPortTemplateCreateForm(BootstrapMixin, forms.Form):
-    device_type = forms.ModelChoiceField(
+    device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         widget=APISelect(
             api_url='/api/dcim/device-types/'
@@ -1482,7 +1494,7 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
 
 
 class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form):
-    device_type = forms.ModelChoiceField(
+    device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         widget=APISelect(
             api_url='/api/dcim/device-types/'
@@ -1653,6 +1665,13 @@ class DeviceRoleCSVForm(forms.ModelForm):
 #
 
 class PlatformForm(BootstrapMixin, forms.ModelForm):
+    manufacturer = DynamicModelChoiceField(
+        queryset=Manufacturer.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/manufacturers/",
+        )
+    )
     slug = SlugField(
         max_length=64
     )
@@ -1663,9 +1682,6 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
             'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args',
         ]
         widgets = {
-            'manufacturer': APISelect(
-                api_url="/api/dcim/manufacturers/"
-            ),
             'napalm_args': SmallTextarea(),
         }
 
@@ -1695,7 +1711,7 @@ class PlatformCSVForm(forms.ModelForm):
 #
 
 class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         widget=APISelect(
             api_url="/api/dcim/sites/",
@@ -1721,7 +1737,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             disabled_indicator='device'
         )
     )
-    manufacturer = forms.ModelChoiceField(
+    manufacturer = DynamicModelChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
         widget=APISelect(
@@ -1734,13 +1750,28 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     )
     device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
-        label='Device type',
         widget=APISelect(
             api_url='/api/dcim/device-types/',
             display_field='model'
         )
     )
-    cluster_group = forms.ModelChoiceField(
+    device_role = DynamicModelChoiceField(
+        queryset=DeviceRole.objects.all(),
+        widget=APISelect(
+            api_url='/api/dcim/device-roles/'
+        )
+    )
+    platform = DynamicModelChoiceField(
+        queryset=Platform.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/platforms/",
+            additional_query_params={
+                "manufacturer_id": "null"
+            }
+        )
+    )
+    cluster_group = DynamicModelChoiceField(
         queryset=ClusterGroup.objects.all(),
         required=False,
         widget=APISelect(
@@ -1786,16 +1817,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
                     'position': 'face'
                 }
             ),
-            'device_role': APISelect(
-                api_url='/api/dcim/device-roles/'
-            ),
             'status': StaticSelect2(),
-            'platform': APISelect(
-                api_url="/api/dcim/platforms/",
-                additional_query_params={
-                    "manufacturer_id": "null"
-                }
-            ),
             'primary_ip4': StaticSelect2(),
             'primary_ip6': StaticSelect2(),
         }
@@ -2069,31 +2091,29 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         queryset=Device.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    device_type = forms.ModelChoiceField(
+    device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
         required=False,
-        label='Type',
         widget=APISelect(
             api_url="/api/dcim/device-types/",
             display_field='display_name'
         )
     )
-    device_role = forms.ModelChoiceField(
+    device_role = DynamicModelChoiceField(
         queryset=DeviceRole.objects.all(),
         required=False,
-        label='Role',
         widget=APISelect(
             api_url="/api/dcim/device-roles/"
         )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(
             api_url="/api/tenancy/tenants/"
         )
     )
-    platform = forms.ModelChoiceField(
+    platform = DynamicModelChoiceField(
         queryset=Platform.objects.all(),
         required=False,
         widget=APISelect(
@@ -2103,7 +2123,6 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
     status = forms.ChoiceField(
         choices=add_blank_choice(DeviceStatusChoices),
         required=False,
-        initial='',
         widget=StaticSelect2()
     )
     serial = forms.CharField(
@@ -2345,7 +2364,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
 
 
 class ConsolePortCreateForm(BootstrapMixin, forms.Form):
-    device = forms.ModelChoiceField(
+    device = DynamicModelChoiceField(
         queryset=Device.objects.prefetch_related('device_type__manufacturer'),
         widget=APISelect(
             api_url="/api/dcim/devices/",
@@ -2430,7 +2449,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
 
 
 class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
-    device = forms.ModelChoiceField(
+    device = DynamicModelChoiceField(
         queryset=Device.objects.prefetch_related('device_type__manufacturer'),
         widget=APISelect(
             api_url="/api/dcim/devices/",
@@ -2529,7 +2548,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
 
 
 class PowerPortCreateForm(BootstrapMixin, forms.Form):
-    device = forms.ModelChoiceField(
+    device = DynamicModelChoiceField(
         queryset=Device.objects.prefetch_related('device_type__manufacturer'),
         widget=APISelect(
             api_url="/api/dcim/devices/",
@@ -2647,7 +2666,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
 
 
 class PowerOutletCreateForm(BootstrapMixin, forms.Form):
-    device = forms.ModelChoiceField(
+    device = DynamicModelChoiceField(
         queryset=Device.objects.prefetch_related('device_type__manufacturer'),
         widget=APISelect(
             api_url="/api/dcim/devices/",
@@ -2806,25 +2825,26 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
 
 
 class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
-    untagged_vlan = forms.ModelChoiceField(
+    untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
+        label='Untagged VLAN',
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             full=True
         )
     )
-    tagged_vlans = forms.ModelMultipleChoiceField(
+    tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
+        label='Tagged VLANs',
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             full=True
         )
     )
-
     tags = TagField(
         required=False
     )
@@ -2866,7 +2886,7 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
 
 
 class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
-    device = forms.ModelChoiceField(
+    device = DynamicModelChoiceField(
         queryset=Device.objects.prefetch_related('device_type__manufacturer'),
         widget=APISelect(
             api_url="/api/dcim/devices/",
@@ -2916,7 +2936,7 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
     tags = TagField(
         required=False
     )
-    untagged_vlan = forms.ModelChoiceField(
+    untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelect(
@@ -2925,7 +2945,7 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
             full=True
         )
     )
-    tagged_vlans = forms.ModelMultipleChoiceField(
+    tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelectMultiple(
@@ -3064,7 +3084,7 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         required=False,
         widget=StaticSelect2()
     )
-    untagged_vlan = forms.ModelChoiceField(
+    untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelect(
@@ -3073,7 +3093,7 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
             full=True
         )
     )
-    tagged_vlans = forms.ModelMultipleChoiceField(
+    tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelectMultiple(
@@ -3166,7 +3186,7 @@ class FrontPortForm(BootstrapMixin, forms.ModelForm):
 
 # TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
 class FrontPortCreateForm(BootstrapMixin, forms.Form):
-    device = forms.ModelChoiceField(
+    device = DynamicModelChoiceField(
         queryset=Device.objects.prefetch_related('device_type__manufacturer'),
         widget=APISelect(
             api_url="/api/dcim/devices/",
@@ -3344,7 +3364,7 @@ class RearPortForm(BootstrapMixin, forms.ModelForm):
 
 
 class RearPortCreateForm(BootstrapMixin, forms.Form):
-    device = forms.ModelChoiceField(
+    device = DynamicModelChoiceField(
         queryset=Device.objects.prefetch_related('device_type__manufacturer'),
         widget=APISelect(
             api_url="/api/dcim/devices/",
@@ -3429,7 +3449,7 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm):
     """
     Base form for connecting a Cable to a Device component
     """
-    termination_b_site = forms.ModelChoiceField(
+    termination_b_site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         label='Site',
         required=False,
@@ -3555,7 +3575,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
 
 
 class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
-    termination_b_provider = forms.ModelChoiceField(
+    termination_b_provider = DynamicModelChoiceField(
         queryset=Provider.objects.all(),
         label='Provider',
         required=False,
@@ -3606,7 +3626,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
 
 
 class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm):
-    termination_b_site = forms.ModelChoiceField(
+    termination_b_site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         label='Site',
         required=False,
@@ -3943,7 +3963,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
 
 
 class DeviceBayCreateForm(BootstrapMixin, forms.Form):
-    device = forms.ModelChoiceField(
+    device = DynamicModelChoiceField(
         queryset=Device.objects.prefetch_related('device_type__manufacturer'),
         widget=APISelect(
             api_url="/api/dcim/devices/",
@@ -4113,6 +4133,19 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
 #
 
 class InventoryItemForm(BootstrapMixin, forms.ModelForm):
+    device = DynamicModelChoiceField(
+        queryset=Device.objects.prefetch_related('device_type__manufacturer'),
+        widget=APISelect(
+            api_url="/api/dcim/devices/"
+        )
+    )
+    manufacturer = DynamicModelChoiceField(
+        queryset=Manufacturer.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/manufacturers/"
+        )
+    )
     tags = TagField(
         required=False
     )
@@ -4122,18 +4155,10 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
         fields = [
             'name', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
         ]
-        widgets = {
-            'device': APISelect(
-                api_url="/api/dcim/devices/"
-            ),
-            'manufacturer': APISelect(
-                api_url="/api/dcim/manufacturers/"
-            )
-        }
 
 
 class InventoryItemCreateForm(BootstrapMixin, forms.Form):
-    device = forms.ModelChoiceField(
+    device = DynamicModelChoiceField(
         queryset=Device.objects.prefetch_related('device_type__manufacturer'),
         widget=APISelect(
             api_url="/api/dcim/devices/",
@@ -4142,7 +4167,7 @@ class InventoryItemCreateForm(BootstrapMixin, forms.Form):
     name_pattern = ExpandableNameField(
         label='Name'
     )
-    manufacturer = forms.ModelChoiceField(
+    manufacturer = DynamicModelChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
         widget=APISelect(
@@ -4197,14 +4222,14 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
         queryset=InventoryItem.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    device = forms.ModelChoiceField(
+    device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         required=False,
         widget=APISelect(
             api_url="/api/dcim/devices/"
         )
     )
-    manufacturer = forms.ModelChoiceField(
+    manufacturer = DynamicModelChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
         widget=APISelect(
@@ -4364,7 +4389,7 @@ class DeviceVCMembershipForm(forms.ModelForm):
 
 
 class VCMemberSelectForm(BootstrapMixin, forms.Form):
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
         widget=APISelect(
@@ -4466,6 +4491,16 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
 #
 
 class PowerPanelForm(BootstrapMixin, forms.ModelForm):
+    site = DynamicModelChoiceField(
+        queryset=Site.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/sites/",
+            filter_for={
+                'rack_group': 'site_id',
+            }
+        )
+    )
     rack_group = DynamicModelChoiceField(
         queryset=RackGroup.objects.all(),
         required=False,
@@ -4479,14 +4514,6 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm):
         fields = [
             'site', 'rack_group', 'name',
         ]
-        widgets = {
-            'site': APISelect(
-                api_url="/api/dcim/sites/",
-                filter_for={
-                    'rack_group': 'site_id',
-                }
-            ),
-        }
 
 
 class PowerPanelCSVForm(forms.ModelForm):
@@ -4581,6 +4608,19 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
             }
         )
     )
+    power_panel = DynamicModelChoiceField(
+        queryset=PowerPanel.objects.all(),
+        widget=APISelect(
+            api_url="/api/dcim/power-panels/"
+        )
+    )
+    rack = DynamicModelChoiceField(
+        queryset=Rack.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/racks/"
+        )
+    )
     comments = CommentField()
     tags = TagField(
         required=False
@@ -4593,12 +4633,6 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
             'max_utilization', 'comments', 'tags',
         ]
         widgets = {
-            'power_panel': APISelect(
-                api_url="/api/dcim/power-panels/"
-            ),
-            'rack': APISelect(
-                api_url="/api/dcim/racks/"
-            ),
             'status': StaticSelect2(),
             'type': StaticSelect2(),
             'supply': StaticSelect2(),
@@ -4697,7 +4731,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         queryset=PowerFeed.objects.all(),
         widget=forms.MultipleHiddenInput
     )
-    power_panel = forms.ModelChoiceField(
+    power_panel = DynamicModelChoiceField(
         queryset=PowerPanel.objects.all(),
         required=False,
         widget=APISelect(
@@ -4707,7 +4741,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
             }
         )
     )
-    rack = forms.ModelChoiceField(
+    rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         required=False,
         widget=APISelect(

+ 52 - 29
netbox/extras/forms.py

@@ -196,7 +196,56 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
         required=False,
         widget=StaticSelect2Multiple()
     )
-    tags = forms.ModelMultipleChoiceField(
+    sites = DynamicModelMultipleChoiceField(
+        queryset=Site.objects.all(),
+        required=False,
+        widget=APISelectMultiple(
+            api_url="/api/dcim/sites/"
+        )
+    )
+    roles = DynamicModelMultipleChoiceField(
+        queryset=DeviceRole.objects.all(),
+        required=False,
+        widget=APISelectMultiple(
+            api_url="/api/dcim/device-roles/"
+        )
+    )
+    platforms = DynamicModelMultipleChoiceField(
+        queryset=Platform.objects.all(),
+        required=False,
+        widget=APISelectMultiple(
+            api_url="/api/dcim/platforms/"
+        )
+    )
+    cluster_groups = DynamicModelMultipleChoiceField(
+        queryset=ClusterGroup.objects.all(),
+        required=False,
+        widget=APISelectMultiple(
+            api_url="/api/virtualization/cluster-groups/"
+        )
+    )
+    clusters = DynamicModelMultipleChoiceField(
+        queryset=Cluster.objects.all(),
+        required=False,
+        widget=APISelectMultiple(
+            api_url="/api/virtualization/clusters/"
+        )
+    )
+    tenant_groups = DynamicModelMultipleChoiceField(
+        queryset=TenantGroup.objects.all(),
+        required=False,
+        widget=APISelectMultiple(
+            api_url="/api/tenancy/tenant-groups/"
+        )
+    )
+    tenants = DynamicModelMultipleChoiceField(
+        queryset=Tenant.objects.all(),
+        required=False,
+        widget=APISelectMultiple(
+            api_url="/api/tenancy/tenants/"
+        )
+    )
+    tags = DynamicModelMultipleChoiceField(
         queryset=Tag.objects.all(),
         to_field_name='slug',
         required=False,
@@ -210,36 +259,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
 
     class Meta:
         model = ConfigContext
-        fields = [
+        fields = (
             'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
             'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
-        ]
-        widgets = {
-            'regions': APISelectMultiple(
-                api_url="/api/dcim/regions/"
-            ),
-            'sites': APISelectMultiple(
-                api_url="/api/dcim/sites/"
-            ),
-            'roles': APISelectMultiple(
-                api_url="/api/dcim/device-roles/"
-            ),
-            'platforms': APISelectMultiple(
-                api_url="/api/dcim/platforms/"
-            ),
-            'cluster_groups': APISelectMultiple(
-                api_url="/api/virtualization/cluster-groups/"
-            ),
-            'clusters': APISelectMultiple(
-                api_url="/api/virtualization/clusters/"
-            ),
-            'tenant_groups': APISelectMultiple(
-                api_url="/api/tenancy/tenant-groups/"
-            ),
-            'tenants': APISelectMultiple(
-                api_url="/api/tenancy/tenants/"
-            ),
-        }
+        )
 
 
 class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):

+ 66 - 39
netbox/ipam/forms.py

@@ -76,7 +76,7 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm
         queryset=VRF.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(
@@ -149,6 +149,12 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
 #
 
 class AggregateForm(BootstrapMixin, CustomFieldModelForm):
+    rir = DynamicModelChoiceField(
+        queryset=RIR.objects.all(),
+        widget=APISelect(
+            api_url="/api/ipam/rirs/"
+        )
+    )
     tags = TagField(
         required=False
     )
@@ -163,9 +169,6 @@ class AggregateForm(BootstrapMixin, CustomFieldModelForm):
             'rir': "Regional Internet Registry responsible for this prefix",
         }
         widgets = {
-            'rir': APISelect(
-                api_url="/api/ipam/rirs/"
-            ),
             'date_added': DatePicker(),
         }
 
@@ -190,7 +193,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         queryset=Aggregate.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    rir = forms.ModelChoiceField(
+    rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
         required=False,
         label='RIR',
@@ -270,7 +273,14 @@ class RoleCSVForm(forms.ModelForm):
 #
 
 class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
-    site = forms.ModelChoiceField(
+    vrf = DynamicModelChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/ipam/vrfs/",
+        )
+    )
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
         widget=APISelect(
@@ -307,6 +317,13 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             display_field='display_name'
         )
     )
+    role = DynamicModelChoiceField(
+        queryset=Role.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/ipam/roles/"
+        )
+    )
     tags = TagField(required=False)
 
     class Meta:
@@ -316,13 +333,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             'tags',
         ]
         widgets = {
-            'vrf': APISelect(
-                api_url="/api/ipam/vrfs/"
-            ),
             'status': StaticSelect2(),
-            'role': APISelect(
-                api_url="/api/ipam/roles/"
-            )
         }
 
     def __init__(self, *args, **kwargs):
@@ -433,14 +444,14 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         queryset=Prefix.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
         widget=APISelect(
             api_url="/api/dcim/sites/"
         )
     )
-    vrf = forms.ModelChoiceField(
+    vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
         label='VRF',
@@ -453,7 +464,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         max_value=PREFIX_LENGTH_MAX,
         required=False
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(
@@ -465,7 +476,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         required=False,
         widget=StaticSelect2()
     )
-    role = forms.ModelChoiceField(
+    role = DynamicModelChoiceField(
         queryset=Role.objects.all(),
         required=False,
         widget=APISelect(
@@ -588,7 +599,15 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
         queryset=Interface.objects.all(),
         required=False
     )
-    nat_site = forms.ModelChoiceField(
+    vrf = DynamicModelChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        label='VRF',
+        widget=APISelect(
+            api_url="/api/ipam/vrfs/"
+        )
+    )
+    nat_site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
         label='Site',
@@ -664,9 +683,6 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
         widgets = {
             'status': StaticSelect2(),
             'role': StaticSelect2(),
-            'vrf': APISelect(
-                api_url="/api/ipam/vrfs/"
-            )
         }
 
     def __init__(self, *args, **kwargs):
@@ -741,6 +757,14 @@ class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
 
 
 class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
+    vrf = DynamicModelChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        label='VRF',
+        widget=APISelect(
+            api_url="/api/ipam/vrfs/"
+        )
+    )
 
     class Meta:
         model = IPAddress
@@ -750,9 +774,6 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         widgets = {
             'status': StaticSelect2(),
             'role': StaticSelect2(),
-            'vrf': APISelect(
-                api_url="/api/ipam/vrfs/"
-            )
         }
 
     def __init__(self, *args, **kwargs):
@@ -888,7 +909,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         queryset=IPAddress.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    vrf = forms.ModelChoiceField(
+    vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
         label='VRF',
@@ -901,7 +922,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         max_value=IPADDRESS_MASK_LENGTH_MAX,
         required=False
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(
@@ -934,7 +955,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
 
 
 class IPAddressAssignForm(BootstrapMixin, forms.Form):
-    vrf_id = forms.ModelChoiceField(
+    vrf_id = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
         label='VRF',
@@ -1014,6 +1035,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
 #
 
 class VLANGroupForm(BootstrapMixin, forms.ModelForm):
+    site = DynamicModelChoiceField(
+        queryset=Site.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/sites/"
+        )
+    )
     slug = SlugField()
 
     class Meta:
@@ -1021,11 +1049,6 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
         fields = [
             'site', 'name', 'slug',
         ]
-        widgets = {
-            'site': APISelect(
-                api_url="/api/dcim/sites/"
-            )
-        }
 
 
 class VLANGroupCSVForm(forms.ModelForm):
@@ -1078,7 +1101,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
 #
 
 class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
         widget=APISelect(
@@ -1098,6 +1121,13 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             api_url='/api/ipam/vlan-groups/',
         )
     )
+    role = DynamicModelChoiceField(
+        queryset=Role.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/ipam/roles/"
+        )
+    )
     tags = TagField(required=False)
 
     class Meta:
@@ -1115,9 +1145,6 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         }
         widgets = {
             'status': StaticSelect2(),
-            'role': APISelect(
-                api_url="/api/ipam/roles/"
-            )
         }
 
 
@@ -1192,21 +1219,21 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         queryset=VLAN.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
         widget=APISelect(
             api_url="/api/dcim/sites/"
         )
     )
-    group = forms.ModelChoiceField(
+    group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         required=False,
         widget=APISelect(
             api_url="/api/ipam/vlan-groups/"
         )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(
@@ -1218,7 +1245,7 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         required=False,
         widget=StaticSelect2()
     )
-    role = forms.ModelChoiceField(
+    role = DynamicModelChoiceField(
         queryset=Role.objects.all(),
         required=False,
         widget=APISelect(

+ 9 - 8
netbox/secrets/forms.py

@@ -8,8 +8,8 @@ from extras.forms import (
     AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
 )
 from utilities.forms import (
-    APISelect, APISelectMultiple, BootstrapMixin, DynamicModelMultipleChoiceField, FlexibleModelChoiceField, SlugField,
-    StaticSelect2Multiple, TagFilterField
+    APISelect, APISelectMultiple, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
+    FlexibleModelChoiceField, SlugField, StaticSelect2Multiple, TagFilterField,
 )
 from .constants import *
 from .models import Secret, SecretRole, UserKey
@@ -87,6 +87,12 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
         label='Plaintext (verify)',
         widget=forms.PasswordInput()
     )
+    role = DynamicModelChoiceField(
+        queryset=SecretRole.objects.all(),
+        widget=APISelect(
+            api_url="/api/secrets/secret-roles/"
+        )
+    )
     tags = TagField(
         required=False
     )
@@ -96,11 +102,6 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
         fields = [
             'role', 'name', 'plaintext', 'plaintext2', 'tags',
         ]
-        widgets = {
-            'role': APISelect(
-                api_url="/api/secrets/secret-roles/"
-            )
-        }
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
@@ -157,7 +158,7 @@ class SecretBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         queryset=Secret.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    role = forms.ModelChoiceField(
+    role = DynamicModelChoiceField(
         queryset=SecretRole.objects.all(),
         required=False,
         widget=APISelect(

+ 10 - 8
netbox/tenancy/forms.py

@@ -42,6 +42,13 @@ class TenantGroupCSVForm(forms.ModelForm):
 
 class TenantForm(BootstrapMixin, CustomFieldModelForm):
     slug = SlugField()
+    group = DynamicModelChoiceField(
+        queryset=TenantGroup.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenant-groups/"
+        )
+    )
     comments = CommentField()
     tags = TagField(
         required=False
@@ -49,14 +56,9 @@ class TenantForm(BootstrapMixin, CustomFieldModelForm):
 
     class Meta:
         model = Tenant
-        fields = [
+        fields = (
             'name', 'slug', 'group', 'description', 'comments', 'tags',
-        ]
-        widgets = {
-            'group': APISelect(
-                api_url="/api/tenancy/tenant-groups/"
-            )
-        }
+        )
 
 
 class TenantCSVForm(CustomFieldModelForm):
@@ -85,7 +87,7 @@ class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         queryset=Tenant.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    group = forms.ModelChoiceField(
+    group = DynamicModelChoiceField(
         queryset=TenantGroup.objects.all(),
         required=False,
         widget=APISelect(

+ 12 - 8
netbox/utilities/forms.py

@@ -550,11 +550,7 @@ class TagFilterField(forms.MultipleChoiceField):
         super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs)
 
 
-class DynamicModelChoiceField(forms.ModelChoiceField):
-    """
-    Override get_bound_field() to avoid pre-populating field choices with a SQL query. The field will be
-    rendered only with choices set via bound data. Choices are populated on-demand via the APISelect widget.
-    """
+class DynamicModelChoiceMixin:
     field_modifier = ''
 
     def get_bound_field(self, form, field_name):
@@ -564,16 +560,24 @@ class DynamicModelChoiceField(forms.ModelChoiceField):
         # will be populated on-demand via the APISelect widget.
         field_name = '{}{}'.format(self.to_field_name or 'pk', self.field_modifier)
         if bound_field.data:
-            self.queryset = self.queryset.filter(**{field_name: bound_field.data})
+            self.queryset = self.queryset.filter(**{field_name: self.prepare_value(bound_field.data)})
         elif bound_field.initial:
-            self.queryset = self.queryset.filter(**{field_name: bound_field.initial})
+            self.queryset = self.queryset.filter(**{field_name: self.prepare_value(bound_field.initial)})
         else:
             self.queryset = self.queryset.none()
 
         return bound_field
 
 
-class DynamicModelMultipleChoiceField(DynamicModelChoiceField):
+class DynamicModelChoiceField(DynamicModelChoiceMixin, forms.ModelChoiceField):
+    """
+    Override get_bound_field() to avoid pre-populating field choices with a SQL query. The field will be
+    rendered only with choices set via bound data. Choices are populated on-demand via the APISelect widget.
+    """
+    pass
+
+
+class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultipleChoiceField):
     """
     A multiple-choice version of DynamicModelChoiceField.
     """

+ 58 - 39
netbox/virtualization/forms.py

@@ -76,6 +76,26 @@ class ClusterGroupCSVForm(forms.ModelForm):
 #
 
 class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
+    type = DynamicModelChoiceField(
+        queryset=ClusterType.objects.all(),
+        widget=APISelect(
+            api_url="/api/virtualization/cluster-types/"
+        )
+    )
+    group = DynamicModelChoiceField(
+        queryset=ClusterGroup.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/virtualization/cluster-groups/"
+        )
+    )
+    site = DynamicModelChoiceField(
+        queryset=Site.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/sites/"
+        )
+    )
     comments = CommentField()
     tags = TagField(
         required=False
@@ -83,20 +103,9 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 
     class Meta:
         model = Cluster
-        fields = [
+        fields = (
             'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
-        ]
-        widgets = {
-            'type': APISelect(
-                api_url="/api/virtualization/cluster-types/"
-            ),
-            'group': APISelect(
-                api_url="/api/virtualization/cluster-groups/"
-            ),
-            'site': APISelect(
-                api_url="/api/dcim/sites/"
-            ),
-        }
+        )
 
 
 class ClusterCSVForm(CustomFieldModelCSVForm):
@@ -146,25 +155,28 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
         queryset=Cluster.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    type = forms.ModelChoiceField(
+    type = DynamicModelChoiceField(
         queryset=ClusterType.objects.all(),
         required=False,
         widget=APISelect(
             api_url="/api/virtualization/cluster-types/"
         )
     )
-    group = forms.ModelChoiceField(
+    group = DynamicModelChoiceField(
         queryset=ClusterGroup.objects.all(),
         required=False,
         widget=APISelect(
             api_url="/api/virtualization/cluster-groups/"
         )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenants/"
+        )
     )
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
         widget=APISelect(
@@ -233,7 +245,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
 
 
 class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
-    region = forms.ModelChoiceField(
+    region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         required=False,
         widget=APISelect(
@@ -318,7 +330,7 @@ class ClusterRemoveDevicesForm(ConfirmationForm):
 #
 
 class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
-    cluster_group = forms.ModelChoiceField(
+    cluster_group = DynamicModelChoiceField(
         queryset=ClusterGroup.objects.all(),
         required=False,
         widget=APISelect(
@@ -337,6 +349,22 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             api_url='/api/virtualization/clusters/'
         )
     )
+    role = DynamicModelChoiceField(
+        queryset=DeviceRole.objects.all(),
+        widget=APISelect(
+            api_url="/api/dcim/device-roles/",
+            additional_query_params={
+                "vm_role": "True"
+            }
+        )
+    )
+    platform = DynamicModelChoiceField(
+        queryset=Platform.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url='/api/dcim/platforms/'
+        )
+    )
     tags = TagField(
         required=False
     )
@@ -357,17 +385,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         }
         widgets = {
             "status": StaticSelect2(),
-            "role": APISelect(
-                api_url="/api/dcim/device-roles/",
-                additional_query_params={
-                    "vm_role": "True"
-                }
-            ),
             'primary_ip4': StaticSelect2(),
             'primary_ip6': StaticSelect2(),
-            'platform': APISelect(
-                api_url='/api/dcim/platforms/'
-            )
         }
 
     def __init__(self, *args, **kwargs):
@@ -477,14 +496,14 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
         initial='',
         widget=StaticSelect2(),
     )
-    cluster = forms.ModelChoiceField(
+    cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
         required=False,
         widget=APISelect(
             api_url='/api/virtualization/clusters/'
         )
     )
-    role = forms.ModelChoiceField(
+    role = DynamicModelChoiceField(
         queryset=DeviceRole.objects.filter(
             vm_role=True
         ),
@@ -496,14 +515,14 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
             }
         )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(
             api_url='/api/tenancy/tenants/'
         )
     )
-    platform = forms.ModelChoiceField(
+    platform = DynamicModelChoiceField(
         queryset=Platform.objects.all(),
         required=False,
         widget=APISelect(
@@ -633,7 +652,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
 #
 
 class InterfaceForm(BootstrapMixin, forms.ModelForm):
-    untagged_vlan = forms.ModelChoiceField(
+    untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelect(
@@ -642,7 +661,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
             full=True
         )
     )
-    tagged_vlans = forms.ModelMultipleChoiceField(
+    tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelectMultiple(
@@ -759,7 +778,7 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
         required=False,
         widget=StaticSelect2(),
     )
-    untagged_vlan = forms.ModelChoiceField(
+    untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelect(
@@ -768,7 +787,7 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
             full=True
         )
     )
-    tagged_vlans = forms.ModelMultipleChoiceField(
+    tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelectMultiple(
@@ -847,7 +866,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
         required=False,
         widget=StaticSelect2()
     )
-    untagged_vlan = forms.ModelChoiceField(
+    untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelect(
@@ -856,7 +875,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
             full=True
         )
     )
-    tagged_vlans = forms.ModelMultipleChoiceField(
+    tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelectMultiple(