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

Issue:
Cannot import datetime field having db_default attribute (new in Django 5.0)
**Describe the bug**
Trying to import datetime fields having [db_default](https://docs.djangoproject.com/en/5.0/ref/models/fields/#django.db.models.Field.db_default) attribute produce `AttributeError: 'DatabaseDefault' object has no attribute 'utcoffset'`.

**To Reproduce**
Steps to reproduce the behavior:
1. Configure a model with field `DateTimeField(auto_now_add=True, db_default=Now())`
2. Try to import data to this model in the admin site

See [this dedicated repository](https://github.com/ipamo/import-db-default) to reproduce the issue.

**Versions**
- Windows 10 22H2 (build 19045.3930)
- Python 3.11.5
- PostgreSQL 15.4
- Django 5.0.1
- Psycopg 3.1.17
- Django-import-export 3.3.6



Discussion:
**Traceback**

```
Traceback (most recent call last):
File "…\import-db-default\.venv\Lib\site-packages\import_export\resources.py", line 785, in import_row
diff = self.get_diff_class()(self, original, new)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "…\import-db-default\.venv\Lib\site-packages\import_export\resources.py", line 259, in __init__
self.left = self._export_resource_fields(resource, instance)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "…\import-db-default\.venv\Lib\site-packages\import_export\resources.py", line 280, in _export_resource_fields
return [
^
File "…\import-db-default\.venv\Lib\site-packages\import_export\resources.py", line 281, in <listcomp>
resource.export_field(f, instance) if instance else ""
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "…\import-db-default\.venv\Lib\site-packages\import_export\resources.py", line 1066, in export_field
return field.export(obj)
^^^^^^^^^^^^^^^^^
File "…\import-db-default\.venv\Lib\site-packages\import_export\fields.py", line 148, in export
return self.widget.render(value, obj)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "…\import-db-default\.venv\Lib\site-packages\import_export\widgets.py", line 266, in render
value = timezone.localtime(value)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "…\import-db-default\.venv\Lib\site-packages\django\utils\timezone.py", line 182, in localtime
if is_naive(value):
^^^^^^^^^^^^^^^
File "…\import-db-default\.venv\Lib\site-packages\django\utils\timezone.py", line 234, in is_naive
return value.utcoffset() is None
^^^^^^^^^^^^^^^
AttributeError: 'DatabaseDefault' object has no attribute 'utcoffset'
```
Thanks for raising.  I have found that you can reproduce the same issue in the shell with:

```
>>> from django.db.models.functions.datetime import Now
>>> from django.utils import timezone
>>> timezone.localtime(Now())
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/matthew/.virtualenvs/django-import-export/lib/python3.11/site-packages/django/utils/timezone.py", line 182, in localtime
    if is_naive(value):
       ^^^^^^^^^^^^^^^
  File "/Users/matthew/.virtualenvs/django-import-export/lib/python3.11/site-packages/django/utils/timezone.py", line 234, in is_naive
    return value.utcoffset() is None
           ^^^^^^^^^^^^^^^
```

The interface of [`timezone.localtime()`](https://github.com/django/django/blob/02eaee12095eebb3d07d02e7b0bdc3f64785d379/django/utils/timezone.py#L167) states that a `datetime` instance is passed, therefore it's not valid to pass a `Now` instance.

The issue is that the 'diff' process used to show the differences in the import uses `DateTimeWidget` which is being asked to render a `Now` instance (it assumes it is being passed a date).  Hence it crashes.  This is only occurring on the import of new objects, so I expect that the issue occurs when diffing against a 'new' instance.

I added a test [here](https://github.com/matthewhegarty/django-import-export/blob/77c9d917550d9e017e5c2cce6cf90216dbc9e0ec/tests/core/tests/test_widgets.py#L236).

I don't know what the solution is at the moment.

As a workaround, you can avoid the crash during import if you set `skip_diff` to True on the resource:

```
class BookResource(ModelResource):
    class Meta:
        model = Book
        skip_diff = True
```

This means that you would not be able to see diffs on import.
Thank you very much for the explanation and workaround.
np - I'll add a fix for this when I get a chance.
I suggest that the best way to fix this would be for each widget `render()` method to have a type check which checks that the value passed corresponds to the underlying type, if not then a TypeError is raised.  Callers further up the stack can then catch this error and handle accordingly.

Resolving patch:
diff --git a/import_export/widgets.py b/import_export/widgets.py
--- a/import_export/widgets.py
+++ b/import_export/widgets.py
@@ -1,6 +1,7 @@
 import json
 import logging
-from datetime import date, datetime, time
+import numbers
+from datetime import date, datetime, time, timedelta
 from decimal import Decimal
 from warnings import warn
 
@@ -87,7 +88,11 @@ def is_empty(self, value):
     def render(self, value, obj=None):
         self._obj_deprecation_warning(obj)
         if self.coerce_to_string:
-            return "" if value is None else number_format(value)
+            return (
+                ""
+                if value is None or not isinstance(value, numbers.Number)
+                else number_format(value)
+            )
         return value
 
 
@@ -204,7 +209,7 @@ def render(self, value, obj=None):
         self._obj_deprecation_warning(obj)
         if self.coerce_to_string is False:
             return value
-        if value in self.NULL_VALUES:
+        if value in self.NULL_VALUES or not type(value) is bool:
             return ""
         return self.TRUE_VALUES[0] if value else self.FALSE_VALUES[0]
 
@@ -248,7 +253,7 @@ def render(self, value, obj=None):
         self._obj_deprecation_warning(obj)
         if self.coerce_to_string is False:
             return value
-        if not value:
+        if not value or not type(value) is date:
             return ""
         return format_datetime(value, self.formats[0])
 
@@ -298,7 +303,7 @@ def render(self, value, obj=None):
         self._obj_deprecation_warning(obj)
         if self.coerce_to_string is False:
             return value
-        if not value:
+        if not value or not type(value) is datetime:
             return ""
         if settings.USE_TZ:
             value = timezone.localtime(value)
@@ -344,7 +349,7 @@ def render(self, value, obj=None):
         self._obj_deprecation_warning(obj)
         if self.coerce_to_string is False:
             return value
-        if not value:
+        if not value or not type(value) is time:
             return ""
         return value.strftime(self.formats[0])
 
@@ -372,7 +377,7 @@ def render(self, value, obj=None):
         self._obj_deprecation_warning(obj)
         if self.coerce_to_string is False:
             return value
-        if value is None:
+        if value is None or not type(value) is timedelta:
             return ""
         return str(value)
 


Test patch:
diff --git a/tests/core/tests/test_widgets.py b/tests/core/tests/test_widgets.py
--- a/tests/core/tests/test_widgets.py
+++ b/tests/core/tests/test_widgets.py
@@ -93,6 +93,9 @@ def test_render_coerce_to_string_is_False(self):
         self.assertFalse(self.widget.render(False))
         self.assertIsNone(self.widget.render(None))
 
+    def test_render_invalid_type(self):
+        self.assertEqual(self.widget.render("a"), "")
+
 
 class FormatDatetimeTest(TestCase):
     date = date(10, 8, 2)
@@ -127,6 +130,9 @@ def test_render(self):
     def test_render_none(self):
         self.assertEqual(self.widget.render(None), "")
 
+    def test_render_invalid_type(self):
+        self.assertEqual(self.widget.render(int(1)), "")
+
     def test_render_coerce_to_string_is_False(self):
         self.widget = widgets.DateWidget(coerce_to_string=False)
         self.assertEqual(self.date, self.widget.render(self.date))
@@ -178,6 +184,9 @@ def test_render(self):
     def test_render_none(self):
         self.assertEqual(self.widget.render(None), "")
 
+    def test_render_invalid_type(self):
+        self.assertEqual(self.widget.render(int(1)), "")
+
     def test_render_coerce_to_string_is_False(self):
         self.widget = widgets.DateTimeWidget(coerce_to_string=False)
         self.assertEqual(self.datetime, self.widget.render(self.datetime))
@@ -279,6 +288,9 @@ def test_render(self):
     def test_render_none(self):
         self.assertEqual(self.widget.render(None), "")
 
+    def test_render_invalid_type(self):
+        self.assertEqual(self.widget.render(int(1)), "")
+
     def test_render_coerce_to_string_is_False(self):
         self.widget = widgets.TimeWidget(coerce_to_string=False)
         self.assertEqual(self.time, self.widget.render(self.time))
@@ -324,6 +336,9 @@ def test_render_coerce_to_string_is_False(self):
         self.widget = widgets.DurationWidget(coerce_to_string=False)
         self.assertEqual(self.duration, self.widget.render(self.duration))
 
+    def test_render_invalid_type(self):
+        self.assertEqual(self.widget.render(int(1)), "")
+
     def test_clean(self):
         self.assertEqual(self.widget.clean("1:57:00"), self.duration)
 
@@ -365,6 +380,9 @@ def test_render(self):
     def test_render_None_coerce_to_string_False(self):
         self.assertEqual("", self.widget.render(None))
 
+    def test_render_invalid_type(self):
+        self.assertEqual(self.widget.render("a"), "")
+
     @skipUnless(
         django.VERSION[0] < 4, f"skipping django {django.VERSION} version specific test"
     )
@@ -396,6 +414,9 @@ def test_clean(self):
     def test_render(self):
         self.assertEqual(self.widget.render(self.value), "11.111")
 
+    def test_render_invalid_type(self):
+        self.assertEqual(self.widget.render("a"), "")
+
     def test_clean_string_zero(self):
         self.assertEqual(self.widget.clean("0"), 0.0)
         self.assertEqual(self.widget.clean("0.0"), 0.0)
@@ -437,6 +458,9 @@ def test_render_coerce_to_string_is_False(self):
     def test_render(self):
         self.assertEqual(self.widget.render(self.value), "11.111")
 
+    def test_render_invalid_type(self):
+        self.assertEqual(self.widget.render("1"), "")
+
     def test_clean_string_zero(self):
         self.assertEqual(self.widget.clean("0"), Decimal("0"))
         self.assertEqual(self.widget.clean("0.0"), Decimal("0"))
@@ -484,6 +508,9 @@ def test_clean_empty_string(self):
         self.assertEqual(self.widget.clean(" "), None)
         self.assertEqual(self.widget.clean("\n\t\r"), None)
 
+    def test_render_invalid_type(self):
+        self.assertEqual(self.widget.render("a"), "")
+
     @skipUnless(
         django.VERSION[0] < 4, f"skipping django {django.VERSION} version specific test"
     )