mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Add in in-line vlan editing and Bulk vlan editing (#3350)
* 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
This commit is contained in:
@ -56,6 +56,25 @@ def get_device_by_name_or_pk(name):
|
|||||||
return device
|
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):
|
class BulkRenameForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
An extendable form to be used for renaming device components in bulk.
|
An extendable form to be used for renaming device components in bulk.
|
||||||
@ -2110,7 +2129,26 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm):
|
|||||||
# Interfaces
|
# 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(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -2149,112 +2187,8 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG
|
device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
|
|
||||||
super().clean()
|
class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
|
||||||
|
|
||||||
# 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):
|
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -2298,6 +2232,24 @@ class InterfaceCreateForm(ComponentForm, forms.Form):
|
|||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
@ -2316,7 +2268,7 @@ class InterfaceCreateForm(ComponentForm, forms.Form):
|
|||||||
self.fields['lag'].queryset = Interface.objects.none()
|
self.fields['lag'].queryset = Interface.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
@ -2360,10 +2312,28 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2()
|
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:
|
class Meta:
|
||||||
nullable_fields = [
|
nullable_fields = [
|
||||||
'lag', 'mac_address', 'mtu', 'description', 'mode',
|
'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -209,7 +209,6 @@ urlpatterns = [
|
|||||||
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: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>/', views.InterfaceView.as_view(), name='interface'),
|
||||||
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
|
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>/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>/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}),
|
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
|
||||||
|
@ -1348,12 +1348,6 @@ class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
template_name = 'dcim/interface_edit.html'
|
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):
|
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_interface'
|
permission_required = 'dcim.delete_interface'
|
||||||
model = Interface
|
model = Interface
|
||||||
|
@ -143,11 +143,13 @@ $(document).ready(function() {
|
|||||||
// Base query params
|
// Base query params
|
||||||
var parameters = {
|
var parameters = {
|
||||||
q: params.term,
|
q: params.term,
|
||||||
brief: 1,
|
|
||||||
limit: 50,
|
limit: 50,
|
||||||
offset: offset,
|
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
|
// filter-for fields from a chain
|
||||||
var attr_name = "data-filter-for-" + $(element).attr("name");
|
var attr_name = "data-filter-for-" + $(element).attr("name");
|
||||||
var form = $(element).closest('form');
|
var form = $(element).closest('form');
|
||||||
@ -194,18 +196,41 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
processResults: function (data) {
|
processResults: function (data) {
|
||||||
var element = this.$element[0];
|
var element = this.$element[0];
|
||||||
// Clear any disabled options
|
|
||||||
$(element).children('option').attr('disabled', false);
|
$(element).children('option').attr('disabled', false);
|
||||||
var results = $.map(data.results, function (obj) {
|
var results = data.results;
|
||||||
obj.text = obj[element.getAttribute('display-field')] || obj.name;
|
|
||||||
obj.id = obj[element.getAttribute('value-field')] || obj.id;
|
|
||||||
|
|
||||||
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
|
// 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
|
// Handle the null option, but only add it once
|
||||||
if (element.getAttribute('data-null-option') && data.previous === null) {
|
if (element.getAttribute('data-null-option') && data.previous === null) {
|
||||||
@ -300,4 +325,34 @@ $(document).ready(function() {
|
|||||||
$('#id_tags').append(option).trigger('change');
|
$('#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');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
{% render_field form.mgmt_only %}
|
{% render_field form.mgmt_only %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
{% render_field form.mode %}
|
{% render_field form.mode %}
|
||||||
|
{% render_field form.untagged_vlan %}
|
||||||
|
{% render_field form.tagged_vlans %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@ -22,21 +24,6 @@
|
|||||||
{% render_field form.tags %}
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default" id="vlans_panel">
|
|
||||||
<div class="panel-heading"><strong>802.1Q VLANs</strong></div>
|
|
||||||
{% if obj.mode %}
|
|
||||||
{% include 'dcim/inc/interface_vlans_table.html' %}
|
|
||||||
<div class="panel-footer text-right">
|
|
||||||
<a href="{% url 'dcim:interface_assign_vlans' pk=obj.pk %}?return_url={% url 'dcim:interface_edit' pk=obj.pk %}" class="btn btn-primary btn-xs{% if form.instance.mode == 100 and form.instance.untagged_vlan %} disabled{% endif %}">
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Add VLANs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="panel-body text-center text-muted">
|
|
||||||
<p>802.1Q mode not set</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block buttons %}
|
{% block buttons %}
|
||||||
@ -48,19 +35,4 @@
|
|||||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
|
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(document).ready(function() {
|
|
||||||
$('#clear_untagged_vlan').click(function () {
|
|
||||||
$('input[name="untagged_vlan"]').prop("checked", false);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
$('#clear_tagged_vlans').click(function () {
|
|
||||||
$('input[name="tagged_vlans"]').prop("checked", false);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -298,6 +298,7 @@ class APISelect(SelectWithDisabled):
|
|||||||
conditional_query_params=None,
|
conditional_query_params=None,
|
||||||
additional_query_params=None,
|
additional_query_params=None,
|
||||||
null_option=False,
|
null_option=False,
|
||||||
|
full=False,
|
||||||
*args,
|
*args,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
@ -306,6 +307,8 @@ class APISelect(SelectWithDisabled):
|
|||||||
|
|
||||||
self.attrs['class'] = 'netbox-select2-api'
|
self.attrs['class'] = 'netbox-select2-api'
|
||||||
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:
|
if display_field:
|
||||||
self.attrs['display-field'] = display_field
|
self.attrs['display-field'] = display_field
|
||||||
if value_field:
|
if value_field:
|
||||||
|
@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import Count, ProtectedError
|
from django.db.models import Count, ProtectedError
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
||||||
from django.http import HttpResponse, HttpResponseServerError
|
from django.http import HttpResponse, HttpResponseServerError
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
@ -530,9 +531,13 @@ class BulkEditView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
# Update standard fields. If a field is listed in _nullify, delete its value.
|
# Update standard fields. If a field is listed in _nullify, delete its value.
|
||||||
for name in standard_fields:
|
for name in standard_fields:
|
||||||
if name in form.nullable_fields and name in nullified_fields:
|
if name in form.nullable_fields and name in nullified_fields and isinstance(form.cleaned_data[name], QuerySet):
|
||||||
|
getattr(obj, name).set([])
|
||||||
|
elif name in form.nullable_fields and name in nullified_fields:
|
||||||
setattr(obj, name, '' if isinstance(form.fields[name], CharField) else None)
|
setattr(obj, name, '' if isinstance(form.fields[name], CharField) else None)
|
||||||
elif form.cleaned_data[name] not in (None, ''):
|
elif isinstance(form.cleaned_data[name], QuerySet) and form.cleaned_data[name]:
|
||||||
|
getattr(obj, name).set(form.cleaned_data[name])
|
||||||
|
elif form.cleaned_data[name] not in (None, '') and not isinstance(form.cleaned_data[name], QuerySet):
|
||||||
setattr(obj, name, form.cleaned_data[name])
|
setattr(obj, name, form.cleaned_data[name])
|
||||||
obj.full_clean()
|
obj.full_clean()
|
||||||
obj.save()
|
obj.save()
|
||||||
|
Reference in New Issue
Block a user