\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
# IP addresses
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index e36162938..25475eec8 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -476,6 +476,20 @@ class PrefixView(View):
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
duplicate_prefix_table.exclude = ('vrf',)
+ return render(request, 'ipam/prefix.html', {
+ 'prefix': prefix,
+ 'aggregate': aggregate,
+ 'parent_prefix_table': parent_prefix_table,
+ 'duplicate_prefix_table': duplicate_prefix_table,
+ })
+
+
+class PrefixPrefixesView(View):
+
+ def get(self, request, pk):
+
+ prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
+
# Child prefixes table
child_prefixes = Prefix.objects.filter(
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
@@ -484,15 +498,16 @@ class PrefixView(View):
).annotate_depth(limit=0)
if child_prefixes:
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
- child_prefix_table = tables.PrefixDetailTable(child_prefixes)
+
+ prefix_table = tables.PrefixDetailTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
- child_prefix_table.columns.show('pk')
+ prefix_table.columns.show('pk')
paginate = {
'klass': EnhancedPaginator,
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
}
- RequestConfig(request, paginate).configure(child_prefix_table)
+ RequestConfig(request, paginate).configure(prefix_table)
# Compile permissions list for rendering the object table
permissions = {
@@ -501,15 +516,12 @@ class PrefixView(View):
'delete': request.user.has_perm('ipam.delete_prefix'),
}
- return render(request, 'ipam/prefix.html', {
+ return render(request, 'ipam/prefix_prefixes.html', {
'prefix': prefix,
- 'aggregate': aggregate,
- 'parent_prefix_table': parent_prefix_table,
- 'child_prefix_table': child_prefix_table,
- 'duplicate_prefix_table': duplicate_prefix_table,
- 'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf or '0', prefix.prefix),
+ 'first_available_prefix': prefix.get_first_available_prefix(),
+ 'prefix_table': prefix_table,
'permissions': permissions,
- 'return_url': prefix.get_absolute_url(),
+ 'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
})
@@ -544,6 +556,7 @@ class PrefixIPAddressesView(View):
return render(request, 'ipam/prefix_ipaddresses.html', {
'prefix': prefix,
+ 'first_available_ip': prefix.get_first_available_ip(),
'ip_table': ip_table,
'permissions': permissions,
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
diff --git a/netbox/netbox/api.py b/netbox/netbox/api.py
index 0bac06977..28a0d7685 100644
--- a/netbox/netbox/api.py
+++ b/netbox/netbox/api.py
@@ -20,6 +20,9 @@ class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer):
def show_form_for_method(self, *args, **kwargs):
return False
+ def get_filter_form(self, data, view, request):
+ return None
+
#
# Authentication
diff --git a/netbox/templates/dcim/device_lldp_neighbors.html b/netbox/templates/dcim/device_lldp_neighbors.html
index c79cf8955..4fe914f64 100644
--- a/netbox/templates/dcim/device_lldp_neighbors.html
+++ b/netbox/templates/dcim/device_lldp_neighbors.html
@@ -58,9 +58,10 @@ $(document).ready(function() {
// Glean configured hostnames/interfaces from the DOM
var configured_device = row.children('td.configured_device').attr('data');
var configured_interface = row.children('td.configured_interface').attr('data');
+ var configured_interface_short = null;
if (configured_interface) {
// Match long-form IOS names against short ones (e.g. Gi0/1 == GigabitEthernet0/1).
- configured_interface = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2");
+ configured_interface_short = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2");
}
// Clean up hostnames/interfaces learned via LLDP
@@ -76,6 +77,8 @@ $(document).ready(function() {
row.addClass('info');
} else if (configured_device == lldp_device && configured_interface == lldp_interface) {
row.addClass('success');
+ } else if (configured_device == lldp_device && configured_interface_short == lldp_interface) {
+ row.addClass('success');
} else {
row.addClass('danger');
}
diff --git a/netbox/templates/dcim/devicetype_list.html b/netbox/templates/dcim/devicetype_list.html
index f246f60b5..69807fcff 100644
--- a/netbox/templates/dcim/devicetype_list.html
+++ b/netbox/templates/dcim/devicetype_list.html
@@ -13,7 +13,7 @@
Import device types
{% endif %}
- {% include 'inc/export_button.html' with obj_type='devicetypes' %}
+ {% include 'inc/export_button.html' with obj_type='device types' %}
{% block title %}Device Types{% endblock %}
diff --git a/netbox/templates/dcim/rackgroup_list.html b/netbox/templates/dcim/rackgroup_list.html
index f8b53df57..d5853f11c 100644
--- a/netbox/templates/dcim/rackgroup_list.html
+++ b/netbox/templates/dcim/rackgroup_list.html
@@ -13,7 +13,7 @@
Import rack groups
{% endif %}
- {% include 'inc/export_button.html' with obj_type='rackgroups' %}
+ {% include 'inc/export_button.html' with obj_type='rack groups' %}
{% block title %}Rack Groups{% endblock %}
diff --git a/netbox/templates/ipam/inc/prefix_header.html b/netbox/templates/ipam/inc/prefix_header.html
index 553af1a66..f3c694c64 100644
--- a/netbox/templates/ipam/inc/prefix_header.html
+++ b/netbox/templates/ipam/inc/prefix_header.html
@@ -22,8 +22,13 @@
- {% if perms.ipam.add_ipaddress %}
-
+ {% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %}
+
+ Add Child Prefix
+
+ {% endif %}
+ {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %}
+
Add an IP Address
@@ -45,5 +50,6 @@
{% include 'inc/created_updated.html' with obj=prefix %}
diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html
index 3447f2ad9..5c168e247 100644
--- a/netbox/templates/ipam/prefix.html
+++ b/netbox/templates/ipam/prefix.html
@@ -139,15 +139,4 @@
{% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %}
-
-
- {% if child_prefix_table.rows %}
- {% include 'utilities/obj_table.html' with table=child_prefix_table table_template='panel_table.html' heading='Child Prefixes' parent=prefix bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
- {% elif prefix.new_subnet %}
-
- Add Child Prefix
-
- {% endif %}
-
-
{% endblock %}
diff --git a/netbox/templates/ipam/prefix_ipaddresses.html b/netbox/templates/ipam/prefix_ipaddresses.html
index 1b777417d..02e90569d 100644
--- a/netbox/templates/ipam/prefix_ipaddresses.html
+++ b/netbox/templates/ipam/prefix_ipaddresses.html
@@ -3,10 +3,10 @@
{% block title %}{{ prefix }} - IP Addresses{% endblock %}
{% block content %}
-{% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %}
-
-
- {% include 'utilities/obj_table.html' with table=ip_table table_template='panel_table.html' heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
+ {% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %}
+
+
+ {% include 'utilities/obj_table.html' with table=ip_table table_template='panel_table.html' heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
+
-
{% endblock %}
diff --git a/netbox/templates/ipam/prefix_prefixes.html b/netbox/templates/ipam/prefix_prefixes.html
new file mode 100644
index 000000000..2535b672d
--- /dev/null
+++ b/netbox/templates/ipam/prefix_prefixes.html
@@ -0,0 +1,12 @@
+{% extends '_base.html' %}
+
+{% block title %}{{ prefix }} - Prefixes{% endblock %}
+
+{% block content %}
+ {% include 'ipam/inc/prefix_header.html' with active_tab='prefixes' %}
+
+
+ {% include 'utilities/obj_table.html' with table=prefix_table table_template='panel_table.html' heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %}
+
+
+{% endblock %}
diff --git a/netbox/templates/search.html b/netbox/templates/search.html
index afd4293ca..36c0fb5ad 100644
--- a/netbox/templates/search.html
+++ b/netbox/templates/search.html
@@ -13,12 +13,14 @@
{% for obj_type in results %}
{{ obj_type.name|bettertitle }}
{% include 'panel_table.html' with table=obj_type.table hide_paginator=True %}
- {% if obj_type.table.page.has_next %}
-
-
+
+
+ {% if obj_type.table.page.has_next %}
See all {{ obj_type.table.page.paginator.count }} results
-
- {% endif %}
+ {% else %}
+ Refine search
+ {% endif %}
+
{% endfor %}
diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py
index 5eb3bda61..00194d4e8 100644
--- a/netbox/tenancy/forms.py
+++ b/netbox/tenancy/forms.py
@@ -81,7 +81,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
group = FilterChoiceField(
queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
to_field_name='slug',
- null_option=(0, 'None')
+ null_label='-- None --'
)
diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py
index 647ecb723..3e403e676 100644
--- a/netbox/utilities/filters.py
+++ b/netbox/utilities/filters.py
@@ -42,7 +42,7 @@ class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""
iterator = forms.models.ModelChoiceIterator
- def __init__(self, null_value=0, null_label='None', *args, **kwargs):
+ def __init__(self, null_value=0, null_label='-- None --', *args, **kwargs):
self.null_value = null_value
self.null_label = null_label
super(NullableModelMultipleChoiceField, self).__init__(*args, **kwargs)
diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py
index 6ba49d02c..1817cd9a9 100644
--- a/netbox/utilities/forms.py
+++ b/netbox/utilities/forms.py
@@ -407,11 +407,25 @@ class SlugField(forms.SlugField):
self.widget.attrs['slug-source'] = slug_source
-class FilterChoiceFieldMixin(object):
- iterator = forms.models.ModelChoiceIterator
+class FilterChoiceIterator(forms.models.ModelChoiceIterator):
- def __init__(self, null_option=None, *args, **kwargs):
- self.null_option = null_option
+ def __iter__(self):
+ # Filter on "empty" choice using FILTERS_NULL_CHOICE_VALUE (instead of an empty string)
+ if self.field.null_label is not None:
+ yield (settings.FILTERS_NULL_CHOICE_VALUE, self.field.null_label)
+ queryset = self.queryset.all()
+ # Can't use iterator() when queryset uses prefetch_related()
+ if not queryset._prefetch_related_lookups:
+ queryset = queryset.iterator()
+ for obj in queryset:
+ yield self.choice(obj)
+
+
+class FilterChoiceFieldMixin(object):
+ iterator = FilterChoiceIterator
+
+ def __init__(self, null_label=None, *args, **kwargs):
+ self.null_label = null_label
if 'required' not in kwargs:
kwargs['required'] = False
if 'widget' not in kwargs:
@@ -424,15 +438,6 @@ class FilterChoiceFieldMixin(object):
return '{} ({})'.format(label, obj.filter_count)
return label
- def _get_choices(self):
- if hasattr(self, '_choices'):
- return self._choices
- if self.null_option is not None:
- return itertools.chain([self.null_option], self.iterator(self))
- return self.iterator(self)
-
- choices = property(_get_choices, forms.ChoiceField._set_choices)
-
class FilterChoiceField(FilterChoiceFieldMixin, forms.ModelMultipleChoiceField):
pass
diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py
index b9c95fc5e..64fb70a07 100644
--- a/netbox/utilities/middleware.py
+++ b/netbox/utilities/middleware.py
@@ -4,7 +4,7 @@ import sys
from django.conf import settings
from django.db import ProgrammingError
-from django.http import HttpResponseRedirect
+from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
@@ -61,6 +61,10 @@ class ExceptionHandlingMiddleware(object):
if settings.DEBUG:
return
+ # Ignore Http404s (defer to Django's built-in 404 handling)
+ if isinstance(exception, Http404):
+ return
+
# Determine the type of exception
if isinstance(exception, ProgrammingError):
template_name = 'exceptions/programming_error.html'
diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py
index e7ee4bdb7..e53e7151a 100644
--- a/netbox/utilities/views.py
+++ b/netbox/utilities/views.py
@@ -309,8 +309,14 @@ class BulkCreateView(View):
def get(self, request):
+ # Set initial values for visible form fields from query args
+ initial = {}
+ for field in getattr(self.model_form._meta, 'fields', []):
+ if request.GET.get(field):
+ initial[field] = request.GET[field]
+
form = self.form()
- model_form = self.model_form()
+ model_form = self.model_form(initial=initial)
return render(request, self.template_name, {
'obj_type': self.model_form._meta.model._meta.verbose_name,
diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py
index 1563e57a8..bd3e19400 100644
--- a/netbox/virtualization/filters.py
+++ b/netbox/virtualization/filters.py
@@ -84,6 +84,17 @@ class VirtualMachineFilter(CustomFieldFilterSet):
to_field_name='slug',
label='Cluster group (slug)',
)
+ cluster_type_id = django_filters.ModelMultipleChoiceFilter(
+ name='cluster__type',
+ queryset=ClusterType.objects.all(),
+ label='Cluster type (ID)',
+ )
+ cluster_type = django_filters.ModelMultipleChoiceFilter(
+ name='cluster__type__slug',
+ queryset=ClusterType.objects.all(),
+ to_field_name='slug',
+ label='Cluster type (slug)',
+ )
cluster_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cluster.objects.all(),
label='Cluster (ID)',
diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py
index 50e2816f1..d697de755 100644
--- a/netbox/virtualization/forms.py
+++ b/netbox/virtualization/forms.py
@@ -137,13 +137,13 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
group = FilterChoiceField(
queryset=ClusterGroup.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug',
- null_option=(0, 'None'),
+ null_label='-- None --',
required=False,
)
site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug',
- null_option=(0, 'None'),
+ null_label='-- None --',
required=False,
)
@@ -338,7 +338,12 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
cluster_group = FilterChoiceField(
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
- null_option=(0, 'None')
+ null_label='-- None --'
+ )
+ cluster_type = FilterChoiceField(
+ queryset=ClusterType.objects.all(),
+ to_field_name='slug',
+ null_label='-- None --'
)
cluster_id = FilterChoiceField(
queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')),
@@ -347,23 +352,23 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('clusters__virtual_machines')),
to_field_name='slug',
- null_option=(0, 'None')
+ null_label='-- None --'
)
role = FilterChoiceField(
queryset=DeviceRole.objects.filter(vm_role=True).annotate(filter_count=Count('virtual_machines')),
to_field_name='slug',
- null_option=(0, 'None')
+ null_label='-- None --'
)
status = forms.MultipleChoiceField(choices=vm_status_choices, required=False)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('virtual_machines')),
to_field_name='slug',
- null_option=(0, 'None')
+ null_label='-- None --'
)
platform = FilterChoiceField(
queryset=Platform.objects.annotate(filter_count=Count('virtual_machines')),
to_field_name='slug',
- null_option=(0, 'None')
+ null_label='-- None --'
)
diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py
index 6aa2575a9..3f2d0ff6d 100644
--- a/netbox/virtualization/models.py
+++ b/netbox/virtualization/models.py
@@ -139,6 +139,7 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
self.name,
self.type.name,
self.group.name if self.group else None,
+ self.site.name if self.site else None,
self.comments,
])