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

Merge pull request #4136 from netbox-community/3912-remove-chained-fields

Closes #3912: Remove chained form fields
Jeremy Stretch 6 лет назад
Родитель
Сommit
ec53e1c74c

+ 27 - 16
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,
-    FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField
+    DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
+    StaticSelect2Multiple, TagFilterField,
 )
 from .choices import CircuitStatusChoices
 from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -107,7 +108,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
         required=False,
         label='Search'
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         to_field_name='slug',
         required=False,
@@ -119,9 +120,10 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
             }
         )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             value_field="slug",
@@ -164,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
@@ -180,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(),
         }
@@ -235,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(
@@ -255,7 +263,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
         initial='',
         widget=StaticSelect2()
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         widget=APISelect(
@@ -290,17 +298,19 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
         required=False,
         label='Search'
     )
-    type = FilterChoiceField(
+    type = DynamicModelMultipleChoiceField(
         queryset=CircuitType.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/circuits/circuit-types/",
             value_field="slug",
         )
     )
-    provider = FilterChoiceField(
+    provider = DynamicModelMultipleChoiceField(
         queryset=Provider.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/circuits/providers/",
             value_field="slug",
@@ -311,7 +321,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
         required=False,
         widget=StaticSelect2Multiple()
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         to_field_name='slug',
         required=False,
@@ -323,9 +333,10 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
             }
         )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             value_field="slug",

Разница между файлами не показана из-за своего большого размера
+ 167 - 169
netbox/dcim/forms.py


+ 79 - 41
netbox/extras/forms.py

@@ -1,14 +1,15 @@
 from django import forms
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
+from mptt.forms import TreeNodeMultipleChoiceField
 from taggit.forms import TagField
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
-    CommentField, ContentTypeSelect, DateTimePicker, FilterChoiceField, JSONField, SlugField, StaticSelect2,
-    BOOLEAN_WITH_BLANK_CHOICES,
+    CommentField, ContentTypeSelect, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
+    StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
 )
 from virtualization.models import Cluster, ClusterGroup
 from .choices import *
@@ -190,7 +191,61 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
 #
 
 class ConfigContextForm(BootstrapMixin, forms.ModelForm):
-    tags = forms.ModelMultipleChoiceField(
+    regions = TreeNodeMultipleChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        widget=StaticSelect2Multiple()
+    )
+    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,
@@ -204,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):
@@ -265,72 +294,81 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
         required=False,
         label='Search'
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/dcim/regions/",
             value_field="slug",
         )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             value_field="slug",
         )
     )
-    role = FilterChoiceField(
+    role = DynamicModelMultipleChoiceField(
         queryset=DeviceRole.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/dcim/device-roles/",
             value_field="slug",
         )
     )
-    platform = FilterChoiceField(
+    platform = DynamicModelMultipleChoiceField(
         queryset=Platform.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/dcim/platforms/",
             value_field="slug",
         )
     )
-    cluster_group = FilterChoiceField(
+    cluster_group = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/virtualization/cluster-groups/",
             value_field="slug",
         )
     )
-    cluster_id = FilterChoiceField(
+    cluster_id = DynamicModelMultipleChoiceField(
         queryset=Cluster.objects.all(),
+        required=False,
         label='Cluster',
         widget=APISelectMultiple(
             api_url="/api/virtualization/clusters/",
         )
     )
-    tenant_group = FilterChoiceField(
+    tenant_group = DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/tenancy/tenant-groups/",
             value_field="slug",
         )
     )
-    tenant = FilterChoiceField(
+    tenant = DynamicModelMultipleChoiceField(
         queryset=Tenant.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/tenancy/tenants/",
             value_field="slug",
         )
     )
-    tag = FilterChoiceField(
+    tag = DynamicModelMultipleChoiceField(
         queryset=Tag.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/extras/tags/",
             value_field="slug",
@@ -390,7 +428,7 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
         required=False,
         widget=StaticSelect2()
     )
-    # TODO: Convert to FilterChoiceField once we have an API endpoint for users
+    # TODO: Convert to DynamicModelMultipleChoiceField once we have an API endpoint for users
     user = forms.ModelChoiceField(
         queryset=User.objects.order_by('username'),
         required=False,

+ 97 - 82
netbox/ipam/forms.py

@@ -10,9 +10,10 @@ from extras.forms import (
 from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant
 from utilities.forms import (
-    add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
-    CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm,
-    SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES
+    add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField,
+    DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField,
+    FlexibleModelChoiceField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
+    BOOLEAN_WITH_BLANK_CHOICES,
 )
 from virtualization.models import VirtualMachine
 from .constants import *
@@ -75,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(
@@ -148,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
     )
@@ -162,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(),
         }
 
@@ -189,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',
@@ -226,9 +230,10 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
         label='Address family',
         widget=StaticSelect2()
     )
-    rir = FilterChoiceField(
+    rir = DynamicModelMultipleChoiceField(
         queryset=RIR.objects.all(),
         to_field_name='slug',
+        required=False,
         label='RIR',
         widget=APISelectMultiple(
             api_url="/api/ipam/rirs/",
@@ -268,10 +273,16 @@ 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,
-        label='Site',
         widget=APISelect(
             api_url="/api/dcim/sites/",
             filter_for={
@@ -283,11 +294,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             }
         )
     )
-    vlan_group = ChainedModelChoiceField(
+    vlan_group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
-        chains=(
-            ('site', 'site'),
-        ),
         required=False,
         label='VLAN group',
         widget=APISelect(
@@ -300,12 +308,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             }
         )
     )
-    vlan = ChainedModelChoiceField(
+    vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
-        chains=(
-            ('site', 'site'),
-            ('group', 'vlan_group'),
-        ),
         required=False,
         label='VLAN',
         widget=APISelect(
@@ -313,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:
@@ -322,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):
@@ -439,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',
@@ -459,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(
@@ -471,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(
@@ -525,8 +530,9 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
         label='Mask length',
         widget=StaticSelect2()
     )
-    vrf_id = FilterChoiceField(
+    vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
+        required=False,
         label='VRF',
         widget=APISelectMultiple(
             api_url="/api/ipam/vrfs/",
@@ -538,7 +544,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
         required=False,
         widget=StaticSelect2Multiple()
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         to_field_name='slug',
         required=False,
@@ -550,18 +556,20 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
             }
         )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             value_field="slug",
             null_option=True,
         )
     )
-    role = FilterChoiceField(
+    role = DynamicModelMultipleChoiceField(
         queryset=Role.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/ipam/roles/",
             value_field="slug",
@@ -591,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',
@@ -603,11 +619,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
             }
         )
     )
-    nat_rack = ChainedModelChoiceField(
+    nat_rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
-        chains=(
-            ('site', 'nat_site'),
-        ),
         required=False,
         label='Rack',
         widget=APISelect(
@@ -621,12 +634,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
             }
         )
     )
-    nat_device = ChainedModelChoiceField(
+    nat_device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
-        chains=(
-            ('site', 'nat_site'),
-            ('rack', 'nat_rack'),
-        ),
         required=False,
         label='Device',
         widget=APISelect(
@@ -648,11 +657,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
             }
         )
     )
-    nat_inside = ChainedModelChoiceField(
+    nat_inside = DynamicModelChoiceField(
         queryset=IPAddress.objects.all(),
-        chains=(
-            ('interface__device', 'nat_device'),
-        ),
         required=False,
         label='IP Address',
         widget=APISelect(
@@ -677,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):
@@ -754,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
@@ -763,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):
@@ -901,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',
@@ -914,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(
@@ -947,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',
@@ -993,8 +1001,9 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
         label='Mask length',
         widget=StaticSelect2()
     )
-    vrf_id = FilterChoiceField(
+    vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
+        required=False,
         label='VRF',
         widget=APISelectMultiple(
             api_url="/api/ipam/vrfs/",
@@ -1026,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:
@@ -1033,11 +1049,6 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
         fields = [
             'site', 'name', 'slug',
         ]
-        widgets = {
-            'site': APISelect(
-                api_url="/api/dcim/sites/"
-            )
-        }
 
 
 class VLANGroupCSVForm(forms.ModelForm):
@@ -1061,7 +1072,7 @@ class VLANGroupCSVForm(forms.ModelForm):
 
 
 class VLANGroupFilterForm(BootstrapMixin, forms.Form):
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         to_field_name='slug',
         required=False,
@@ -1073,9 +1084,10 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
             }
         )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             value_field="slug",
@@ -1089,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(
@@ -1102,17 +1114,20 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             }
         )
     )
-    group = ChainedModelChoiceField(
+    group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
-        chains=(
-            ('site', 'site'),
-        ),
         required=False,
-        label='Group',
         widget=APISelect(
             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:
@@ -1130,9 +1145,6 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         }
         widgets = {
             'status': StaticSelect2(),
-            'role': APISelect(
-                api_url="/api/ipam/roles/"
-            )
         }
 
 
@@ -1207,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(
@@ -1233,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(
@@ -1258,7 +1270,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
         required=False,
         label='Search'
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         to_field_name='slug',
         required=False,
@@ -1271,17 +1283,19 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
             }
         )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             value_field="slug",
             null_option=True,
         )
     )
-    group_id = FilterChoiceField(
+    group_id = DynamicModelMultipleChoiceField(
         queryset=VLANGroup.objects.all(),
+        required=False,
         label='VLAN group',
         widget=APISelectMultiple(
             api_url="/api/ipam/vlan-groups/",
@@ -1293,9 +1307,10 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
         required=False,
         widget=StaticSelect2Multiple()
     )
-    role = FilterChoiceField(
+    role = DynamicModelMultipleChoiceField(
         queryset=Role.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/ipam/roles/",
             value_field="slug",

+ 11 - 9
netbox/secrets/forms.py

@@ -8,8 +8,8 @@ from extras.forms import (
     AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
 )
 from utilities.forms import (
-    APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, 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(
@@ -181,9 +182,10 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
         required=False,
         label='Search'
     )
-    role = FilterChoiceField(
+    role = DynamicModelMultipleChoiceField(
         queryset=SecretRole.objects.all(),
         to_field_name='slug',
+        required=True,
         widget=APISelectMultiple(
             api_url="/api/secrets/secret-roles/",
             value_field="slug",

+ 22 - 20
netbox/tenancy/forms.py

@@ -2,11 +2,11 @@ from django import forms
 from taggit.forms import TagField
 
 from extras.forms import (
-    AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm,
+    AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm,
 )
 from utilities.forms import (
-    APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField,
-    FilterChoiceField, SlugField, TagFilterField
+    APISelect, APISelectMultiple, BootstrapMixin, CommentField, DynamicModelChoiceField,
+    DynamicModelMultipleChoiceField, SlugField, TagFilterField,
 )
 from .models import Tenant, TenantGroup
 
@@ -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(
@@ -105,9 +107,10 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
         required=False,
         label='Search'
     )
-    group = FilterChoiceField(
+    group = DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/tenancy/tenant-groups/",
             value_field="slug",
@@ -121,8 +124,8 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
 # Form extensions
 #
 
-class TenancyForm(ChainedFieldsMixin, forms.Form):
-    tenant_group = forms.ModelChoiceField(
+class TenancyForm(forms.Form):
+    tenant_group = DynamicModelChoiceField(
         queryset=TenantGroup.objects.all(),
         required=False,
         widget=APISelect(
@@ -135,11 +138,8 @@ class TenancyForm(ChainedFieldsMixin, forms.Form):
             }
         )
     )
-    tenant = ChainedModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
-        chains=(
-            ('group', 'tenant_group'),
-        ),
         required=False,
         widget=APISelect(
             api_url='/api/tenancy/tenants/'
@@ -159,9 +159,10 @@ class TenancyForm(ChainedFieldsMixin, forms.Form):
 
 
 class TenancyFilterForm(forms.Form):
-    tenant_group = FilterChoiceField(
+    tenant_group = DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/tenancy/tenant-groups/",
             value_field="slug",
@@ -171,9 +172,10 @@ class TenancyFilterForm(forms.Form):
             }
         )
     )
-    tenant = FilterChoiceField(
+    tenant = DynamicModelMultipleChoiceField(
         queryset=Tenant.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url="/api/tenancy/tenants/",
             value_field="slug",

+ 21 - 80
netbox/utilities/forms.py

@@ -522,34 +522,6 @@ class FlexibleModelChoiceField(forms.ModelChoiceField):
         return value
 
 
-class ChainedModelChoiceField(forms.ModelChoiceField):
-    """
-    A ModelChoiceField which is initialized based on the values of other fields within a form. `chains` is a dictionary
-    mapping of model fields to peer fields within the form. For example:
-
-        country1 = forms.ModelChoiceField(queryset=Country.objects.all())
-        city1 = ChainedModelChoiceField(queryset=City.objects.all(), chains={'country': 'country1'}
-
-    The queryset of the `city1` field will be modified as
-
-        .filter(country=<value>)
-
-    where <value> is the value of the `country1` field. (Note: The form must inherit from ChainedFieldsMixin.)
-    """
-    def __init__(self, chains=None, *args, **kwargs):
-        self.chains = chains
-        super().__init__(*args, **kwargs)
-
-
-class ChainedModelMultipleChoiceField(forms.ModelMultipleChoiceField):
-    """
-    See ChainedModelChoiceField
-    """
-    def __init__(self, chains=None, *args, **kwargs):
-        self.chains = chains
-        super().__init__(*args, **kwargs)
-
-
 class SlugField(forms.SlugField):
     """
     Extend the built-in SlugField to automatically populate from a field called `name` unless otherwise specified.
@@ -578,31 +550,40 @@ class TagFilterField(forms.MultipleChoiceField):
         super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs)
 
 
-class FilterChoiceField(forms.ModelMultipleChoiceField):
-    """
-    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.
-    """
-    def __init__(self, *args, **kwargs):
-        # Filter fields are not required by default
-        if 'required' not in kwargs:
-            kwargs['required'] = False
-        super().__init__(*args, **kwargs)
+class DynamicModelChoiceMixin:
+    field_modifier = ''
 
     def get_bound_field(self, form, field_name):
         bound_field = BoundField(form, self, field_name)
 
         # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
         # 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:
-            kwargs = {'{}__in'.format(self.to_field_name or 'pk'): bound_field.data}
-            self.queryset = self.queryset.filter(**kwargs)
+            self.queryset = self.queryset.filter(**{field_name: self.prepare_value(bound_field.data)})
+        elif 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 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.
+    """
+    field_modifier = '__in'
+
+
 class LaxURLField(forms.URLField):
     """
     Modifies Django's built-in URLField in two ways:
@@ -655,46 +636,6 @@ class BootstrapMixin(forms.BaseForm):
                 field.widget.attrs['placeholder'] = field.label
 
 
-class ChainedFieldsMixin(forms.BaseForm):
-    """
-    Iterate through all ChainedModelChoiceFields in the form and modify their querysets based on chained fields.
-    """
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        for field_name, field in self.fields.items():
-
-            if isinstance(field, ChainedModelChoiceField):
-
-                filters_dict = {}
-                for (db_field, parent_field) in field.chains:
-                    if self.is_bound and parent_field in self.data and self.data[parent_field]:
-                        filters_dict[db_field] = self.data[parent_field] or None
-                    elif self.initial.get(parent_field):
-                        filters_dict[db_field] = self.initial[parent_field]
-                    elif self.fields[parent_field].widget.attrs.get('nullable'):
-                        filters_dict[db_field] = None
-                    else:
-                        break
-
-                # Limit field queryset by chained field values
-                if filters_dict:
-                    field.queryset = field.queryset.filter(**filters_dict)
-                # Editing an existing instance; limit field to its current value
-                elif not self.is_bound and getattr(self, 'instance', None) and hasattr(self.instance, field_name):
-                    obj = getattr(self.instance, field_name)
-                    if obj is not None:
-                        field.queryset = field.queryset.filter(pk=obj.pk)
-                    else:
-                        field.queryset = field.queryset.none()
-                # Creating a new instance with no bound data; nullify queryset
-                elif not self.data.get(field_name):
-                    field.queryset = field.queryset.none()
-                # Creating a new instance with bound data; limit queryset to the specified value
-                else:
-                    field.queryset = field.queryset.filter(pk=self.data.get(field_name))
-
-
 class ReturnURLForm(forms.Form):
     """
     Provides a hidden return URL field to control where the user is directed after the form is submitted.

+ 82 - 71
netbox/virtualization/forms.py

@@ -14,9 +14,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant
 from utilities.forms import (
     add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
-    ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ConfirmationForm,
-    CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea, StaticSelect2,
-    StaticSelect2Multiple, TagFilterField,
+    CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
+    ExpandableNameField, JSONField, SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField,
 )
 from .choices import *
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -77,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
@@ -84,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):
@@ -147,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(
@@ -189,7 +200,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
         'q', 'type', 'region', 'site', 'group', 'tenant_group', 'tenant'
     ]
     q = forms.CharField(required=False, label='Search')
-    type = FilterChoiceField(
+    type = DynamicModelMultipleChoiceField(
         queryset=ClusterType.objects.all(),
         to_field_name='slug',
         required=False,
@@ -198,7 +209,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
             value_field='slug',
         )
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         to_field_name='slug',
         required=False,
@@ -210,7 +221,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
             }
         )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         to_field_name='slug',
         required=False,
@@ -220,7 +231,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
             null_option=True,
         )
     )
-    group = FilterChoiceField(
+    group = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
         required=False,
@@ -233,8 +244,8 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
     tag = TagFilterField(model)
 
 
-class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
-    region = forms.ModelChoiceField(
+class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
+    region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         required=False,
         widget=APISelect(
@@ -247,11 +258,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
             }
         )
     )
-    site = ChainedModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
-        chains=(
-            ('region', 'region'),
-        ),
         required=False,
         widget=APISelect(
             api_url='/api/dcim/sites/',
@@ -261,11 +269,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
             }
         )
     )
-    rack = ChainedModelChoiceField(
+    rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
-        chains=(
-            ('site', 'site'),
-        ),
         required=False,
         widget=APISelect(
             api_url='/api/dcim/racks/',
@@ -277,12 +282,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
             }
         )
     )
-    devices = ChainedModelMultipleChoiceField(
+    devices = DynamicModelMultipleChoiceField(
         queryset=Device.objects.filter(cluster__isnull=True),
-        chains=(
-            ('site', 'site'),
-            ('rack', 'rack'),
-        ),
         widget=APISelectMultiple(
             api_url='/api/dcim/devices/',
             display_field='display_name',
@@ -329,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(
@@ -342,15 +343,28 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             }
         )
     )
-    cluster = ChainedModelChoiceField(
+    cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
-        chains=(
-            ('group', 'cluster_group'),
-        ),
         widget=APISelect(
             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
     )
@@ -371,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):
@@ -491,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
         ),
@@ -510,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(
@@ -557,32 +562,35 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
         required=False,
         label='Search'
     )
-    cluster_group = FilterChoiceField(
+    cluster_group = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url='/api/virtualization/cluster-groups/',
             value_field="slug",
             null_option=True,
         )
     )
-    cluster_type = FilterChoiceField(
+    cluster_type = DynamicModelMultipleChoiceField(
         queryset=ClusterType.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url='/api/virtualization/cluster-types/',
             value_field="slug",
             null_option=True,
         )
     )
-    cluster_id = FilterChoiceField(
+    cluster_id = DynamicModelMultipleChoiceField(
         queryset=Cluster.objects.all(),
+        required=False,
         label='Cluster',
         widget=APISelectMultiple(
             api_url='/api/virtualization/clusters/',
         )
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         to_field_name='slug',
         required=False,
@@ -594,18 +602,20 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
             }
         )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url='/api/dcim/sites/',
             value_field="slug",
             null_option=True,
         )
     )
-    role = FilterChoiceField(
+    role = DynamicModelMultipleChoiceField(
         queryset=DeviceRole.objects.filter(vm_role=True),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url='/api/dcim/device-roles/',
             value_field="slug",
@@ -620,9 +630,10 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
         required=False,
         widget=StaticSelect2Multiple()
     )
-    platform = FilterChoiceField(
+    platform = DynamicModelMultipleChoiceField(
         queryset=Platform.objects.all(),
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
             api_url='/api/dcim/platforms/',
             value_field="slug",
@@ -641,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(
@@ -650,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(
@@ -767,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(
@@ -776,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(
@@ -855,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(
@@ -864,7 +875,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
             full=True
         )
     )
-    tagged_vlans = forms.ModelMultipleChoiceField(
+    tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         widget=APISelectMultiple(

Некоторые файлы не были показаны из-за большого количества измененных файлов