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

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

Closes #3912: Remove chained form fields
Jeremy Stretch 6 лет назад
Родитель
Сommit
ec53e1c74c
8 измененных файлов с 506 добавлено и 488 удалено
  1. 27 16
      netbox/circuits/forms.py
  2. 167 169
      netbox/dcim/forms.py
  3. 79 41
      netbox/extras/forms.py
  4. 97 82
      netbox/ipam/forms.py
  5. 11 9
      netbox/secrets/forms.py
  6. 22 20
      netbox/tenancy/forms.py
  7. 21 80
      netbox/utilities/forms.py
  8. 82 71
      netbox/virtualization/forms.py

+ 27 - 16
netbox/circuits/forms.py

@@ -9,7 +9,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
     APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker,
     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 .choices import CircuitStatusChoices
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -107,7 +108,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -119,9 +120,10 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
             }
             }
         )
         )
     )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             api_url="/api/dcim/sites/",
             value_field="slug",
             value_field="slug",
@@ -164,6 +166,18 @@ class CircuitTypeCSVForm(forms.ModelForm):
 #
 #
 
 
 class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 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()
     comments = CommentField()
     tags = TagField(
     tags = TagField(
         required=False
         required=False
@@ -180,12 +194,6 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             'commit_rate': "Committed rate",
             'commit_rate': "Committed rate",
         }
         }
         widgets = {
         widgets = {
-            'provider': APISelect(
-                api_url="/api/circuits/providers/"
-            ),
-            'type': APISelect(
-                api_url="/api/circuits/circuit-types/"
-            ),
             'status': StaticSelect2(),
             'status': StaticSelect2(),
             'install_date': DatePicker(),
             'install_date': DatePicker(),
         }
         }
@@ -235,14 +243,14 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
         queryset=Circuit.objects.all(),
         queryset=Circuit.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
     )
     )
-    type = forms.ModelChoiceField(
+    type = DynamicModelChoiceField(
         queryset=CircuitType.objects.all(),
         queryset=CircuitType.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url="/api/circuits/circuit-types/"
             api_url="/api/circuits/circuit-types/"
         )
         )
     )
     )
-    provider = forms.ModelChoiceField(
+    provider = DynamicModelChoiceField(
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -255,7 +263,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -290,17 +298,19 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
-    type = FilterChoiceField(
+    type = DynamicModelMultipleChoiceField(
         queryset=CircuitType.objects.all(),
         queryset=CircuitType.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/circuits/circuit-types/",
             api_url="/api/circuits/circuit-types/",
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
-    provider = FilterChoiceField(
+    provider = DynamicModelMultipleChoiceField(
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/circuits/providers/",
             api_url="/api/circuits/providers/",
             value_field="slug",
             value_field="slug",
@@ -311,7 +321,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -323,9 +333,10 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
             }
             }
         )
         )
     )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             api_url="/api/dcim/sites/",
             value_field="slug",
             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 import forms
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from mptt.forms import TreeNodeMultipleChoiceField
 from taggit.forms import TagField
 from taggit.forms import TagField
 
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
-    CommentField, ContentTypeSelect, DateTimePicker, 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 virtualization.models import Cluster, ClusterGroup
 from .choices import *
 from .choices import *
@@ -190,7 +191,61 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
 #
 #
 
 
 class ConfigContextForm(BootstrapMixin, forms.ModelForm):
 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(),
         queryset=Tag.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -204,36 +259,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
 
 
     class Meta:
     class Meta:
         model = ConfigContext
         model = ConfigContext
-        fields = [
+        fields = (
             'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
             'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
             'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
             '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):
 class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
@@ -265,72 +294,81 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/dcim/regions/",
             api_url="/api/dcim/regions/",
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             api_url="/api/dcim/sites/",
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
-    role = FilterChoiceField(
+    role = DynamicModelMultipleChoiceField(
         queryset=DeviceRole.objects.all(),
         queryset=DeviceRole.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/dcim/device-roles/",
             api_url="/api/dcim/device-roles/",
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
-    platform = FilterChoiceField(
+    platform = DynamicModelMultipleChoiceField(
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/dcim/platforms/",
             api_url="/api/dcim/platforms/",
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
-    cluster_group = FilterChoiceField(
+    cluster_group = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/virtualization/cluster-groups/",
             api_url="/api/virtualization/cluster-groups/",
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
-    cluster_id = FilterChoiceField(
+    cluster_id = DynamicModelMultipleChoiceField(
         queryset=Cluster.objects.all(),
         queryset=Cluster.objects.all(),
+        required=False,
         label='Cluster',
         label='Cluster',
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/virtualization/clusters/",
             api_url="/api/virtualization/clusters/",
         )
         )
     )
     )
-    tenant_group = FilterChoiceField(
+    tenant_group = DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/tenancy/tenant-groups/",
             api_url="/api/tenancy/tenant-groups/",
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
-    tenant = FilterChoiceField(
+    tenant = DynamicModelMultipleChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/tenancy/tenants/",
             api_url="/api/tenancy/tenants/",
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
-    tag = FilterChoiceField(
+    tag = DynamicModelMultipleChoiceField(
         queryset=Tag.objects.all(),
         queryset=Tag.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/extras/tags/",
             api_url="/api/extras/tags/",
             value_field="slug",
             value_field="slug",
@@ -390,7 +428,7 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
         required=False,
         required=False,
         widget=StaticSelect2()
         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(
     user = forms.ModelChoiceField(
         queryset=User.objects.order_by('username'),
         queryset=User.objects.order_by('username'),
         required=False,
         required=False,

+ 97 - 82
netbox/ipam/forms.py

@@ -10,9 +10,10 @@ from extras.forms import (
 from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
-    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 virtualization.models import VirtualMachine
 from .constants import *
 from .constants import *
@@ -75,7 +76,7 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -148,6 +149,12 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
 #
 #
 
 
 class AggregateForm(BootstrapMixin, CustomFieldModelForm):
 class AggregateForm(BootstrapMixin, CustomFieldModelForm):
+    rir = DynamicModelChoiceField(
+        queryset=RIR.objects.all(),
+        widget=APISelect(
+            api_url="/api/ipam/rirs/"
+        )
+    )
     tags = TagField(
     tags = TagField(
         required=False
         required=False
     )
     )
@@ -162,9 +169,6 @@ class AggregateForm(BootstrapMixin, CustomFieldModelForm):
             'rir': "Regional Internet Registry responsible for this prefix",
             'rir': "Regional Internet Registry responsible for this prefix",
         }
         }
         widgets = {
         widgets = {
-            'rir': APISelect(
-                api_url="/api/ipam/rirs/"
-            ),
             'date_added': DatePicker(),
             'date_added': DatePicker(),
         }
         }
 
 
@@ -189,7 +193,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         queryset=Aggregate.objects.all(),
         queryset=Aggregate.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
-    rir = forms.ModelChoiceField(
+    rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
         required=False,
         required=False,
         label='RIR',
         label='RIR',
@@ -226,9 +230,10 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
         label='Address family',
         label='Address family',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
-    rir = FilterChoiceField(
+    rir = DynamicModelMultipleChoiceField(
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         label='RIR',
         label='RIR',
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/rirs/",
             api_url="/api/ipam/rirs/",
@@ -268,10 +273,16 @@ class RoleCSVForm(forms.ModelForm):
 #
 #
 
 
 class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 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(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
-        label='Site',
         widget=APISelect(
         widget=APISelect(
             api_url="/api/dcim/sites/",
             api_url="/api/dcim/sites/",
             filter_for={
             filter_for={
@@ -283,11 +294,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             }
             }
         )
         )
     )
     )
-    vlan_group = ChainedModelChoiceField(
+    vlan_group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
-        chains=(
-            ('site', 'site'),
-        ),
         required=False,
         required=False,
         label='VLAN group',
         label='VLAN group',
         widget=APISelect(
         widget=APISelect(
@@ -300,12 +308,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             }
             }
         )
         )
     )
     )
-    vlan = ChainedModelChoiceField(
+    vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
-        chains=(
-            ('site', 'site'),
-            ('group', 'vlan_group'),
-        ),
         required=False,
         required=False,
         label='VLAN',
         label='VLAN',
         widget=APISelect(
         widget=APISelect(
@@ -313,6 +317,13 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             display_field='display_name'
             display_field='display_name'
         )
         )
     )
     )
+    role = DynamicModelChoiceField(
+        queryset=Role.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/ipam/roles/"
+        )
+    )
     tags = TagField(required=False)
     tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
@@ -322,13 +333,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             'tags',
             'tags',
         ]
         ]
         widgets = {
         widgets = {
-            'vrf': APISelect(
-                api_url="/api/ipam/vrfs/"
-            ),
             'status': StaticSelect2(),
             'status': StaticSelect2(),
-            'role': APISelect(
-                api_url="/api/ipam/roles/"
-            )
         }
         }
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -439,14 +444,14 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         queryset=Prefix.objects.all(),
         queryset=Prefix.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url="/api/dcim/sites/"
             api_url="/api/dcim/sites/"
         )
         )
     )
     )
-    vrf = forms.ModelChoiceField(
+    vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
         required=False,
         required=False,
         label='VRF',
         label='VRF',
@@ -459,7 +464,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         max_value=PREFIX_LENGTH_MAX,
         max_value=PREFIX_LENGTH_MAX,
         required=False
         required=False
     )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -471,7 +476,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
-    role = forms.ModelChoiceField(
+    role = DynamicModelChoiceField(
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -525,8 +530,9 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
         label='Mask length',
         label='Mask length',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
-    vrf_id = FilterChoiceField(
+    vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
+        required=False,
         label='VRF',
         label='VRF',
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/vrfs/",
             api_url="/api/ipam/vrfs/",
@@ -538,7 +544,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -550,18 +556,20 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
             }
             }
         )
         )
     )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             api_url="/api/dcim/sites/",
             value_field="slug",
             value_field="slug",
             null_option=True,
             null_option=True,
         )
         )
     )
     )
-    role = FilterChoiceField(
+    role = DynamicModelMultipleChoiceField(
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/roles/",
             api_url="/api/ipam/roles/",
             value_field="slug",
             value_field="slug",
@@ -591,7 +599,15 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
         queryset=Interface.objects.all(),
         queryset=Interface.objects.all(),
         required=False
         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(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
         label='Site',
         label='Site',
@@ -603,11 +619,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
             }
             }
         )
         )
     )
     )
-    nat_rack = ChainedModelChoiceField(
+    nat_rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
-        chains=(
-            ('site', 'nat_site'),
-        ),
         required=False,
         required=False,
         label='Rack',
         label='Rack',
         widget=APISelect(
         widget=APISelect(
@@ -621,12 +634,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
             }
             }
         )
         )
     )
     )
-    nat_device = ChainedModelChoiceField(
+    nat_device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
-        chains=(
-            ('site', 'nat_site'),
-            ('rack', 'nat_rack'),
-        ),
         required=False,
         required=False,
         label='Device',
         label='Device',
         widget=APISelect(
         widget=APISelect(
@@ -648,11 +657,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
             }
             }
         )
         )
     )
     )
-    nat_inside = ChainedModelChoiceField(
+    nat_inside = DynamicModelChoiceField(
         queryset=IPAddress.objects.all(),
         queryset=IPAddress.objects.all(),
-        chains=(
-            ('interface__device', 'nat_device'),
-        ),
         required=False,
         required=False,
         label='IP Address',
         label='IP Address',
         widget=APISelect(
         widget=APISelect(
@@ -677,9 +683,6 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
         widgets = {
         widgets = {
             'status': StaticSelect2(),
             'status': StaticSelect2(),
             'role': StaticSelect2(),
             'role': StaticSelect2(),
-            'vrf': APISelect(
-                api_url="/api/ipam/vrfs/"
-            )
         }
         }
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -754,6 +757,14 @@ class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
 
 
 
 
 class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
+    vrf = DynamicModelChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        label='VRF',
+        widget=APISelect(
+            api_url="/api/ipam/vrfs/"
+        )
+    )
 
 
     class Meta:
     class Meta:
         model = IPAddress
         model = IPAddress
@@ -763,9 +774,6 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         widgets = {
         widgets = {
             'status': StaticSelect2(),
             'status': StaticSelect2(),
             'role': StaticSelect2(),
             'role': StaticSelect2(),
-            'vrf': APISelect(
-                api_url="/api/ipam/vrfs/"
-            )
         }
         }
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -901,7 +909,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         queryset=IPAddress.objects.all(),
         queryset=IPAddress.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
-    vrf = forms.ModelChoiceField(
+    vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
         required=False,
         required=False,
         label='VRF',
         label='VRF',
@@ -914,7 +922,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         max_value=IPADDRESS_MASK_LENGTH_MAX,
         max_value=IPADDRESS_MASK_LENGTH_MAX,
         required=False
         required=False
     )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -947,7 +955,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
 
 
 
 
 class IPAddressAssignForm(BootstrapMixin, forms.Form):
 class IPAddressAssignForm(BootstrapMixin, forms.Form):
-    vrf_id = forms.ModelChoiceField(
+    vrf_id = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
         required=False,
         required=False,
         label='VRF',
         label='VRF',
@@ -993,8 +1001,9 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
         label='Mask length',
         label='Mask length',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
-    vrf_id = FilterChoiceField(
+    vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
+        required=False,
         label='VRF',
         label='VRF',
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/vrfs/",
             api_url="/api/ipam/vrfs/",
@@ -1026,6 +1035,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
 #
 #
 
 
 class VLANGroupForm(BootstrapMixin, forms.ModelForm):
 class VLANGroupForm(BootstrapMixin, forms.ModelForm):
+    site = DynamicModelChoiceField(
+        queryset=Site.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/sites/"
+        )
+    )
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
@@ -1033,11 +1049,6 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
         fields = [
         fields = [
             'site', 'name', 'slug',
             'site', 'name', 'slug',
         ]
         ]
-        widgets = {
-            'site': APISelect(
-                api_url="/api/dcim/sites/"
-            )
-        }
 
 
 
 
 class VLANGroupCSVForm(forms.ModelForm):
 class VLANGroupCSVForm(forms.ModelForm):
@@ -1061,7 +1072,7 @@ class VLANGroupCSVForm(forms.ModelForm):
 
 
 
 
 class VLANGroupFilterForm(BootstrapMixin, forms.Form):
 class VLANGroupFilterForm(BootstrapMixin, forms.Form):
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -1073,9 +1084,10 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
             }
             }
         )
         )
     )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             api_url="/api/dcim/sites/",
             value_field="slug",
             value_field="slug",
@@ -1089,7 +1101,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
 #
 #
 
 
 class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -1102,17 +1114,20 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             }
             }
         )
         )
     )
     )
-    group = ChainedModelChoiceField(
+    group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
-        chains=(
-            ('site', 'site'),
-        ),
         required=False,
         required=False,
-        label='Group',
         widget=APISelect(
         widget=APISelect(
             api_url='/api/ipam/vlan-groups/',
             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)
     tags = TagField(required=False)
 
 
     class Meta:
     class Meta:
@@ -1130,9 +1145,6 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         }
         }
         widgets = {
         widgets = {
             'status': StaticSelect2(),
             'status': StaticSelect2(),
-            'role': APISelect(
-                api_url="/api/ipam/roles/"
-            )
         }
         }
 
 
 
 
@@ -1207,21 +1219,21 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url="/api/dcim/sites/"
             api_url="/api/dcim/sites/"
         )
         )
     )
     )
-    group = forms.ModelChoiceField(
+    group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url="/api/ipam/vlan-groups/"
             api_url="/api/ipam/vlan-groups/"
         )
         )
     )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -1233,7 +1245,7 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
-    role = forms.ModelChoiceField(
+    role = DynamicModelChoiceField(
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -1258,7 +1270,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -1271,17 +1283,19 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
             }
             }
         )
         )
     )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/dcim/sites/",
             api_url="/api/dcim/sites/",
             value_field="slug",
             value_field="slug",
             null_option=True,
             null_option=True,
         )
         )
     )
     )
-    group_id = FilterChoiceField(
+    group_id = DynamicModelMultipleChoiceField(
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
+        required=False,
         label='VLAN group',
         label='VLAN group',
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/vlan-groups/",
             api_url="/api/ipam/vlan-groups/",
@@ -1293,9 +1307,10 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
-    role = FilterChoiceField(
+    role = DynamicModelMultipleChoiceField(
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/roles/",
             api_url="/api/ipam/roles/",
             value_field="slug",
             value_field="slug",

+ 11 - 9
netbox/secrets/forms.py

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

+ 22 - 20
netbox/tenancy/forms.py

@@ -2,11 +2,11 @@ from django import forms
 from taggit.forms import TagField
 from taggit.forms import TagField
 
 
 from extras.forms import (
 from extras.forms import (
-    AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm,
+    AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm,
 )
 )
 from utilities.forms import (
 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
 from .models import Tenant, TenantGroup
 
 
@@ -42,6 +42,13 @@ class TenantGroupCSVForm(forms.ModelForm):
 
 
 class TenantForm(BootstrapMixin, CustomFieldModelForm):
 class TenantForm(BootstrapMixin, CustomFieldModelForm):
     slug = SlugField()
     slug = SlugField()
+    group = DynamicModelChoiceField(
+        queryset=TenantGroup.objects.all(),
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenant-groups/"
+        )
+    )
     comments = CommentField()
     comments = CommentField()
     tags = TagField(
     tags = TagField(
         required=False
         required=False
@@ -49,14 +56,9 @@ class TenantForm(BootstrapMixin, CustomFieldModelForm):
 
 
     class Meta:
     class Meta:
         model = Tenant
         model = Tenant
-        fields = [
+        fields = (
             'name', 'slug', 'group', 'description', 'comments', 'tags',
             'name', 'slug', 'group', 'description', 'comments', 'tags',
-        ]
-        widgets = {
-            'group': APISelect(
-                api_url="/api/tenancy/tenant-groups/"
-            )
-        }
+        )
 
 
 
 
 class TenantCSVForm(CustomFieldModelForm):
 class TenantCSVForm(CustomFieldModelForm):
@@ -85,7 +87,7 @@ class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
-    group = forms.ModelChoiceField(
+    group = DynamicModelChoiceField(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -105,9 +107,10 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
-    group = FilterChoiceField(
+    group = DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/tenancy/tenant-groups/",
             api_url="/api/tenancy/tenant-groups/",
             value_field="slug",
             value_field="slug",
@@ -121,8 +124,8 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
 # Form extensions
 # Form extensions
 #
 #
 
 
-class TenancyForm(ChainedFieldsMixin, forms.Form):
-    tenant_group = forms.ModelChoiceField(
+class TenancyForm(forms.Form):
+    tenant_group = DynamicModelChoiceField(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -135,11 +138,8 @@ class TenancyForm(ChainedFieldsMixin, forms.Form):
             }
             }
         )
         )
     )
     )
-    tenant = ChainedModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
-        chains=(
-            ('group', 'tenant_group'),
-        ),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url='/api/tenancy/tenants/'
             api_url='/api/tenancy/tenants/'
@@ -159,9 +159,10 @@ class TenancyForm(ChainedFieldsMixin, forms.Form):
 
 
 
 
 class TenancyFilterForm(forms.Form):
 class TenancyFilterForm(forms.Form):
-    tenant_group = FilterChoiceField(
+    tenant_group = DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/tenancy/tenant-groups/",
             api_url="/api/tenancy/tenant-groups/",
             value_field="slug",
             value_field="slug",
@@ -171,9 +172,10 @@ class TenancyFilterForm(forms.Form):
             }
             }
         )
         )
     )
     )
-    tenant = FilterChoiceField(
+    tenant = DynamicModelMultipleChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/tenancy/tenants/",
             api_url="/api/tenancy/tenants/",
             value_field="slug",
             value_field="slug",

+ 21 - 80
netbox/utilities/forms.py

@@ -522,34 +522,6 @@ class FlexibleModelChoiceField(forms.ModelChoiceField):
         return value
         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):
 class SlugField(forms.SlugField):
     """
     """
     Extend the built-in SlugField to automatically populate from a field called `name` unless otherwise specified.
     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)
         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):
     def get_bound_field(self, form, field_name):
         bound_field = BoundField(form, self, 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
         # 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.
         # 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:
         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:
         else:
             self.queryset = self.queryset.none()
             self.queryset = self.queryset.none()
 
 
         return bound_field
         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):
 class LaxURLField(forms.URLField):
     """
     """
     Modifies Django's built-in URLField in two ways:
     Modifies Django's built-in URLField in two ways:
@@ -655,46 +636,6 @@ class BootstrapMixin(forms.BaseForm):
                 field.widget.attrs['placeholder'] = field.label
                 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):
 class ReturnURLForm(forms.Form):
     """
     """
     Provides a hidden return URL field to control where the user is directed after the form is submitted.
     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 tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
     add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
-    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 .choices import *
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -77,6 +76,26 @@ class ClusterGroupCSVForm(forms.ModelForm):
 #
 #
 
 
 class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 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()
     comments = CommentField()
     tags = TagField(
     tags = TagField(
         required=False
         required=False
@@ -84,20 +103,9 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 
 
     class Meta:
     class Meta:
         model = Cluster
         model = Cluster
-        fields = [
+        fields = (
             'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
             '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):
 class ClusterCSVForm(CustomFieldModelCSVForm):
@@ -147,25 +155,28 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
         queryset=Cluster.objects.all(),
         queryset=Cluster.objects.all(),
         widget=forms.MultipleHiddenInput()
         widget=forms.MultipleHiddenInput()
     )
     )
-    type = forms.ModelChoiceField(
+    type = DynamicModelChoiceField(
         queryset=ClusterType.objects.all(),
         queryset=ClusterType.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url="/api/virtualization/cluster-types/"
             api_url="/api/virtualization/cluster-types/"
         )
         )
     )
     )
-    group = forms.ModelChoiceField(
+    group = DynamicModelChoiceField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url="/api/virtualization/cluster-groups/"
             api_url="/api/virtualization/cluster-groups/"
         )
         )
     )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenants/"
+        )
     )
     )
-    site = forms.ModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -189,7 +200,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
         'q', 'type', 'region', 'site', 'group', 'tenant_group', 'tenant'
         'q', 'type', 'region', 'site', 'group', 'tenant_group', 'tenant'
     ]
     ]
     q = forms.CharField(required=False, label='Search')
     q = forms.CharField(required=False, label='Search')
-    type = FilterChoiceField(
+    type = DynamicModelMultipleChoiceField(
         queryset=ClusterType.objects.all(),
         queryset=ClusterType.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -198,7 +209,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
             value_field='slug',
             value_field='slug',
         )
         )
     )
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -210,7 +221,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
             }
             }
         )
         )
     )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -220,7 +231,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
             null_option=True,
             null_option=True,
         )
         )
     )
     )
-    group = FilterChoiceField(
+    group = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -233,8 +244,8 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
-    region = forms.ModelChoiceField(
+class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
+    region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -247,11 +258,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
             }
             }
         )
         )
     )
     )
-    site = ChainedModelChoiceField(
+    site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        chains=(
-            ('region', 'region'),
-        ),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url='/api/dcim/sites/',
             api_url='/api/dcim/sites/',
@@ -261,11 +269,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
             }
             }
         )
         )
     )
     )
-    rack = ChainedModelChoiceField(
+    rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
-        chains=(
-            ('site', 'site'),
-        ),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url='/api/dcim/racks/',
             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),
         queryset=Device.objects.filter(cluster__isnull=True),
-        chains=(
-            ('site', 'site'),
-            ('rack', 'rack'),
-        ),
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url='/api/dcim/devices/',
             api_url='/api/dcim/devices/',
             display_field='display_name',
             display_field='display_name',
@@ -329,7 +330,7 @@ class ClusterRemoveDevicesForm(ConfirmationForm):
 #
 #
 
 
 class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
-    cluster_group = forms.ModelChoiceField(
+    cluster_group = DynamicModelChoiceField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -342,15 +343,28 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             }
             }
         )
         )
     )
     )
-    cluster = ChainedModelChoiceField(
+    cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
         queryset=Cluster.objects.all(),
-        chains=(
-            ('group', 'cluster_group'),
-        ),
         widget=APISelect(
         widget=APISelect(
             api_url='/api/virtualization/clusters/'
             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(
     tags = TagField(
         required=False
         required=False
     )
     )
@@ -371,17 +385,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         }
         }
         widgets = {
         widgets = {
             "status": StaticSelect2(),
             "status": StaticSelect2(),
-            "role": APISelect(
-                api_url="/api/dcim/device-roles/",
-                additional_query_params={
-                    "vm_role": "True"
-                }
-            ),
             'primary_ip4': StaticSelect2(),
             'primary_ip4': StaticSelect2(),
             'primary_ip6': StaticSelect2(),
             'primary_ip6': StaticSelect2(),
-            'platform': APISelect(
-                api_url='/api/dcim/platforms/'
-            )
         }
         }
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -491,14 +496,14 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
         initial='',
         initial='',
         widget=StaticSelect2(),
         widget=StaticSelect2(),
     )
     )
-    cluster = forms.ModelChoiceField(
+    cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
         queryset=Cluster.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url='/api/virtualization/clusters/'
             api_url='/api/virtualization/clusters/'
         )
         )
     )
     )
-    role = forms.ModelChoiceField(
+    role = DynamicModelChoiceField(
         queryset=DeviceRole.objects.filter(
         queryset=DeviceRole.objects.filter(
             vm_role=True
             vm_role=True
         ),
         ),
@@ -510,14 +515,14 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
             }
             }
         )
         )
     )
     )
-    tenant = forms.ModelChoiceField(
+    tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url='/api/tenancy/tenants/'
             api_url='/api/tenancy/tenants/'
         )
         )
     )
     )
-    platform = forms.ModelChoiceField(
+    platform = DynamicModelChoiceField(
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -557,32 +562,35 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
-    cluster_group = FilterChoiceField(
+    cluster_group = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url='/api/virtualization/cluster-groups/',
             api_url='/api/virtualization/cluster-groups/',
             value_field="slug",
             value_field="slug",
             null_option=True,
             null_option=True,
         )
         )
     )
     )
-    cluster_type = FilterChoiceField(
+    cluster_type = DynamicModelMultipleChoiceField(
         queryset=ClusterType.objects.all(),
         queryset=ClusterType.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url='/api/virtualization/cluster-types/',
             api_url='/api/virtualization/cluster-types/',
             value_field="slug",
             value_field="slug",
             null_option=True,
             null_option=True,
         )
         )
     )
     )
-    cluster_id = FilterChoiceField(
+    cluster_id = DynamicModelMultipleChoiceField(
         queryset=Cluster.objects.all(),
         queryset=Cluster.objects.all(),
+        required=False,
         label='Cluster',
         label='Cluster',
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url='/api/virtualization/clusters/',
             api_url='/api/virtualization/clusters/',
         )
         )
     )
     )
-    region = FilterChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
@@ -594,18 +602,20 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
             }
             }
         )
         )
     )
     )
-    site = FilterChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url='/api/dcim/sites/',
             api_url='/api/dcim/sites/',
             value_field="slug",
             value_field="slug",
             null_option=True,
             null_option=True,
         )
         )
     )
     )
-    role = FilterChoiceField(
+    role = DynamicModelMultipleChoiceField(
         queryset=DeviceRole.objects.filter(vm_role=True),
         queryset=DeviceRole.objects.filter(vm_role=True),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url='/api/dcim/device-roles/',
             api_url='/api/dcim/device-roles/',
             value_field="slug",
             value_field="slug",
@@ -620,9 +630,10 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
         required=False,
         required=False,
         widget=StaticSelect2Multiple()
         widget=StaticSelect2Multiple()
     )
     )
-    platform = FilterChoiceField(
+    platform = DynamicModelMultipleChoiceField(
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
+        required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url='/api/dcim/platforms/',
             api_url='/api/dcim/platforms/',
             value_field="slug",
             value_field="slug",
@@ -641,7 +652,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
 #
 #
 
 
 class InterfaceForm(BootstrapMixin, forms.ModelForm):
 class InterfaceForm(BootstrapMixin, forms.ModelForm):
-    untagged_vlan = forms.ModelChoiceField(
+    untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -650,7 +661,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
             full=True
             full=True
         )
         )
     )
     )
-    tagged_vlans = forms.ModelMultipleChoiceField(
+    tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         required=False,
         required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
@@ -767,7 +778,7 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
         required=False,
         required=False,
         widget=StaticSelect2(),
         widget=StaticSelect2(),
     )
     )
-    untagged_vlan = forms.ModelChoiceField(
+    untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -776,7 +787,7 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
             full=True
             full=True
         )
         )
     )
     )
-    tagged_vlans = forms.ModelMultipleChoiceField(
+    tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         required=False,
         required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(
@@ -855,7 +866,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
         required=False,
         required=False,
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
-    untagged_vlan = forms.ModelChoiceField(
+    untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
@@ -864,7 +875,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
             full=True
             full=True
         )
         )
     )
     )
-    tagged_vlans = forms.ModelMultipleChoiceField(
+    tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         required=False,
         required=False,
         widget=APISelectMultiple(
         widget=APISelectMultiple(

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