From 23b58ccbe82be8f8362232ba271c8e772d3ab48a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Mar 2021 15:56:12 -0500 Subject: [PATCH 1/8] Override value() on custom table columns --- netbox/utilities/tables.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index f2f567783..bd52851ca 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -133,6 +133,9 @@ class BooleanColumn(tables.Column): rendered = '' return mark_safe(rendered) + def value(self, value): + return str(value) + class ButtonsColumn(tables.TemplateColumn): """ @@ -177,6 +180,10 @@ class ButtonsColumn(tables.TemplateColumn): super().__init__(template_code=template_code, *args, **kwargs) + # Exclude from export by default + if 'exclude_from_export' not in kwargs: + self.exclude_from_export = True + self.extra_context.update({ 'buttons': buttons or self.buttons, 'return_url_extra': return_url_extra, @@ -201,6 +208,9 @@ class ChoiceFieldColumn(tables.Column): ) return self.default + def value(self, value): + return value + class ColorColumn(tables.Column): """ @@ -211,6 +221,9 @@ class ColorColumn(tables.Column): f' ' ) + def value(self, value): + return f'#{value}' + class ColoredLabelColumn(tables.TemplateColumn): """ @@ -224,6 +237,9 @@ class ColoredLabelColumn(tables.TemplateColumn): def __init__(self, *args, **kwargs): super().__init__(template_code=self.template_code, *args, **kwargs) + def value(self, value): + return str(value) + class LinkedCountColumn(tables.Column): """ @@ -247,6 +263,9 @@ class LinkedCountColumn(tables.Column): return mark_safe(f'{value}') return value + def value(self, value): + return value + class TagColumn(tables.TemplateColumn): """ @@ -265,3 +284,6 @@ class TagColumn(tables.TemplateColumn): template_code=self.template_code, extra_context={'url_name': url_name} ) + + def value(self, value): + return ",".join([tag.name for tag in value.all()]) From 20a85c1ef264ecfffcbe8602ab103baed5a7cf5b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Mar 2021 16:07:55 -0500 Subject: [PATCH 2/8] Introduce TenantColumn to simplify representation --- netbox/circuits/tables.py | 6 ++-- netbox/dcim/tables/devices.py | 10 ++---- netbox/dcim/tables/racks.py | 10 ++---- netbox/dcim/tables/sites.py | 6 ++-- netbox/ipam/tables.py | 56 +++++++-------------------------- netbox/tenancy/tables.py | 31 +++++++++++++----- netbox/virtualization/tables.py | 6 ++-- 7 files changed, 48 insertions(+), 77 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index e4b859d3c..efa7e4c49 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from tenancy.tables import COL_TENANT +from tenancy.tables import TenantColumn from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn from .models import Circuit, CircuitType, Provider @@ -60,9 +60,7 @@ class CircuitTable(BaseTable): linkify=True ) status = ChoiceFieldColumn() - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() a_side = tables.Column( verbose_name='A Side' ) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index c844bb004..8a3944a28 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -5,7 +5,7 @@ from dcim.models import ( ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis, ) -from tenancy.tables import COL_TENANT +from tenancy.tables import TenantColumn from utilities.tables import ( BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn, ToggleColumn, @@ -109,9 +109,7 @@ class DeviceTable(BaseTable): template_code=DEVICE_LINK ) status = ChoiceFieldColumn() - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() site = tables.Column( linkify=True ) @@ -178,9 +176,7 @@ class DeviceImportTable(BaseTable): template_code=DEVICE_LINK ) status = ChoiceFieldColumn() - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() site = tables.Column( linkify=True ) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 5cb513faa..11fc946ca 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Rack, Location, RackReservation, RackRole -from tenancy.tables import COL_TENANT +from tenancy.tables import TenantColumn from utilities.tables import ( BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn, ToggleColumn, @@ -79,9 +79,7 @@ class RackTable(BaseTable): site = tables.Column( linkify=True ) - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() status = ChoiceFieldColumn() role = ColoredLabelColumn() u_height = tables.TemplateColumn( @@ -143,9 +141,7 @@ class RackReservationTable(BaseTable): accessor=Accessor('rack__site'), linkify=True ) - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() rack = tables.Column( linkify=True ) diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 50a5e5ec7..58d661fa9 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -1,7 +1,7 @@ import django_tables2 as tables from dcim.models import Region, Site -from tenancy.tables import COL_TENANT +from tenancy.tables import TenantColumn from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn from .template_code import MPTT_LINK @@ -46,9 +46,7 @@ class SiteTable(BaseTable): region = tables.Column( linkify=True ) - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() tags = TagColumn( url_name='dcim:site_list' ) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 8101a581d..779b37804 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -3,7 +3,7 @@ from django.utils.safestring import mark_safe from django_tables2.utils import Accessor from dcim.models import Interface -from tenancy.tables import COL_TENANT +from tenancy.tables import TenantColumn from utilities.tables import ( BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn, ) @@ -109,16 +109,6 @@ VLAN_MEMBER_TAGGED = """ {% endif %} """ -TENANT_LINK = """ -{% if record.tenant %} - {{ record.tenant }} -{% elif record.vrf.tenant %} - {{ record.vrf.tenant }}* -{% else %} - — -{% endif %} -""" - # # VRFs @@ -130,9 +120,7 @@ class VRFTable(BaseTable): rd = tables.Column( verbose_name='RD' ) - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() enforce_unique = BooleanColumn( verbose_name='Unique' ) @@ -163,9 +151,7 @@ class VRFTable(BaseTable): class RouteTargetTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() tags = TagColumn( url_name='ipam:vrf_list' ) @@ -208,9 +194,7 @@ class AggregateTable(BaseTable): prefix = tables.LinkColumn( verbose_name='Aggregate' ) - tenant = tables.TemplateColumn( - template_code=TENANT_LINK - ) + tenant = TenantColumn() date_added = tables.DateColumn( format="Y-m-d", verbose_name='Added' @@ -279,9 +263,7 @@ class PrefixTable(BaseTable): template_code=VRF_LINK, verbose_name='VRF' ) - tenant = tables.TemplateColumn( - template_code=TENANT_LINK - ) + tenant = TenantColumn() site = tables.Column( linkify=True ) @@ -312,9 +294,7 @@ class PrefixDetailTable(PrefixTable): template_code=UTILIZATION_GRAPH, orderable=False ) - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() tags = TagColumn( url_name='ipam:prefix_list' ) @@ -347,9 +327,7 @@ class IPAddressTable(BaseTable): default=AVAILABLE_LABEL ) role = ChoiceFieldColumn() - tenant = tables.TemplateColumn( - template_code=TENANT_LINK - ) + tenant = TenantColumn() assigned_object = tables.Column( linkify=True, orderable=False, @@ -379,9 +357,7 @@ class IPAddressDetailTable(IPAddressTable): orderable=False, verbose_name='NAT (Inside)' ) - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() assigned = BooleanColumn( accessor='assigned_object_id', verbose_name='Assigned' @@ -428,9 +404,7 @@ class InterfaceIPAddressTable(BaseTable): verbose_name='VRF' ) status = ChoiceFieldColumn() - tenant = tables.TemplateColumn( - template_code=TENANT_LINK - ) + tenant = TenantColumn() class Meta(BaseTable.Meta): model = IPAddress @@ -480,9 +454,7 @@ class VLANTable(BaseTable): viewname='ipam:vlangroup_vlans', args=[Accessor('group__pk')] ) - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() status = ChoiceFieldColumn( default=AVAILABLE_LABEL ) @@ -504,9 +476,7 @@ class VLANDetailTable(VLANTable): orderable=False, verbose_name='Prefixes' ) - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() tags = TagColumn( url_name='ipam:vlan_list' ) @@ -564,9 +534,7 @@ class InterfaceVLANTable(BaseTable): accessor=Accessor('group__name'), verbose_name='Group' ) - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() status = ChoiceFieldColumn() role = tables.TemplateColumn( template_code=VLAN_ROLE_LINK diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 8872a7012..0536bbcfc 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -10,13 +10,30 @@ MPTT_LINK = """ {{ record.name }} """ -COL_TENANT = """ -{% if record.tenant %} - {{ record.tenant }} -{% else %} - — -{% endif %} -""" + +# +# Table columns +# + +class TenantColumn(tables.TemplateColumn): + """ + Render a colored label (e.g. for DeviceRoles). + """ + template_code = """ + {% if record.tenant %} + {{ record.tenant }} + {% elif record.vrf.tenant %} + {{ record.vrf.tenant }}* + {% else %} + — + {% endif %} + """ + + def __init__(self, *args, **kwargs): + super().__init__(template_code=self.template_code, *args, **kwargs) + + def value(self, value): + return str(value) # diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 808832224..1850c7bdd 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from dcim.tables.devices import BaseInterfaceTable -from tenancy.tables import COL_TENANT +from tenancy.tables import TenantColumn from utilities.tables import ( BaseTable, ButtonsColumn, ChoiceFieldColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn, ToggleColumn, ) @@ -107,9 +107,7 @@ class VirtualMachineTable(BaseTable): linkify=True ) role = ColoredLabelColumn() - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) + tenant = TenantColumn() class Meta(BaseTable.Meta): model = VirtualMachine From a8a272b06825bfa7cf5bf3e3e1227f85dc050184 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Mar 2021 16:58:56 -0500 Subject: [PATCH 3/8] Enable table-based export --- netbox/netbox/views/generic.py | 62 ++++++++++++------- .../utilities/templates/buttons/export.html | 35 +++++------ netbox/utilities/testing/views.py | 23 +++++-- 3 files changed, 72 insertions(+), 48 deletions(-) diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index af09dd546..25ef7dc50 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -15,6 +15,7 @@ from django.utils.http import is_safe_url from django.utils.safestring import mark_safe from django.views.generic import View from django_tables2 import RequestConfig +from django_tables2.export import TableExport from extras.models import CustomField, ExportTemplate from utilities.error_handlers import handle_protectederror @@ -137,32 +138,35 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): if self.filterset: self.queryset = self.filterset(request.GET, self.queryset).qs - # Check for export template rendering - if request.GET.get('export'): - et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET.get('export')) - try: - return et.render_to_response(self.queryset) - except Exception as e: - messages.error( - request, - "There was an error rendering the selected export template ({}): {}".format( - et.name, e + # Check for export rendering (except for table-based) + if 'export' in request.GET and request.GET['export'] != 'table': + + # An export template has been specified + if request.GET['export']: + et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export']) + try: + return et.render_to_response(self.queryset) + except Exception as e: + messages.error( + request, + "There was an error rendering the selected export template ({}): {}".format( + et.name, e + ) ) - ) - # Check for YAML export support - elif 'export' in request.GET and hasattr(model, 'to_yaml'): - response = HttpResponse(self.queryset_to_yaml(), content_type='text/yaml') - filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural) - response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) - return response + # Check for YAML export support + elif hasattr(model, 'to_yaml'): + response = HttpResponse(self.queryset_to_yaml(), content_type='text/yaml') + filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural) + response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) + return response - # Fall back to built-in CSV formatting if export requested but no template specified - elif 'export' in request.GET and hasattr(model, 'to_csv'): - response = HttpResponse(self.queryset_to_csv(), content_type='text/csv') - filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural) - response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) - return response + # Fall back to built-in CSV formatting if export requested but no template specified + elif 'export' in request.GET and hasattr(model, 'to_csv'): + response = HttpResponse(self.queryset_to_csv(), content_type='text/csv') + filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural) + response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) + return response # Compile a dictionary indicating which permissions are available to the current user for this model permissions = {} @@ -175,6 +179,18 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): table.columns.show('pk') + # Handle table-based export + if request.GET.get('export') == 'table': + exporter = TableExport( + export_format=TableExport.CSV, + table=table, + exclude_columns=['pk'], + dataset_kwargs={}, + ) + return exporter.response( + filename=f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv' + ) + # Apply the request context paginate = { 'paginator_class': EnhancedPaginator, diff --git a/netbox/utilities/templates/buttons/export.html b/netbox/utilities/templates/buttons/export.html index a6ec52d1f..341dca3f0 100644 --- a/netbox/utilities/templates/buttons/export.html +++ b/netbox/utilities/templates/buttons/export.html @@ -1,19 +1,16 @@ -{% if export_templates %} -
- - -
-{% else %} - - Export - -{% endif %} \ No newline at end of file +
+ + +
diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index 89bac51cf..3b4eca99a 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -559,12 +559,6 @@ class ViewTestCases: # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('list')), 200) - # Built-in CSV export - if hasattr(self.model, 'csv_headers'): - response = self.client.get('{}?export'.format(self._get_url('list'))) - self.assertHttpStatus(response, 200) - self.assertEqual(response.get('Content-Type'), 'text/csv') - @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) def test_list_objects_with_constrained_permission(self): instance1, instance2 = self._get_queryset().all()[:2] @@ -590,6 +584,23 @@ class ViewTestCases: self.assertIn(instance1.get_absolute_url(), content) self.assertNotIn(instance2.get_absolute_url(), content) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_export_objects(self): + url = self._get_url('list') + + # Test default CSV export + response = self.client.get(f'{url}?export') + self.assertHttpStatus(response, 200) + if hasattr(self.model, 'csv_headers'): + self.assertEqual(response.get('Content-Type'), 'text/csv') + content = response.content.decode('utf-8') + self.assertEqual(content.splitlines()[0], ','.join(self.model.csv_headers)) + + # Test table-based export + response = self.client.get(f'{url}?export=table') + self.assertHttpStatus(response, 200) + self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8') + class CreateMultipleObjectsViewTestCase(ModelViewTestCase): """ Create multiple instances using a single form. Expects the creation of three new instances by default. From 0d44a320a05341590006c1cc9cf5ee990bf61a2d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Mar 2021 17:15:07 -0500 Subject: [PATCH 4/8] Add tablib as a required package --- base_requirements.txt | 4 ++++ requirements.txt | 1 + 2 files changed, 5 insertions(+) diff --git a/base_requirements.txt b/base_requirements.txt index a57e88604..bf03bf71e 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -93,3 +93,7 @@ redis # SVG image rendering (used for rack elevations) # https://github.com/mozman/svgwrite svgwrite + +# Tabular dataset library (for table-based exports) +# https://github.com/jazzband/tablib +tablib diff --git a/requirements.txt b/requirements.txt index f9e616c5e..d7325e299 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,4 @@ psycopg2-binary==2.8.6 pycryptodome==3.9.9 PyYAML==5.3.1 svgwrite==1.4 +tablib==3.0.0 From 6149ba6e366788178cb907b7937c90e64f493e49 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Mar 2021 17:31:37 -0500 Subject: [PATCH 5/8] Export only visible columns --- netbox/netbox/views/generic.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index 25ef7dc50..e27447ad0 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -181,10 +181,14 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): # Handle table-based export if request.GET.get('export') == 'table': + exclude_columns = {'pk'} + exclude_columns.update({ + col for col in table.base_columns if col not in table.visible_columns + }) exporter = TableExport( export_format=TableExport.CSV, table=table, - exclude_columns=['pk'], + exclude_columns=exclude_columns, dataset_kwargs={}, ) return exporter.response( From 32501c96e5f0dc13c12a995718a9d69b65a5a417 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Mar 2021 20:47:24 -0500 Subject: [PATCH 6/8] Introduce MPTTColumn to represent MPTT models --- netbox/dcim/tables/racks.py | 12 ++++-------- netbox/dcim/tables/sites.py | 9 ++------- netbox/dcim/tables/template_code.py | 7 ------- netbox/tenancy/tables.py | 15 ++------------- netbox/utilities/tables.py | 17 +++++++++++++++++ 5 files changed, 25 insertions(+), 35 deletions(-) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 11fc946ca..b1194c74b 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -4,10 +4,10 @@ from django_tables2.utils import Accessor from dcim.models import Rack, Location, RackReservation, RackRole from tenancy.tables import TenantColumn from utilities.tables import ( - BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn, - ToggleColumn, + BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MPTTColumn, + TagColumn, ToggleColumn, ) -from .template_code import MPTT_LINK, LOCATION_ELEVATIONS, UTILIZATION_GRAPH +from .template_code import LOCATION_ELEVATIONS, UTILIZATION_GRAPH __all__ = ( 'RackTable', @@ -24,11 +24,7 @@ __all__ = ( class LocationTable(BaseTable): pk = ToggleColumn() - name = tables.TemplateColumn( - template_code=MPTT_LINK, - orderable=False, - attrs={'td': {'class': 'text-nowrap'}} - ) + name = MPTTColumn() site = tables.Column( linkify=True ) diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 58d661fa9..9ef6d873b 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -2,8 +2,7 @@ import django_tables2 as tables from dcim.models import Region, Site from tenancy.tables import TenantColumn -from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn -from .template_code import MPTT_LINK +from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, MPTTColumn, TagColumn, ToggleColumn __all__ = ( 'RegionTable', @@ -17,11 +16,7 @@ __all__ = ( class RegionTable(BaseTable): pk = ToggleColumn() - name = tables.TemplateColumn( - template_code=MPTT_LINK, - orderable=False, - attrs={'td': {'class': 'text-nowrap'}} - ) + name = MPTTColumn() site_count = tables.Column( verbose_name='Sites' ) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 86a5e62aa..00a5c2184 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -56,13 +56,6 @@ INTERFACE_TAGGED_VLANS = """ {% endif %} """ -MPTT_LINK = """ -{% for i in record.get_ancestors %} - -{% endfor %} -{{ record.name }} -""" - POWERFEED_CABLE = """ {{ value }} diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 0536bbcfc..5b5bc6d73 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -1,15 +1,8 @@ import django_tables2 as tables -from utilities.tables import BaseTable, ButtonsColumn, LinkedCountColumn, TagColumn, ToggleColumn +from utilities.tables import BaseTable, ButtonsColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn from .models import Tenant, TenantGroup -MPTT_LINK = """ -{% for i in record.get_ancestors %} - -{% endfor %} -{{ record.name }} -""" - # # Table columns @@ -42,11 +35,7 @@ class TenantColumn(tables.TemplateColumn): class TenantGroupTable(BaseTable): pk = ToggleColumn() - name = tables.TemplateColumn( - template_code=MPTT_LINK, - orderable=False, - attrs={'td': {'class': 'text-nowrap'}} - ) + name = MPTTColumn() tenant_count = LinkedCountColumn( viewname='tenancy:tenant_list', url_params={'group': 'slug'}, diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index bd52851ca..661958712 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -287,3 +287,20 @@ class TagColumn(tables.TemplateColumn): def value(self, value): return ",".join([tag.name for tag in value.all()]) + + +class MPTTColumn(tables.TemplateColumn): + template_code = """{% for i in record.get_ancestors %}{% endfor %}""" \ + """{{ record.name }}""" + + def __init__(self, *args, **kwargs): + super().__init__( + template_code=self.template_code, + orderable=False, + attrs={'td': {'class': 'text-nowrap'}}, + *args, + **kwargs + ) + + def value(self, value): + return value From e703d9ff78ca4c952f69684c543dc06417e45ceb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Mar 2021 20:58:43 -0500 Subject: [PATCH 7/8] Introduce UtilizationColumn to render utilization graphs consistently --- netbox/dcim/tables/racks.py | 11 ++++------- netbox/dcim/tables/template_code.py | 5 ----- netbox/ipam/tables.py | 14 +++++--------- netbox/utilities/tables.py | 16 ++++++++++++++++ 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index b1194c74b..eb4b28710 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -5,9 +5,9 @@ from dcim.models import Rack, Location, RackReservation, RackRole from tenancy.tables import TenantColumn from utilities.tables import ( BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MPTTColumn, - TagColumn, ToggleColumn, + TagColumn, ToggleColumn, UtilizationColumn, ) -from .template_code import LOCATION_ELEVATIONS, UTILIZATION_GRAPH +from .template_code import LOCATION_ELEVATIONS __all__ = ( 'RackTable', @@ -98,13 +98,10 @@ class RackDetailTable(RackTable): url_params={'rack_id': 'pk'}, verbose_name='Devices' ) - get_utilization = tables.TemplateColumn( - template_code=UTILIZATION_GRAPH, - orderable=False, + get_utilization = UtilizationColumn( verbose_name='Space' ) - get_power_utilization = tables.TemplateColumn( - template_code=UTILIZATION_GRAPH, + get_power_utilization = UtilizationColumn( orderable=False, verbose_name='Power' ) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 00a5c2184..f634cf426 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -75,11 +75,6 @@ LOCATION_ELEVATIONS = """ """ -UTILIZATION_GRAPH = """ -{% load helpers %} -{% utilization_graph value %} -""" - # # Device component buttons # diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 779b37804..6553480c6 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -6,17 +6,13 @@ from dcim.models import Interface from tenancy.tables import TenantColumn from utilities.tables import ( BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn, + UtilizationColumn, ) from virtualization.models import VMInterface from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF AVAILABLE_LABEL = mark_safe('Available') -UTILIZATION_GRAPH = """ -{% load helpers %} -{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %} -""" - PREFIX_LINK = """ {% load helpers %} {% for i in record.parents|as_range %} @@ -209,8 +205,8 @@ class AggregateDetailTable(AggregateTable): child_count = tables.Column( verbose_name='Prefixes' ) - utilization = tables.TemplateColumn( - template_code=UTILIZATION_GRAPH, + utilization = UtilizationColumn( + accessor='get_utilization', orderable=False ) tags = TagColumn( @@ -290,8 +286,8 @@ class PrefixTable(BaseTable): class PrefixDetailTable(PrefixTable): - utilization = tables.TemplateColumn( - template_code=UTILIZATION_GRAPH, + utilization = UtilizationColumn( + accessor='get_utilization', orderable=False ) tenant = TenantColumn() diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 661958712..424cc6cd9 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -290,6 +290,9 @@ class TagColumn(tables.TemplateColumn): class MPTTColumn(tables.TemplateColumn): + """ + Display a nested hierarchy for MPTT-enabled models. + """ template_code = """{% for i in record.get_ancestors %}{% endfor %}""" \ """{{ record.name }}""" @@ -304,3 +307,16 @@ class MPTTColumn(tables.TemplateColumn): def value(self, value): return value + + +class UtilizationColumn(tables.TemplateColumn): + """ + Display a colored utilization bar graph. + """ + template_code = """{% load helpers %}{% if record.pk %}{% utilization_graph value %}{% endif %}""" + + def __init__(self, *args, **kwargs): + super().__init__(template_code=self.template_code, *args, **kwargs) + + def value(self, value): + return f'{value}%' From 0015c8dfa25a5fd669aa63d0c5d6d83cf2188e54 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 5 Mar 2021 09:15:27 -0500 Subject: [PATCH 8/8] Changelog for #4999 --- docs/release-notes/version-2.11.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index b92dcaa13..9ca13d639 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -16,6 +16,12 @@ In addition to the new `mark_connected` boolean field, the REST API representati Devices can now be assigned to locations (formerly known as rack groups) within a site without needing to be assigned to a particular rack. This is handy for assigning devices to rooms or floors within a building where racks are not used. The `location` foreign key field has been added to the Device model to support this. +#### Dynamic Object Exports ([#4999](https://github.com/netbox-community/netbox/issues/4999)) + +When exporting a list of objects in NetBox, users now have the option of selecting the "current view". This will render CSV output matching the configuration of the current table. For example, if you modify the sites list to display on the site name, tenant, and status, the rendered CSV will include only these columns. + +The legacy static export behavior has been retained to ensure backward compatibility for dependent integrations. However, users are strongly encouraged to adapt custom export templates where needed as this functionality will be removed in v2.12. + #### Improved Change Logging ([#5913](https://github.com/netbox-community/netbox/issues/5913)) The ObjectChange model (which is used to record the creation, modification, and deletion of NetBox objects) now explicitly records the pre-change and post-change state of each object, rather than only the post-change state. This was done to present a more clear depiction of each change being made, and to prevent the erroneous association of a previous unlogged change with its successor.