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

Issue:
tests breaking due to `log_action` deprecation in django5
**Describe the bug**

```
django.utils.deprecation.RemovedInDjango60Warning: LogEntryManager.log_action() is deprecated. Use log_actions() instead.
```

**To Reproduce**

```
tox -r -e py311-djangomain 
```

**Versions (please complete the following information):**
 - Django Import Export: 3.3.1
 - Python 3.11
 - Django main

**Expected behavior**

Tests should pass

**Additional context**

[link](https://github.com/django/django/blob/36173cf29d6ad0b0f0cd24326834dddfff2db7f3/docs/internals/deprecation.txt#L59)



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
@@ -1,5 +1,4 @@
 import logging
-import warnings
 
 import django
 from django import forms
@@ -182,6 +181,7 @@ def process_dataset(
         )
         resource = self.choose_import_resource_class(confirm_form)(**res_kwargs)
         imp_kwargs = self.get_import_data_kwargs(request, form=confirm_form, **kwargs)
+        imp_kwargs["retain_instance_in_row_result"] = True
 
         return resource.import_data(
             dataset,
@@ -205,21 +205,20 @@ def process_result(self, result, request):
     def generate_log_entries(self, result, request):
         if not self.get_skip_admin_log():
             # Add imported objects to LogEntry
-            logentry_map = {
-                RowResult.IMPORT_TYPE_NEW: ADDITION,
-                RowResult.IMPORT_TYPE_UPDATE: CHANGE,
-                RowResult.IMPORT_TYPE_DELETE: DELETION,
-            }
-            content_type_id = ContentType.objects.get_for_model(self.model).pk
-            for row in result:
-                if (
-                    row.import_type != row.IMPORT_TYPE_ERROR
-                    and row.import_type != row.IMPORT_TYPE_SKIP
-                ):
-                    with warnings.catch_warnings():
-                        warnings.filterwarnings(
-                            "ignore", category=PendingDeprecationWarning
-                        )
+            if django.VERSION >= (5, 0):
+                self._log_actions(result, request)
+            else:
+                logentry_map = {
+                    RowResult.IMPORT_TYPE_NEW: ADDITION,
+                    RowResult.IMPORT_TYPE_UPDATE: CHANGE,
+                    RowResult.IMPORT_TYPE_DELETE: DELETION,
+                }
+                content_type_id = ContentType.objects.get_for_model(self.model).pk
+                for row in result:
+                    if (
+                        row.import_type != row.IMPORT_TYPE_ERROR
+                        and row.import_type != row.IMPORT_TYPE_SKIP
+                    ):
                         LogEntry.objects.log_action(
                             user_id=request.user.pk,
                             content_type_id=content_type_id,
@@ -535,6 +534,37 @@ def changelist_view(self, request, extra_context=None):
         extra_context["has_import_permission"] = self.has_import_permission(request)
         return super().changelist_view(request, extra_context)
 
+    def _log_actions(self, result, request):
+        """
+        Create appropriate LogEntry instances for the result.
+        """
+        rows = dict()
+        for row in result:
+            rows.setdefault(row.import_type, list())
+            rows[row.import_type].append(row.instance)
+
+        self._create_log_entries(request.user.pk, rows)
+
+    def _create_log_entries(self, user_pk, rows):
+        logentry_map = {
+            RowResult.IMPORT_TYPE_NEW: ADDITION,
+            RowResult.IMPORT_TYPE_UPDATE: CHANGE,
+            RowResult.IMPORT_TYPE_DELETE: DELETION,
+        }
+        for import_type, instances in rows.items():
+            action_flag = logentry_map[import_type]
+            self._create_log_entry(user_pk, rows[import_type], import_type, action_flag)
+
+    def _create_log_entry(self, user_pk, rows, import_type, action_flag):
+        if len(rows) > 0:
+            LogEntry.objects.log_actions(
+                user_pk,
+                rows,
+                action_flag,
+                change_message=_("%s through import_export" % import_type),
+                single_object=len(rows) == 1,
+            )
+
 
 class ExportMixin(BaseExportMixin, ImportExportMixinBase):
     """
diff --git a/import_export/resources.py b/import_export/resources.py
--- a/import_export/resources.py
+++ b/import_export/resources.py
@@ -234,6 +234,9 @@ class ResourceOptions:
     :class:`~import_export.results.RowResult`.
     Enabling this parameter will increase the memory usage during import
     which should be considered when importing large datasets.
+
+    This value will always be set to ``True`` when importing via the Admin UI.
+    This is so that appropriate ``LogEntry`` instances can be created.
     """
 
     use_natural_foreign_keys = False
@@ -900,6 +903,12 @@ def import_row(self, row, instance_loader, **kwargs):
               The index of the row being imported.
         """
         skip_diff = self._meta.skip_diff
+
+        if not self._meta.store_instance:
+            self._meta.store_instance = kwargs.get(
+                "retain_instance_in_row_result", False
+            )
+
         row_result = self.get_row_result_class()()
         if self._meta.store_row_values:
             row_result.row_values = row
@@ -924,7 +933,8 @@ def import_row(self, row, instance_loader, **kwargs):
                     row_result.import_type = RowResult.IMPORT_TYPE_DELETE
                     row_result.add_instance_info(instance)
                     if self._meta.store_instance:
-                        row_result.instance = instance
+                        # create a copy before deletion so id fields are retained
+                        row_result.instance = deepcopy(instance)
                     self.delete_instance(instance, row, **kwargs)
                     if not skip_diff:
                         diff.compare_with(self, None)
diff --git a/import_export/results.py b/import_export/results.py
--- a/import_export/results.py
+++ b/import_export/results.py
@@ -46,7 +46,7 @@ def __init__(self):
         #: A string identifier which identifies what type of import was performed.
         self.import_type = None
 
-        #: Can the raw values associated with each imported row.
+        #: Retain the raw values associated with each imported row.
         self.row_values = {}
 
         #: The instance id (used in Admin UI)
@@ -62,6 +62,49 @@ def __init__(self):
         #: This value is only set for updates.
         self.original = None
 
+    def is_update(self):
+        """
+        :return: ``True`` if import type is 'update', otherwise ``False``.
+        """
+        return self.import_type == RowResult.IMPORT_TYPE_UPDATE
+
+    def is_new(self):
+        """
+        :return: ``True`` if import type is 'new', otherwise ``False``.
+        """
+        return self.import_type == RowResult.IMPORT_TYPE_NEW
+
+    def is_delete(self):
+        """
+        :return: ``True`` if import type is 'delete', otherwise ``False``.
+        """
+        return self.import_type == RowResult.IMPORT_TYPE_DELETE
+
+    def is_skip(self):
+        """
+        :return: ``True`` if import type is 'skip', otherwise ``False``.
+        """
+        return self.import_type == RowResult.IMPORT_TYPE_SKIP
+
+    def is_error(self):
+        """
+        :return: ``True`` if import type is 'error', otherwise ``False``.
+        """
+        return self.import_type == RowResult.IMPORT_TYPE_ERROR
+
+    def is_invalid(self):
+        """
+        :return: ``True`` if import type is 'invalid', otherwise ``False``.
+        """
+        return self.import_type == RowResult.IMPORT_TYPE_INVALID
+
+    def is_valid(self):
+        """
+        :return: ``True`` if import type is not 'error' or 'invalid', otherwise
+          ``False``.
+        """
+        return self.import_type in self.valid_import_types
+
     def add_instance_info(self, instance):
         if instance is not None:
             # Add object info to RowResult (e.g. for LogEntry)


Test patch:
diff --git a/tests/core/tests/test_results.py b/tests/core/tests/test_results.py
--- a/tests/core/tests/test_results.py
+++ b/tests/core/tests/test_results.py
@@ -64,3 +64,45 @@ def __str__(self):
         row_result.add_instance_info(BookWithObjectRepr(pk=1, name="some book"))
         self.assertEqual(1, row_result.object_id)
         self.assertEqual("some book", row_result.object_repr)
+
+    def test_is_new(self):
+        row_result = RowResult()
+        self.assertFalse(row_result.is_new())
+        row_result.import_type = RowResult.IMPORT_TYPE_NEW
+        self.assertTrue(row_result.is_new())
+        self.assertTrue(row_result.is_valid())
+
+    def test_is_update(self):
+        row_result = RowResult()
+        self.assertFalse(row_result.is_update())
+        row_result.import_type = RowResult.IMPORT_TYPE_UPDATE
+        self.assertTrue(row_result.is_update())
+        self.assertTrue(row_result.is_valid())
+
+    def test_is_skip(self):
+        row_result = RowResult()
+        self.assertFalse(row_result.is_skip())
+        row_result.import_type = RowResult.IMPORT_TYPE_SKIP
+        self.assertTrue(row_result.is_skip())
+        self.assertTrue(row_result.is_valid())
+
+    def test_is_delete(self):
+        row_result = RowResult()
+        self.assertFalse(row_result.is_delete())
+        row_result.import_type = RowResult.IMPORT_TYPE_DELETE
+        self.assertTrue(row_result.is_delete())
+        self.assertTrue(row_result.is_valid())
+
+    def test_is_error(self):
+        row_result = RowResult()
+        self.assertFalse(row_result.is_error())
+        row_result.import_type = RowResult.IMPORT_TYPE_ERROR
+        self.assertTrue(row_result.is_error())
+        self.assertFalse(row_result.is_valid())
+
+    def test_is_invalid(self):
+        row_result = RowResult()
+        self.assertFalse(row_result.is_invalid())
+        row_result.import_type = RowResult.IMPORT_TYPE_INVALID
+        self.assertTrue(row_result.is_invalid())
+        self.assertFalse(row_result.is_valid())