Bladeren bron

Enable table-based export

Jeremy Stretch 5 jaren geleden
bovenliggende
commit
a8a272b068
3 gewijzigde bestanden met toevoegingen van 72 en 48 verwijderingen
  1. 39 23
      netbox/netbox/views/generic.py
  2. 16 19
      netbox/utilities/templates/buttons/export.html
  3. 17 6
      netbox/utilities/testing/views.py

+ 39 - 23
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,

+ 16 - 19
netbox/utilities/templates/buttons/export.html

@@ -1,19 +1,16 @@
-{% if export_templates %}
-    <div class="btn-group">
-        <button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-            <span class="mdi mdi-database-export" aria-hidden="true"></span>
-            Export <span class="caret"></span>
-        </button>
-        <ul class="dropdown-menu dropdown-menu-right">
-            <li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export">Default format</a></li>
-            <li class="divider"></li>
-            {% for et in export_templates %}
-                <li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export={{ et.name }}"{% if et.description %} title="{{ et.description }}"{% endif %}>{{ et.name }}</a></li>
-            {% endfor %}
-        </ul>
-    </div>
-{% else %}
-    <a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export" class="btn btn-success">
-        <span class="mdi mdi-database-export" aria-hidden="true"></span> Export
-    </a>
-{% endif %}
+<div class="btn-group">
+  <button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+    <span class="mdi mdi-database-export" aria-hidden="true"></span>
+    Export <span class="caret"></span>
+  </button>
+  <ul class="dropdown-menu dropdown-menu-right">
+    <li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export=table">Current view</a></li>
+    <li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export">Default format</a></li>
+    {% if export_templates %}
+      <li class="divider"></li>
+      {% for et in export_templates %}
+        <li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export={{ et.name }}"{% if et.description %} title="{{ et.description }}"{% endif %}>{{ et.name }}</a></li>
+      {% endfor %}
+    {% endif %}
+  </ul>
+</div>

+ 17 - 6
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.