From 8bab66d07547831ef246dea555420d12ca6ea8f8 Mon Sep 17 00:00:00 2001 From: Nathan Osman Date: Sat, 10 May 2025 23:27:19 -0700 Subject: [PATCH] Add new option xlsx_specify_headers The xlsx_ignore_headers option allows specific fields to be excluded when enumerating the serializer. This adds the ability to explicitly specify the fields that should be included. The absence of the option will result in the default behavior. --- README.md | 8 +++++--- drf_excel/renderers.py | 10 +++++++--- tests/test_viewset_mixin.py | 16 ++++++++++++++++ tests/testapp/views.py | 8 ++++++++ tests/urls.py | 2 ++ 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c923236..743890c 100644 --- a/README.md +++ b/README.md @@ -235,11 +235,13 @@ By default, headers will use the same 'names' as they are returned by the API. T Instead of using the field names, the export will use the labels as they are defined inside your Serializer. A serializer field defined as `title = serializers.CharField(label=_("Some title"))` would return `Some title` instead of `title`, also supporting translations. If no label is set, it will fall back to using `title`. -### Ignore fields +### Specify or ignore fields -By default, all fields are exported, but you might want to exclude some fields from your export. To do so, you can set an array with fields you want to exclude: `xlsx_ignore_headers = []`. +By default, all fields are exported. However, this behavior can be changed. -This also works with nested fields, separated with a dot (i.e. `icon.url`). +To include only a specified list of fields, provide them with: `xlsx_specify_headers = []`. Conversely, to exclude certain fields from your export, provide them with: `xlsx_ignore_headers = []`. + +These both work with nested fields, separated with a dot (i.e. `icon.url`). ### Date/time and number formatting Formatting for cells follows [openpyxl formats](https://openpyxl.readthedocs.io/en/stable/_modules/openpyxl/styles/numbers.html). diff --git a/drf_excel/renderers.py b/drf_excel/renderers.py index 002d7ba..e3c7814 100644 --- a/drf_excel/renderers.py +++ b/drf_excel/renderers.py @@ -42,6 +42,7 @@ class XLSXRenderer(BaseRenderer): format = "xlsx" # Reserved word, but required by BaseRenderer combined_header_dict = {} fields_dict = {} + specify_headers = None ignore_headers = [] boolean_display = None column_data_styles = None @@ -99,7 +100,8 @@ def render(self, data, accepted_media_type=None, renderer_context=None): # Set `xlsx_use_labels = True` inside the API View to enable labels. use_labels = getattr(drf_view, "xlsx_use_labels", False) - # A list of header keys to ignore in our export + # A list of header keys to use or ignore in our export + self.specify_headers = getattr(drf_view, "xlsx_specify_headers", None) self.ignore_headers = getattr(drf_view, "xlsx_ignore_headers", []) # Create a mapping dict named `xlsx_boolean_labels` inside the API View. @@ -277,8 +279,10 @@ def _get_label(parent_label, label_sep, obj): _fields = serializer.fields for k, v in _fields.items(): new_key = f"{parent_key}{key_sep}{k}" if parent_key else k - # Skip headers we want to ignore - if new_key in self.ignore_headers or getattr(v, "write_only", False): + # Skip headers that weren't in the list (if present) or were specifically ignored + if self.specify_headers is not None and new_key not in self.specify_headers or \ + new_key in self.ignore_headers or \ + getattr(v, "write_only", False): continue # Iterate through fields if field is a serializer. Check for labels and # append if `use_labels` is True. Fallback to using keys. diff --git a/tests/test_viewset_mixin.py b/tests/test_viewset_mixin.py index 31b2911..5e34edd 100644 --- a/tests/test_viewset_mixin.py +++ b/tests/test_viewset_mixin.py @@ -126,3 +126,19 @@ def test_dynamic_field_viewset(api_client, workbook_reader): row_1_values = [cell.value for cell in data] assert row_1_values == ["YUL", "CDG", "YYZ", "MAR"] + + +def test_specify_headers(api_client, workbook_reader): + AllFieldsModel.objects.create(title="Hello", age=36) + + response = api_client.get("/specify-headers/") + assert response.status_code == 200 + + wb = workbook_reader(response.content) + sheet = wb.worksheets[0] + + header, data = list(sheet.rows) + + assert len(header) == 1 + assert len(data) == 1 + assert header[0].value == "title" diff --git a/tests/testapp/views.py b/tests/testapp/views.py index 77c43a8..de2d005 100644 --- a/tests/testapp/views.py +++ b/tests/testapp/views.py @@ -51,3 +51,11 @@ def list(self, request, *args, **kwargs): ) serializer.is_valid(raise_exception=True) return Response(serializer.data) + + +class SpecifyHeadersViewSet(XLSXFileMixin, ReadOnlyModelViewSet): + queryset = AllFieldsModel.objects.all() + serializer_class = AllFieldsSerializer + renderer_classes = (XLSXRenderer,) + + xlsx_specify_headers = ['title'] diff --git a/tests/urls.py b/tests/urls.py index 8eadca0..bf850af 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -5,6 +5,7 @@ DynamicFieldViewSet, ExampleViewSet, SecretFieldViewSet, + SpecifyHeadersViewSet, ) router = routers.SimpleRouter() @@ -12,5 +13,6 @@ router.register(r"all-fields", AllFieldsViewSet) router.register(r"secret-field", SecretFieldViewSet) router.register(r"dynamic-field", DynamicFieldViewSet, basename="dynamic-field") +router.register(r"specify-headers", SpecifyHeadersViewSet, basename="specify-headers") urlpatterns = router.urls