From a8a272b06825bfa7cf5bf3e3e1227f85dc050184 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Mar 2021 16:58:56 -0500 Subject: [PATCH] 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.