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

Issue:
Admin export confirm page runs a query for the entire table
When the export page is loaded, the code runs a query for the entire table of the model (i.e. the equivalent of `SELECT * FROM table`):

https://github.com/django-import-export/django-import-export/blob/9839a28089575baef1ecab686ab81682751ed761/import_export/admin.py#L736

When trying to export even a moderately sized table, which I think is a common scenario for django-import-export, such a query is very problematic, especially because it ignores any filtering on the queryset.

I'm not sure if this query is really intentional, but if it is, would it possible to add a way to turn it off?


Discussion:
Thanks for raising - you make a valid point.  I will take a look.

Resolving patch:
diff --git a/import_export/admin.py b/import_export/admin.py
--- a/import_export/admin.py
+++ b/import_export/admin.py
@@ -721,11 +721,14 @@ def export_action(self, request):
             self.get_export_resource_classes(request),
             data=request.POST or None,
         )
-        form.fields["export_items"] = MultipleChoiceField(
-            widget=MultipleHiddenInput,
-            required=False,
-            choices=[(o.pk, o.pk) for o in self.model.objects.all()],
-        )
+        if request.POST and "export_items" in request.POST:
+            # this field is instantiated if the export is POSTed from the
+            # 'action' drop down
+            form.fields["export_items"] = MultipleChoiceField(
+                widget=MultipleHiddenInput,
+                required=False,
+                choices=[(pk, pk) for pk in self.get_valid_export_item_pks(request)],
+            )
         if form.is_valid():
             file_format = formats[int(form.cleaned_data["format"])]()
 
@@ -762,6 +765,18 @@ def export_action(self, request):
         request.current_app = self.admin_site.name
         return TemplateResponse(request, [self.export_template_name], context=context)
 
+    def get_valid_export_item_pks(self, request):
+        """
+        Returns a list of valid pks for export.
+        This is used to validate which objects can be exported when exports are
+        triggered from the Admin UI 'action' dropdown.
+        This can be overridden to filter returned pks for performance and/or security
+        reasons.
+        :param request: The request object.
+        :returns: a list of valid pks (by default is all pks in table).
+        """
+        return self.model.objects.all().values_list("pk", flat=True)
+
     def changelist_view(self, request, extra_context=None):
         if extra_context is None:
             extra_context = {}
@@ -835,6 +850,7 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
         )
 
     def response_change(self, request, obj):
+        # called if the export is triggered from the instance detail page.
         if "_export-item" in request.POST:
             return self.export_admin_action(
                 request, self.model.objects.filter(id=obj.id)
@@ -864,16 +880,15 @@ def export_admin_action(self, request, queryset):
 
         form_type = self.get_export_form_class()
         formats = self.get_export_formats()
+        export_items = list(queryset.values_list("pk", flat=True))
         form = form_type(
             formats=formats,
             resources=self.get_export_resource_classes(request),
-            initial={"export_items": list(queryset.values_list("pk", flat=True))},
+            initial={"export_items": export_items},
         )
         # selected items are to be stored as a hidden input on the form
         form.fields["export_items"] = MultipleChoiceField(
-            widget=MultipleHiddenInput,
-            required=False,
-            choices=[(str(o), str(o)) for o in queryset.all()],
+            widget=MultipleHiddenInput, required=False, choices=export_items
         )
         context = self.init_request_context_data(request, form)
 


Test patch:
diff --git a/tests/core/tests/admin_integration/test_action_export.py b/tests/core/tests/admin_integration/test_action_export.py
--- a/tests/core/tests/admin_integration/test_action_export.py
+++ b/tests/core/tests/admin_integration/test_action_export.py
@@ -128,6 +128,25 @@ def test_export_admin_action(self):
             self.assertTrue(200 <= response.status_code <= 399)
             mock_export_admin_action.assert_called()
 
+    def test_export_admin_action_with_restricted_pks(self):
+        data = {
+            "format": "0",
+            "export_items": [str(self.cat1.id)],
+            **self.resource_fields_payload,
+        }
+        # mock returning a set of pks which is not in the submitted range
+        with mock.patch(
+            "import_export.admin.ExportMixin.get_valid_export_item_pks"
+        ) as mock_valid_pks:
+            mock_valid_pks.return_value = [999]
+            response = self.client.post(self.category_export_url, data)
+            self.assertEqual(response.status_code, 200)
+            self.assertIn(
+                "Select a valid choice. "
+                f"{self.cat1.id} is not one of the available choices.",
+                str(response.content),
+            )
+
     def test_get_export_data_raises_PermissionDenied_when_no_export_permission_assigned(
         self,
     ):
diff --git a/tests/core/tests/admin_integration/test_export.py b/tests/core/tests/admin_integration/test_export.py
--- a/tests/core/tests/admin_integration/test_export.py
+++ b/tests/core/tests/admin_integration/test_export.py
@@ -45,7 +45,7 @@ def test_export(self):
         data = {"format": "0", **self.bookresource_export_fields_payload}
         date_str = datetime.now().strftime("%Y-%m-%d")
         # Should not contain COUNT queries from ModelAdmin.get_results()
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(5):
             response = self.client.post(self.book_export_url, data)
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.has_header("Content-Disposition"))