IPAM Select2 forms and changelog

This commit is contained in:
John Anderson 2019-01-10 21:19:13 -05:00
parent 5f1f8ee73b
commit ad4fb3ce8b
6 changed files with 268 additions and 140 deletions

View File

@ -1,5 +1,9 @@
Select2 issues Select2 issues
* [#2516](https://github.com/digitalocean/netbox/issues/2516) - Implemented Select2 for all Model backed selection fields
* [#2590](https://github.com/digitalocean/netbox/issues/2590) - Implemented the color picker with Select2 to show colors in the background * [#2590](https://github.com/digitalocean/netbox/issues/2590) - Implemented the color picker with Select2 to show colors in the background
* [#2735](https://github.com/digitalocean/netbox/issues/2735) - Implemented Select2 for all list filter form select elements
* [#2753](https://github.com/digitalocean/netbox/issues/2753) - Implemented Select2 to replace most all instances of select fields in forms
v2.5.3 (FUTURE) v2.5.3 (FUTURE)

View File

@ -289,8 +289,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=CIRCUIT_STATUS_CHOICES, choices=CIRCUIT_STATUS_CHOICES,
annotate=Circuit.objects.all(),
annotate_field='status',
required=False, required=False,
widget=StaticSelect2Multiple() widget=StaticSelect2Multiple()
) )

View File

@ -1600,7 +1600,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
required=False, required=False,
label='Type', label='Type',
widget=APISelect( widget=APISelect(
api_url="/api/dcim/device-types" api_url="/api/dcim/device-types/"
) )
) )
device_role = forms.ModelChoiceField( device_role = forms.ModelChoiceField(
@ -1608,21 +1608,21 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
required=False, required=False,
label='Role', label='Role',
widget=APISelect( widget=APISelect(
api_url="/api/dcim/device-roles" api_url="/api/dcim/device-roles/"
) )
) )
tenant = forms.ModelChoiceField( tenant = forms.ModelChoiceField(
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 = forms.ModelChoiceField(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
api_url="/api/dcim/platforms" api_url="/api/dcim/platforms/"
) )
) )
status = forms.ChoiceField( status = forms.ChoiceField(

View File

@ -1,7 +1,6 @@
from django import forms from django import forms
from django.core.exceptions import MultipleObjectsReturned from django.core.exceptions import MultipleObjectsReturned
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db.models import Count
from taggit.forms import TagField from taggit.forms import TagField
from dcim.models import Site, Rack, Device, Interface from dcim.models import Site, Rack, Device, Interface
@ -9,9 +8,9 @@ from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEdit
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
AnnotatedMultipleChoiceField, APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, SlugField,
SlugField, add_blank_choice, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .constants import ( from .constants import (
@ -77,7 +76,10 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm
) )
tenant = forms.ModelChoiceField( tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False,
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
) )
enforce_unique = forms.NullBooleanField( enforce_unique = forms.NullBooleanField(
required=False, required=False,
@ -102,11 +104,14 @@ class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
label='Search' label='Search'
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate( queryset=Tenant.objects.all(),
filter_count=Count('vrfs')
),
to_field_name='slug', to_field_name='slug',
null_label='-- None --' null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
) )
@ -139,12 +144,8 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
is_private = forms.NullBooleanField( is_private = forms.NullBooleanField(
required=False, required=False,
label='Private', label='Private',
widget=forms.Select( widget=StaticSelect2(
choices=[ choices=BOOLEAN_WITH_BLANK_CHOICES
('', '---------'),
('True', 'Yes'),
('False', 'No'),
]
) )
) )
@ -168,6 +169,11 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
'rir': "Regional Internet Registry responsible for this prefix", 'rir': "Regional Internet Registry responsible for this prefix",
'date_added': "Format: YYYY-MM-DD", 'date_added': "Format: YYYY-MM-DD",
} }
widgets = {
'rir': APISelect(
api_url="/api/ipam/rirs/"
)
}
class AggregateCSVForm(forms.ModelForm): class AggregateCSVForm(forms.ModelForm):
@ -193,7 +199,10 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
rir = forms.ModelChoiceField( rir = forms.ModelChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
required=False, required=False,
label='RIR' label='RIR',
widget=APISelect(
api_url="/api/ipam/rirs/"
)
) )
date_added = forms.DateField( date_added = forms.DateField(
required=False required=False
@ -218,12 +227,17 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
family = forms.ChoiceField( family = forms.ChoiceField(
required=False, required=False,
choices=IP_FAMILY_CHOICES, choices=IP_FAMILY_CHOICES,
label='Address family' label='Address family',
widget=StaticSelect2()
) )
rir = FilterChoiceField( rir = FilterChoiceField(
queryset=RIR.objects.annotate(filter_count=Count('aggregates')), queryset=RIR.objects.all(),
to_field_name='slug', to_field_name='slug',
label='RIR' label='RIR',
widget=APISelectMultiple(
api_url="/api/ipam/rirs/",
value_field="slug",
)
) )
@ -261,9 +275,13 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
label='Site', label='Site',
widget=forms.Select( widget=APISelect(
api_url="/api/dcim/sites/",
filter_for={
'vlan_group': 'site_id',
'vlan': 'site_id',
},
attrs={ attrs={
'filter-for': 'vlan_group',
'nullable': 'true', 'nullable': 'true',
} }
) )
@ -276,9 +294,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
required=False, required=False,
label='VLAN group', label='VLAN group',
widget=APISelect( widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}', api_url='/api/ipam/vlan-groups/',
filter_for={
'vlan': 'group_id'
},
attrs={ attrs={
'filter-for': 'vlan',
'nullable': 'true', 'nullable': 'true',
} }
) )
@ -292,7 +312,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
required=False, required=False,
label='VLAN', label='VLAN',
widget=APISelect( widget=APISelect(
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', api_url='/api/ipam/vlans/',
display_field='display_name' display_field='display_name'
) )
) )
@ -304,6 +324,15 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant', 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant',
'tags', 'tags',
] ]
widgets = {
'vrf': APISelect(
api_url="/api/ipam/vrfs/"
),
'status': StaticSelect2(),
'role': APISelect(
api_url="/api/ipam/roles/"
)
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -415,12 +444,18 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
) )
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False required=False,
widget=APISelect(
api_url="/api/dcim/sites/"
)
) )
vrf = forms.ModelChoiceField( vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF' label='VRF',
widget=APISelect(
api_url="/api/ipam/vrfs/"
)
) )
prefix_length = forms.IntegerField( prefix_length = forms.IntegerField(
min_value=1, min_value=1,
@ -429,15 +464,22 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
) )
tenant = forms.ModelChoiceField( tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False,
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
) )
status = forms.ChoiceField( status = forms.ChoiceField(
choices=add_blank_choice(PREFIX_STATUS_CHOICES), choices=add_blank_choice(PREFIX_STATUS_CHOICES),
required=False required=False,
widget=StaticSelect2()
) )
role = forms.ModelChoiceField( role = forms.ModelChoiceField(
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False,
widget=APISelect(
api_url="/api/ipam/roles/"
)
) )
is_pool = forms.NullBooleanField( is_pool = forms.NullBooleanField(
required=False, required=False,
@ -473,47 +515,60 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
family = forms.ChoiceField( family = forms.ChoiceField(
required=False, required=False,
choices=IP_FAMILY_CHOICES, choices=IP_FAMILY_CHOICES,
label='Address family' label='Address family',
widget=StaticSelect2()
) )
mask_length = forms.ChoiceField( mask_length = forms.ChoiceField(
required=False, required=False,
choices=PREFIX_MASK_LENGTH_CHOICES, choices=PREFIX_MASK_LENGTH_CHOICES,
label='Mask length' label='Mask length',
widget=StaticSelect2()
) )
vrf = FilterChoiceField( vrf = FilterChoiceField(
queryset=VRF.objects.annotate( queryset=VRF.objects.all(),
filter_count=Count('prefixes')
),
to_field_name='rd', to_field_name='rd',
label='VRF', label='VRF',
null_label='-- Global --' null_label='-- Global --',
widget=APISelectMultiple(
api_url="/api/ipam/vrfs/",
value_field="slug",
null_option=True,
)
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate( queryset=Tenant.objects.all(),
filter_count=Count('prefixes')
),
to_field_name='slug', to_field_name='slug',
null_label='-- None --' null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
) )
status = AnnotatedMultipleChoiceField( )
status = forms.MultipleChoiceField(
choices=PREFIX_STATUS_CHOICES, choices=PREFIX_STATUS_CHOICES,
annotate=Prefix.objects.all(), required=False,
annotate_field='status', widget=StaticSelect2Multiple()
required=False
) )
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate( queryset=Site.objects.all(),
filter_count=Count('prefixes')
),
to_field_name='slug', to_field_name='slug',
null_label='-- None --' null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
null_option=True,
)
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=Role.objects.annotate( queryset=Role.objects.all(),
filter_count=Count('prefixes')
),
to_field_name='slug', to_field_name='slug',
null_label='-- None --' null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/ipam/roles/",
value_field="slug",
null_option=True,
)
) )
expand = forms.BooleanField( expand = forms.BooleanField(
required=False, required=False,
@ -534,9 +589,11 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
label='Site', label='Site',
widget=forms.Select( widget=APISelect(
attrs={ api_url="/api/dcim/sites/",
'filter-for': 'nat_rack' filter_for={
'nat_rack': 'site_id',
'nat_device': 'site_id'
} }
) )
) )
@ -548,10 +605,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
required=False, required=False,
label='Rack', label='Rack',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/racks/?site_id={{nat_site}}', api_url='/api/dcim/racks/',
display_field='display_name', display_field='display_name',
filter_for={
'nat_device': 'rack_id'
},
attrs={ attrs={
'filter-for': 'nat_device',
'nullable': 'true' 'nullable': 'true'
} }
) )
@ -565,9 +624,11 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
required=False, required=False,
label='Device', label='Device',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/devices/?site_id={{nat_site}}&rack_id={{nat_rack}}', api_url='/api/dcim/devices/',
display_field='display_name', display_field='display_name',
attrs={'filter-for': 'nat_inside'} filter_for={
'nat_inside': 'device_id'
}
) )
) )
nat_inside = ChainedModelChoiceField( nat_inside = ChainedModelChoiceField(
@ -578,20 +639,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
required=False, required=False,
label='IP Address', label='IP Address',
widget=APISelect( widget=APISelect(
api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', api_url='/api/ipam/ip-addresses/',
display_field='address' display_field='address'
) )
) )
livesearch = forms.CharField(
required=False,
label='Search',
widget=Livesearch(
query_key='q',
query_url='ipam-api:ipaddress-list',
field_to_update='nat_inside',
obj_label='address'
)
)
primary_for_parent = forms.BooleanField( primary_for_parent = forms.BooleanField(
required=False, required=False,
label='Make this the primary IP for the device/VM' label='Make this the primary IP for the device/VM'
@ -606,6 +657,13 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site', 'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site',
'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags', 'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags',
] ]
widgets = {
'status': StaticSelect2(),
'role': StaticSelect2(),
'vrf': APISelect(
api_url="/api/ipam/vrfs/"
)
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -685,6 +743,13 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
fields = [ fields = [
'address', 'vrf', 'status', 'role', 'description', 'tenant_group', 'tenant', 'address', 'vrf', 'status', 'role', 'description', 'tenant_group', 'tenant',
] ]
widgets = {
'status': StaticSelect2(),
'role': StaticSelect2(),
'vrf': APISelect(
api_url="/api/ipam/vrfs/"
)
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -822,7 +887,10 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
vrf = forms.ModelChoiceField( vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF' label='VRF',
widget=APISelect(
api_url="/api/ipam/vrfs/"
)
) )
mask_length = forms.IntegerField( mask_length = forms.IntegerField(
min_value=1, min_value=1,
@ -831,15 +899,20 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
) )
tenant = forms.ModelChoiceField( tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False,
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
) )
status = forms.ChoiceField( status = forms.ChoiceField(
choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), choices=add_blank_choice(IPADDRESS_STATUS_CHOICES),
required=False required=False,
widget=StaticSelect2()
) )
role = forms.ChoiceField( role = forms.ChoiceField(
choices=add_blank_choice(IPADDRESS_ROLE_CHOICES), choices=add_blank_choice(IPADDRESS_ROLE_CHOICES),
required=False required=False,
widget=StaticSelect2()
) )
description = forms.CharField( description = forms.CharField(
max_length=100, required=False max_length=100, required=False
@ -856,7 +929,10 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF', label='VRF',
empty_label='Global' empty_label='Global',
widget=APISelect(
api_url="/api/ipam/vrfs/"
)
) )
address = forms.CharField( address = forms.CharField(
label='IP Address' label='IP Address'
@ -881,39 +957,45 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
family = forms.ChoiceField( family = forms.ChoiceField(
required=False, required=False,
choices=IP_FAMILY_CHOICES, choices=IP_FAMILY_CHOICES,
label='Address family' label='Address family',
widget=StaticSelect2()
) )
mask_length = forms.ChoiceField( mask_length = forms.ChoiceField(
required=False, required=False,
choices=IPADDRESS_MASK_LENGTH_CHOICES, choices=IPADDRESS_MASK_LENGTH_CHOICES,
label='Mask length' label='Mask length',
widget=StaticSelect2()
) )
vrf = FilterChoiceField( vrf = FilterChoiceField(
queryset=VRF.objects.annotate( queryset=VRF.objects.all(),
filter_count=Count('ip_addresses')
),
to_field_name='rd', to_field_name='rd',
label='VRF', label='VRF',
null_label='-- Global --' null_label='-- Global --',
widget=APISelectMultiple(
api_url="/api/ipam/vrfs/",
value_field="slug",
null_option=True,
)
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate( queryset=Tenant.objects.all(),
filter_count=Count('ip_addresses')
),
to_field_name='slug', to_field_name='slug',
null_label='-- None --' null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
) )
status = AnnotatedMultipleChoiceField( )
status = forms.MultipleChoiceField(
choices=IPADDRESS_STATUS_CHOICES, choices=IPADDRESS_STATUS_CHOICES,
annotate=IPAddress.objects.all(), required=False,
annotate_field='status', widget=StaticSelect2Multiple()
required=False
) )
role = AnnotatedMultipleChoiceField( role = forms.MultipleChoiceField(
choices=IPADDRESS_ROLE_CHOICES, choices=IPADDRESS_ROLE_CHOICES,
annotate=IPAddress.objects.all(), required=False,
annotate_field='role', widget=StaticSelect2Multiple()
required=False
) )
@ -929,6 +1011,11 @@ 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):
@ -953,11 +1040,14 @@ class VLANGroupCSVForm(forms.ModelForm):
class VLANGroupFilterForm(BootstrapMixin, forms.Form): class VLANGroupFilterForm(BootstrapMixin, forms.Form):
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate( queryset=Site.objects.all(),
filter_count=Count('vlan_groups')
),
to_field_name='slug', to_field_name='slug',
null_label='-- Global --' null_label='-- Global --',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
null_option=True,
)
) )
@ -969,9 +1059,12 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
widget=forms.Select( widget=APISelect(
api_url="/api/dcim/sites/",
filter_for={
'group': 'site_id'
},
attrs={ attrs={
'filter-for': 'group',
'nullable': 'true', 'nullable': 'true',
} }
) )
@ -984,7 +1077,7 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
required=False, required=False,
label='Group', label='Group',
widget=APISelect( widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}', api_url='/api/ipam/vlan-groups/',
) )
) )
tags = TagField(required=False) tags = TagField(required=False)
@ -1002,6 +1095,12 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
'status': "Operational status of this VLAN", 'status': "Operational status of this VLAN",
'role': "The primary function of this VLAN", 'role': "The primary function of this VLAN",
} }
widgets = {
'status': StaticSelect2(),
'role': APISelect(
api_url="/api/ipam/roles/"
)
}
class VLANCSVForm(forms.ModelForm): class VLANCSVForm(forms.ModelForm):
@ -1077,23 +1176,36 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
) )
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False required=False,
widget=APISelect(
api_url="/api/dcim/sites/"
)
) )
group = forms.ModelChoiceField( group = forms.ModelChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False required=False,
widget=APISelect(
api_url="/api/ipam/vlan-groups/"
)
) )
tenant = forms.ModelChoiceField( tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False,
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
) )
status = forms.ChoiceField( status = forms.ChoiceField(
choices=add_blank_choice(VLAN_STATUS_CHOICES), choices=add_blank_choice(VLAN_STATUS_CHOICES),
required=False required=False,
widget=StaticSelect2()
) )
role = forms.ModelChoiceField( role = forms.ModelChoiceField(
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False,
widget=APISelect(
api_url="/api/ipam/roles/"
)
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=100,
@ -1113,38 +1225,48 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
label='Search' label='Search'
) )
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate( queryset=Site.objects.all(),
filter_count=Count('vlans')
),
to_field_name='slug', to_field_name='slug',
null_label='-- Global --' null_label='-- Global --',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
null_option=True,
)
) )
group_id = FilterChoiceField( group_id = FilterChoiceField(
queryset=VLANGroup.objects.annotate( queryset=VLANGroup.objects.all(),
filter_count=Count('vlans')
),
label='VLAN group', label='VLAN group',
null_label='-- None --' null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/ipam/vlan-groups/",
null_option=True,
)
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate( queryset=Tenant.objects.all(),
filter_count=Count('vlans')
),
to_field_name='slug', to_field_name='slug',
null_label='-- None --' null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
) )
status = AnnotatedMultipleChoiceField( )
status = forms.MultipleChoiceField(
choices=VLAN_STATUS_CHOICES, choices=VLAN_STATUS_CHOICES,
annotate=VLAN.objects.all(), required=False,
annotate_field='status', widget=StaticSelect2Multiple()
required=False
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=Role.objects.annotate( queryset=Role.objects.all(),
filter_count=Count('vlans')
),
to_field_name='slug', to_field_name='slug',
null_label='-- None --' null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/ipam/roles/",
value_field="slug",
null_option=True,
)
) )
@ -1166,6 +1288,10 @@ class ServiceForm(BootstrapMixin, CustomFieldForm):
'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be " 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
"reachable via all IPs assigned to the device.", "reachable via all IPs assigned to the device.",
} }
widgets = {
'protocol': StaticSelect2(),
'ipaddresses': StaticSelect2Multiple(),
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -1192,10 +1318,11 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
) )
protocol = forms.ChoiceField( protocol = forms.ChoiceField(
choices=add_blank_choice(IP_PROTOCOL_CHOICES), choices=add_blank_choice(IP_PROTOCOL_CHOICES),
required=False required=False,
widget=StaticSelect2Multiple()
) )
port = forms.IntegerField( port = forms.IntegerField(
required=False required=False,
) )
@ -1206,7 +1333,8 @@ class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
) )
protocol = forms.ChoiceField( protocol = forms.ChoiceField(
choices=add_blank_choice(IP_PROTOCOL_CHOICES), choices=add_blank_choice(IP_PROTOCOL_CHOICES),
required=False required=False,
widget=StaticSelect2()
) )
port = forms.IntegerField( port = forms.IntegerField(
validators=[ validators=[
@ -1222,5 +1350,5 @@ class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class Meta: class Meta:
nullable_fields = [ nullable_fields = [
'site', 'group', 'tenant', 'role', 'description', 'site', 'tenant', 'role', 'description',
] ]

View File

@ -60,7 +60,7 @@
{% render_field form.nat_device %} {% render_field form.nat_device %}
</div> </div>
<div class="tab-pane" id="search"> <div class="tab-pane" id="search">
{% render_field form.livesearch %} &nbsp;
</div> </div>
</div> </div>
{% render_field form.nat_inside %} {% render_field form.nat_inside %}

View File

@ -717,17 +717,15 @@ class ChainedFieldsMixin(forms.BaseForm):
filters_dict = {} filters_dict = {}
for (db_field, parent_field) in field.chains: for (db_field, parent_field) in field.chains:
if self.fields[parent_field].widget.attrs.get('nullable'): if self.is_bound and parent_field in self.data and self.data[parent_field]:
filters_dict[db_field] = None
elif self.is_bound and parent_field in self.data and self.data[parent_field]:
filters_dict[db_field] = self.data[parent_field] or None filters_dict[db_field] = self.data[parent_field] or None
elif self.initial.get(parent_field): elif self.initial.get(parent_field):
filters_dict[db_field] = self.initial[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: else:
break break
print(filters_dict)
if filters_dict: if filters_dict:
field.queryset = field.queryset.filter(**filters_dict) field.queryset = field.queryset.filter(**filters_dict)
elif not self.is_bound and getattr(self, 'instance', None) and hasattr(self.instance, field_name): elif not self.is_bound and getattr(self, 'instance', None) and hasattr(self.instance, field_name):