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

Issue:
When using the admin only update rows that are effectively modified
That could be an option or IMHO that should be the default, and since the dry-run actually finds and displays all differences, it should be possible to only list and display rows that will be either add/deleted/updated and only these rows. 



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
@@ -50,6 +50,12 @@ class ResourceOptions(object):
     * ``use_transactions`` - Controls if import should use database
       transactions. Default value is ``None`` meaning
       ``settings.IMPORT_EXPORT_USE_TRANSACTIONS`` will be evaluated.
+      
+    * ``skip_unchanged`` - Controls if the import should skip unchanged records.
+      Default value is False
+
+    * ``report_skipped`` - Controls if the result reports skipped rows
+      Default value is True
 
     """
     fields = None
@@ -60,6 +66,8 @@ class ResourceOptions(object):
     export_order = None
     widgets = None
     use_transactions = None
+    skip_unchanged = False
+    report_skipped = True
 
     def __new__(cls, meta=None):
         overrides = {}
@@ -206,6 +214,26 @@ def for_delete(self, row, instance):
         """
         return False
 
+    def skip_row(self, instance, original):
+        """
+        Returns ``True`` if ``row`` importing should be skipped.
+
+        Default implementation returns ``False`` unless skip_unchanged == True.
+        Override this method to handle skipping rows meeting certain conditions.
+        """
+        if not self._meta.skip_unchanged:
+            return False
+        for field in self.get_fields():
+            try:
+                # For fields that are models.fields.related.ManyRelatedManager
+                # we need to compare the results
+                if list(field.get_value(instance).all()) != list(field.get_value(original).all()):
+                    return False
+            except AttributeError:
+                if field.get_value(instance) != field.get_value(original):
+                    return False
+        return True
+
     def get_diff(self, original, current, dry_run=False):
         """
         Get diff between original and current object when ``import_data``
@@ -280,8 +308,11 @@ def import_data(self, dataset, dry_run=False, raise_errors=False,
                                 real_dry_run)
                 else:
                     self.import_obj(instance, row)
-                    self.save_instance(instance, real_dry_run)
-                    self.save_m2m(instance, row, real_dry_run)
+                    if self.skip_row(instance, original):
+                        row_result.import_type = RowResult.IMPORT_TYPE_SKIP
+                    else:
+                        self.save_instance(instance, real_dry_run)
+                        self.save_m2m(instance, row, real_dry_run)
                     row_result.diff = self.get_diff(original, instance,
                             real_dry_run)
             except Exception, e:
@@ -292,7 +323,9 @@ def import_data(self, dataset, dry_run=False, raise_errors=False,
                         transaction.rollback()
                         transaction.leave_transaction_management()
                     raise
-            result.rows.append(row_result)
+            if (row_result.import_type != RowResult.IMPORT_TYPE_SKIP or 
+                        self._meta.report_skipped): 
+                result.rows.append(row_result)
 
         if use_transactions:
             if dry_run or result.has_errors():


Test patch:
diff --git a/tests/core/tests/resources_tests.py b/tests/core/tests/resources_tests.py
--- a/tests/core/tests/resources_tests.py
+++ b/tests/core/tests/resources_tests.py
@@ -1,5 +1,6 @@
 from decimal import Decimal
 from datetime import date
+from copy import deepcopy
 
 from django.test import (
         TestCase,
@@ -28,7 +29,7 @@ class Meta:
         export_order = ('email', 'name')
 
 
-class ResourceTest(TestCase):
+class ResourceTestCase(TestCase):
 
     def setUp(self):
         self.my_resource = MyResource()
@@ -291,6 +292,35 @@ def test_empty_get_queryset(self):
         dataset = self.resource.export(Book.objects.none())
         self.assertEqual(len(dataset), 0)
 
+    def test_import_data_skip_unchanged(self):
+        def attempted_save(instance, real_dry_run): 
+            self.fail('Resource attempted to save instead of skipping')
+
+        # Make sure we test with ManyToMany related objects
+        cat1 = Category.objects.create(name='Cat 1')
+        cat2 = Category.objects.create(name='Cat 2')
+        self.book.categories.add(cat1)
+        self.book.categories.add(cat2)
+        dataset = self.resource.export()
+
+        # Create a new resource that attempts to reimport the data currently
+        # in the database while skipping unchanged rows (i.e. all of them)
+        resource = deepcopy(self.resource)
+        resource._meta.skip_unchanged = True
+        # Fail the test if the resource attempts to save the row
+        resource.save_instance = attempted_save 
+        result = resource.import_data(dataset, raise_errors=True)
+        self.assertFalse(result.has_errors())
+        self.assertEqual(len(result.rows), len(dataset))
+        self.assertTrue(result.rows[0].diff)
+        self.assertEqual(result.rows[0].import_type, 
+                results.RowResult.IMPORT_TYPE_SKIP)
+
+        # Test that we can suppress reporting of skipped rows
+        resource._meta.report_skipped = False
+        result = resource.import_data(dataset, raise_errors=True)
+        self.assertFalse(result.has_errors())
+        self.assertEqual(len(result.rows), 0)
 
 class ModelResourceTransactionTest(TransactionTestCase):