Closes #19739: Add a user preference for CSV delimiter in table exports (#19824)

* Closes #19739: Add a user preference for CSV delimiter in table exports

* Pass custom delimiter when exporting entire table
This commit is contained in:
Jeremy Stretch 2025-07-08 15:11:40 -04:00 committed by GitHub
parent a1cd81ff35
commit 90e8a61670
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 53 additions and 6 deletions

View File

@ -44,3 +44,10 @@ CENSOR_TOKEN_CHANGED = '***CHANGED***'
# Placeholder text for empty tables # Placeholder text for empty tables
EMPTY_TABLE_TEXT = 'No results found' EMPTY_TABLE_TEXT = 'No results found'
# CSV delimiters
CSV_DELIMITERS = {
'comma': ',',
'semicolon': ';',
'pipe': '|',
}

View File

@ -72,6 +72,16 @@ PREFERENCES = {
), ),
description=_('The preferred syntax for displaying generic data within the UI') description=_('The preferred syntax for displaying generic data within the UI')
), ),
'csv_delimiter': UserPreference(
label=_('CSV delimiter'),
choices=(
('comma', 'Comma (,)'),
('semicolon', 'Semicolon (;)'),
('pipe', 'Pipe (|)'),
),
default='comma',
description=_('The character used to separate fields in CSV data')
),
} }

View File

@ -15,7 +15,6 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_tables2.export import TableExport
from mptt.models import MPTTModel from mptt.models import MPTTModel
from core.models import ObjectType from core.models import ObjectType
@ -25,6 +24,7 @@ from extras.models import CustomField, ExportTemplate
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
from utilities.error_handlers import handle_protectederror from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.export import TableExport
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
from utilities.forms.bulk_import import BulkImportForm from utilities.forms.bulk_import import BulkImportForm
from utilities.htmx import htmx_partial from utilities.htmx import htmx_partial
@ -77,7 +77,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
return '---\n'.join(yaml_data) return '---\n'.join(yaml_data)
def export_table(self, table, columns=None, filename=None): def export_table(self, table, columns=None, filename=None, delimiter=None):
""" """
Export all table data in CSV format. Export all table data in CSV format.
@ -86,6 +86,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
columns: A list of specific columns to include. If None, all columns will be exported. columns: A list of specific columns to include. If None, all columns will be exported.
filename: The name of the file attachment sent to the client. If None, will be determined automatically filename: The name of the file attachment sent to the client. If None, will be determined automatically
from the queryset model name. from the queryset model name.
delimiter: The character used to separate columns (a comma is used by default)
""" """
exclude_columns = {'pk', 'actions'} exclude_columns = {'pk', 'actions'}
if columns: if columns:
@ -96,7 +97,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
exporter = TableExport( exporter = TableExport(
export_format=TableExport.CSV, export_format=TableExport.CSV,
table=table, table=table,
exclude_columns=exclude_columns exclude_columns=exclude_columns,
delimiter=delimiter,
) )
return exporter.response( return exporter.response(
filename=filename or f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv' filename=filename or f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv'
@ -159,7 +161,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
if request.GET['export'] == 'table': if request.GET['export'] == 'table':
table = self.get_table(self.queryset, request, has_table_actions) table = self.get_table(self.queryset, request, has_table_actions)
columns = [name for name, _ in table.selected_columns] columns = [name for name, _ in table.selected_columns]
return self.export_table(table, columns) delimiter = request.user.config.get('csv_delimiter')
return self.export_table(table, columns, delimiter=delimiter)
# Render an ExportTemplate # Render an ExportTemplate
elif request.GET['export']: elif request.GET['export']:
@ -176,7 +179,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
# Fall back to default table/YAML export # Fall back to default table/YAML export
else: else:
table = self.get_table(self.queryset, request, has_table_actions) table = self.get_table(self.queryset, request, has_table_actions)
return self.export_table(table) delimiter = request.user.config.get('csv_delimiter')
return self.export_table(table, delimiter=delimiter)
# Render the objects table # Render the objects table
table = self.get_table(self.queryset, request, has_table_actions) table = self.get_table(self.queryset, request, has_table_actions)

View File

@ -62,7 +62,7 @@ class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass):
'ui.tables.striping', 'ui.tables.striping',
name=_('User Interface') name=_('User Interface')
), ),
FieldSet('data_format', name=_('Miscellaneous')), FieldSet('data_format', 'csv_delimiter', name=_('Miscellaneous')),
) )
# List of clearable preferences # List of clearable preferences
pk = forms.MultipleChoiceField( pk = forms.MultipleChoiceField(

View File

@ -0,0 +1,26 @@
from django.utils.translation import gettext_lazy as _
from django_tables2.export import TableExport as TableExport_
from netbox.constants import CSV_DELIMITERS
__all__ = (
'TableExport',
)
class TableExport(TableExport_):
"""
A subclass of django-tables2's TableExport class which allows us to specify a delimiting
characters for CSV exports.
"""
def __init__(self, *args, delimiter=None, **kwargs):
if delimiter and delimiter not in CSV_DELIMITERS.keys():
raise ValueError(_("Invalid delimiter name: {name}").format(name=delimiter))
self.delimiter = delimiter or 'comma'
super().__init__(*args, **kwargs)
def export(self):
if self.format == self.CSV and self.delimiter is not None:
delimiter = CSV_DELIMITERS[self.delimiter]
return self.dataset.export(self.format, delimiter=delimiter)
return super().export()