mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
Enable table-based export
This commit is contained in:
parent
20a85c1ef2
commit
a8a272b068
@ -15,6 +15,7 @@ from django.utils.http import is_safe_url
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
from django_tables2.export import TableExport
|
||||||
|
|
||||||
from extras.models import CustomField, ExportTemplate
|
from extras.models import CustomField, ExportTemplate
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
@ -137,32 +138,35 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|||||||
if self.filterset:
|
if self.filterset:
|
||||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||||
|
|
||||||
# Check for export template rendering
|
# Check for export rendering (except for table-based)
|
||||||
if request.GET.get('export'):
|
if 'export' in request.GET and request.GET['export'] != 'table':
|
||||||
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET.get('export'))
|
|
||||||
try:
|
# An export template has been specified
|
||||||
return et.render_to_response(self.queryset)
|
if request.GET['export']:
|
||||||
except Exception as e:
|
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
|
||||||
messages.error(
|
try:
|
||||||
request,
|
return et.render_to_response(self.queryset)
|
||||||
"There was an error rendering the selected export template ({}): {}".format(
|
except Exception as e:
|
||||||
et.name, e
|
messages.error(
|
||||||
|
request,
|
||||||
|
"There was an error rendering the selected export template ({}): {}".format(
|
||||||
|
et.name, e
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
# Check for YAML export support
|
# Check for YAML export support
|
||||||
elif 'export' in request.GET and hasattr(model, 'to_yaml'):
|
elif hasattr(model, 'to_yaml'):
|
||||||
response = HttpResponse(self.queryset_to_yaml(), content_type='text/yaml')
|
response = HttpResponse(self.queryset_to_yaml(), content_type='text/yaml')
|
||||||
filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
|
filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
|
||||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Fall back to built-in CSV formatting if export requested but no template specified
|
# Fall back to built-in CSV formatting if export requested but no template specified
|
||||||
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
||||||
response = HttpResponse(self.queryset_to_csv(), content_type='text/csv')
|
response = HttpResponse(self.queryset_to_csv(), content_type='text/csv')
|
||||||
filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural)
|
filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural)
|
||||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Compile a dictionary indicating which permissions are available to the current user for this model
|
# Compile a dictionary indicating which permissions are available to the current user for this model
|
||||||
permissions = {}
|
permissions = {}
|
||||||
@ -175,6 +179,18 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|||||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||||
table.columns.show('pk')
|
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
|
# Apply the request context
|
||||||
paginate = {
|
paginate = {
|
||||||
'paginator_class': EnhancedPaginator,
|
'paginator_class': EnhancedPaginator,
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
{% if export_templates %}
|
<div class="btn-group">
|
||||||
<div class="btn-group">
|
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<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>
|
||||||
<span class="mdi mdi-database-export" aria-hidden="true"></span>
|
Export <span class="caret"></span>
|
||||||
Export <span class="caret"></span>
|
</button>
|
||||||
</button>
|
<ul class="dropdown-menu dropdown-menu-right">
|
||||||
<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>
|
<li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export">Default format</a></li>
|
||||||
<li class="divider"></li>
|
{% if export_templates %}
|
||||||
{% for et in export_templates %}
|
<li class="divider"></li>
|
||||||
<li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export={{ et.name }}"{% if et.description %} title="{{ et.description }}"{% endif %}>{{ et.name }}</a></li>
|
{% for et in export_templates %}
|
||||||
{% endfor %}
|
<li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export={{ et.name }}"{% if et.description %} title="{{ et.description }}"{% endif %}>{{ et.name }}</a></li>
|
||||||
</ul>
|
{% endfor %}
|
||||||
</div>
|
{% endif %}
|
||||||
{% else %}
|
</ul>
|
||||||
<a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export" class="btn btn-success">
|
</div>
|
||||||
<span class="mdi mdi-database-export" aria-hidden="true"></span> Export
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
@ -559,12 +559,6 @@ class ViewTestCases:
|
|||||||
# Try GET with model-level permission
|
# Try GET with model-level permission
|
||||||
self.assertHttpStatus(self.client.get(self._get_url('list')), 200)
|
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=[])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||||
def test_list_objects_with_constrained_permission(self):
|
def test_list_objects_with_constrained_permission(self):
|
||||||
instance1, instance2 = self._get_queryset().all()[:2]
|
instance1, instance2 = self._get_queryset().all()[:2]
|
||||||
@ -590,6 +584,23 @@ class ViewTestCases:
|
|||||||
self.assertIn(instance1.get_absolute_url(), content)
|
self.assertIn(instance1.get_absolute_url(), content)
|
||||||
self.assertNotIn(instance2.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):
|
class CreateMultipleObjectsViewTestCase(ModelViewTestCase):
|
||||||
"""
|
"""
|
||||||
Create multiple instances using a single form. Expects the creation of three new instances by default.
|
Create multiple instances using a single form. Expects the creation of three new instances by default.
|
||||||
|
Loading…
Reference in New Issue
Block a user