From d5ef1c7f30d568fc128d1e29e6ff971c988cf7ef Mon Sep 17 00:00:00 2001 From: Nathan Osman Date: Sun, 11 May 2025 11:54:53 -0700 Subject: [PATCH 1/4] Add xlsx_auto_filter option Filters can automatically be added to the header row by setting xlsx_auto_filter. --- README.md | 4 ++++ drf_excel/renderers.py | 8 ++++++++ tests/conftest.py | 4 ++-- tests/test_viewset_mixin.py | 13 +++++++++++++ tests/testapp/views.py | 8 ++++++++ tests/urls.py | 2 ++ 6 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c923236..9fec3df 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,10 @@ 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`. +### Auto filter header fields + +Filters can automatically be added to the header row by setting `xlsx_auto_filter = True`. The filter will include all header columns in the worksheet. + ### 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 = []`. diff --git a/drf_excel/renderers.py b/drf_excel/renderers.py index 002d7ba..243c99f 100644 --- a/drf_excel/renderers.py +++ b/drf_excel/renderers.py @@ -94,6 +94,9 @@ def render(self, data, accepted_media_type=None, renderer_context=None): # Make column headers column_titles = column_header.get("titles", []) + # Check for auto_filter + auto_filter = get_attribute(drf_view, 'xlsx_auto_filter', False) + # If we have results, then flatten field names if len(results): # Set `xlsx_use_labels = True` inside the API View to enable labels. @@ -216,6 +219,11 @@ def render(self, data, accepted_media_type=None, renderer_context=None): self._make_body(body, row, row_count) row_count += 1 + # Enable auto filters if requested + if auto_filter and column_count: + self.ws.auto_filter.ref = \ + f'A1:{get_column_letter(column_count)}{row_count}' + # Set sheet view options # Example: # sheet_view_options = { diff --git a/tests/conftest.py b/tests/conftest.py index d92a6ec..8f2aa06 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,8 +19,8 @@ def worksheet(workbook: Workbook) -> Worksheet: @pytest.fixture def workbook_reader() -> Callable[[Union[bytes, str]], Workbook]: - def reader_func(buffer: Union[bytes, str]) -> Workbook: + def reader_func(buffer: Union[bytes, str], read_only: bool = True) -> Workbook: io_buffer = io.BytesIO(buffer) - return load_workbook(io_buffer, read_only=True) + return load_workbook(io_buffer, read_only=read_only) return reader_func diff --git a/tests/test_viewset_mixin.py b/tests/test_viewset_mixin.py index 31b2911..5f40e92 100644 --- a/tests/test_viewset_mixin.py +++ b/tests/test_viewset_mixin.py @@ -126,3 +126,16 @@ 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_auto_filter_viewset(api_client, workbook_reader): + ExampleModel.objects.create(title="test 1", description="This is a test") + + response = api_client.get("/auto-filter/") + assert response.status_code == 200 + + # Note: auto_filter.ref is not available when read_only=True + wb = workbook_reader(response.content, False) + sheet = wb.worksheets[0] + + assert sheet.auto_filter.ref == 'A1:B2' diff --git a/tests/testapp/views.py b/tests/testapp/views.py index 77c43a8..d34efbb 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 AutoFilterViewSet(XLSXFileMixin, ReadOnlyModelViewSet): + queryset = ExampleModel.objects.all() + serializer_class = ExampleSerializer + renderer_classes = (XLSXRenderer,) + + xlsx_auto_filter = True diff --git a/tests/urls.py b/tests/urls.py index 8eadca0..d940093 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -2,6 +2,7 @@ from .testapp.views import ( AllFieldsViewSet, + AutoFilterViewSet, DynamicFieldViewSet, ExampleViewSet, SecretFieldViewSet, @@ -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"auto-filter", AutoFilterViewSet, basename='auto-filter') urlpatterns = router.urls From f23a4422ede7b744d95650a83143a1060ffd3720 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Tue, 5 Aug 2025 23:57:39 +0100 Subject: [PATCH 2/4] Reformat with Ruff --- drf_excel/renderers.py | 5 ++--- tests/test_viewset_mixin.py | 2 +- tests/urls.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/drf_excel/renderers.py b/drf_excel/renderers.py index 243c99f..ec67fea 100644 --- a/drf_excel/renderers.py +++ b/drf_excel/renderers.py @@ -95,7 +95,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None): column_titles = column_header.get("titles", []) # Check for auto_filter - auto_filter = get_attribute(drf_view, 'xlsx_auto_filter', False) + auto_filter = get_attribute(drf_view, "xlsx_auto_filter", False) # If we have results, then flatten field names if len(results): @@ -221,8 +221,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None): # Enable auto filters if requested if auto_filter and column_count: - self.ws.auto_filter.ref = \ - f'A1:{get_column_letter(column_count)}{row_count}' + self.ws.auto_filter.ref = f"A1:{get_column_letter(column_count)}{row_count}" # Set sheet view options # Example: diff --git a/tests/test_viewset_mixin.py b/tests/test_viewset_mixin.py index 5f40e92..5acc972 100644 --- a/tests/test_viewset_mixin.py +++ b/tests/test_viewset_mixin.py @@ -138,4 +138,4 @@ def test_auto_filter_viewset(api_client, workbook_reader): wb = workbook_reader(response.content, False) sheet = wb.worksheets[0] - assert sheet.auto_filter.ref == 'A1:B2' + assert sheet.auto_filter.ref == "A1:B2" diff --git a/tests/urls.py b/tests/urls.py index d940093..2cf3d99 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -13,6 +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"auto-filter", AutoFilterViewSet, basename='auto-filter') +router.register(r"auto-filter", AutoFilterViewSet, basename="auto-filter") urlpatterns = router.urls From a02b948576b4de0224dfceb15aaa0f8338fd4557 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Tue, 5 Aug 2025 23:58:23 +0100 Subject: [PATCH 3/4] Fix type hint for fixture --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8f2aa06..bc989f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ def worksheet(workbook: Workbook) -> Worksheet: @pytest.fixture -def workbook_reader() -> Callable[[Union[bytes, str]], Workbook]: +def workbook_reader() -> Callable[[Union[bytes, str], bool], Workbook]: def reader_func(buffer: Union[bytes, str], read_only: bool = True) -> Workbook: io_buffer = io.BytesIO(buffer) return load_workbook(io_buffer, read_only=read_only) From b36799f98f73edb8159589c98f37acc4623854fd Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Tue, 5 Aug 2025 23:58:55 +0100 Subject: [PATCH 4/4] Tweak test for readability --- tests/test_viewset_mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_viewset_mixin.py b/tests/test_viewset_mixin.py index 5acc972..e7dfe5c 100644 --- a/tests/test_viewset_mixin.py +++ b/tests/test_viewset_mixin.py @@ -134,8 +134,8 @@ def test_auto_filter_viewset(api_client, workbook_reader): response = api_client.get("/auto-filter/") assert response.status_code == 200 - # Note: auto_filter.ref is not available when read_only=True - wb = workbook_reader(response.content, False) + # Note: auto_filter.ref is not available for read-only workbooks + wb = workbook_reader(response.content, read_only=False) sheet = wb.worksheets[0] assert sheet.auto_filter.ref == "A1:B2"