implements #150 - inteface vlan tagging

This commit is contained in:
John Anderson 2017-10-13 17:59:18 -04:00
parent 34259d5d9d
commit 2335b90e29
9 changed files with 432 additions and 57 deletions

View File

@ -4,8 +4,9 @@ from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from ipam.models import IPAddress from ipam.models import IPAddress, VLAN
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination
from dcim.constants import *
from dcim.models import ( from dcim.models import (
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface, DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
@ -624,6 +625,15 @@ class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
] ]
# Cannot import ipam.api.NestedVLANSerializer due to circular dependency
class InterfaceVLANSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
class Meta:
model = VLAN
fields = ['id', 'url', 'vid', 'name', 'display_name']
class InterfaceSerializer(serializers.ModelSerializer): class InterfaceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES) form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
@ -631,12 +641,15 @@ class InterfaceSerializer(serializers.ModelSerializer):
is_connected = serializers.SerializerMethodField(read_only=True) is_connected = serializers.SerializerMethodField(read_only=True)
interface_connection = serializers.SerializerMethodField(read_only=True) interface_connection = serializers.SerializerMethodField(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer() circuit_termination = InterfaceCircuitTerminationSerializer()
untagged_vlan = InterfaceVLANSerializer()
mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES)
tagged_vlans = InterfaceVLANSerializer(many=True)
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', 'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'is_connected', 'interface_connection', 'circuit_termination', 'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans',
] ]
def get_is_connected(self, obj): def get_is_connected(self, obj):
@ -681,8 +694,35 @@ class WritableInterfaceSerializer(ValidatedModelSerializer):
model = Interface model = Interface
fields = [ fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', 'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'mode', 'untagged_vlan', 'tagged_vlans',
] ]
def validate(self, data):
# Get the device for later use
if self.instance:
device = self.instance.device
else:
device = data.get('device')
# Validate VLANs belong to the device's site or global
# We have to do this here decause of the ManyToMany relationship
native_vlan = data.get('native_vlan')
if native_vlan:
if native_vlan.site != device.site and native_vlan.site is not None:
raise serializers.ValidationError("Native VLAN is invalid for the interface's device.")
tagged_vlan_members = data.get('tagged_vlan_members')
if tagged_vlan_members:
for vlan in tagged_vlan_members:
if vlan.site != device.site and vlan.site is not None:
raise serializers.ValidationError("Tagged VLAN {} is invalid for the interface's device.".format(vlan))
# Enforce model validation
super(WritableInterfaceSerializer, self).validate(data)
return data
# #
# Device bays # Device bays

View File

@ -193,6 +193,15 @@ WIRELESS_IFACE_TYPES = [
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
IFACE_MODE_ACCESS = 100
IFACE_MODE_TAGGED = 200
IFACE_MODE_TAGGED_ALL = 300
IFACE_MODE_CHOICES = [
[IFACE_MODE_ACCESS, 'Access'],
[IFACE_MODE_TAGGED, 'Tagged'],
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
]
# Device statuses # Device statuses
STATUS_OFFLINE = 0 STATUS_OFFLINE = 0
STATUS_ACTIVE = 1 STATUS_ACTIVE = 1

View File

@ -8,14 +8,15 @@ from django.contrib.postgres.forms.array import SimpleArrayField
from django.db.models import Count, Q from django.db.models import Count, Q
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from ipam.models import IPAddress from ipam.models import IPAddress, VLAN, VLANGroup
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 (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField,
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField,
SlugField, FilterTreeNodeMultipleChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
FilterTreeNodeMultipleChoiceField,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from .formfields import MACAddressFormField from .formfields import MACAddressFormField
@ -27,6 +28,7 @@ from .models import (
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
) )
from .constants import *
DEVICE_BY_PK_RE = '{\d+\}' DEVICE_BY_PK_RE = '{\d+\}'
@ -1587,11 +1589,59 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm):
# Interfaces # Interfaces
# #
class InterfaceForm(BootstrapMixin, forms.ModelForm): class InterfaceForm(BootstrapMixin, forms.ModelForm, ChainedFieldsMixin):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
label='VLAN Site',
widget=forms.Select(
attrs={'filter-for': 'vlan_group', 'nullable': 'true'},
)
)
vlan_group = ChainedModelChoiceField(
queryset=VLANGroup.objects.all(),
chains=(
('site', 'site'),
),
required=False,
label='VLAN group',
widget=APISelect(
attrs={'filter-for': 'untagged_vlan tagged_vlans', 'nullable': 'true'},
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
)
)
untagged_vlan = ChainedModelChoiceField(
queryset=VLAN.objects.all(),
chains=(
('site', 'site'),
('group', 'vlan_group'),
),
required=False,
label='Untagged VLAN',
widget=APISelect(
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',# display_field='display_name'
)
)
tagged_vlans = ChainedModelMultipleChoiceField(
queryset=VLAN.objects.all(),
chains=(
('site', 'site'),
('group', 'vlan_group'),
),
required=False,
label='Tagged VLANs',
widget=APISelectMultiple(
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',# display_field='display_name'
)
)
class Meta: class Meta:
model = Interface model = Interface
fields = ['device', 'name', 'form_factor', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description'] fields = [
'device', 'name', 'form_factor', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only',
'description', 'mode', 'site', 'vlan_group', 'untagged_vlan', 'tagged_vlans',
]
widgets = { widgets = {
'device': forms.HiddenInput(), 'device': forms.HiddenInput(),
} }
@ -1604,13 +1654,65 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
self.fields['lag'].queryset = Interface.objects.order_naturally().filter( self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
device_id=self.data['device'], form_factor=IFACE_FF_LAG device_id=self.data['device'], form_factor=IFACE_FF_LAG
) )
device = Device.objects.get(pk=self.data['device'])
else: else:
self.fields['lag'].queryset = Interface.objects.order_naturally().filter( self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
device=self.instance.device, form_factor=IFACE_FF_LAG device=self.instance.device, form_factor=IFACE_FF_LAG
) )
device = self.instance.device
# Limit the queryset for the site to only include the interface's device's site
if device and device.site:
self.fields['site'].queryset = Site.objects.filter(pk=device.site.id)
self.fields['site'].initial = None
else:
self.fields['site'].queryset = Site.objects.none()
self.fields['site'].initial = None
# Limit the initial vlan choices
if self.is_bound:
filter_dict = {
'group_id': self.data.get('vlan_group') or None,
'site_id': self.data.get('site') or None,
}
elif self.initial.get('untagged_vlan'):
filter_dict = {
'group_id': self.instance.untagged_vlan.group,
'site_id': self.instance.untagged_vlan.site,
}
elif self.initial.get('tagged_vlans'):
filter_dict = {
'group_id': self.instance.tagged_vlans.first().group,
'site_id': self.instance.tagged_vlans.first().site,
}
else:
filter_dict = {
'group_id': None,
'site_id': None,
}
self.fields['untagged_vlan'].queryset = VLAN.objects.filter(**filter_dict)
self.fields['tagged_vlans'].queryset = VLAN.objects.filter(**filter_dict)
def clean_tagged_vlans(self):
"""
Becasue tagged_vlans is a many-to-many relationship, validation must be done in the form
"""
if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and self.cleaned_data['tagged_vlans']:
raise forms.ValidationError(
"An Access interface cannot have tagged VLANs."
)
if self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL and self.cleaned_data['tagged_vlans']:
raise forms.ValidationError(
"Interface mode Tagged All implies all VLANs are tagged. "
"Do not select any tagged VLANs."
)
return self.cleaned_data['tagged_vlans']
class InterfaceCreateForm(ComponentForm): class InterfaceCreateForm(ComponentForm, ChainedFieldsMixin):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES) form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
enabled = forms.BooleanField(required=False) enabled = forms.BooleanField(required=False)
@ -1619,6 +1721,51 @@ class InterfaceCreateForm(ComponentForm):
mac_address = MACAddressFormField(required=False, label='MAC Address') mac_address = MACAddressFormField(required=False, label='MAC Address')
mgmt_only = forms.BooleanField(required=False, label='OOB Management') mgmt_only = forms.BooleanField(required=False, label='OOB Management')
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
mode = forms.ChoiceField(choices=IFACE_MODE_CHOICES)
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
label='VLAN Site',
widget=forms.Select(
attrs={'filter-for': 'vlan_group', 'nullable': 'true'},
)
)
vlan_group = ChainedModelChoiceField(
queryset=VLANGroup.objects.all(),
chains=(
('site', 'site'),
),
required=False,
label='VLAN group',
widget=APISelect(
attrs={'filter-for': 'untagged_vlan tagged_vlans', 'nullable': 'true'},
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
)
)
untagged_vlan = ChainedModelChoiceField(
queryset=VLAN.objects.all(),
chains=(
('site', 'site'),
('group', 'vlan_group'),
),
required=False,
label='Untagged VLAN',
widget=APISelect(
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',# display_field='display_name'
)
)
tagged_vlans = ChainedModelMultipleChoiceField(
queryset=VLAN.objects.all(),
chains=(
('site', 'site'),
('group', 'vlan_group'),
),
required=False,
label='Tagged VLANs',
widget=APISelectMultiple(
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',# display_field='display_name'
)
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -1636,8 +1783,41 @@ class InterfaceCreateForm(ComponentForm):
else: else:
self.fields['lag'].queryset = Interface.objects.none() self.fields['lag'].queryset = Interface.objects.none()
# Limit the queryset for the site to only include the interface's device's site
if self.parent is not None and self.parent.site:
self.fields['site'].queryset = Site.objects.filter(pk=self.parent.site.id)
self.fields['site'].initial = None
else:
self.fields['site'].queryset = Site.objects.none()
self.fields['site'].initial = None
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): # Limit the initial vlan choices
if self.is_bound:
filter_dict = {
'group_id': self.data.get('vlan_group') or None,
'site_id': self.data.get('site') or None,
}
elif self.initial.get('untagged_vlan'):
filter_dict = {
'group_id': self.untagged_vlan.group,
'site_id': self.untagged_vlan.site,
}
elif self.initial.get('tagged_vlans'):
filter_dict = {
'group_id': self.tagged_vlans.first().group,
'site_id': self.tagged_vlans.first().site,
}
else:
filter_dict = {
'group_id': None,
'site_id': None,
}
self.fields['untagged_vlan'].queryset = VLAN.objects.filter(**filter_dict)
self.fields['tagged_vlans'].queryset = VLAN.objects.filter(**filter_dict)
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm, ChainedFieldsMixin):
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput) device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False) form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
@ -1646,9 +1826,54 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU') mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only') mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
mode = forms.ChoiceField(choices=add_blank_choice(IFACE_MODE_CHOICES), required=False)
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
label='VLAN Site',
widget=forms.Select(
attrs={'filter-for': 'vlan_group', 'nullable': 'true'},
)
)
vlan_group = ChainedModelChoiceField(
queryset=VLANGroup.objects.all(),
chains=(
('site', 'site'),
),
required=False,
label='VLAN group',
widget=APISelect(
attrs={'filter-for': 'untagged_vlan tagged_vlans', 'nullable': 'true'},
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
)
)
untagged_vlan = ChainedModelChoiceField(
queryset=VLAN.objects.all(),
chains=(
('site', 'site'),
('group', 'vlan_group'),
),
required=False,
label='Untagged VLAN',
widget=APISelect(
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',# display_field='display_name'
)
)
tagged_vlans = ChainedModelMultipleChoiceField(
queryset=VLAN.objects.all(),
chains=(
('site', 'site'),
('group', 'vlan_group'),
),
required=False,
label='Tagged VLANs',
widget=APISelectMultiple(
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',# display_field='display_name'
)
)
class Meta: class Meta:
nullable_fields = ['lag', 'mtu', 'description'] nullable_fields = ['lag', 'mtu', 'description', 'untagged_vlan', 'tagged_vlans']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs) super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
@ -1668,6 +1893,22 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
else: else:
self.fields['lag'].choices = [] self.fields['lag'].choices = []
# Limit the queryset for the site to only include the interface's device's site
if device and device.site:
self.fields['site'].queryset = Site.objects.filter(pk=device.site.id)
self.fields['site'].initial = None
else:
self.fields['site'].queryset = Site.objects.none()
self.fields['site'].initial = None
filter_dict = {
'group_id': None,
'site_id': None,
}
self.fields['untagged_vlan'].queryset = VLAN.objects.filter(**filter_dict)
self.fields['tagged_vlans'].queryset = VLAN.objects.filter(**filter_dict)
class InterfaceBulkDisconnectForm(ConfirmationForm): class InterfaceBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-13 20:22
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('ipam', '0020_ipaddress_add_role_carp'),
('dcim', '0048_rack_serial'),
]
operations = [
migrations.AddField(
model_name='interface',
name='mode',
field=models.PositiveSmallIntegerField(choices=[[100, 'Access'], [200, 'Tagged'], [300, 'Tagged All']], default=100),
),
migrations.AddField(
model_name='interface',
name='tagged_vlans',
field=models.ManyToManyField(blank=True, related_name='interfaces_as_tagged', to='ipam.VLAN', verbose_name='Tagged VLANs'),
),
migrations.AddField(
model_name='interface',
name='untagged_vlan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'),
),
]

View File

@ -1220,6 +1220,20 @@ class Interface(models.Model):
help_text="This interface is used only for out-of-band management" help_text="This interface is used only for out-of-band management"
) )
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
mode = models.PositiveSmallIntegerField(choices=IFACE_MODE_CHOICES, default=IFACE_MODE_ACCESS)
untagged_vlan = models.ForeignKey(
to='ipam.VLAN',
null=True,
blank=True,
verbose_name='Untagged VLAN',
related_name='interfaces_as_untagged'
)
tagged_vlans = models.ManyToManyField(
to='ipam.VLAN',
blank=True,
verbose_name='Tagged VLANs',
related_name='interfaces_as_tagged'
)
objects = InterfaceQuerySet.as_manager() objects = InterfaceQuerySet.as_manager()

View File

@ -71,59 +71,65 @@ $(document).ready(function() {
$('select[filter-for]').change(function() { $('select[filter-for]').change(function() {
// Resolve child field by ID specified in parent // Resolve child field by ID specified in parent
var child_name = $(this).attr('filter-for'); var child_names = $(this).attr('filter-for');
var child_field = $('#id_' + child_name); var parent = this;
var child_selected = child_field.val();
// Wipe out any existing options within the child field and create a default option // allow more than one child
child_field.empty(); $.each(child_names.split(" "), function(_, child_name){
if (!child_field.attr('multiple')) {
child_field.append($("<option></option>").attr("value", "").text("---------"));
}
if ($(this).val() || $(this).attr('nullable') == 'true') { var child_field = $('#id_' + child_name);
var api_url = child_field.attr('api-url') + '&limit=1000'; var child_selected = child_field.val();
var disabled_indicator = child_field.attr('disabled-indicator');
var initial_value = child_field.attr('initial');
var display_field = child_field.attr('display-field') || 'name';
// Determine the filter fields needed to make an API call // Wipe out any existing options within the child field and create a default option
var filter_regex = /\{\{([a-z_]+)\}\}/g; child_field.empty();
var match; if (!child_field.attr('multiple')) {
var rendered_url = api_url; child_field.append($("<option></option>").attr("value", "").text("---------"));
while (match = filter_regex.exec(api_url)) {
var filter_field = $('#id_' + match[1]);
if (filter_field.val()) {
rendered_url = rendered_url.replace(match[0], filter_field.val());
} else if (filter_field.attr('nullable') == 'true') {
rendered_url = rendered_url.replace(match[0], '0');
}
} }
// If all URL variables have been replaced, make the API call if ($(parent).val() || $(parent).attr('nullable') == 'true') {
if (rendered_url.search('{{') < 0) { var api_url = child_field.attr('api-url') + '&limit=1000';
console.log(child_name + ": Fetching " + rendered_url); var disabled_indicator = child_field.attr('disabled-indicator');
$.ajax({ var initial_value = child_field.attr('initial');
url: rendered_url, var display_field = child_field.attr('display-field') || 'name';
dataType: 'json',
success: function(response, status) { // Determine the filter fields needed to make an API call
$.each(response.results, function(index, choice) { var filter_regex = /\{\{([a-z_]+)\}\}/g;
var option = $("<option></option>").attr("value", choice.id).text(choice[display_field]); var match;
if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) { var rendered_url = api_url;
option.attr("disabled", "disabled"); while (match = filter_regex.exec(api_url)) {
} else if (choice.id == child_selected) { var filter_field = $('#id_' + match[1]);
option.attr("selected", "selected"); if (filter_field.val()) {
} rendered_url = rendered_url.replace(match[0], filter_field.val());
child_field.append(option); } else if (filter_field.attr('nullable') == 'true') {
}); rendered_url = rendered_url.replace(match[0], '0');
} }
}); }
// If all URL variables have been replaced, make the API call
if (rendered_url.search('{{') < 0) {
console.log(child_name + ": Fetching " + rendered_url);
$.ajax({
url: rendered_url,
dataType: 'json',
success: function(response, status) {
$.each(response.results, function(index, choice) {
var option = $("<option></option>").attr("value", choice.id).text(choice[display_field]);
if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) {
option.attr("disabled", "disabled");
} else if (choice.id == child_selected) {
option.attr("selected", "selected");
}
child_field.append(option);
});
}
});
}
} }
} // Trigger change event in case the child field is the parent of another field
child_field.change();
// Trigger change event in case the child field is the parent of another field });
child_field.change();
}); });
}); });

View File

@ -0,0 +1,7 @@
from utilities.forms import ChainedModelMultipleChoiceField
# Fields which are used on ManyToMany relationships
M2M_FIELD_TYPES = [
ChainedModelMultipleChoiceField,
]

View File

@ -766,6 +766,26 @@ class ComponentCreateView(View):
if not form.errors: if not form.errors:
self.model.objects.bulk_create(new_components) self.model.objects.bulk_create(new_components)
# ManyToMany relations are bulk created via the through model
m2m_fields = [field for field in component_form.fields if type(component_form.fields[field]) in M2M_FIELD_TYPES]
if m2m_fields:
for field in m2m_fields:
field_links = []
for new_component in new_components:
for related_obj in component_form.cleaned_data[field]:
# The through model columns are the id's of our M2M relation objects
through_kwargs = {}
new_component_column = new_component.__class__.__name__ + '_id'
related_obj_column = related_obj.__class__.__name__ + '_id'
through_kwargs.update({
new_component_column.lower(): new_component.id,
related_obj_column.lower(): related_obj.id
})
field_link = getattr(self.model, field).through(**through_kwargs)
field_links.append(field_link)
getattr(self.model, field).through.objects.bulk_create(field_links)
messages.success(request, "Added {} {} to {}.".format( messages.success(request, "Added {} {} to {}.".format(
len(new_components), self.model._meta.verbose_name_plural, parent len(new_components), self.model._meta.verbose_name_plural, parent
)) ))

View File

@ -255,3 +255,9 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
def get_status_class(self): def get_status_class(self):
return VM_STATUS_CLASSES[self.status] return VM_STATUS_CLASSES[self.status]
@property
def site(self):
# used when a child compent (eg Interface) needs to know its parent's site but
# the parent could be either a device or a virtual machine
return self.cluster.site