mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 02:48:38 -06:00
Merge pull request #4959 from netbox-community/4639-prefix-annotate-depth
Closes #4639: Replace annotate_depth() on Prefix manager
This commit is contained in:
commit
6055debf04
@ -144,8 +144,16 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Cre
|
||||
label='Prefixes which contain this prefix or IP',
|
||||
)
|
||||
mask_length = django_filters.NumberFilter(
|
||||
method='filter_mask_length',
|
||||
label='Mask length',
|
||||
field_name='prefix',
|
||||
lookup_expr='net_mask_length'
|
||||
)
|
||||
mask_length__gte = django_filters.NumberFilter(
|
||||
field_name='prefix',
|
||||
lookup_expr='net_mask_length__gte'
|
||||
)
|
||||
mask_length__lte = django_filters.NumberFilter(
|
||||
field_name='prefix',
|
||||
lookup_expr='net_mask_length__lte'
|
||||
)
|
||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VRF.objects.all(),
|
||||
@ -262,11 +270,6 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Cre
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
|
||||
def filter_mask_length(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(prefix__net_mask_length=value)
|
||||
|
||||
|
||||
class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
|
@ -437,6 +437,9 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'region', 'site', 'role', 'tenant_group',
|
||||
'tenant', 'is_pool', 'expand',
|
||||
]
|
||||
mask_length__lte = forms.IntegerField(
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
@ -511,10 +514,6 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
expand = forms.BooleanField(
|
||||
required=False,
|
||||
label='Expand prefix hierarchy'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
|
@ -3,34 +3,20 @@ from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
class PrefixQuerySet(RestrictedQuerySet):
|
||||
|
||||
def annotate_depth(self, limit=None):
|
||||
def annotate_tree(self):
|
||||
"""
|
||||
Iterate through a QuerySet of Prefixes and annotate the hierarchical level of each. While it would be preferable
|
||||
to do this using .annotate() on the QuerySet to count the unique parents of each prefix, that approach introduces
|
||||
performance issues at scale.
|
||||
|
||||
Because we're adding a non-field attribute to the model, annotation must be made *after* any QuerySet
|
||||
modifications.
|
||||
Annotate the number of parent and child prefixes for each Prefix. Raw SQL is needed for these subqueries
|
||||
because we need to cast NULL VRF values to integers for comparison. (NULL != NULL).
|
||||
"""
|
||||
queryset = self
|
||||
stack = []
|
||||
for p in queryset:
|
||||
try:
|
||||
prev_p = stack[-1]
|
||||
except IndexError:
|
||||
prev_p = None
|
||||
if prev_p is not None:
|
||||
while (p.prefix not in prev_p.prefix) or p.prefix == prev_p.prefix:
|
||||
stack.pop()
|
||||
try:
|
||||
prev_p = stack[-1]
|
||||
except IndexError:
|
||||
prev_p = None
|
||||
break
|
||||
if prev_p is not None:
|
||||
prev_p.has_children = True
|
||||
stack.append(p)
|
||||
p.depth = len(stack) - 1
|
||||
if limit is None:
|
||||
return queryset
|
||||
return list(filter(lambda p: p.depth <= limit, queryset))
|
||||
return self.extra(
|
||||
select={
|
||||
'parents': 'SELECT COUNT(U0."prefix") AS "c" '
|
||||
'FROM "ipam_prefix" U0 '
|
||||
'WHERE (U0."prefix" >> "ipam_prefix"."prefix" '
|
||||
'AND COALESCE(U0."vrf_id", 0) = COALESCE("ipam_prefix"."vrf_id", 0))',
|
||||
'children': 'SELECT COUNT(U1."prefix") AS "c" '
|
||||
'FROM "ipam_prefix" U1 '
|
||||
'WHERE (U1."prefix" << "ipam_prefix"."prefix" '
|
||||
'AND COALESCE(U1."vrf_id", 0) = COALESCE("ipam_prefix"."vrf_id", 0))',
|
||||
}
|
||||
)
|
||||
|
@ -39,10 +39,10 @@ ROLE_VLAN_COUNT = """
|
||||
"""
|
||||
|
||||
PREFIX_LINK = """
|
||||
{% if record.has_children %}
|
||||
<span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
|
||||
{% if record.children %}
|
||||
<span class="text-nowrap" style="padding-left: {{ record.parents }}0px "><i class="fa fa-caret-right"></i></a>
|
||||
{% else %}
|
||||
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px">
|
||||
<span class="text-nowrap" style="padding-left: {{ record.parents }}9px">
|
||||
{% endif %}
|
||||
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% if parent.tenant %}&tenant_group={{ parent.tenant.group.pk }}&tenant={{ parent.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
|
||||
</span>
|
||||
@ -336,7 +336,9 @@ class PrefixTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Prefix
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description')
|
||||
fields = (
|
||||
'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description',
|
||||
)
|
||||
default_columns = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not record.pk else '',
|
||||
@ -357,11 +359,11 @@ class PrefixDetailTable(PrefixTable):
|
||||
|
||||
class Meta(PrefixTable.Meta):
|
||||
fields = (
|
||||
'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description',
|
||||
'tags',
|
||||
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool',
|
||||
'description', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
|
||||
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
|
||||
)
|
||||
|
||||
|
||||
|
@ -221,9 +221,7 @@ class AggregateView(ObjectView):
|
||||
'site', 'role'
|
||||
).order_by(
|
||||
'prefix'
|
||||
).annotate_depth(
|
||||
limit=0
|
||||
)
|
||||
).annotate_tree()
|
||||
|
||||
# Add available prefixes to the table if requested
|
||||
if request.GET.get('show_available', 'true') == 'true':
|
||||
@ -320,17 +318,12 @@ class RoleBulkDeleteView(BulkDeleteView):
|
||||
#
|
||||
|
||||
class PrefixListView(ObjectListView):
|
||||
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role').annotate_tree()
|
||||
filterset = filters.PrefixFilterSet
|
||||
filterset_form = forms.PrefixFilterForm
|
||||
table = tables.PrefixDetailTable
|
||||
template_name = 'ipam/prefix_list.html'
|
||||
|
||||
def alter_queryset(self, request):
|
||||
# Show only top-level prefixes by default (unless searching)
|
||||
limit = None if request.GET.get('expand') or request.GET.get('q') else 0
|
||||
return self.queryset.annotate_depth(limit=limit)
|
||||
|
||||
|
||||
class PrefixView(ObjectView):
|
||||
queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role')
|
||||
@ -353,7 +346,7 @@ class PrefixView(ObjectView):
|
||||
prefix__net_contains=str(prefix.prefix)
|
||||
).prefetch_related(
|
||||
'site', 'role'
|
||||
).annotate_depth()
|
||||
).annotate_tree()
|
||||
parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
|
||||
parent_prefix_table.exclude = ('vrf',)
|
||||
|
||||
@ -386,7 +379,7 @@ class PrefixPrefixesView(ObjectView):
|
||||
# Child prefixes table
|
||||
child_prefixes = prefix.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
|
||||
'site', 'vlan', 'role',
|
||||
).annotate_depth(limit=0)
|
||||
).annotate_tree()
|
||||
|
||||
# Add available prefixes to the table if requested
|
||||
if child_prefixes and request.GET.get('show_available', 'true') == 'true':
|
||||
|
@ -7,33 +7,36 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="." method="get" class="form">
|
||||
{% for field in filter_form %}
|
||||
<div class="form-group">
|
||||
{% if field.name == "q" %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
<label for="{{ field.id_for_label }}">{{ field }} {{ field.label }}</label>
|
||||
{% else %}
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="text-right noprint">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span> Apply
|
||||
</button>
|
||||
<a href="." class="btn btn-default">
|
||||
<span class="fa fa-remove" aria-hidden="true"></span> Clear
|
||||
</a>
|
||||
{% for field in filter_form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
{% for field in filter_form.visible_fields %}
|
||||
<div class="form-group">
|
||||
{% if field.name == "q" %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
<label for="{{ field.id_for_label }}">{{ field }} {{ field.label }}</label>
|
||||
{% else %}
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="text-right noprint">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span> Apply
|
||||
</button>
|
||||
<a href="." class="btn btn-default">
|
||||
<span class="fa fa-remove" aria-hidden="true"></span> Clear
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,7 +3,18 @@
|
||||
|
||||
{% block buttons %}
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand=None page=1 %}" class="btn btn-default{% if not request.GET.expand %} active{% endif %}">Collapse</a>
|
||||
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" id="max_length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
Max Length{% if "mask_length__lte" in request.GET %}: {{ request.GET.mask_length__lte }}{% endif %}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="max_length">
|
||||
{% for i in "4,8,12,16,20,24,28,32,40,48,56,64"|split %}
|
||||
<li><a href="{% url 'ipam:prefix_list' %}{% querystring request mask_length__lte=i page=1 %}">
|
||||
{{ i }} {% if request.GET.mask_length__lte == i %}<i class="fa fa-check"></i>{% endif %}
|
||||
</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -199,6 +199,14 @@ def has_perms(user, permissions_list):
|
||||
return user.has_perms(permissions_list)
|
||||
|
||||
|
||||
@register.filter()
|
||||
def split(string, sep=','):
|
||||
"""
|
||||
Split a string by the given value (default: comma)
|
||||
"""
|
||||
return string.split(sep)
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user