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

Issue:
Support dynamic selection of Resource class based on request property
It would be ideal to allow the selection of a Resource based on a request property.

For example, see [this SO question](https://stackoverflow.com/questions/78207094/how-to-dynamically-change-resource-class-in-django-import-export-admin-based-on).

To do this, it would be necessary to pass the request object as a kwarg into the [`get_export_resource_classes()`](https://github.com/django-import-export/django-import-export/blob/493ff3c65131814ecd5ef5d12ee547c60bc253fd/import_export/admin.py#L824) method.  Then the user could override this to dynamically select a Resource class based on a request property.




Discussion:

Resolving patch:
diff --git a/import_export/admin.py b/import_export/admin.py
--- a/import_export/admin.py
+++ b/import_export/admin.py
@@ -149,7 +149,7 @@ def get_urls(self):
         return my_urls + urls
 
     @method_decorator(require_POST)
-    def process_import(self, request, *args, **kwargs):
+    def process_import(self, request, **kwargs):
         """
         Perform the actual import action (after the user has confirmed the import)
         """
@@ -173,9 +173,7 @@ def process_import(self, request, *args, **kwargs):
 
             data = tmp_storage.read()
             dataset = input_format.create_dataset(data)
-            result = self.process_dataset(
-                dataset, confirm_form, request, *args, **kwargs
-            )
+            result = self.process_dataset(dataset, confirm_form, request, **kwargs)
 
             tmp_storage.remove()
 
@@ -184,22 +182,19 @@ def process_import(self, request, *args, **kwargs):
     def process_dataset(
         self,
         dataset,
-        confirm_form,
+        form,
         request,
-        *args,
         **kwargs,
     ):
-        res_kwargs = self.get_import_resource_kwargs(
-            request, form=confirm_form, **kwargs
-        )
-        resource = self.choose_import_resource_class(confirm_form)(**res_kwargs)
-        imp_kwargs = self.get_import_data_kwargs(request, form=confirm_form, **kwargs)
+        res_kwargs = self.get_import_resource_kwargs(request, **kwargs)
+        resource = self.choose_import_resource_class(form, request)(**res_kwargs)
+        imp_kwargs = self.get_import_data_kwargs(request=request, form=form, **kwargs)
         imp_kwargs["retain_instance_in_row_result"] = True
 
         return resource.import_data(
             dataset,
             dry_run=False,
-            file_name=confirm_form.cleaned_data.get("original_file_name"),
+            file_name=form.cleaned_data.get("original_file_name"),
             user=request.user,
             **imp_kwargs,
         )
@@ -286,7 +281,7 @@ def create_import_form(self, request):
         form_class = self.get_import_form_class(request)
         kwargs = self.get_import_form_kwargs(request)
 
-        return form_class(formats, self.get_import_resource_classes(), **kwargs)
+        return form_class(formats, self.get_import_resource_classes(request), **kwargs)
 
     def get_import_form_class(self, request):
         """
@@ -389,7 +384,7 @@ def get_confirm_form_initial(self, request, import_form):
             "resource": import_form.cleaned_data.get("resource", ""),
         }
 
-    def get_import_data_kwargs(self, request, *args, **kwargs):
+    def get_import_data_kwargs(self, **kwargs):
         """
         Prepare kwargs for import_data.
         """
@@ -397,7 +392,7 @@ def get_import_data_kwargs(self, request, *args, **kwargs):
         if form:
             kwargs.pop("form")
             return kwargs
-        return {}
+        return kwargs
 
     def write_to_tmp_storage(self, import_file, input_format):
         encoding = None
@@ -425,7 +420,7 @@ def add_data_read_fail_error_to_form(self, form, e):
         ) % {"exc_name": exc_name}
         form.add_error("import_file", msg)
 
-    def import_action(self, request, *args, **kwargs):
+    def import_action(self, request, **kwargs):
         """
         Perform a dry_run of the import to make sure the import will not
         result in errors.  If there are no errors, save the user
@@ -465,7 +460,6 @@ def import_action(self, request, *args, **kwargs):
                         dataset,
                         import_form,
                         request,
-                        *args,
                         raise_errors=False,
                         rollback_on_validation_errors=True,
                         **kwargs,
@@ -504,14 +498,14 @@ def import_action(self, request, *args, **kwargs):
                     res_kwargs = self.get_import_resource_kwargs(
                         request, form=import_form, **kwargs
                     )
-                    resource = self.choose_import_resource_class(import_form)(
+                    resource = self.choose_import_resource_class(import_form, request)(
                         **res_kwargs
                     )
                     resources = [resource]
 
                     # prepare additional kwargs for import_data, if needed
                     imp_kwargs = self.get_import_data_kwargs(
-                        request, *args, form=import_form, **kwargs
+                        request=request, form=import_form, **kwargs
                     )
                     try:
                         result = resource.import_data(
@@ -536,9 +530,9 @@ def import_action(self, request, *args, **kwargs):
 
         else:
             res_kwargs = self.get_import_resource_kwargs(
-                request, form=import_form, **kwargs
+                request=request, form=import_form, **kwargs
             )
-            resource_classes = self.get_import_resource_classes()
+            resource_classes = self.get_import_resource_classes(request)
             resources = [
                 resource_class(**res_kwargs) for resource_class in resource_classes
             ]
@@ -696,11 +690,10 @@ def get_results(self, request):
         # Fallback in case the ChangeList doesn't have queryset attribute set
         return cl.get_queryset(request)
 
-    def get_export_data(self, file_format, queryset, *args, **kwargs):
+    def get_export_data(self, file_format, request, queryset, **kwargs):
         """
         Returns file_format representation for given queryset.
         """
-        request = kwargs.pop("request")
         if not self.has_export_permission(request):
             raise PermissionDenied
 
@@ -735,7 +728,7 @@ def export_action(self, request):
         formats = self.get_export_formats()
         form = form_type(
             formats,
-            self.get_export_resource_classes(),
+            self.get_export_resource_classes(request),
             data=request.POST or None,
         )
         form.fields["export_items"] = MultipleChoiceField(
@@ -759,8 +752,8 @@ def export_action(self, request):
             try:
                 export_data = self.get_export_data(
                     file_format,
+                    request,
                     queryset,
-                    request=request,
                     encoding=self.to_encoding,
                     export_form=form,
                 )
@@ -804,7 +797,7 @@ def init_request_context_data(self, request, form):
                     ).get_user_visible_fields()
                 ],
             )
-            for res in self.get_export_resource_classes()
+            for res in self.get_export_resource_classes(request)
         ]
         return context
 
@@ -870,7 +863,7 @@ def export_admin_action(self, request, queryset):
             file_format = formats[0]()
 
             export_data = self.get_export_data(
-                file_format, queryset, request=request, encoding=self.to_encoding
+                file_format, request, queryset, encoding=self.to_encoding
             )
             content_type = file_format.get_content_type()
             response = HttpResponse(export_data, content_type=content_type)
@@ -883,7 +876,7 @@ def export_admin_action(self, request, queryset):
         formats = self.get_export_formats()
         form = form_type(
             formats=formats,
-            resources=self.get_export_resource_classes(),
+            resources=self.get_export_resource_classes(request),
             initial={"export_items": list(queryset.values_list("id", flat=True))},
         )
         # selected items are to be stored as a hidden input on the form
diff --git a/import_export/forms.py b/import_export/forms.py
--- a/import_export/forms.py
+++ b/import_export/forms.py
@@ -21,8 +21,8 @@ class ImportExportFormBase(forms.Form):
         choices=(),
     )
 
-    def __init__(self, formats, resources, *args, **kwargs):
-        super().__init__(*args, **kwargs)
+    def __init__(self, formats, resources, **kwargs):
+        super().__init__(**kwargs)
         self._init_resources(resources)
         self._init_formats(formats)
 
@@ -62,8 +62,8 @@ class ImportForm(ImportExportFormBase):
     # so that the 'guess_format' js logic makes sense
     field_order = ["resource", "import_file", "format"]
 
-    def __init__(self, formats, resources, *args, **kwargs):
-        super().__init__(formats, resources, *args, **kwargs)
+    def __init__(self, formats, resources, **kwargs):
+        super().__init__(formats, resources, **kwargs)
         if len(formats) > 1:
             self.fields["import_file"].widget.attrs["class"] = "guess_format"
             self.fields["format"].widget.attrs["class"] = "guess_format"
@@ -99,8 +99,8 @@ class ExportForm(ImportExportFormBase):
 
 
 class SelectableFieldsExportForm(ExportForm):
-    def __init__(self, formats, resources, *args, **kwargs):
-        super().__init__(formats, resources, *args, **kwargs)
+    def __init__(self, formats, resources, **kwargs):
+        super().__init__(formats, resources, **kwargs)
         self._init_selectable_fields(resources)
 
     @property
diff --git a/import_export/mixins.py b/import_export/mixins.py
--- a/import_export/mixins.py
+++ b/import_export/mixins.py
@@ -20,7 +20,6 @@ class BaseImportExportMixin:
     interface.
     """
 
-    resource_class = None
     resource_classes = []
 
     @property
@@ -42,19 +41,22 @@ def check_resource_classes(self, resource_classes):
                 "subscriptable (list, tuple, ...)"
             )
 
-    def get_resource_classes(self):
-        """Return subscriptable type (list, tuple, ...) containing resource classes"""
+    def get_resource_classes(self, request):
+        """
+        Return subscriptable type (list, tuple, ...) containing resource classes
+        :param request: The request object.
+        :returns: The Resource classes.
+        """
         if not self.resource_classes:
             return [modelresource_factory(self.model)]
         return self.resource_classes
 
-    def get_resource_kwargs(self, request, *args, **kwargs):
+    def get_resource_kwargs(self, request, **kwargs):
         """
         Return the kwargs which are to be passed to the Resource constructor.
         Can be overridden to provide additional kwarg params.
 
         :param request: The request object.
-        :param args: Positional arguments.
         :param kwargs: Keyword arguments.
         :returns: The Resource kwargs (by default, is the kwargs passed).
         """
@@ -77,11 +79,12 @@ def get_resource_index(self, form):
 
 
 class BaseImportMixin(BaseImportExportMixin):
-    def get_import_resource_classes(self):
+    def get_import_resource_classes(self, request):
         """
+        :param request: The request object.
         Returns ResourceClass subscriptable (list, tuple, ...) to use for import.
         """
-        resource_classes = self.get_resource_classes()
+        resource_classes = self.get_resource_classes(request)
         self.check_resource_classes(resource_classes)
         return resource_classes
 
@@ -92,11 +95,23 @@ def get_import_formats(self):
         return [f for f in self.import_formats if f().can_import()]
 
     def get_import_resource_kwargs(self, request, **kwargs):
+        """
+        Returns kwargs which will be passed to the Resource constructor.
+        :param request: The request object.
+        :param kwargs: Keyword arguments.
+        :returns: The kwargs (dict)
+        """
         return self.get_resource_kwargs(request, **kwargs)
 
-    def choose_import_resource_class(self, form):
+    def choose_import_resource_class(self, form, request):
+        """
+        Identify which class should be used for import
+        :param form: The form object.
+        :param request: The request object.
+        :returns: The export Resource class.
+        """
         resource_index = self.get_resource_index(form)
-        return self.get_import_resource_classes()[resource_index]
+        return self.get_import_resource_classes(request)[resource_index]
 
 
 class BaseExportMixin(BaseImportExportMixin):
@@ -108,19 +123,33 @@ def get_export_formats(self):
         """
         return [f for f in self.export_formats if f().can_export()]
 
-    def get_export_resource_classes(self):
+    def get_export_resource_classes(self, request):
         """
         Returns ResourceClass subscriptable (list, tuple, ...) to use for export.
+        :param request: The request object.
+        :returns: The Resource classes.
         """
-        resource_classes = self.get_resource_classes()
+        resource_classes = self.get_resource_classes(request)
         self.check_resource_classes(resource_classes)
         return resource_classes
 
-    def choose_export_resource_class(self, form):
+    def choose_export_resource_class(self, form, request):
+        """
+        Identify which class should be used for export
+        :param request: The request object.
+        :param form: The form object.
+        :returns: The export Resource class.
+        """
         resource_index = self.get_resource_index(form)
-        return self.get_export_resource_classes()[resource_index]
+        return self.get_export_resource_classes(request)[resource_index]
 
     def get_export_resource_kwargs(self, request, **kwargs):
+        """
+        Returns kwargs which will be passed to the Resource constructor.
+        :param request: The request object.
+        :param kwargs: Keyword arguments.
+        :returns: The kwargs (dict)
+        """
         return self.get_resource_kwargs(request, **kwargs)
 
     def get_export_resource_fields_from_form(self, form):
@@ -133,7 +162,7 @@ def get_export_resource_fields_from_form(self, form):
 
     def get_data_for_export(self, request, queryset, **kwargs):
         export_form = kwargs.get("export_form")
-        export_class = self.choose_export_resource_class(export_form)
+        export_class = self.choose_export_resource_class(export_form, request)
         export_resource_kwargs = self.get_export_resource_kwargs(request, **kwargs)
         export_fields = self.get_export_resource_fields_from_form(export_form)
         cls = export_class(**export_resource_kwargs)
@@ -155,11 +184,11 @@ def get_export_filename(self, file_format):
 class ExportViewMixin(BaseExportMixin):
     form_class = SelectableFieldsExportForm
 
-    def get_export_data(self, file_format, queryset, *args, **kwargs):
+    def get_export_data(self, file_format, queryset, **kwargs):
         """
         Returns file_format representation for given queryset.
         """
-        data = self.get_data_for_export(self.request, queryset, *args, **kwargs)
+        data = self.get_data_for_export(self.request, queryset, **kwargs)
         export_data = file_format.export_data(data)
         return export_data
 
@@ -170,7 +199,7 @@ def get_context_data(self, **kwargs):
     def get_form_kwargs(self):
         kwargs = super().get_form_kwargs()
         kwargs["formats"] = self.get_export_formats()
-        kwargs["resources"] = self.get_export_resource_classes()
+        kwargs["resources"] = self.get_export_resource_classes(self.request)
         return kwargs
 
 


Test patch:
diff --git a/tests/core/admin.py b/tests/core/admin.py
--- a/tests/core/admin.py
+++ b/tests/core/admin.py
@@ -50,7 +50,7 @@ def __init__(self, **kwargs):
         super().__init__()
         self.author_id = kwargs.get("author_id")
 
-    def filter_export(self, queryset, *args, **kwargs):
+    def filter_export(self, queryset, **kwargs):
         return queryset.filter(author_id=self.author_id)
 
     class Meta:
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
@@ -130,7 +130,7 @@ def has_export_permission(self, request):
 
         m = TestMixin()
         with self.assertRaises(PermissionDenied):
-            m.get_export_data("0", Book.objects.none(), request=request)
+            m.get_export_data("0", request, Book.objects.none())
 
 
 class TestExportButtonOnChangeForm(AdminTestMixin, TestCase):
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
@@ -72,7 +72,7 @@ def test_export_passes_export_resource_kwargs(
         self.assertEqual(response.status_code, 200)
         self.assertEqual(2, mock_get_export_resource_kwargs.call_count)
 
-    def book_resource_init(self):
+    def book_resource_init(self, **kwargs):
         # stub call to the resource constructor
         pass
 
@@ -343,7 +343,7 @@ class TestMixin(ExportMixin):
         def __init__(self, test_str=None):
             self.test_str = test_str
 
-        def get_data_for_export(self, request, queryset, *args, **kwargs):
+        def get_data_for_export(self, request, queryset, **kwargs):
             dataset = Dataset(headers=["id", "name"])
             dataset.append([1, self.test_str])
             return dataset
@@ -361,7 +361,7 @@ def setUp(self):
     def test_to_encoding_not_set_default_encoding_is_utf8(self):
         self.export_mixin = self.TestMixin(test_str="teststr")
         data = self.export_mixin.get_export_data(
-            self.file_format, list(), request=self.mock_request
+            self.file_format, self.mock_request, list()
         )
         csv_dataset = tablib.import_set(data)
         self.assertEqual("teststr", csv_dataset.dict[0]["name"])
@@ -369,7 +369,7 @@ def test_to_encoding_not_set_default_encoding_is_utf8(self):
     def test_to_encoding_set(self):
         self.export_mixin = self.TestMixin(test_str="ハローワールド")
         data = self.export_mixin.get_export_data(
-            self.file_format, list(), request=self.mock_request, encoding="shift-jis"
+            self.file_format, self.mock_request, list(), encoding="shift-jis"
         )
         encoding = chardet.detect(bytes(data))["encoding"]
         self.assertEqual("SHIFT_JIS", encoding)
@@ -379,8 +379,8 @@ def test_to_encoding_set_incorrect(self):
         with self.assertRaises(LookupError):
             self.export_mixin.get_export_data(
                 self.file_format,
+                self.mock_request,
                 list(),
-                request=self.mock_request,
                 encoding="bad-encoding",
             )
 
@@ -389,7 +389,9 @@ def test_to_encoding_not_set_for_binary_file(self):
         self.export_mixin = self.TestMixin(test_str="teststr")
         self.file_format = formats.base_formats.XLSX()
         data = self.export_mixin.get_export_data(
-            self.file_format, list(), request=self.mock_request
+            self.file_format,
+            self.mock_request,
+            list(),
         )
         binary_dataset = tablib.import_set(data)
         self.assertEqual("teststr", binary_dataset.dict[0]["name"])
diff --git a/tests/core/tests/admin_integration/test_import.py b/tests/core/tests/admin_integration/test_import.py
--- a/tests/core/tests/admin_integration/test_import.py
+++ b/tests/core/tests/admin_integration/test_import.py
@@ -2,7 +2,7 @@
 import warnings
 from io import StringIO
 from unittest import mock
-from unittest.mock import MagicMock, patch
+from unittest.mock import patch
 
 import django
 from core.admin import AuthorAdmin, BookAdmin, CustomBookAdmin, ImportMixin
@@ -12,7 +12,6 @@
 from django.contrib.admin.models import DELETION, LogEntry
 from django.contrib.admin.sites import AdminSite
 from django.contrib.auth.models import User
-from django.http import HttpRequest
 from django.test import RequestFactory
 from django.test.testcases import TestCase, TransactionTestCase
 from django.test.utils import override_settings
@@ -480,27 +479,25 @@ def test_get_tmp_storage_class_attribute(self):
 
     def test_get_import_data_kwargs_with_form_kwarg(self):
         """
-        Test that if a the method is called with a 'form' kwarg,
+        Test that if the method is called with a 'form' kwarg,
         then it is removed and the updated dict is returned
         """
-        request = MagicMock(spec=HttpRequest)
         m = ImportMixin()
         kw = {"a": 1, "form": "some_form"}
         target = {"a": 1}
-        self.assertEqual(target, m.get_import_data_kwargs(request, **kw))
+        self.assertEqual(target, m.get_import_data_kwargs(**kw))
 
-    def test_get_import_data_kwargs_with_no_form_kwarg_returns_empty_dict(self):
+    def test_get_import_data_kwargs_with_no_form_kwarg_returns_kwarg_dict(self):
         """
         Test that if the method is called with no 'form' kwarg,
         then an empty dict is returned
         """
-        request = MagicMock(spec=HttpRequest)
         m = ImportMixin()
         kw = {
             "a": 1,
         }
-        target = {}
-        self.assertEqual(target, m.get_import_data_kwargs(request, **kw))
+        target = {"a": 1}
+        self.assertEqual(target, m.get_import_data_kwargs(**kw))
 
     def test_get_context_data_returns_empty_dict(self):
         m = ExportMixin()
diff --git a/tests/core/tests/resources.py b/tests/core/tests/resources.py
--- a/tests/core/tests/resources.py
+++ b/tests/core/tests/resources.py
@@ -44,10 +44,10 @@ class Meta:
 
 
 class BookResourceWithLineNumberLogger(BookResource):
-    def __init__(self, *args, **kwargs):
+    def __init__(self, **kwargs):
         self.before_lines = []
         self.after_lines = []
-        return super().__init__(*args, **kwargs)
+        return super().__init__(**kwargs)
 
     def before_import_row(self, row, **kwargs):
         row_number = kwargs.pop("row_number")
diff --git a/tests/core/tests/test_mixins.py b/tests/core/tests/test_mixins.py
--- a/tests/core/tests/test_mixins.py
+++ b/tests/core/tests/test_mixins.py
@@ -150,24 +150,24 @@ class MixinModelAdminTest(TestCase):
     class BaseImportModelAdminTest(mixins.BaseImportMixin):
         call_count = 0
 
-        def get_resource_classes(self):
+        def get_resource_classes(self, request, **kwargs):
             self.call_count += 1
 
-        def get_resource_kwargs(self, request, *args, **kwargs):
+        def get_resource_kwargs(self, request, **kwargs):
             self.call_count += 1
 
     class BaseExportModelAdminTest(mixins.BaseExportMixin):
         call_count = 0
 
-        def get_resource_classes(self):
+        def get_resource_classes(self, request, **kwargs):
             self.call_count += 1
 
-        def get_resource_kwargs(self, request, *args, **kwargs):
+        def get_export_resource_kwargs(self, request, **kwargs):
             self.call_count += 1
 
     def test_get_import_resource_class_calls_self_get_resource_class(self):
         admin = self.BaseImportModelAdminTest()
-        admin.get_import_resource_classes()
+        admin.get_import_resource_classes(self.request)
         self.assertEqual(1, admin.call_count)
 
     def test_get_import_resource_kwargs_calls_self_get_resource_kwargs(self):
@@ -177,7 +177,7 @@ def test_get_import_resource_kwargs_calls_self_get_resource_kwargs(self):
 
     def test_get_export_resource_class_calls_self_get_resource_class(self):
         admin = self.BaseExportModelAdminTest()
-        admin.get_export_resource_classes()
+        admin.get_export_resource_classes(self.request)
         self.assertEqual(1, admin.call_count)
 
     def test_get_export_resource_kwargs_calls_self_get_resource_kwargs(self):
@@ -194,7 +194,7 @@ def test_faulty_resource_class_raises_exception(self):
         with self.assertRaisesRegex(
             Exception, r"^The resource_classes field type must be subscriptable"
         ):
-            admin.get_export_resource_classes()
+            admin.get_export_resource_classes(self.request)
 
     class BaseModelAdminBothResourceTest(mixins.BaseExportMixin):
         call_count = 0
@@ -209,10 +209,14 @@ class BaseModelExportChooseTest(mixins.BaseExportMixin):
     def test_choose_export_resource_class(self, form):
         """Test choose_export_resource_class() method"""
         admin = self.BaseModelExportChooseTest()
-        self.assertEqual(admin.choose_export_resource_class(form), resources.Resource)
+        self.assertEqual(
+            admin.choose_export_resource_class(form, self.request), resources.Resource
+        )
 
         form.cleaned_data = {"resource": 1}
-        self.assertEqual(admin.choose_export_resource_class(form), FooResource)
+        self.assertEqual(
+            admin.choose_export_resource_class(form, self.request), FooResource
+        )
 
     class BaseModelImportChooseTest(mixins.BaseImportMixin):
         resource_classes = [resources.Resource, FooResource]
@@ -221,10 +225,14 @@ class BaseModelImportChooseTest(mixins.BaseImportMixin):
     def test_choose_import_resource_class(self, form):
         """Test choose_import_resource_class() method"""
         admin = self.BaseModelImportChooseTest()
-        self.assertEqual(admin.choose_import_resource_class(form), resources.Resource)
+        request = MagicMock(spec=HttpRequest)
+        self.assertEqual(
+            admin.choose_import_resource_class(form, request),
+            resources.Resource,
+        )
 
         form.cleaned_data = {"resource": 1}
-        self.assertEqual(admin.choose_import_resource_class(form), FooResource)
+        self.assertEqual(admin.choose_import_resource_class(form, request), FooResource)
 
 
 class BaseExportMixinTest(TestCase):
@@ -301,7 +309,7 @@ class TestMixin(mixins.BaseImportExportMixin):
     def test_get_resource_kwargs(self):
         mixin_instance = self.TestMixin()
         test_kwargs = {"key1": "value1", "key2": "value2"}
-
-        result = mixin_instance.get_resource_kwargs(HttpRequest, **test_kwargs)
+        mock_request = MagicMock(spec=HttpRequest)
+        result = mixin_instance.get_resource_kwargs(mock_request, **test_kwargs)
 
         self.assertEqual(result, test_kwargs)