From c2793e90dc1191ed21d50a10f138818e6f889d88 Mon Sep 17 00:00:00 2001 From: Shawn Peng Date: Thu, 12 Jan 2017 18:52:20 -0800 Subject: [PATCH] Enable global vlan 1. Decouple site/vlan, make site optional for vlan/vlangroup 2. Change html generation code to check site existence before dereference 3. Create site search function, if site is None for a VLAN, view it as global VLAN --- netbox/ipam/filters.py | 24 ++++++++++++++++++++---- netbox/ipam/forms.py | 9 +++++---- netbox/ipam/models.py | 7 ++++--- netbox/project-static/js/forms.js | 14 +++++++++++--- netbox/templates/ipam/vlan.html | 6 +++++- 5 files changed, 45 insertions(+), 15 deletions(-) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index a0dc9f633..558f5eade 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -262,37 +262,47 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): class VLANGroupFilter(django_filters.FilterSet): - site_id = django_filters.ModelMultipleChoiceFilter( + site_id = NullableModelMultipleChoiceFilter( name='site', queryset=Site.objects.all(), label='Site (ID)', + method='site_search', ) - site = django_filters.ModelMultipleChoiceFilter( + site = NullableModelMultipleChoiceFilter( name='site__slug', queryset=Site.objects.all(), to_field_name='slug', label='Site (slug)', + method='site_search', ) class Meta: model = VLANGroup + def site_search(self, queryset, name, value): + q = Q(**{name: None}) + for v in value: + q |= Q(**{name: v}) + return queryset.filter(q) + class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): q = django_filters.MethodFilter( action='search', label='Search', ) - site_id = django_filters.ModelMultipleChoiceFilter( + site_id = NullableModelMultipleChoiceFilter( name='site', queryset=Site.objects.all(), label='Site (ID)', + method='site_search', ) - site = django_filters.ModelMultipleChoiceFilter( + site = NullableModelMultipleChoiceFilter( name='site__slug', queryset=Site.objects.all(), to_field_name='slug', label='Site (slug)', + method='site_search', ) group_id = NullableModelMultipleChoiceFilter( name='group', @@ -349,6 +359,12 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): pass return queryset.filter(qs_filter) + def site_search(self, queryset, name, value): + q = Q(**{name: None}) + for v in value: + q |= Q(**{name: v}) + return queryset.filter(q) + class ServiceFilter(django_filters.FilterSet): device_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 7cb04cc60..67a70fe94 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -151,7 +151,8 @@ class RoleForm(BootstrapMixin, forms.ModelForm): class PrefixForm(BootstrapMixin, CustomFieldForm): site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site', - widget=forms.Select(attrs={'filter-for': 'vlan'})) + widget=forms.Select(attrs={'filter-for': 'vlan', + 'default_value': '0'})) vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN', widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name')) @@ -171,7 +172,7 @@ class PrefixForm(BootstrapMixin, CustomFieldForm): elif self.initial.get('site'): self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site']) else: - self.fields['vlan'].choices = [] + self.fields['vlan'].queryset = VLAN.objects.filter(site=None) class PrefixFromCSVForm(forms.ModelForm): @@ -490,7 +491,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm): 'role': "The primary function of this VLAN", } widgets = { - 'site': forms.Select(attrs={'filter-for': 'group'}), + 'site': forms.Select(attrs={'filter-for': 'group', 'default_value': '0'}), } def __init__(self, *args, **kwargs): @@ -503,7 +504,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm): elif self.initial.get('site'): self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site']) else: - self.fields['group'].choices = [] + self.fields['group'].queryset = VLANGroup.objects.filter(site=None) class VLANFromCSVForm(forms.ModelForm): diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index fa42d68f1..814c33f38 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -460,7 +460,7 @@ class VLANGroup(models.Model): """ name = models.CharField(max_length=50) slug = models.SlugField() - site = models.ForeignKey('dcim.Site', related_name='vlan_groups') + site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.SET_NULL, blank=True, null=True) class Meta: ordering = ['site', 'name'] @@ -472,7 +472,8 @@ class VLANGroup(models.Model): verbose_name_plural = 'VLAN groups' def __unicode__(self): - return u'{} - {}'.format(self.site.name, self.name) + site_name = self.site.name if self.site else '__global' + return u'{} - {}'.format(site_name, self.name) def get_absolute_url(self): return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) @@ -487,7 +488,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero or more Prefixes assigned to it. """ - site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT) + site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT, blank=True, null=True) group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT) vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[ MinValueValidator(1), diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 3adc9c13f..f93e63127 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -61,6 +61,7 @@ $(document).ready(function() { // API select widget $('select[filter-for]').change(function () { + var choice = $(this).val() // Resolve child field by ID specified in parent var child_name = $(this).attr('filter-for'); @@ -70,7 +71,10 @@ $(document).ready(function() { child_field.empty(); child_field.append($("").attr("value", "").text("")); - if ($(this).val()) { + if (!choice && $(this).attr('default_value')) { + choice = $(this).attr('default_value') + } + if (choice) { var api_url = child_field.attr('api-url'); var disabled_indicator = child_field.attr('disabled-indicator'); @@ -80,8 +84,12 @@ $(document).ready(function() { // Gather the values of all other filter fields for this child $("select[filter-for='" + child_name + "']").each(function() { var filter_field = $(this); - if (filter_field.val()) { - api_url = api_url.replace('{{' + filter_field.attr('name') + '}}', filter_field.val()); + var choice = filter_field.val() + if (!choice && filter_field.attr('default_value')) { + choice = filter_field.attr('default_value') + } + if (choice) { + api_url = api_url.replace('{{' + filter_field.attr('name') + '}}', choice); } else { // Not all filters have been selected yet return false; diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 3d81392e0..f6d17a8d9 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -53,7 +53,11 @@ - +
Site{{ vlan.site }} + {% if vlan.site %} + {{ vlan.site }} + {% endif %} +
Group