Skip to content

Commit

Permalink
Add in in-line vlan editing and Bulk vlan editing (#3350)
Browse files Browse the repository at this point in the history
* Fixes #3341 - Added in-line vlan editing
* Fixes #2160 - Added bulk vlan editing

Inconsequential behaviour changes:

* APISelect can now take "full=True" to return a non-brief set
* Select2 will no group by "group & site, group, site, global" if full=True is set in APISelect
  • Loading branch information
DanSheps authored Sep 6, 2019
1 parent 8f5e73a commit 9c6dbd7
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 157 deletions.
186 changes: 78 additions & 108 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ def get_device_by_name_or_pk(name):
return device


class InterfaceCommonForm:
def clean(self):

super().clean()

# Validate VLAN assignments
tagged_vlans = self.cleaned_data['tagged_vlans']

# Untagged interfaces cannot be assigned tagged VLANs
if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans:
raise forms.ValidationError({
'mode': "An access interface cannot have tagged VLANs assigned."
})

# Remove all tagged VLAN assignments from "tagged all" interfaces
elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
self.cleaned_data['tagged_vlans'] = []


class BulkRenameForm(forms.Form):
"""
An extendable form to be used for renaming device components in bulk.
Expand Down Expand Up @@ -2110,7 +2129,26 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm):
# Interfaces
#

class InterfaceForm(BootstrapMixin, forms.ModelForm):
class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
untagged_vlan = forms.ModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)
tagged_vlans = forms.ModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)

tags = TagField(
required=False
)
Expand Down Expand Up @@ -2149,112 +2187,8 @@ def __init__(self, *args, **kwargs):
device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG
)

def clean(self):

super().clean()

# Validate VLAN assignments
tagged_vlans = self.cleaned_data['tagged_vlans']

# Untagged interfaces cannot be assigned tagged VLANs
if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans:
raise forms.ValidationError({
'mode': "An access interface cannot have tagged VLANs assigned."
})

# Remove all tagged VLAN assignments from "tagged all" interfaces
elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
self.cleaned_data['tagged_vlans'] = []


class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
vlans = forms.MultipleChoiceField(
choices=[],
label='VLANs',
widget=StaticSelect2Multiple(
attrs={
'size': 20,
}
)
)
tagged = forms.BooleanField(
required=False,
initial=True
)

class Meta:
model = Interface
fields = []

def __init__(self, *args, **kwargs):

super().__init__(*args, **kwargs)

if self.instance.mode == IFACE_MODE_ACCESS:
self.initial['tagged'] = False

# Find all VLANs already assigned to the interface for exclusion from the list
assigned_vlans = [v.pk for v in self.instance.tagged_vlans.all()]
if self.instance.untagged_vlan is not None:
assigned_vlans.append(self.instance.untagged_vlan.pk)

# Compile VLAN choices
vlan_choices = []

# Add non-grouped global VLANs
global_vlans = VLAN.objects.filter(site=None, group=None).exclude(pk__in=assigned_vlans)
vlan_choices.append(
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
)

# Add grouped global VLANs
for group in VLANGroup.objects.filter(site=None):
global_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)
vlan_choices.append(
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
)

site = getattr(self.instance.parent, 'site', None)
if site is not None:

# Add non-grouped site VLANs
site_vlans = VLAN.objects.filter(site=site, group=None).exclude(pk__in=assigned_vlans)
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))

# Add grouped site VLANs
for group in VLANGroup.objects.filter(site=site):
site_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)
vlan_choices.append((
'{} / {}'.format(group.site.name, group.name),
[(vlan.pk, vlan) for vlan in site_group_vlans]
))

self.fields['vlans'].choices = vlan_choices

def clean(self):

super().clean()

# Only untagged VLANs permitted on an access interface
if self.instance.mode == IFACE_MODE_ACCESS and len(self.cleaned_data['vlans']) > 1:
raise forms.ValidationError("Only one VLAN may be assigned to an access interface.")

# 'tagged' is required if more than one VLAN is selected
if not self.cleaned_data['tagged'] and len(self.cleaned_data['vlans']) > 1:
raise forms.ValidationError("Only one untagged VLAN may be selected.")

def save(self, *args, **kwargs):

if self.cleaned_data['tagged']:
for vlan in self.cleaned_data['vlans']:
self.instance.tagged_vlans.add(vlan)
else:
self.instance.untagged_vlan_id = self.cleaned_data['vlans'][0]

return super().save(*args, **kwargs)


class InterfaceCreateForm(ComponentForm, forms.Form):
class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
name_pattern = ExpandableNameField(
label='Name'
)
Expand Down Expand Up @@ -2298,6 +2232,24 @@ class InterfaceCreateForm(ComponentForm, forms.Form):
tags = TagField(
required=False
)
untagged_vlan = forms.ModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)
tagged_vlans = forms.ModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)

def __init__(self, *args, **kwargs):

Expand All @@ -2316,7 +2268,7 @@ def __init__(self, *args, **kwargs):
self.fields['lag'].queryset = Interface.objects.none()


class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Interface.objects.all(),
widget=forms.MultipleHiddenInput()
Expand Down Expand Up @@ -2360,10 +2312,28 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
required=False,
widget=StaticSelect2()
)
untagged_vlan = forms.ModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)
tagged_vlans = forms.ModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)

class Meta:
nullable_fields = [
'lag', 'mac_address', 'mtu', 'description', 'mode',
'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
]

def __init__(self, *args, **kwargs):
Expand Down
1 change: 0 additions & 1 deletion netbox/dcim/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@
path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
path(r'interfaces/<int:pk>/assign-vlans/', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
path(r'interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
path(r'interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
Expand Down
6 changes: 0 additions & 6 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,12 +1348,6 @@ class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
template_name = 'dcim/interface_edit.html'


class InterfaceAssignVLANsView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_interface'
model = Interface
model_form = forms.InterfaceAssignVLANsForm


class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_interface'
model = Interface
Expand Down
73 changes: 64 additions & 9 deletions netbox/project-static/js/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,13 @@ $(document).ready(function() {
// Base query params
var parameters = {
q: params.term,
brief: 1,
limit: 50,
offset: offset,
};

// Allow for controlling the brief setting from within APISelect
parameters.brief = ( $('#id_untagged_vlan').is('[data-full]') ? undefined : true );

// filter-for fields from a chain
var attr_name = "data-filter-for-" + $(element).attr("name");
var form = $(element).closest('form');
Expand Down Expand Up @@ -194,18 +196,41 @@ $(document).ready(function() {

processResults: function (data) {
var element = this.$element[0];
// Clear any disabled options
$(element).children('option').attr('disabled', false);
var results = $.map(data.results, function (obj) {
obj.text = obj[element.getAttribute('display-field')] || obj.name;
obj.id = obj[element.getAttribute('value-field')] || obj.id;
var results = data.results;

if(element.getAttribute('disabled-indicator') && obj[element.getAttribute('disabled-indicator')]) {
results = results.reduce((results,record) => {
record.text = record[element.getAttribute('display-field')] || record.name;
record.id = record[element.getAttribute('value-field')] || record.id;
if(element.getAttribute('disabled-indicator') && record[element.getAttribute('disabled-indicator')]) {
// The disabled-indicator equated to true, so we disable this option
obj.disabled = true;
record.disabled = true;
}
return obj;
});

if( record.group !== undefined && record.group !== null && record.site !== undefined && record.site !== null ) {
results[record.site.name + ":" + record.group.name] = results[record.site.name + ":" + record.group.name] || { text: record.site.name + " / " + record.group.name, children: [] }
results[record.site.name + ":" + record.group.name].children.push(record);
}
else if( record.group !== undefined && record.group !== null ) {
results[record.group.name] = results[record.group.name] || { text: record.group.name, children: [] }
results[record.group.name].children.push(record);
}
else if( record.site !== undefined && record.site !== null ) {
results[record.site.name] = results[record.site.name] || { text: record.site.name, children: [] }
results[record.site.name].children.push(record);
}
else if ( (record.group !== undefined || record.group == null) && (record.site !== undefined || record.site === null) ) {
results['global'] = results['global'] || { text: 'Global', children: [] }
results['global'].children.push(record);
}
else {
results[record.id] = record
}

return results;
},Object.create(null));

results = Object.values(results);

// Handle the null option, but only add it once
if (element.getAttribute('data-null-option') && data.previous === null) {
Expand Down Expand Up @@ -300,4 +325,34 @@ $(document).ready(function() {
$('#id_tags').append(option).trigger('change');
}
});

if( $('select#id_mode').length > 0 ) {
$('select#id_mode').on('change', function () {
if ($(this).val() == '') {
$('select#id_untagged_vlan').val();
$('select#id_untagged_vlan').trigger('change');
$('select#id_tagged_vlans').val([]);
$('select#id_tagged_vlans').trigger('change');
$('select#id_untagged_vlan').parent().parent().hide();
$('select#id_tagged_vlans').parent().parent().hide();
}
else if ($(this).val() == 100) {
$('select#id_tagged_vlans').val([]);
$('select#id_tagged_vlans').trigger('change');
$('select#id_untagged_vlan').parent().parent().show();
$('select#id_tagged_vlans').parent().parent().hide();
}
else if ($(this).val() == 200) {
$('select#id_untagged_vlan').parent().parent().show();
$('select#id_tagged_vlans').parent().parent().show();
}
else if ($(this).val() == 300) {
$('select#id_tagged_vlans').val([]);
$('select#id_tagged_vlans').trigger('change');
$('select#id_untagged_vlan').parent().parent().show();
$('select#id_tagged_vlans').parent().parent().hide();
}
});
$('select#id_mode').trigger('change');
}
});
Loading

0 comments on commit 9c6dbd7

Please sign in to comment.