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

Issue:
Importing should not call export logic
**Describe the bug**

By default, import logic will call `dehydrate_*()` methods, even though dehydrate methods are supposed to only be called when [exporting](https://django-import-export.readthedocs.io/en/latest/advanced_usage.html#advanced-data-manipulation-on-export).

This occurs because the `Diff` class calls into export workflow.

The workaround is to set `skip_diff` to True if diff logic is not required.

**To Reproduce**

- Run the example app
- Define a dehydrate method on `Book` and set a breakpoint

```python
    def dehydrate_name(self, book):
        book_name = getattr(book, "name", "unknown")
        return '%s' % (book_name.upper())
```

- Import a file in the admin UI
- The dehydrate method is called



**Versions (please complete the following information):**
 - Django Import Export: 3.2.0
 - Python 3.10
 - Django 4.1

**Expected behavior**

Export logic (dehydrate methods) should not be called when importing.


**Screenshots**
If applicable, add screenshots to help explain your problem.

**Additional context**

This was raised in #1575 as it was reported that it caused performance problems on import.

See also:  #974, #1574 



Discussion:

Resolving patch:
diff --git a/import_export/resources.py b/import_export/resources.py
--- a/import_export/resources.py
+++ b/import_export/resources.py
@@ -83,6 +83,11 @@ class ResourceOptions:
     identify existing instances.
     """
 
+    import_order = None
+    """
+    Controls import order for columns.
+    """
+
     export_order = None
     """
     Controls export order for columns.
@@ -256,12 +261,12 @@ def __new__(cls, name, bases, attrs):
 
 class Diff:
     def __init__(self, resource, instance, new):
-        self.left = self._export_resource_fields(resource, instance)
+        self.left = Diff._read_field_values(resource, instance)
         self.right = []
         self.new = new
 
-    def compare_with(self, resource, instance, dry_run=False):
-        self.right = self._export_resource_fields(resource, instance)
+    def compare_with(self, resource, instance):
+        self.right = Diff._read_field_values(resource, instance)
 
     def as_html(self):
         data = []
@@ -276,11 +281,9 @@ def as_html(self):
             data.append(html)
         return data
 
-    def _export_resource_fields(self, resource, instance):
-        return [
-            resource.export_field(f, instance) if instance else ""
-            for f in resource.get_user_visible_fields()
-        ]
+    @classmethod
+    def _read_field_values(cls, resource, instance):
+        return [f.export(instance) for f in resource.get_import_fields()]
 
 
 class Resource(metaclass=DeclarativeMetaclass):
@@ -356,10 +359,9 @@ def get_chunk_size(self):
 
     def get_fields(self, **kwargs):
         """
-        Returns fields sorted according to
-        :attr:`~import_export.resources.ResourceOptions.export_order`.
+        Returns list of fields (unordered).
         """
-        return [self.fields[f] for f in self.get_export_order()]
+        return list(self.fields.values())
 
     def get_field_name(self, field):
         """
@@ -577,7 +579,7 @@ def import_field(self, field, obj, data, is_m2m=False, **kwargs):
             field.save(obj, data, is_m2m, **kwargs)
 
     def get_import_fields(self):
-        return self.get_fields()
+        return [self.fields[f] for f in self.get_import_order()]
 
     def import_obj(self, obj, data, dry_run, **kwargs):
         """
@@ -787,7 +789,7 @@ def import_row(
                 if new:
                     row_result.import_type = RowResult.IMPORT_TYPE_SKIP
                     if not skip_diff:
-                        diff.compare_with(self, None, dry_run)
+                        diff.compare_with(self, None)
                 else:
                     row_result.import_type = RowResult.IMPORT_TYPE_DELETE
                     row_result.add_instance_info(instance)
@@ -795,7 +797,7 @@ def import_row(
                         row_result.instance = instance
                     self.delete_instance(instance, using_transactions, dry_run)
                     if not skip_diff:
-                        diff.compare_with(self, None, dry_run)
+                        diff.compare_with(self, None)
             else:
                 import_validation_errors = {}
                 try:
@@ -818,7 +820,7 @@ def import_row(
                 if self._meta.store_instance:
                     row_result.instance = instance
                 if not skip_diff:
-                    diff.compare_with(self, instance, dry_run)
+                    diff.compare_with(self, instance)
                     if not new:
                         row_result.original = original
 
@@ -1034,9 +1036,11 @@ def import_data_inner(
 
         return result
 
+    def get_import_order(self):
+        return self._get_ordered_field_names("import_order")
+
     def get_export_order(self):
-        order = tuple(self._meta.export_order or ())
-        return order + tuple(k for k in self.fields if k not in order)
+        return self._get_ordered_field_names("export_order")
 
     def before_export(self, queryset, *args, **kwargs):
         """
@@ -1066,7 +1070,7 @@ def export_field(self, field, obj):
         return field.export(obj)
 
     def get_export_fields(self):
-        return self.get_fields()
+        return [self.fields[f] for f in self.get_export_order()]
 
     def export_resource(self, obj):
         return [self.export_field(field, obj) for field in self.get_export_fields()]
@@ -1136,6 +1140,19 @@ def export(self, *args, queryset=None, **kwargs):
 
         return data
 
+    def _get_ordered_field_names(self, order_field):
+        """
+        Return a list of field names, respecting any defined ordering.
+        """
+        # get any declared 'order' fields
+        order_fields = getattr(self._meta, order_field) or ()
+        # get any defined fields
+        defined_fields = order_fields + tuple(getattr(self._meta, "fields") or ())
+
+        order = list()
+        [order.append(f) for f in defined_fields if f not in order]
+        return tuple(order) + tuple(k for k in self.fields if k not in order)
+
 
 class ModelDeclarativeMetaclass(DeclarativeMetaclass):
     def __new__(cls, name, bases, attrs):


Test patch:
diff --git a/tests/core/tests/test_resources.py b/tests/core/tests/test_resources.py
--- a/tests/core/tests/test_resources.py
+++ b/tests/core/tests/test_resources.py
@@ -107,7 +107,7 @@ class A(MyResource):
 
         A()
 
-    def test_get_export_order(self):
+    def test_get_export_headers_order(self):
         self.assertEqual(
             self.my_resource.get_export_headers(), ["email", "name", "extra"]
         )
@@ -1847,7 +1847,8 @@ class Meta:
         resource = MyPersonResource()
         dataset = resource.export(Person.objects.all())
         self.assertEqual(len(dataset), 1)
-        self.assertEqual(dataset[0][0], "foo")
+        self.assertEqual(1, dataset[0][0])
+        self.assertEqual("foo", dataset[0][1])
 
         self.role.user = None
         self.role.save()
@@ -1855,7 +1856,8 @@ class Meta:
         resource = MyPersonResource()
         dataset = resource.export(Person.objects.all())
         self.assertEqual(len(dataset), 1)
-        self.assertEqual(dataset[0][0], None)
+        self.assertEqual(1, dataset[0][0])
+        self.assertEqual(None, dataset[0][1])
 
 
 class ManyRelatedManagerDiffTest(TestCase):
@@ -2849,3 +2851,109 @@ def test_after_import_row_check_for_change(self):
         # issue 1583 - assert that `original` object is available to after_import_row()
         self.resource.import_data(self.dataset, raise_errors=True)
         self.assertTrue(self.resource.is_published)
+
+
+class ImportExportFieldOrderTest(TestCase):
+    class BaseBookResource(resources.ModelResource):
+        def __init__(self):
+            self.field_names = list()
+
+        def import_field(self, field, obj, data, is_m2m=False, **kwargs):
+            # mock out import_field() so that we can see the order
+            # fields were called
+            self.field_names.append(field.column_name)
+
+    class UnorderedBookResource(BaseBookResource):
+        class Meta:
+            fields = ("price", "id", "name")
+            model = Book
+
+    class OrderedBookResource(BaseBookResource):
+        class Meta:
+            fields = ("price", "id", "name")
+            import_order = ("price", "name", "id")
+            export_order = ("price", "name", "id")
+            model = Book
+
+    class SubsetOrderedBookResource(BaseBookResource):
+        class Meta:
+            fields = ("price", "id", "name", "published")
+            import_order = ("name",)
+            export_order = ("published",)
+            model = Book
+
+    class DuplicateFieldsBookResource(BaseBookResource):
+        class Meta:
+            fields = ("id", "price", "name", "price")
+            model = Book
+
+    class FieldsAsListBookResource(BaseBookResource):
+        class Meta:
+            fields = ["id", "price", "name"]
+            model = Book
+
+    def setUp(self):
+        super().setUp()
+        Book.objects.create(name="Ulysses", price="1.99")
+        self.dataset = tablib.Dataset(headers=["id", "name", "price"])
+        row = [1, "Some book", "19.99"]
+        self.dataset.append(row)
+
+    def test_defined_import_order(self):
+        self.resource = ImportExportFieldOrderTest.OrderedBookResource()
+        self.resource.import_data(self.dataset)
+        self.assertEqual(["price", "name", "id"], self.resource.field_names)
+
+    def test_undefined_import_order(self):
+        self.resource = ImportExportFieldOrderTest.UnorderedBookResource()
+        self.resource.import_data(self.dataset)
+        self.assertEqual(["price", "id", "name"], self.resource.field_names)
+
+    def test_defined_export_order(self):
+        self.resource = ImportExportFieldOrderTest.OrderedBookResource()
+        data = self.resource.export()
+        target = "price,name,id\r\n1.99,Ulysses,1\r\n"
+        self.assertEqual(target, data.csv)
+
+    def test_undefined_export_order(self):
+        # When export order is not defined,
+        # exported order should correspond with 'fields' definition
+        self.resource = ImportExportFieldOrderTest.UnorderedBookResource()
+        data = self.resource.export()
+        target = "price,id,name\r\n1.99,1,Ulysses\r\n"
+        self.assertEqual(target, data.csv)
+
+    def test_subset_import_order(self):
+        self.resource = ImportExportFieldOrderTest.SubsetOrderedBookResource()
+        self.resource.import_data(self.dataset)
+        self.assertEqual(
+            ["name", "price", "id", "published"], self.resource.field_names
+        )
+
+    def test_subset_export_order(self):
+        self.resource = ImportExportFieldOrderTest.SubsetOrderedBookResource()
+        data = self.resource.export()
+        target = "published,price,id,name\r\n,1.99,1,Ulysses\r\n"
+        self.assertEqual(target, data.csv)
+
+    def test_duplicate_import_order(self):
+        self.resource = ImportExportFieldOrderTest.DuplicateFieldsBookResource()
+        self.resource.import_data(self.dataset)
+        self.assertEqual(["id", "price", "name"], self.resource.field_names)
+
+    def test_duplicate_export_order(self):
+        self.resource = ImportExportFieldOrderTest.DuplicateFieldsBookResource()
+        data = self.resource.export()
+        target = "id,price,name\r\n1,1.99,Ulysses\r\n"
+        self.assertEqual(target, data.csv)
+
+    def test_fields_as_list_import_order(self):
+        self.resource = ImportExportFieldOrderTest.FieldsAsListBookResource()
+        self.resource.import_data(self.dataset)
+        self.assertEqual(["id", "price", "name"], self.resource.field_names)
+
+    def test_fields_as_list_export_order(self):
+        self.resource = ImportExportFieldOrderTest.FieldsAsListBookResource()
+        data = self.resource.export()
+        target = "id,price,name\r\n1,1.99,Ulysses\r\n"
+        self.assertEqual(target, data.csv)