Repository: django-import-export/django-import-export

Issue:
Export from a specific model view page in Django Admin
Does anyone have any ideas on the best way to add an export button on a model instance's page in Django Admin? The thought being that if you browse to a resource, it would be fairly natural workflow-wise to want to be able to click Export on that page and export that model instance.

I imagine that it is fairly easy to custom code this but it might be a cool feature as a mixin for the model admin, or as an additional feature for the ExportActionMixin. 


Discussion:
I think on this that it should be coupled to the export action. If you have the export action enabled you should be able to export a model from its page (and trigger the export workflow etc just like when you select models and do the drop down).  I’ll wish I was better at the django admin code so maybe this would be a decent learning opportunity for me. 
Export from actions are being refactored in #1671.  Perhaps we could look into this also.

Resolving patch:
diff --git a/import_export/admin.py b/import_export/admin.py
--- a/import_export/admin.py
+++ b/import_export/admin.py
@@ -778,10 +778,33 @@ class ExportActionMixin(ExportMixin):
     Mixin with export functionality implemented as an admin action.
     """
 
+    #: template for change form
+    change_form_template = "admin/import_export/change_form.html"
+
+    #: Flag to indicate whether to show 'export' button on change form
+    show_change_form_export = True
+
     # This action will receive a selection of items as a queryset,
     # store them in the context, and then render the 'export' admin form page,
     # so that users can select file format and resource
 
+    def change_view(self, request, object_id, form_url="", extra_context=None):
+        extra_context = extra_context or {}
+        extra_context["show_change_form_export"] = self.show_change_form_export
+        return super().change_view(
+            request,
+            object_id,
+            form_url,
+            extra_context=extra_context,
+        )
+
+    def response_change(self, request, obj):
+        if "_export-item" in request.POST:
+            return self.export_admin_action(
+                request, self.model.objects.filter(id=obj.id)
+            )
+        return super().response_change(request, obj)
+
     def export_admin_action(self, request, queryset):
         """
         Action runs on POST from instance action menu (if enabled).
@@ -820,7 +843,15 @@ def export_admin_action(self, request, queryset):
 
         # this is necessary to render the FORM action correctly
         # i.e. so the POST goes to the correct URL
-        context["export_suffix"] = "export/"
+        export_url = reverse(
+            "%s:%s_%s_export"
+            % (
+                self.admin_site.name,
+                self.model._meta.app_label,
+                self.model._meta.model_name,
+            )
+        )
+        context["export_url"] = export_url
 
         return render(request, "admin/import_export/export.html", context=context)
 


Test patch:
diff --git a/tests/core/admin.py b/tests/core/admin.py
--- a/tests/core/admin.py
+++ b/tests/core/admin.py
@@ -38,8 +38,7 @@ class BookAdmin(ImportExportModelAdmin):
 
 
 class CategoryAdmin(ExportActionModelAdmin):
-    def export_admin_action(self, request, queryset):
-        return super().export_admin_action(request, queryset)
+    pass
 
 
 class AuthorAdmin(ImportMixin, admin.ModelAdmin):
diff --git a/tests/core/tests/admin_integration/test_action.py b/tests/core/tests/admin_integration/test_action.py
--- a/tests/core/tests/admin_integration/test_action.py
+++ b/tests/core/tests/admin_integration/test_action.py
@@ -2,16 +2,19 @@
 from unittest import mock
 from unittest.mock import MagicMock
 
+from core.admin import CategoryAdmin
 from core.models import Book, Category
 from core.tests.admin_integration.mixins import AdminTestMixin
 from core.tests.utils import ignore_widget_deprecation_warning
+from django.contrib import admin
 from django.core.exceptions import PermissionDenied
 from django.http import HttpRequest
+from django.test import RequestFactory
 from django.test.testcases import TestCase
 from django.test.utils import override_settings
+from django.urls import reverse
 
-from import_export.admin import ExportMixin, ImportExportActionModelAdmin
-from import_export.tmp_storages import TempFolderStorage
+from import_export.admin import ExportMixin
 
 
 class ExportActionAdminIntegrationTest(AdminTestMixin, TestCase):
@@ -120,13 +123,51 @@ def has_export_permission(self, request):
             m.get_export_data("0", Book.objects.none(), request=request)
 
 
-class TestImportExportActionModelAdmin(ImportExportActionModelAdmin):
-    def __init__(self, mock_model, mock_site, error_instance):
-        self.error_instance = error_instance
-        super().__init__(mock_model, mock_site)
+class TestExportButtonOnChangeForm(AdminTestMixin, TestCase):
+    def setUp(self):
+        super().setUp()
+        self.cat1 = Category.objects.create(name="Cat 1")
+        self.change_url = reverse(
+            "%s:%s_%s_change"
+            % (
+                "admin",
+                "core",
+                "category",
+            ),
+            args=[self.cat1.id],
+        )
+        self.target_str = (
+            '<input type="submit" value="Export" '
+            'class="default" name="_export-item">'
+        )
+
+    def test_export_button_on_change_form(self):
+        response = self.client.get(self.change_url)
+        self.assertIn(
+            self.target_str,
+            str(response.content),
+        )
+        response = self.client.post(
+            self.change_url, data={"_export-item": "Export", "name": self.cat1.name}
+        )
+        self.assertIn("Export 1 selected item", str(response.content))
+
+    def test_export_button_on_change_form_disabled(self):
+        class MockCategoryAdmin(CategoryAdmin):
+            show_change_form_export = True
+
+        factory = RequestFactory()
+        category_admin = MockCategoryAdmin(Category, admin.site)
+
+        request = factory.get(self.change_url)
+        request.user = self.user
+
+        response = category_admin.change_view(request, str(self.cat1.id))
+        response.render()
 
-    def write_to_tmp_storage(self, import_file, input_format):
-        mock_storage = MagicMock(spec=TempFolderStorage)
+        self.assertIn(self.target_str, str(response.content))
 
-        mock_storage.read.side_effect = self.error_instance
-        return mock_storage
+        category_admin.show_change_form_export = False
+        response = category_admin.change_view(request, str(self.cat1.id))
+        response.render()
+        self.assertNotIn(self.target_str, str(response.content))