Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-django-import-export for
openSUSE:Factory checked in at 2026-03-07 20:09:17
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-django-import-export (Old)
and /work/SRC/openSUSE:Factory/.python-django-import-export.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-django-import-export"
Sat Mar 7 20:09:17 2026 rev:12 rq:1337351 version:4.4.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-django-import-export/python-django-import-export.changes
2025-12-15 12:04:19.750298995 +0100
+++
/work/SRC/openSUSE:Factory/.python-django-import-export.new.8177/python-django-import-export.changes
2026-03-07 20:14:09.782373188 +0100
@@ -1,0 +2,6 @@
+Fri Mar 6 16:30:56 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 4.4.0:
+ * Added CachedForeignKeyWidget (2142)
+
+-------------------------------------------------------------------
Old:
----
django_import_export-4.3.14.tar.gz
New:
----
django_import_export-4.4.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-django-import-export.spec ++++++
--- /var/tmp/diff_new_pack.ZrMcKk/_old 2026-03-07 20:14:11.194431600 +0100
+++ /var/tmp/diff_new_pack.ZrMcKk/_new 2026-03-07 20:14:11.206432097 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-django-import-export
#
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-django-import-export
-Version: 4.3.14
+Version: 4.4.0
Release: 0
Summary: Django data importing and exporting
License: BSD-2-Clause
++++++ django_import_export-4.3.14.tar.gz -> django_import_export-4.4.0.tar.gz
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_import_export-4.3.14/AUTHORS
new/django_import_export-4.4.0/AUTHORS
--- old/django_import_export-4.3.14/AUTHORS 2025-11-13 11:06:16.000000000
+0100
+++ new/django_import_export-4.4.0/AUTHORS 2026-01-10 21:57:04.000000000
+0100
@@ -166,3 +166,4 @@
* siddharth1012 (Siddharth Saraswat)
* borisovodov (Boris Ovodov)
* ghostishev (Yevhen Hostishchev)
+* Lokker29 (Bohdan Bilokon)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_import_export-4.3.14/PKG-INFO
new/django_import_export-4.4.0/PKG-INFO
--- old/django_import_export-4.3.14/PKG-INFO 2025-11-13 11:06:23.201599400
+0100
+++ new/django_import_export-4.4.0/PKG-INFO 2026-01-10 21:57:09.042009400
+0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: django-import-export
-Version: 4.3.14
+Version: 4.4.0
Summary: Django application and library for importing and exporting data with
included admin integration.
Author-email: Bojan Mihelač <[email protected]>
Maintainer-email: Matthew Hegarty <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_import_export-4.3.14/RELEASE.md
new/django_import_export-4.4.0/RELEASE.md
--- old/django_import_export-4.3.14/RELEASE.md 2025-11-13 11:06:16.000000000
+0100
+++ new/django_import_export-4.4.0/RELEASE.md 2026-01-10 21:57:04.000000000
+0100
@@ -54,8 +54,7 @@
1. Ensure that the branch is up-to-date locally (`git pull upstream rel/4-x`)
2. Tag the branch as required (`git tag -a 4.3.11 -m "v4.3.11"`)
-3. Push upstream (`git push upstream`)
-4. Push tags (`git push --tags upstream`)
+3. Push upstream (`git push upstream rel/4-x --follow-tags`)
Now release as above but use the appropriate git tag.
Remember to merge the release branch into the `main` branch.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/django_import_export-4.3.14/django_import_export.egg-info/PKG-INFO
new/django_import_export-4.4.0/django_import_export.egg-info/PKG-INFO
--- old/django_import_export-4.3.14/django_import_export.egg-info/PKG-INFO
2025-11-13 11:06:23.000000000 +0100
+++ new/django_import_export-4.4.0/django_import_export.egg-info/PKG-INFO
2026-01-10 21:57:08.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: django-import-export
-Version: 4.3.14
+Version: 4.4.0
Summary: Django application and library for importing and exporting data with
included admin integration.
Author-email: Bojan Mihelač <[email protected]>
Maintainer-email: Matthew Hegarty <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_import_export-4.3.14/docs/advanced_usage.rst
new/django_import_export-4.4.0/docs/advanced_usage.rst
--- old/django_import_export-4.3.14/docs/advanced_usage.rst 2025-11-13
11:06:16.000000000 +0100
+++ new/django_import_export-4.4.0/docs/advanced_usage.rst 2026-01-10
21:57:05.000000000 +0100
@@ -512,6 +512,8 @@
See also :ref:`creating-non-existent-relations`.
Refer to the :class:`~.ForeignKeyWidget` documentation for more detailed
information.
+If importing large datasets, see the notes in
:ref:`foreign_key_widget_performance`
+and consider using :class:`~.CachedForeignKeyWidget`
.. note::
@@ -902,13 +904,14 @@
Accessing fields within ``JSONField`` or ``JSONObject``
-------------------------------------------------------
In the same way that it is possible to refer to the relationships of the model
by defining a field with double underscore ``__``
-syntax, values within ``JSONObject``/ ``JSONField`` can also be accessed but
in this case it is necessary to specify it in ``attribute``::
+syntax, values within ``JSONObject``/ ``JSONField`` can also be accessed but
in this case it is necessary to specify it in ``attribute``.
+If you will use the resource for import as well, you must mark the field as
readonly. If you only want the field for export, marking it as readonly is not
necessary::
from import_export.fields import Field
class BookResource(resources.ModelResource):
- author_name = Field(attribute="author_json__name")
- author_birthday = Field(attribute="author_json__birthday")
+ author_name = Field(attribute="author_json__name", readonly=True)
+ author_birthday = Field(attribute="author_json__birthday",
readonly=True)
class Meta:
model = Book
@@ -935,6 +938,7 @@
Remember that the types that are annotated/stored within these fields are
primitive JSON
data types (strings, numbers, boolean, null) and also composite JSON data
types (array and object).
That is why, in the example, the birthday field within the author_json
dictionary is displayed as a string.
+ It is recommended that you always declare these fields as readonly even if
you only want to use them for export.
Using dehydrate methods
-----------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_import_export-4.3.14/docs/api_widgets.rst
new/django_import_export-4.4.0/docs/api_widgets.rst
--- old/django_import_export-4.3.14/docs/api_widgets.rst 2025-11-13
11:06:16.000000000 +0100
+++ new/django_import_export-4.4.0/docs/api_widgets.rst 2026-01-10
21:57:05.000000000 +0100
@@ -41,5 +41,8 @@
.. autoclass:: import_export.widgets.ForeignKeyWidget
:members:
+.. autoclass:: import_export.widgets.CachedForeignKeyWidget
+ :members:
+
.. autoclass:: import_export.widgets.ManyToManyWidget
:members:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_import_export-4.3.14/docs/bulk_import.rst
new/django_import_export-4.4.0/docs/bulk_import.rst
--- old/django_import_export-4.3.14/docs/bulk_import.rst 2025-11-13
11:06:16.000000000 +0100
+++ new/django_import_export-4.4.0/docs/bulk_import.rst 2026-01-10
21:57:05.000000000 +0100
@@ -31,10 +31,6 @@
* In bulk mode, exceptions are not linked to a row. Any exceptions raised by
bulk operations are logged and returned
as critical (non-validation) errors (and re-raised if ``raise_errors`` is
true).
-* If you use :class:`~import_export.widgets.ForeignKeyWidget` then this should
not affect performance during lookups,
- because the ``QuerySet`` cache should be used. Some more information
- `here <https://stackoverflow.com/a/78309357/39296>`_.
-
* If there is the potential for concurrent writes to a table during a bulk
operation, then you need to consider the
potential impact of this. Refer to :ref:`concurrent-writes` for more
information.
@@ -42,7 +38,24 @@
`bulk_create()
<https://docs.djangoproject.com/en/stable/ref/models/querysets/#bulk-create>`_
and
`bulk_update()
<https://docs.djangoproject.com/en/stable/ref/models/querysets/#bulk-update>`_.
-.. _performance_tuning
+.. _foreign_key_widget_performance:
+
+ForeignKeyWidget performance considerations
+===========================================
+
+When using :class:`~import_export.widgets.ForeignKeyWidget`, the related
object is looked up using ``QuerySet.get()``
+during import. This lookup occurs once per imported row. For large imports,
this can result in a significant number of
+database queries and impact performance.
+
+You can subclass ``ForeignKeyWidget`` and override ``get_queryset()`` to limit
the pool of candidate objects.
+However, overriding ``get_queryset()`` alone does not necessarily eliminate
per-row database queries, because
+``ForeignKeyWidget.clean()`` calls ``.get()`` for each row.
+
+If import performance is critical, consider using
:class:`~import_export.widgets.CachedForeignKeyWidget` instead.
+This widget caches all related objects in memory before the import begins,
eliminating per-row database queries.
+
+.. _performance_tuning:
+
Performance tuning
==================
@@ -56,6 +69,9 @@
* If your import is updating or creating instances, and you have a set of
existing instances which can be stored in
memory, use :class:`~import_export.instance_loaders.CachedInstanceLoader`
+* If your import has relations on per-row basis, consider using
+ :class:`~import_export.widgets.CachedForeignKeyWidget` for ForeignKey fields.
+
* By default, import rows are compared with the persisted representation, and
the difference is stored against each row
result. If you don't need this diff, then disable it with ``skip_diff =
True``.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_import_export-4.3.14/docs/changelog.rst
new/django_import_export-4.4.0/docs/changelog.rst
--- old/django_import_export-4.3.14/docs/changelog.rst 2025-11-13
11:06:16.000000000 +0100
+++ new/django_import_export-4.4.0/docs/changelog.rst 2026-01-10
21:57:05.000000000 +0100
@@ -5,6 +5,11 @@
If upgrading from v3, v4 introduces breaking changes. Please refer to
:doc:`release notes<release_notes>`.
+4.4.0 (2026-01-10)
+-------------------
+
+- Added CachedForeignKeyWidget (`2142
<https://github.com/django-import-export/django-import-export/pull/2142>`_)
+
4.3.14 (2025-11-13)
-------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/django_import_export-4.3.14/import_export/_version.py
new/django_import_export-4.4.0/import_export/_version.py
--- old/django_import_export-4.3.14/import_export/_version.py 2025-11-13
11:06:23.000000000 +0100
+++ new/django_import_export-4.4.0/import_export/_version.py 2026-01-10
21:57:08.000000000 +0100
@@ -28,7 +28,7 @@
commit_id: COMMIT_ID
__commit_id__: COMMIT_ID
-__version__ = version = '4.3.14'
-__version_tuple__ = version_tuple = (4, 3, 14)
+__version__ = version = '4.4.0'
+__version_tuple__ = version_tuple = (4, 4, 0)
-__commit_id__ = commit_id = 'g7422d0e38'
+__commit_id__ = commit_id = 'gc8a10790b'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_import_export-4.3.14/import_export/widgets.py
new/django_import_export-4.4.0/import_export/widgets.py
--- old/django_import_export-4.3.14/import_export/widgets.py 2025-11-13
11:06:16.000000000 +0100
+++ new/django_import_export-4.4.0/import_export/widgets.py 2026-01-10
21:57:05.000000000 +0100
@@ -1,6 +1,7 @@
import json
import logging
import numbers
+from collections import defaultdict, namedtuple
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from warnings import warn
@@ -613,18 +614,24 @@
val = super().clean(value)
if val:
if self.use_natural_foreign_keys:
- # natural keys will always be a tuple, which ends up as a json
list.
- value = json.loads(value)
- return self.model.objects.get_by_natural_key(*value)
+ return self.get_instance_by_natural_key(value)
else:
- lookup_kwargs = self.get_lookup_kwargs(value, row, **kwargs)
- obj = self.get_queryset(value, row,
**kwargs).get(**lookup_kwargs)
+ obj = self.get_instance_by_lookup_fields(value, row, **kwargs)
if self.key_is_id:
return obj.pk
return obj
else:
return None
+ def get_instance_by_natural_key(self, value):
+ # natural keys will always be a tuple, which ends up as a json list.
+ value = json.loads(value)
+ return self.model.objects.get_by_natural_key(*value)
+
+ def get_instance_by_lookup_fields(self, value, row, **kwargs):
+ lookup_kwargs = self.get_lookup_kwargs(value, row, **kwargs)
+ return self.get_queryset(value, row, **kwargs).get(**lookup_kwargs)
+
def get_lookup_kwargs(self, value, row, **kwargs):
"""
:return: the key value pairs used to identify a model instance.
@@ -669,6 +676,138 @@
return value
+class _CachedQuerySetWrapper:
+ """
+ A wrapper around a Django QuerySet that caches its results in a dictionary
+ for quick lookups.
+
+ This class has the same 'get()' method signature as a QuerySet
+ because it is intended to be used as a drop-in replacement for QuerySet
+ in case of ForeignKeyWidget that calls 'get()' method for every row
+ in the import dataset.
+ """
+
+ def __init__(self, queryset):
+ self.queryset = queryset
+ self.model = queryset.model
+
+ def _get_instances(self, queryset, key_cls):
+ """
+ Converts a queryset into a dictionary mapping lookup fields values
+ to lists of instances. The values of the returned dict are lists
+ to handle potential multiple instances with the same lookup fields
values.
+ """
+ if hasattr(self, "_instances"):
+ return self._instances
+
+ self._instances = defaultdict(list)
+ for instance in queryset:
+ key = key_cls(
+ **{field: getattr(instance, field) for field in
key_cls._fields}
+ )
+ self._instances[key].append(instance)
+
+ return self._instances
+
+ def get(self, **lookup_fields):
+ Key = namedtuple("Key", list(lookup_fields.keys()))
+
+ instances = self._get_instances(self.queryset, Key)
+ key = Key(**lookup_fields)
+ result = instances.get(key, [])
+
+ if len(result) == 1:
+ return result[0]
+
+ if len(result) == 0:
+ raise self.model.DoesNotExist(
+ "%s matching query does not exist." %
self.model._meta.object_name
+ )
+ raise self.model.MultipleObjectsReturned(
+ "get() returned more than one %s -- it returned %s!"
+ % (self.model._meta.object_name, len(result))
+ )
+
+
+class CachedForeignKeyWidget(ForeignKeyWidget):
+ """
+ A :class:`~import_export.widgets.ForeignKeyWidget` subclass that caches
+ the queryset results to minimize database hits during import. The default
+ :class:`~import_export.widgets.ForeignKeyWidget` makes query for each row,
+ which can be inefficient for large imports. This widget fetches all related
+ instances once and caches them in memory for subsequent lookups.
+
+ Using this class has some limitations:
+
+ - It does not support caching when ``use_natural_foreign_keys=True`` is
set.
+
+ - It calls :meth:`~import_export.widgets.ForeignKeyWidget.get_queryset`
only once,
+ so if the queryset depends on the row data, this widget may not work as
expected.
+ You must be sure that the queryset is static for all rows.
+ Avoid using :class:`~import_export.widgets.CachedForeignKeyWidget`
+ in the following way::
+
+ class FullNameForeignKeyWidget(CachedForeignKeyWidget):
+ def get_queryset(self, value, row, *args, **kwargs):
+ return self.model.objects.filter(
+ first_name__iexact=row["first_name"],
+ last_name__iexact=row["last_name"]
+ )
+
+ It makes more sense to filter by static values::
+
+ class ActiveForeignKeyWidget(CachedForeignKeyWidget):
+ def get_queryset(self, value, row, *args, **kwargs):
+ return self.model.objects.filter(active=True)
+
+ - It stores data in a hash table where the key is a tuple of the fields
that
+ returned by
:meth:`~import_export.widgets.ForeignKeyWidget.get_lookup_kwargs`.
+ You must be sure that the lookup fields are the same for all rows.
+ If the lookup fields differ between rows, this widget may not work as
expected.
+ The following example is incorrect usage::
+
+ class MultiColumnForeignKeyWidget(CachedForeignKeyWidget):
+ def get_lookup_kwargs(self, value, row, **kwargs):
+ if row['active'] == 'yes':
+ return {self.field: value, 'active': True}
+ else:
+ return {self.field: value, 'inactive': True}
+
+ - It performs lookup on Python side, so the filtering logic
+ with non-text data types may not work::
+
+ class MultiColumnForeignKeyWidget(CachedForeignKeyWidget):
+ def get_lookup_kwargs(self, value, row, **kwargs):
+ # row['birthday'] is a string like '01-01-2000'.
+ #
+ # It won't match the instance because the birthday values
+ # in the cached instances are datetime objects, not
strings.
+ return {self.field: value, 'birthday': row['birthday']}
+
+ - It does not support complex lookups like ``__gt``, ``__lt``,
+ or filtering over relationships in the ``get_lookup_kwargs()``.
+ For example, the following code won't work::
+
+ class BookForeignKeyWidget(CachedForeignKeyWidget):
+ def get_lookup_kwargs(self, value, row, **kwargs):
+ return {f'{self.field}__author': value}
+
+ :param model: The Model the ForeignKey refers to (required).
+ :param field: A field on the related model used for looking up a particular
+ object.
+ :param use_natural_foreign_keys: Use natural key functions to identify
+ related object, default to False
+ """
+
+ def get_instance_by_lookup_fields(self, value, row, **kwargs):
+ if not hasattr(self, "_cached_qs"):
+ queryset = self.get_queryset(value, row, **kwargs)
+ self._cached_qs = _CachedQuerySetWrapper(queryset)
+
+ lookup_kwargs = self.get_lookup_kwargs(value, row, **kwargs)
+ return self._cached_qs.get(**lookup_kwargs)
+
+
class ManyToManyWidget(Widget):
"""
Widget that converts between representations of a ManyToMany relationships
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/django_import_export-4.3.14/tests/core/tests/test_widgets.py
new/django_import_export-4.4.0/tests/core/tests/test_widgets.py
--- old/django_import_export-4.3.14/tests/core/tests/test_widgets.py
2025-11-13 11:06:16.000000000 +0100
+++ new/django_import_export-4.4.0/tests/core/tests/test_widgets.py
2026-01-10 21:57:05.000000000 +0100
@@ -5,13 +5,15 @@
from unittest.mock import patch
import django
+import tablib
from core.models import Author, Book, Category
from core.tests.utils import ignore_utcnow_deprecation_warning
+from django.db import connection
from django.test import TestCase
-from django.test.utils import override_settings
+from django.test.utils import CaptureQueriesContext, override_settings
from django.utils import timezone
-from import_export import widgets
+from import_export import fields, resources, widgets
from import_export.exceptions import WidgetError
@@ -667,6 +669,191 @@
Author, use_natural_foreign_keys=True, key_is_id=True
)
self.assertEqual(
+ "use_natural_foreign_keys and key_is_id " "cannot both be True",
+ str(e.exception),
+ )
+
+
+class CachedForeignKeyWidgetTest(TestCase, RowDeprecationTestMixin):
+ def setUp(self):
+ self.widget = widgets.CachedForeignKeyWidget(Author)
+ self.natural_key_author_widget = widgets.CachedForeignKeyWidget(
+ Author, use_natural_foreign_keys=True
+ )
+ self.natural_key_book_widget = widgets.CachedForeignKeyWidget(
+ Book, use_natural_foreign_keys=True
+ )
+ self.author = Author.objects.create(name="Foo")
+ self.book = Book.objects.create(name="Bar", author=self.author)
+
+ def tearDown(self):
+ if hasattr(self.widget, "_cached_qs"):
+ del self.widget._cached_qs
+ if hasattr(self.natural_key_author_widget, "_cached_qs"):
+ del self.natural_key_author_widget._cached_qs
+ if hasattr(self.natural_key_book_widget, "_cached_qs"):
+ del self.natural_key_book_widget._cached_qs
+
+ def test_clean(self):
+ self.assertEqual(self.widget.clean(self.author.id), self.author)
+
+ def test_clean_empty(self):
+ self.assertEqual(self.widget.clean(""), None)
+
+ def test_render(self):
+ self.assertEqual(self.widget.render(self.author), self.author.pk)
+
+ def test_render_empty(self):
+ self.assertEqual(self.widget.render(None), "")
+
+ def test_cache_hit(self):
+ author2 = Author.objects.create(name="Baz")
+ with CaptureQueriesContext(connection) as ctx:
+ self.assertEqual(self.widget.clean(self.author.id), self.author)
# query
+ self.assertEqual(self.widget.clean(author2.id), author2) # cache
hit
+ self.assertEqual(len(ctx.captured_queries), 1)
+
+ with CaptureQueriesContext(connection) as ctx:
+ self.assertEqual(
+ self.widget.clean(self.author.id), self.author
+ ) # cache hit
+ self.assertEqual(self.widget.clean(author2.id), author2) # cache
hit
+ self.assertEqual(len(ctx.captured_queries), 0)
+
+ def test_cache_is_not_shared_for_different_resource_instances(self):
+ class BookResource(resources.ModelResource):
+ author = fields.Field(
+ attribute="author",
+ column_name="Author",
+ widget=widgets.CachedForeignKeyWidget(Author),
+ )
+
+ class Meta:
+ model = Book
+
+ headers = ["Author"]
+ row = [self.author.id]
+ dataset = tablib.Dataset(row, headers=headers)
+ resource = BookResource()
+
+ self.assertFalse(hasattr(resource.fields["author"].widget,
"_cached_qs"))
+ resource.import_data(dataset, dry_run=True)
+ self.assertTrue(hasattr(resource.fields["author"].widget,
"_cached_qs"))
+
+ resource2 = BookResource() # new instance. The cache must be reset
+ self.assertFalse(hasattr(resource2.fields["author"].widget,
"_cached_qs"))
+
+ def test_clean_multi_column(self):
+ class BirthdayWidget(widgets.CachedForeignKeyWidget):
+ def get_queryset(self, value, row, *args, **kwargs):
+ return self.model.objects.filter(birthday=row["birthday"])
+
+ author2 = Author.objects.create(name="Foo")
+ author2.birthday = "2016-01-01"
+ author2.save()
+ birthday_widget = BirthdayWidget(Author, "name")
+ row_dict = {"name": "Foo", "birthday": author2.birthday}
+ self.assertEqual(birthday_widget.clean("Foo", row=row_dict), author2)
+
+ def test_invalid_get_queryset(self):
+ class BirthdayWidget(widgets.CachedForeignKeyWidget):
+ def get_queryset(self, value, row):
+ return self.model.objects.filter(birthday=row["birthday"])
+
+ birthday_widget = BirthdayWidget(Author, "name")
+ row_dict = {"name": "Foo", "age": 38}
+ with self.assertRaises(TypeError):
+ birthday_widget.clean("Foo", row=row_dict, row_number=1)
+
+ def test_lookup_multiple_columns(self):
+ # issue 1516 - override the values used to lookup an entry
+ class BookWidget(widgets.CachedForeignKeyWidget):
+ def get_lookup_kwargs(self, value, row, *args, **kwargs):
+ return {"name": row["name"], "author_email":
row["authoremail"]}
+
+ target_book = Book.objects.create(
+ name="Baz", author=self.author, author_email="[email protected]"
+ )
+ row_dict = {"name": "Baz", "authoremail": "[email protected]"}
+ book_widget = BookWidget(Book, "name")
+ # prove that the overridden kwargs identify a row
+ res = book_widget.clean("non-existent name", row=row_dict)
+ self.assertEqual(target_book, res)
+
+ def test_render_handles_value_error(self):
+ class TestObj:
+ @property
+ def attr(self):
+ raise ValueError("some error")
+
+ t = TestObj()
+ self.widget = widgets.CachedForeignKeyWidget(mock.Mock(), "attr")
+ self.assertIsNone(self.widget.render(t))
+
+ def test_multiple_objects_error(self):
+ Author.objects.create(name=self.author.name) # an author with
duplicated name
+ widget = widgets.CachedForeignKeyWidget(Author, "name")
+
+ with self.assertRaises(Author.MultipleObjectsReturned) as e:
+ widget.clean(self.author.name)
+
+ self.assertEqual(
+ str(e.exception), "get() returned more than one Author -- it
returned 2!"
+ )
+
+ def test_object_does_not_exist_error(self):
+ with self.assertRaises(Author.DoesNotExist) as e:
+ self.widget.clean("non-existent-id")
+
+ self.assertEqual(str(e.exception), "Author matching query does not
exist.")
+
+ def test_author_natural_key_clean(self):
+ """
+ Ensure that we can import an author by its natural key. Note that
+ this will always need to be an iterable.
+ Generally this will be rendered as a list.
+ """
+ self.assertEqual(
+
self.natural_key_author_widget.clean(json.dumps(self.author.natural_key())),
+ self.author,
+ )
+
+ def test_author_natural_key_render(self):
+ """
+ Ensure we can render an author by its natural key. Natural keys will
always be
+ tuples.
+ """
+ self.assertEqual(
+ self.natural_key_author_widget.render(self.author),
+ json.dumps(self.author.natural_key()),
+ )
+
+ def test_book_natural_key_clean(self):
+ """
+ Use the book case to validate a composite natural key of book name and
author
+ can be cleaned.
+ """
+ self.assertEqual(
+
self.natural_key_book_widget.clean(json.dumps(self.book.natural_key())),
+ self.book,
+ )
+
+ def test_book_natural_key_render(self):
+ """
+ Use the book case to validate a composite natural key of book name and
author
+ can be rendered
+ """
+ self.assertEqual(
+ self.natural_key_book_widget.render(self.book),
+ json.dumps(self.book.natural_key()),
+ )
+
+ def test_natural_foreign_key_with_key_is_id(self):
+ with self.assertRaises(WidgetError) as e:
+ widgets.CachedForeignKeyWidget(
+ Author, use_natural_foreign_keys=True, key_is_id=True
+ )
+ self.assertEqual(
"use_natural_foreign_keys and key_is_id " "cannot both be True",
str(e.exception),
)