From 0550b03dadc504ba3764db30f94607b4a56d36cf Mon Sep 17 00:00:00 2001
From: Sage Abdullah <sage.abdullah@torchbox.com>
Date: Mon, 10 Jul 2023 10:20:04 +0100
Subject: [PATCH] Allow customising the spreadsheet file name via
 SnippetViewSet.export_filename

---
 docs/reference/viewsets.md             |  1 +
 docs/topics/snippets/customising.md    |  2 +-
 wagtail/admin/views/mixins.py          |  4 +++-
 wagtail/snippets/tests/test_viewset.py | 10 ++++++++++
 wagtail/snippets/views/snippets.py     |  8 ++++++++
 wagtail/test/testapp/wagtail_hooks.py  |  1 +
 6 files changed, 24 insertions(+), 2 deletions(-)

diff --git a/docs/reference/viewsets.md b/docs/reference/viewsets.md
index 6a19124322..73cb301828 100644
--- a/docs/reference/viewsets.md
+++ b/docs/reference/viewsets.md
@@ -94,6 +94,7 @@ Viewsets are Wagtail's mechanism for defining a group of related admin views wit
    .. autoattribute:: search_backend_name
    .. autoattribute:: list_per_page
    .. autoattribute:: chooser_per_page
+   .. autoattribute:: export_filename
    .. autoattribute:: ordering
    .. autoattribute:: admin_url_namespace
    .. autoattribute:: base_url_path
diff --git a/docs/topics/snippets/customising.md b/docs/topics/snippets/customising.md
index fba810db33..7d326fba22 100644
--- a/docs/topics/snippets/customising.md
+++ b/docs/topics/snippets/customising.md
@@ -93,7 +93,7 @@ You can add the ability to filter the listing view by defining a {attr}`~wagtail
 
 If you would like to make further customisations to the filtering mechanism, you can also use a custom `wagtail.admin.filters.WagtailFilterSet` subclass by overriding the {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.filterset_class` attribute. The `list_filter` attribute is ignored if `filterset_class` is set. For more details, refer to [django-filter's documentation](https://django-filter.readthedocs.io/en/stable/guide/usage.html#the-filter).
 
-You can add the ability to export the listing view to a spreadsheet by setting the {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.list_export` attribute to specify the columns to be exported.
+You can add the ability to export the listing view to a spreadsheet by setting the {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.list_export` attribute to specify the columns to be exported. The {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.export_filename` attribute can be used to customise the file name of the exported spreadsheet.
 
 ```{versionadded} 5.1
 The ability to export the listing view was added.
diff --git a/wagtail/admin/views/mixins.py b/wagtail/admin/views/mixins.py
index 6108550198..963c84b540 100644
--- a/wagtail/admin/views/mixins.py
+++ b/wagtail/admin/views/mixins.py
@@ -154,6 +154,8 @@ class SpreadsheetExportMixin:
 
     export_buttons_template_name = "wagtailadmin/shared/export_buttons.html"
 
+    export_filename = "spreadsheet-export"
+
     def setup(self, request, *args, **kwargs):
         super().setup(request, *args, **kwargs)
         self.is_export = request.GET.get("export") in self.FORMATS
@@ -165,7 +167,7 @@ class SpreadsheetExportMixin:
 
     def get_filename(self):
         """Gets the base filename for the exported spreadsheet, without extensions"""
-        return "spreadsheet-export"
+        return self.export_filename
 
     def to_row_dict(self, item):
         """Returns an OrderedDict (in the order given by list_export) of the exportable information for a model instance"""
diff --git a/wagtail/snippets/tests/test_viewset.py b/wagtail/snippets/tests/test_viewset.py
index 60448a1f7c..0a786609a1 100644
--- a/wagtail/snippets/tests/test_viewset.py
+++ b/wagtail/snippets/tests/test_viewset.py
@@ -707,6 +707,11 @@ class TestListExport(BaseSnippetViewSetTests):
         response = self.client.get(self.get_url("list"), {"export": "csv"})
 
         self.assertEqual(response.status_code, 200)
+        self.assertEqual(
+            response.get("Content-Disposition"),
+            'attachment; filename="all-fullfeatured-snippets.csv"',
+        )
+
         data_lines = response.getvalue().decode().split("\n")
         self.assertEqual(
             data_lines[0],
@@ -725,6 +730,11 @@ class TestListExport(BaseSnippetViewSetTests):
         response = self.client.get(self.get_url("list"), {"export": "xlsx"})
 
         self.assertEqual(response.status_code, 200)
+        self.assertEqual(
+            response.get("Content-Disposition"),
+            'attachment; filename="all-fullfeatured-snippets.xlsx"',
+        )
+
         workbook_data = response.getvalue()
         worksheet = load_workbook(filename=BytesIO(workbook_data)).active
         cell_array = [[cell.value for cell in row] for row in worksheet.rows]
diff --git a/wagtail/snippets/views/snippets.py b/wagtail/snippets/views/snippets.py
index 6094b2aa46..4bbaff8c3a 100644
--- a/wagtail/snippets/views/snippets.py
+++ b/wagtail/snippets/views/snippets.py
@@ -672,6 +672,9 @@ class SnippetViewSet(ModelViewSet):
     #: A list or tuple, where each item is the name of a field, an attribute, or a single-argument callable on the model.
     list_export = []
 
+    #: The base file name for the exported listing, without extensions. If unset, the model's :attr:`~django.db.models.Options.db_table` will be used instead.
+    export_filename = None
+
     #: The number of items to display per page in the index view. Defaults to 20.
     list_per_page = 20
 
@@ -868,6 +871,7 @@ class SnippetViewSet(ModelViewSet):
             list_display=self.list_display,
             list_filter=self.list_filter,
             list_export=self.list_export,
+            export_filename=self.get_export_filename(),
             paginate_by=self.list_per_page,
             default_ordering=self.ordering,
             search_fields=self.search_fields,
@@ -891,6 +895,7 @@ class SnippetViewSet(ModelViewSet):
             list_display=self.list_display,
             list_filter=self.list_filter,
             list_export=self.list_export,
+            export_filename=self.get_export_filename(),
             paginate_by=self.list_per_page,
             default_ordering=self.ordering,
             search_fields=self.search_fields,
@@ -1238,6 +1243,9 @@ class SnippetViewSet(ModelViewSet):
         """
         return None
 
+    def get_export_filename(self):
+        return self.export_filename or self.model_opts.db_table
+
     def get_templates(self, action="index", fallback=""):
         """
         Utility function that provides a list of templates to try for a given
diff --git a/wagtail/test/testapp/wagtail_hooks.py b/wagtail/test/testapp/wagtail_hooks.py
index 0e0b36e2ff..a987c5f5b4 100644
--- a/wagtail/test/testapp/wagtail_hooks.py
+++ b/wagtail/test/testapp/wagtail_hooks.py
@@ -264,6 +264,7 @@ class FullFeaturedSnippetViewSet(SnippetViewSet):
         "some_date",
         "first_published_at",
     ]
+    export_filename = "all-fullfeatured-snippets"
     index_template_name = "tests/fullfeaturedsnippet_index.html"
     ordering = ["text", "-_updated_at", "-pk"]
     add_to_admin_menu = True