Merge pull request #4987 from netbox-community/4982-apiselect-improvements

Closes #4982: Overhaul the APISelect widget and support ObjectVar filtering
This commit is contained in:
Jeremy Stretch 2020-08-13 09:32:32 -04:00 committed by GitHub
commit 76dc80a45d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 713 additions and 1110 deletions

View File

@ -163,12 +163,46 @@ In the example above, selecting the choice labeled "North" will submit the value
### ObjectVar ### ObjectVar
A NetBox object of a particular type, identified by the associated queryset. A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below.
* `queryset` - The base [Django queryset](https://docs.djangoproject.com/en/stable/topics/db/queries/) for the model * `model` - The model class
* `display_field` - The name of the REST API object field to display in the selection list (default: `'name'`)
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
* `null_option` - A label representing a "null" or empty choice (optional)
!!! warning The `display_field` argument is useful when referencing a model which does not have a `name` field. For example, when displaying a list of device types, you would likely use the `model` field:
Because the available options for this field are populated using the REST API, any filtering or exclusions performed on the specified queryset will not have any effect. While it is possible to influence the manner in which field options are populated using NetBox's `APISelect` widget, please note that this component is not officially supported and is planned to be replaced in a future release.
```python
device_type = ObjectVar(
model=DeviceType,
display_field='model'
)
```
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
```python
device = ObjectVar(
model=Device,
query_params={
'status': 'active'
}
)
```
Multiple values can be specified by assigning a list to the dictionary key. It is also possible to reference the value of other fields in the form by prepending a dollar sign (`$`) to the variable's name.
```python
region = ObjectVar(
model=Region
)
site = ObjectVar(
model=Site,
query_params={
'region_id': '$region'
}
)
```
### MultiObjectVar ### MultiObjectVar
@ -207,9 +241,8 @@ These variables are presented as a web form to be completed by the user. Once su
from django.utils.text import slugify from django.utils.text import slugify
from dcim.choices import DeviceStatusChoices, SiteStatusChoices from dcim.choices import DeviceStatusChoices, SiteStatusChoices
from dcim.models import Device, DeviceRole, DeviceType, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from extras.scripts import * from extras.scripts import *
from utilities.forms import APISelect
class NewBranchScript(Script): class NewBranchScript(Script):
@ -225,12 +258,17 @@ class NewBranchScript(Script):
switch_count = IntegerVar( switch_count = IntegerVar(
description="Number of access switches to create" description="Number of access switches to create"
) )
manufacturer = ObjectVar(
model=Manufacturer,
required=False
)
switch_model = ObjectVar( switch_model = ObjectVar(
description="Access switch model", description="Access switch model",
queryset=DeviceType.objects.all(), model=DeviceType,
widget=APISelect( display_field='model',
display_field='model' query_params={
) 'manufacturer_id': '$manufacturer'
}
) )
def run(self, data, commit): def run(self, data, commit):

View File

@ -8,9 +8,9 @@ from extras.models import Tag
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 (
APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DatePicker,
CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
StaticSelect2, StaticSelect2Multiple, TagFilterField, StaticSelect2Multiple, TagFilterField,
) )
from .choices import CircuitStatusChoices from .choices import CircuitStatusChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -106,21 +106,15 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
region = DynamicModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
filter_for={
'site': 'region'
}
)
) )
site = DynamicModelMultipleChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( query_params={
value_field="slug", 'region': '$region'
) }
) )
asn = forms.IntegerField( asn = forms.IntegerField(
required=False, required=False,
@ -271,18 +265,12 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
type = DynamicModelMultipleChoiceField( type = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
provider = DynamicModelMultipleChoiceField( provider = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
@ -292,21 +280,15 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
region = DynamicModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
filter_for={
'site': 'region'
}
)
) )
site = DynamicModelMultipleChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( query_params={
value_field="slug", 'region': '$region'
) }
) )
commit_rate = forms.IntegerField( commit_rate = forms.IntegerField(
required=False, required=False,

File diff suppressed because it is too large Load Diff

View File

@ -290,42 +290,27 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
region = DynamicModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
site = DynamicModelMultipleChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
role = DynamicModelMultipleChoiceField( role = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
platform = DynamicModelMultipleChoiceField( platform = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
cluster_group = DynamicModelMultipleChoiceField( cluster_group = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
cluster_id = DynamicModelMultipleChoiceField( cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
@ -335,26 +320,17 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
tenant_group = DynamicModelMultipleChoiceField( tenant_group = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
tenant = DynamicModelMultipleChoiceField( tenant = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
tag = DynamicModelMultipleChoiceField( tag = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
@ -413,9 +389,9 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
user = DynamicModelMultipleChoiceField( user = DynamicModelMultipleChoiceField(
queryset=User.objects.all(), queryset=User.objects.all(),
required=False, required=False,
display_field='username',
widget=APISelectMultiple( widget=APISelectMultiple(
api_url='/api/users/users/', api_url='/api/users/users/',
display_field='username'
) )
) )
changed_object_type = forms.ModelChoiceField( changed_object_type = forms.ModelChoiceField(

View File

@ -170,28 +170,42 @@ class ChoiceVar(ScriptVariable):
class ObjectVar(ScriptVariable): class ObjectVar(ScriptVariable):
""" """
A single object within NetBox. A single object within NetBox.
:param model: The NetBox model being referenced
:param display_field: The attribute of the returned object to display in the selection list (default: 'name')
:param query_params: A dictionary of additional query parameters to attach when making REST API requests (optional)
:param null_option: The label to use as a "null" selection option (optional)
""" """
form_field = DynamicModelChoiceField form_field = DynamicModelChoiceField
def __init__(self, queryset, *args, **kwargs): def __init__(self, model=None, queryset=None, display_field='name', query_params=None, null_option=None, *args,
**kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Queryset for field choices # Set the form field's queryset. Support backward compatibility for the "queryset" argument for now.
self.field_attrs['queryset'] = queryset if model is not None:
self.field_attrs['queryset'] = model.objects.all()
elif queryset is not None:
warnings.warn(
f'{self}: Specifying a queryset for ObjectVar is no longer supported. Please use "model" instead.'
)
self.field_attrs['queryset'] = queryset
else:
raise TypeError('ObjectVar must specify a model')
self.field_attrs.update({
'display_field': display_field,
'query_params': query_params,
'null_option': null_option,
})
class MultiObjectVar(ScriptVariable): class MultiObjectVar(ObjectVar):
""" """
Like ObjectVar, but can represent one or more objects. Like ObjectVar, but can represent one or more objects.
""" """
form_field = DynamicModelMultipleChoiceField form_field = DynamicModelMultipleChoiceField
def __init__(self, queryset, *args, **kwargs):
super().__init__(*args, **kwargs)
# Queryset for field choices
self.field_attrs['queryset'] = queryset
class FileVar(ScriptVariable): class FileVar(ScriptVariable):
""" """

View File

@ -1,5 +1,4 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from dcim.models import Device, Interface, Rack, Region, Site from dcim.models import Device, Interface, Rack, Region, Site
@ -10,10 +9,9 @@ from extras.models import Tag
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, CSVChoiceField, add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, CSVModelChoiceField, CSVModelForm,
CSVModelChoiceField, CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, ReturnURLForm,
ExpandableIPAddressField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
BOOLEAN_WITH_BLANK_CHOICES,
) )
from virtualization.models import VirtualMachine, VMInterface from virtualization.models import VirtualMachine, VMInterface
from .choices import * from .choices import *
@ -217,10 +215,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
label='RIR', label='RIR'
widget=APISelectMultiple(
value_field="slug",
)
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -255,41 +250,32 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
vrf = DynamicModelChoiceField( vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF' label='VRF',
display_field='display_name'
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
widget=APISelect( null_option='None'
filter_for={
'vlan_group': 'site_id',
'vlan': 'site_id',
},
attrs={
'nullable': 'true',
}
)
) )
vlan_group = DynamicModelChoiceField( vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False, required=False,
label='VLAN group', label='VLAN group',
widget=APISelect( null_option='None',
filter_for={ query_params={
'vlan': 'group_id' 'site_id': '$site'
}, }
attrs={
'nullable': 'true',
}
)
) )
vlan = DynamicModelChoiceField( vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False, required=False,
label='VLAN', label='VLAN',
widget=APISelect( display_field='display_name',
display_field='display_name' query_params={
) 'site_id': '$site',
'group_id': '$vlan_group',
}
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
queryset=Role.objects.all(), queryset=Role.objects.all(),
@ -469,9 +455,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF', label='VRF',
widget=APISelectMultiple( null_option='Global'
null_option=True,
)
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=PrefixStatusChoices, choices=PrefixStatusChoices,
@ -481,31 +465,22 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
region = DynamicModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
filter_for={
'site': 'region'
}
)
) )
site = DynamicModelMultipleChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None',
value_field="slug", query_params={
null_option=True, 'region': '$region'
) }
) )
role = DynamicModelMultipleChoiceField( role = DynamicModelMultipleChoiceField(
queryset=Role.objects.all(), queryset=Role.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None'
value_field="slug",
null_option=True,
)
) )
is_pool = forms.NullBooleanField( is_pool = forms.NullBooleanField(
required=False, required=False,
@ -525,29 +500,26 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
widget=APISelect( display_field='display_name'
filter_for={
'interface': 'device_id'
}
)
) )
interface = DynamicModelChoiceField( interface = DynamicModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False required=False,
query_params={
'device_id': '$device'
}
) )
virtual_machine = DynamicModelChoiceField( virtual_machine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
required=False, required=False
widget=APISelect(
filter_for={
'vminterface': 'virtual_machine_id'
}
)
) )
vminterface = DynamicModelChoiceField( vminterface = DynamicModelChoiceField(
queryset=VMInterface.objects.all(), queryset=VMInterface.objects.all(),
required=False, required=False,
label='Interface' label='Interface',
query_params={
'virtual_machine_id': '$virtual_machine'
}
) )
vrf = DynamicModelChoiceField( vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
@ -557,56 +529,42 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
nat_site = DynamicModelChoiceField( nat_site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
label='Site', label='Site'
widget=APISelect(
filter_for={
'nat_rack': 'site_id',
'nat_device': 'site_id'
}
)
) )
nat_rack = DynamicModelChoiceField( nat_rack = DynamicModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
required=False, required=False,
label='Rack', label='Rack',
widget=APISelect( display_field='display_name',
display_field='display_name', null_option='None',
filter_for={ query_params={
'nat_device': 'rack_id' 'site_id': '$site'
}, }
attrs={
'nullable': 'true'
}
)
) )
nat_device = DynamicModelChoiceField( nat_device = DynamicModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
label='Device', label='Device',
widget=APISelect( display_field='display_name',
display_field='display_name', query_params={
filter_for={ 'site_id': '$site',
'nat_inside': 'device_id' 'rack_id': '$nat_rack',
} }
)
) )
nat_vrf = DynamicModelChoiceField( nat_vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF', label='VRF'
widget=APISelect(
filter_for={
'nat_inside': 'vrf_id'
}
)
) )
nat_inside = DynamicModelChoiceField( nat_inside = DynamicModelChoiceField(
queryset=IPAddress.objects.all(), queryset=IPAddress.objects.all(),
required=False, required=False,
label='IP Address', label='IP Address',
widget=APISelect( display_field='address',
display_field='address' query_params={
) 'device_id': '$nat_device',
'vrf_id': '$nat_vrf',
}
) )
primary_for_parent = forms.BooleanField( primary_for_parent = forms.BooleanField(
required=False, required=False,
@ -920,9 +878,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF', label='VRF',
widget=APISelectMultiple( null_option='Global'
null_option=True,
)
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=IPAddressStatusChoices, choices=IPAddressStatusChoices,
@ -980,22 +936,16 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
region = DynamicModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
filter_for={
'site': 'region',
}
)
) )
site = DynamicModelMultipleChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None',
value_field="slug", query_params={
null_option=True, 'region': '$region'
) }
) )
@ -1007,18 +957,14 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
widget=APISelect( null_option='None'
filter_for={
'group': 'site_id'
},
attrs={
'nullable': 'true',
}
)
) )
group = DynamicModelChoiceField( group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False required=False,
query_params={
'site_id': '$site'
}
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
queryset=Role.objects.all(), queryset=Role.objects.all(),
@ -1102,16 +1048,14 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False
widget=APISelect(
filter_for={
'group': 'site_id'
}
)
) )
group = DynamicModelChoiceField( group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False required=False,
query_params={
'site_id': '$site'
}
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
@ -1147,31 +1091,25 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
region = DynamicModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
filter_for={
'site': 'region',
'group_id': 'region'
}
)
) )
site = DynamicModelMultipleChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None',
value_field="slug", query_params={
null_option=True, 'region': '$region'
) }
) )
group_id = DynamicModelMultipleChoiceField( group_id = DynamicModelMultipleChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False, required=False,
label='VLAN group', label='VLAN group',
widget=APISelectMultiple( null_option='None',
null_option=True, query_params={
) 'region': '$region'
}
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=VLANStatusChoices, choices=VLANStatusChoices,
@ -1182,10 +1120,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
queryset=Role.objects.all(), queryset=Role.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None'
value_field="slug",
null_option=True,
)
) )
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -74,7 +74,7 @@ $(document).ready(function() {
form.submit(); form.submit();
}); });
// Parse URLs which may contain variable refrences to other field values // Parse URLs which may contain variable references to other field values
function parseURL(url) { function parseURL(url) {
var filter_regex = /\{\{([a-z_]+)\}\}/g; var filter_regex = /\{\{([a-z_]+)\}\}/g;
var match; var match;
@ -87,7 +87,7 @@ $(document).ready(function() {
rendered_url = rendered_url.replace(match[0], custom_attr); rendered_url = rendered_url.replace(match[0], custom_attr);
} else if (filter_field.val()) { } else if (filter_field.val()) {
rendered_url = rendered_url.replace(match[0], filter_field.val()); rendered_url = rendered_url.replace(match[0], filter_field.val());
} else if (filter_field.attr('nullable') == 'true') { } else if (filter_field.attr('data-null-option')) {
rendered_url = rendered_url.replace(match[0], 'null'); rendered_url = rendered_url.replace(match[0], 'null');
} }
} }
@ -123,7 +123,7 @@ $(document).ready(function() {
// API backed selection // API backed selection
// Includes live search and chained fields // Includes live search and chained fields
// The `multiple` setting may be controled via a data-* attribute // The `multiple` setting may be controlled via a data-* attribute
$('.netbox-select2-api').select2({ $('.netbox-select2-api').select2({
allowClear: true, allowClear: true,
placeholder: "---------", placeholder: "---------",
@ -157,47 +157,23 @@ $(document).ready(function() {
// Allow for controlling the brief setting from within APISelect // Allow for controlling the brief setting from within APISelect
parameters.brief = ( $(element).is('[data-full]') ? undefined : true ); parameters.brief = ( $(element).is('[data-full]') ? undefined : true );
// filter-for fields from a chain // Attach any extra query parameters
var attr_name = "data-filter-for-" + $(element).attr("name");
var form = $(element).closest('form');
var filter_for_elements = form.find("select[" + attr_name + "]");
filter_for_elements.each(function(index, filter_for_element) {
var param_name = $(filter_for_element).attr(attr_name);
var is_required = $(filter_for_element).attr("required");
var is_nullable = $(filter_for_element).attr("nullable");
var is_visible = $(filter_for_element).is(":visible");
var value = $(filter_for_element).val();
if (param_name && is_visible) {
if (value) {
parameters[param_name] = value;
} else if (is_required && is_nullable) {
parameters[param_name] = "null";
}
}
});
// Conditional query params
$.each(element.attributes, function(index, attr){ $.each(element.attributes, function(index, attr){
if (attr.name.includes("data-conditional-query-param-")){ if (attr.name.includes("data-query-param-")){
var conditional = attr.name.split("data-conditional-query-param-")[1].split("__"); var param_name = attr.name.split("data-query-param-")[1];
var field = $("#id_" + conditional[0]);
var field_value = conditional[1];
if ($('option:selected', field).attr('api-value') === field_value){
var _val = attr.value.split("=");
parameters[_val[0]] = _val[1];
}
}
});
// Additional query params
$.each(element.attributes, function(index, attr){
if (attr.name.includes("data-additional-query-param-")){
var param_name = attr.name.split("data-additional-query-param-")[1];
$.each($.parseJSON(attr.value), function(index, value) { $.each($.parseJSON(attr.value), function(index, value) {
// Referencing the value of another form field
if (value.startsWith('$')) {
let ref_field = $('#id_' + value.slice(1));
if (ref_field.val() && ref_field.is(":visible")) {
value = ref_field.val();
} else if (ref_field.attr("required") && ref_field.attr("data-null-option")) {
value = "null";
} else {
return true; // Skip if ref_field has no value
}
}
if (param_name in parameters) { if (param_name in parameters) {
if (Array.isArray(parameters[param_name])) { if (Array.isArray(parameters[param_name])) {
parameters[param_name].push(value); parameters[param_name].push(value);
@ -261,7 +237,7 @@ $(document).ready(function() {
if (element.getAttribute('data-null-option') && data.previous === null) { if (element.getAttribute('data-null-option') && data.previous === null) {
results.unshift({ results.unshift({
id: 'null', id: 'null',
text: 'None' text: element.getAttribute('data-null-option')
}); });
} }

View File

@ -8,8 +8,8 @@ from extras.forms import (
) )
from extras.models import Tag from extras.models import Tag
from utilities.forms import ( from utilities.forms import (
APISelectMultiple, BootstrapMixin, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, BootstrapMixin, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
DynamicModelMultipleChoiceField, SlugField, StaticSelect2Multiple, TagFilterField, SlugField, TagFilterField,
) )
from .constants import * from .constants import *
from .models import Secret, SecretRole, UserKey from .models import Secret, SecretRole, UserKey
@ -63,7 +63,8 @@ class SecretRoleCSVForm(CSVModelForm):
class SecretForm(BootstrapMixin, CustomFieldModelForm): class SecretForm(BootstrapMixin, CustomFieldModelForm):
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
queryset=Device.objects.all() queryset=Device.objects.all(),
display_field='display_name'
) )
plaintext = forms.CharField( plaintext = forms.CharField(
max_length=SECRET_PLAINTEXT_MAX_LENGTH, max_length=SECRET_PLAINTEXT_MAX_LENGTH,
@ -178,10 +179,7 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
role = DynamicModelMultipleChoiceField( role = DynamicModelMultipleChoiceField(
queryset=SecretRole.objects.all(), queryset=SecretRole.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
)
) )
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -35,33 +35,3 @@
</div> </div>
</form> </form>
{% endblock %} {% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
var device_list = $('#id_devices');
var disabled_indicator = device_list.attr('disabled-indicator');
$('#id_search').autocomplete({
source: function(request, response) {
$.ajax({
type: 'GET',
url: netbox_api_path + 'dcim/devices/',
data: 'q=' + request.term,
beforeSend: function() {
device_list.empty();
},
success: function(data) {
response($.map(data.results, function(item) {
var option = $("<option></option>").attr("value", item['id']).text(item['display_name']);
if (disabled_indicator && item[disabled_indicator]) {
option.attr("disabled", "disabled");
}
device_list.append(option);
}));
}
});
}
});
});
</script>
{% endblock %}

View File

@ -5,8 +5,8 @@ from extras.forms import (
) )
from extras.models import Tag from extras.models import Tag
from utilities.forms import ( from utilities.forms import (
APISelect, APISelectMultiple, BootstrapMixin, CommentField, CSVModelChoiceField, CSVModelForm, BootstrapMixin, CommentField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField,
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, TagFilterField, DynamicModelMultipleChoiceField, SlugField, TagFilterField,
) )
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
@ -106,10 +106,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None'
value_field="slug",
null_option=True,
)
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -122,18 +119,14 @@ class TenancyForm(forms.Form):
tenant_group = DynamicModelChoiceField( tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
required=False, required=False,
widget=APISelect( null_option='None'
filter_for={
'tenant': 'group_id',
},
attrs={
'nullable': 'true',
}
)
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False,
query_params={
'group_id': '$tenant_group'
}
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -153,20 +146,14 @@ class TenancyFilterForm(forms.Form):
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None'
value_field="slug",
null_option=True,
filter_for={
'tenant': 'group'
}
)
) )
tenant = DynamicModelMultipleChoiceField( tenant = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None',
value_field="slug", query_params={
null_option=True, 'group': '$tenant_group'
) }
) )

View File

@ -11,6 +11,7 @@ from django.db.models import Count
from django.forms import BoundField from django.forms import BoundField
from django.urls import reverse from django.urls import reverse
from utilities.api import get_serializer_for_model
from utilities.choices import unpack_grouped_choices from utilities.choices import unpack_grouped_choices
from utilities.validators import EnhancedURLValidator from utilities.validators import EnhancedURLValidator
from . import widgets from . import widgets
@ -244,9 +245,58 @@ class TagFilterField(forms.MultipleChoiceField):
class DynamicModelChoiceMixin: class DynamicModelChoiceMixin:
"""
:param display_field: The name of the attribute of an API response object to display in the selection list
:param query_params: A dictionary of additional key/value pairs to attach to the API request
:param null_option: The string used to represent a null selection (if any)
:param disabled_indicator: The name of the field which, if populated, will disable selection of the
choice (optional)
:param brief_mode: Use the "brief" format (?brief=true) when making API requests (default)
"""
filter = django_filters.ModelChoiceFilter filter = django_filters.ModelChoiceFilter
widget = widgets.APISelect widget = widgets.APISelect
def __init__(self, display_field='name', query_params=None, null_option=None, disabled_indicator=None,
brief_mode=True, *args, **kwargs):
self.display_field = display_field
self.query_params = query_params or {}
self.null_option = null_option
self.disabled_indicator = disabled_indicator
self.brief_mode = brief_mode
# to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
# by widget_attrs()
self.to_field_name = kwargs.get('to_field_name')
super().__init__(*args, **kwargs)
def widget_attrs(self, widget):
attrs = {
'display-field': self.display_field,
}
# Set value-field attribute if the field specifies to_field_name
if self.to_field_name:
attrs['value-field'] = self.to_field_name
# Set the string used to represent a null option
if self.null_option is not None:
attrs['data-null-option'] = self.null_option
# Set the disabled indicator, if any
if self.disabled_indicator is not None:
attrs['disabled-indicator'] = self.disabled_indicator
# Toggle brief mode
if not self.brief_mode:
attrs['data-full'] = 'true'
# Attach any static query parameters
for key, value in self.query_params.items():
widget.add_query_param(key, value)
return attrs
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)

View File

@ -79,29 +79,12 @@ class SelectWithDisabled(forms.Select):
class StaticSelect2(SelectWithDisabled): class StaticSelect2(SelectWithDisabled):
""" """
A static content using the Select2 widget A static <select> form widget using the Select2 library.
:param filter_for: (Optional) A dict of chained form fields for which this field is a filter. The key is the
name of the filter-for field (child field) and the value is the name of the query param filter.
""" """
def __init__(self, *args, **kwargs):
def __init__(self, filter_for=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.attrs['class'] = 'netbox-select2-static' self.attrs['class'] = 'netbox-select2-static'
if filter_for:
for key, value in filter_for.items():
self.add_filter_for(key, value)
def add_filter_for(self, name, value):
"""
Add details for an additional query param in the form of a data-filter-for-* attribute.
:param name: The name of the query param
:param value: The value of the query param
"""
self.attrs['data-filter-for-{}'.format(name)] = value
class StaticSelect2Multiple(StaticSelect2, forms.SelectMultiple): class StaticSelect2Multiple(StaticSelect2, forms.SelectMultiple):
@ -140,93 +123,31 @@ class APISelect(SelectWithDisabled):
A select widget populated via an API call A select widget populated via an API call
:param api_url: API endpoint URL. Required if not set automatically by the parent field. :param api_url: API endpoint URL. Required if not set automatically by the parent field.
:param display_field: (Optional) Field to display for child in selection list. Defaults to `name`.
:param value_field: (Optional) Field to use for the option value in selection list. Defaults to `id`.
:param disabled_indicator: (Optional) Mark option as disabled if this field equates true.
:param filter_for: (Optional) A dict of chained form fields for which this field is a filter. The key is the
name of the filter-for field (child field) and the value is the name of the query param filter.
:param conditional_query_params: (Optional) A dict of URL query params to append to the URL if the
condition is met. The condition is the dict key and is specified in the form `<field_name>__<field_value>`.
If the provided field value is selected for the given field, the URL query param will be appended to
the rendered URL. The value is the in the from `<param_name>=<param_value>`. This is useful in cases where
a particular field value dictates an additional API filter.
:param additional_query_params: Optional) A dict of query params to append to the API request. The key is the
name of the query param and the value if the query param's value.
:param null_option: If true, include the static null option in the selection list.
""" """
def __init__( def __init__(self, api_url=None, full=False, *args, **kwargs):
self,
api_url=None,
display_field=None,
value_field=None,
disabled_indicator=None,
filter_for=None,
conditional_query_params=None,
additional_query_params=None,
null_option=False,
full=False,
*args,
**kwargs
):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.attrs['class'] = 'netbox-select2-api' self.attrs['class'] = 'netbox-select2-api'
if api_url: if api_url:
self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/')) # Inject BASE_PATH self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/')) # Inject BASE_PATH
if full:
self.attrs['data-full'] = full
if display_field:
self.attrs['display-field'] = display_field
if value_field:
self.attrs['value-field'] = value_field
if disabled_indicator:
self.attrs['disabled-indicator'] = disabled_indicator
if filter_for:
for key, value in filter_for.items():
self.add_filter_for(key, value)
if conditional_query_params:
for key, value in conditional_query_params.items():
self.add_conditional_query_param(key, value)
if additional_query_params:
for key, value in additional_query_params.items():
self.add_additional_query_param(key, value)
if null_option:
self.attrs['data-null-option'] = 1
def add_filter_for(self, name, value): def add_query_param(self, name, value):
"""
Add details for an additional query param in the form of a data-filter-for-* attribute.
:param name: The name of the query param
:param value: The value of the query param
"""
self.attrs['data-filter-for-{}'.format(name)] = value
def add_additional_query_param(self, name, value):
""" """
Add details for an additional query param in the form of a data-* JSON-encoded list attribute. Add details for an additional query param in the form of a data-* JSON-encoded list attribute.
:param name: The name of the query param :param name: The name of the query param
:param value: The value of the query param :param value: The value of the query param
""" """
key = 'data-additional-query-param-{}'.format(name) key = f'data-query-param-{name}'
values = json.loads(self.attrs.get(key, '[]')) values = json.loads(self.attrs.get(key, '[]'))
values.append(value) if type(value) is list:
values.extend(value)
else:
values.append(value)
self.attrs[key] = json.dumps(values) self.attrs[key] = json.dumps(values)
def add_conditional_query_param(self, condition, value):
"""
Add details for a URL query strings to append to the URL if the condition is met.
The condition is specified in the form `<field_name>__<field_value>`.
:param condition: The condition for the query param
:param value: The value of the query param
"""
self.attrs['data-conditional-query-param-{}'.format(condition)] = value
class APISelectMultiple(APISelect, forms.SelectMultiple): class APISelectMultiple(APISelect, forms.SelectMultiple):

View File

@ -13,10 +13,10 @@ from ipam.models import IPAddress, VLAN
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, BulkEditForm, BulkEditNullBooleanSelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, BulkRenameForm, CommentField,
BulkRenameForm, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField,
DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea,
SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
) )
from .choices import * from .choices import *
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -166,39 +166,27 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
type = DynamicModelMultipleChoiceField( type = DynamicModelMultipleChoiceField(
queryset=ClusterType.objects.all(), queryset=ClusterType.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field='slug',
)
) )
region = DynamicModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
filter_for={
'site': 'region'
}
)
) )
site = DynamicModelMultipleChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None',
value_field='slug', query_params={
null_option=True, 'region': '$region'
) }
) )
group = DynamicModelMultipleChoiceField( group = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None'
value_field='slug',
null_option=True,
)
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -207,43 +195,32 @@ class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
widget=APISelect( null_option='None'
filter_for={
"site": "region_id",
},
attrs={
'nullable': 'true',
}
)
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
widget=APISelect( query_params={
filter_for={ 'region_id': '$region'
"rack": "site_id", }
"devices": "site_id",
}
)
) )
rack = DynamicModelChoiceField( rack = DynamicModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
required=False, required=False,
widget=APISelect( null_option='None',
filter_for={ display_field='display_name',
"devices": "rack_id" query_params={
}, 'site_id': '$site'
attrs={ }
'nullable': 'true',
}
)
) )
devices = DynamicModelMultipleChoiceField( devices = DynamicModelMultipleChoiceField(
queryset=Device.objects.filter(cluster__isnull=True), queryset=Device.objects.all(),
widget=APISelectMultiple( display_field='display_name',
display_field='display_name', query_params={
disabled_indicator='cluster' 'site_id': '$site',
) 'rack_id': '$rack',
'cluster_id': 'null',
}
) )
class Meta: class Meta:
@ -288,26 +265,20 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
cluster_group = DynamicModelChoiceField( cluster_group = DynamicModelChoiceField(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
required=False, required=False,
widget=APISelect( null_option='None'
filter_for={
"cluster": "group_id",
},
attrs={
'nullable': 'true',
}
)
) )
cluster = DynamicModelChoiceField( cluster = DynamicModelChoiceField(
queryset=Cluster.objects.all() queryset=Cluster.objects.all(),
query_params={
'group_id': '$cluster_group'
}
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
required=False, required=False,
widget=APISelect( query_params={
additional_query_params={ "vm_role": "True"
"vm_role": "True" }
}
)
) )
platform = DynamicModelChoiceField( platform = DynamicModelChoiceField(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
@ -444,11 +415,9 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
vm_role=True vm_role=True
), ),
required=False, required=False,
widget=APISelect( query_params={
additional_query_params={ "vm_role": "True"
"vm_role": "True" }
}
)
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
@ -495,19 +464,13 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None'
value_field="slug",
null_option=True,
)
) )
cluster_type = DynamicModelMultipleChoiceField( cluster_type = DynamicModelMultipleChoiceField(
queryset=ClusterType.objects.all(), queryset=ClusterType.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None'
value_field="slug",
null_option=True,
)
) )
cluster_id = DynamicModelMultipleChoiceField( cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
@ -517,34 +480,25 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
region = DynamicModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False
widget=APISelectMultiple(
value_field="slug",
filter_for={
'site': 'region'
}
)
) )
site = DynamicModelMultipleChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None',
value_field="slug", query_params={
null_option=True, 'region': '$region'
) }
) )
role = DynamicModelMultipleChoiceField( 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, required=False,
widget=APISelectMultiple( null_option='None',
value_field="slug", query_params={
null_option=True, 'vm_role': "True"
additional_query_params={ }
'vm_role': "True"
}
)
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=VirtualMachineStatusChoices, choices=VirtualMachineStatusChoices,
@ -555,10 +509,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
widget=APISelectMultiple( null_option='None'
value_field="slug",
null_option=True,
)
) )
mac_address = forms.CharField( mac_address = forms.CharField(
required=False, required=False,
@ -575,24 +526,20 @@ class VMInterfaceForm(BootstrapMixin, forms.ModelForm):
untagged_vlan = DynamicModelChoiceField( untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False, required=False,
widget=APISelect( display_field='display_name',
display_field='display_name', brief_mode=False,
full=True, query_params={
additional_query_params={ 'site_id': 'null',
'site_id': 'null', }
},
)
) )
tagged_vlans = DynamicModelMultipleChoiceField( tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False, required=False,
widget=APISelectMultiple( display_field='display_name',
display_field='display_name', brief_mode=False,
full=True, query_params={
additional_query_params={ 'site_id': 'null',
'site_id': 'null', }
},
)
) )
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
@ -626,8 +573,8 @@ class VMInterfaceForm(BootstrapMixin, forms.ModelForm):
# Add current site to VLANs query params # Add current site to VLANs query params
site = virtual_machine.site site = virtual_machine.site
if site: if site:
self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk) self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk) self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
def clean(self): def clean(self):
super().clean() super().clean()
@ -679,24 +626,20 @@ class VMInterfaceCreateForm(BootstrapMixin, forms.Form):
untagged_vlan = DynamicModelChoiceField( untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False, required=False,
widget=APISelect( display_field='display_name',
display_field='display_name', brief_mode=False,
full=True, query_params={
additional_query_params={ 'site_id': 'null',
'site_id': 'null', }
},
)
) )
tagged_vlans = DynamicModelMultipleChoiceField( tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False, required=False,
widget=APISelectMultiple( display_field='display_name',
display_field='display_name', brief_mode=False,
full=True, query_params={
additional_query_params={ 'site_id': 'null',
'site_id': 'null', }
},
)
) )
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
@ -713,8 +656,8 @@ class VMInterfaceCreateForm(BootstrapMixin, forms.Form):
# Add current site to VLANs query params # Add current site to VLANs query params
site = virtual_machine.site site = virtual_machine.site
if site: if site:
self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk) self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk) self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
class VMInterfaceCSVForm(CSVModelForm): class VMInterfaceCSVForm(CSVModelForm):
@ -773,24 +716,20 @@ class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
untagged_vlan = DynamicModelChoiceField( untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False, required=False,
widget=APISelect( display_field='display_name',
display_field='display_name', brief_mode=False,
full=True, query_params={
additional_query_params={ 'site_id': 'null',
'site_id': 'null', }
},
)
) )
tagged_vlans = DynamicModelMultipleChoiceField( tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False, required=False,
widget=APISelectMultiple( display_field='display_name',
display_field='display_name', brief_mode=False,
full=True, query_params={
additional_query_params={ 'site_id': 'null',
'site_id': 'null', }
},
)
) )
class Meta: class Meta:
@ -808,8 +747,8 @@ class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
site = getattr(parent_obj.cluster, 'site', None) site = getattr(parent_obj.cluster, 'site', None)
if site is not None: if site is not None:
# Add current site to VLANs query params # Add current site to VLANs query params
self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk) self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk) self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
class VMInterfaceBulkRenameForm(BulkRenameForm): class VMInterfaceBulkRenameForm(BulkRenameForm):
@ -824,17 +763,15 @@ class VMInterfaceFilterForm(forms.Form):
cluster_id = DynamicModelMultipleChoiceField( cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
required=False, required=False,
label='Cluster', label='Cluster'
widget=APISelectMultiple(
filter_for={
'virtual_machine_id': 'cluster_id'
}
)
) )
virtual_machine_id = DynamicModelMultipleChoiceField( virtual_machine_id = DynamicModelMultipleChoiceField(
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
required=False, required=False,
label='Virtual machine' label='Virtual machine',
query_params={
'cluster_id': '$cluster_id'
}
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
required=False, required=False,