From d5ef1c7f30d568fc128d1e29e6ff971c988cf7ef Mon Sep 17 00:00:00 2001 From: Nathan Osman Date: Sun, 11 May 2025 11:54:53 -0700 Subject: [PATCH] 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