diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index f916f5083..ce72351b3 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -3,6 +3,9 @@ ## Enhancements * [#2365](https://github.com/netbox-community/netbox/issues/2365) - Toggle for showing available prefixes/ip addresses +* [#2892](https://github.com/netbox-community/netbox/issues/2892) - Extend admin UI to allow deleting old report results +* [#3062](https://github.com/netbox-community/netbox/issues/3062) - Add `assigned_to_interface` filter for IP addresses +* [#3461](https://github.com/netbox-community/netbox/issues/3461) - Fail gracefully on custom link rendering exception * [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts * [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets * [#3788](https://github.com/netbox-community/netbox/issues/3788) - Enable partial search for inventory items diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index f99848b1b..ee21b4f5d 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -3,7 +3,10 @@ from django.contrib import admin from netbox.admin import admin_site from utilities.forms import LaxURLField -from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook +from .models import ( + CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, TopologyMap, Webhook, +) +from .reports import get_report def order_content_types(field): @@ -166,6 +169,36 @@ class ExportTemplateAdmin(admin.ModelAdmin): form = ExportTemplateForm +# +# Reports +# + +@admin.register(ReportResult, site=admin_site) +class ReportResultAdmin(admin.ModelAdmin): + list_display = [ + 'report', 'active', 'created', 'user', 'passing', + ] + fields = [ + 'report', 'user', 'passing', 'data', + ] + list_filter = [ + 'failed', + ] + readonly_fields = fields + + def has_add_permission(self, request): + return False + + def active(self, obj): + module, report_name = obj.report.split('.') + return True if get_report(module, report_name) else False + active.boolean = True + + def passing(self, obj): + return not obj.failed + passing.boolean = True + + # # Topology maps # diff --git a/netbox/extras/models.py b/netbox/extras/models.py index e9ca396cc..038576b63 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -915,6 +915,13 @@ class ReportResult(models.Model): class Meta: ordering = ['report'] + def __str__(self): + return "{} {} at {}".format( + self.report, + "passed" if not self.failed else "failed", + self.created + ) + # # Change logging diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py index 0d2239ecc..8c927a0ae 100644 --- a/netbox/extras/templatetags/custom_links.py +++ b/netbox/extras/templatetags/custom_links.py @@ -46,12 +46,17 @@ def custom_links(obj): # Add non-grouped links else: - text_rendered = render_jinja2(cl.text, context) - if text_rendered: - link_target = ' target="_blank"' if cl.new_window else '' - template_code += LINK_BUTTON.format( - cl.url, link_target, cl.button_class, text_rendered - ) + try: + text_rendered = render_jinja2(cl.text, context) + if text_rendered: + link_rendered = render_jinja2(cl.url, context) + link_target = ' target="_blank"' if cl.new_window else '' + template_code += LINK_BUTTON.format( + link_rendered, link_target, cl.button_class, text_rendered + ) + except Exception as e: + template_code += '' \ + ' {}\n'.format(e, cl.name) # Add grouped links to template for group, links in group_names.items(): @@ -59,11 +64,17 @@ def custom_links(obj): links_rendered = [] for cl in links: - text_rendered = render_jinja2(cl.text, context) - if text_rendered: - link_target = ' target="_blank"' if cl.new_window else '' + try: + text_rendered = render_jinja2(cl.text, context) + if text_rendered: + link_target = ' target="_blank"' if cl.new_window else '' + links_rendered.append( + GROUP_LINK.format(cl.url, link_target, cl.text) + ) + except Exception as e: links_rendered.append( - GROUP_LINK.format(cl.url, link_target, cl.text) + '
  • ' + ' {}
  • '.format(e, cl.name) ) if links_rendered: @@ -71,7 +82,4 @@ def custom_links(obj): links[0].button_class, group, ''.join(links_rendered) ) - # Render template - rendered = render_jinja2(template_code, context) - - return mark_safe(rendered) + return mark_safe(template_code) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index c54ba2f62..1f8fd2caf 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -309,6 +309,10 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt queryset=Interface.objects.all(), label='Interface (ID)', ) + assigned_to_interface = django_filters.BooleanFilter( + method='_assigned_to_interface', + label='Is assigned to an interface', + ) status = django_filters.MultipleChoiceFilter( choices=IPADDRESS_STATUS_CHOICES, null_value=None @@ -366,6 +370,9 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt except Device.DoesNotExist: return queryset.none() + def _assigned_to_interface(self, queryset, name, value): + return queryset.exclude(interface__isnull=value) + class VLANGroupFilter(NameSlugSearchFilterSet): site_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index a95967b20..44056653b 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -938,7 +938,8 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = IPAddress field_order = [ - 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant', + 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'assigned_to_interface', 'tenant_group', + 'tenant', ] q = forms.CharField( required=False, @@ -984,6 +985,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo required=False, widget=StaticSelect2Multiple() ) + assigned_to_interface = forms.NullBooleanField( + required=False, + label='Assigned to an interface', + widget=StaticSelect2( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) #