Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,13 @@ Instead of using the field names, the export will use the labels as they are def

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
### 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 = [<excluded fields>]`.
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 = [<fields to include>]`. Conversely, to exclude certain fields from your export, provide them with: `xlsx_ignore_headers = [<excluded fields>]`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether these 2 options should be mutually exclusive: you could use one option or the other, but not both...

I looked at the Django model forms API with fields and exclude which have a similar concern and they're not mutually exclusive:

exclude is an optional list of field names. If provided, the named fields will be excluded from the returned fields, even if they are listed in the fields argument.

Which -I think- matches the current implementation here. Maybe add a sentence to explain that ignore takes precendence over specify?


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).
Expand Down
13 changes: 10 additions & 3 deletions drf_excel/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -102,7 +103,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.
Expand Down Expand Up @@ -284,8 +286,13 @@ 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.
Expand Down
16 changes: 16 additions & 0 deletions tests/test_viewset_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,19 @@ def test_auto_filter_viewset(api_client, workbook_reader):
sheet = wb.worksheets[0]

assert sheet.auto_filter.ref == "A1:B2"


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"
8 changes: 8 additions & 0 deletions tests/testapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,11 @@ class AutoFilterViewSet(XLSXFileMixin, ReadOnlyModelViewSet):
renderer_classes = (XLSXRenderer,)

xlsx_auto_filter = True


class SpecifyHeadersViewSet(XLSXFileMixin, ReadOnlyModelViewSet):
queryset = AllFieldsModel.objects.all()
serializer_class = AllFieldsSerializer
renderer_classes = (XLSXRenderer,)

xlsx_specify_headers = ["title"]
2 changes: 2 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
DynamicFieldViewSet,
ExampleViewSet,
SecretFieldViewSet,
SpecifyHeadersViewSet,
)

router = routers.SimpleRouter()
Expand All @@ -14,5 +15,6 @@
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"specify-headers", SpecifyHeadersViewSet, basename="specify-headers")

urlpatterns = router.urls
Loading