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
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')
),
'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.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django_tables2.export import TableExport
from mptt.models import MPTTModel
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 utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.export import TableExport
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
from utilities.forms.bulk_import import BulkImportForm
from utilities.htmx import htmx_partial
@ -77,7 +77,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
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.
@ -86,6 +86,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
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
from the queryset model name.
delimiter: The character used to separate columns (a comma is used by default)
"""
exclude_columns = {'pk', 'actions'}
if columns:
@ -96,7 +97,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
exporter = TableExport(
export_format=TableExport.CSV,
table=table,
exclude_columns=exclude_columns
exclude_columns=exclude_columns,
delimiter=delimiter,
)
return exporter.response(
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':
table = self.get_table(self.queryset, request, has_table_actions)
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
elif request.GET['export']:
@ -176,7 +179,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
# Fall back to default table/YAML export
else:
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
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',
name=_('User Interface')
),
FieldSet('data_format', name=_('Miscellaneous')),
FieldSet('data_format', 'csv_delimiter', name=_('Miscellaneous')),
)
# List of clearable preferences
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()