Skip to content

Commit 8a4c710

Browse files
Merge branch 'main' into issue-1823-readd-resource-deprecation-warnings
2 parents 61cdc32 + b2479b3 commit 8a4c710

File tree

9 files changed

+145
-5
lines changed

9 files changed

+145
-5
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,4 @@ The following is a list of much appreciated contributors:
151151
* RobTilton (Robert Tilton)
152152
* ulliholtgrave
153153
* mishka251 (Mikhail Belov)
154+
* jhthompson (Jeremy Thompson)

docs/advanced_usage.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,48 @@ For example, you can use the 'isbn' number instead of 'id' to uniquely identify
577577
field(s) select more than one row, then a ``MultipleObjectsReturned`` exception will be raised. If no row is
578578
identified, then ``DoesNotExist`` exception will be raised.
579579

580+
.. _dynamic_fields:
581+
582+
Using 'dynamic fields' to identify existing instances
583+
-----------------------------------------------------
584+
585+
There are some use-cases where a field defined in ``import_id_fields`` is not present in the dataset. An example of
586+
this would be dynamic fields, where a field is generated from other data and then used as an identifier. For example::
587+
588+
class BookResource(resources.ModelResource):
589+
590+
def before_import_row(self, row, **kwargs):
591+
# generate a value for an existing field, based on another field
592+
row["hash_id"] = hashlib.sha256(row["name"].encode()).hexdigest()
593+
594+
class Meta:
595+
model = Book
596+
# A 'dynamic field' - i.e. is used to identify existing rows
597+
# but is not present in the dataset
598+
import_id_fields = ("hash_id",)
599+
600+
In the above example, a dynamic field called *hash_id* is generated and added to the dataset. In this example, an
601+
error will be raised because *hash_id* is not present in the dataset. To resolve this, update the dataset before
602+
import to add the dynamic field as a header::
603+
604+
class BookResource(resources.ModelResource):
605+
606+
def before_import(self, dataset, **kwargs):
607+
# mimic a 'dynamic field' - i.e. append field which exists on
608+
# Book model, but not in dataset
609+
dataset.headers.append("hash_id")
610+
super().before_import(dataset, **kwargs)
611+
612+
def before_import_row(self, row, **kwargs):
613+
row["hash_id"] = hashlib.sha256(row["name"].encode()).hexdigest()
614+
615+
class Meta:
616+
model = Book
617+
# A 'dynamic field' - i.e. is used to identify existing rows
618+
# but is not present in the dataset
619+
import_id_fields = ("hash_id",)
620+
621+
580622
Access instances after import
581623
=============================
582624

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Changelog
88
4.0.3 (unreleased)
99
------------------
1010

11+
- Clarified documentation when importing with ``import_id_fields`` (`1836 <https://github.com/django-import-export/django-import-export/pull/1836>`_)
1112
- re-add ``resource_class`` deprecation warning (`1837 <https://github.com/django-import-export/django-import-export/pull/1837>`_)
1213

1314
4.0.2 (2024-05-13)

docs/faq.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ We encourage you to read the :doc:`contributing guidelines <contributing>`.
3030
Common issues
3131
=============
3232

33-
FieldError on import
34-
--------------------
33+
.. _import_id_fields_error_on_import:
34+
35+
``import_id_fields`` error on import
36+
------------------------------------
3537

3638
The following error message can be seen on import:
3739

@@ -40,9 +42,11 @@ The following error message can be seen on import:
4042
This indicates that the Resource has not been configured correctly, and the import logic fails. Specifically,
4143
the import process is attempting to use either the defined or default values for
4244
:attr:`~import_export.options.ResourceOptions.import_id_fields` and no matching field has been detected in the resource
43-
fields.
45+
fields. See :ref:`advanced_usage:Create or update model instances`.
4446

45-
See :ref:`advanced_usage:Create or update model instances`.
47+
In cases where you are deliberately using generated fields in ``import_id_fields`` and these fields are not present in
48+
the dataset, then you need to modify the resource definition to accommodate this.
49+
See :ref:`dynamic_fields`.
4650

4751
How to handle double-save from Signals
4852
--------------------------------------

docs/release_notes.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ of :class:`~import_export.exceptions.ImportError` is raised. This exception wra
6969

7070
See `this PR <https://github.com/django-import-export/django-import-export/issues/1729>`_.
7171

72+
Check ``import_id_fields``
73+
--------------------------
74+
75+
Prior to v4 we had numerous issues where users were confused when imports failed due to declared ``import_id_fields``
76+
not being present in the dataset. We added functionality in v4 to check for this and to raise a clearer error message.
77+
78+
In some use-cases, it is a requirement that ``import_id_fields`` are not in the dataset, and are generated dynamically.
79+
If this affects your implementation, start with the documentation :ref:`here<import_id_fields_error_on_import>`.
80+
7281
Deprecations
7382
============
7483

import_export/forms.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ def __init__(self, formats, resources, **kwargs):
7070

7171
@property
7272
def media(self):
73+
media = super().media
7374
extra = "" if settings.DEBUG else ".min"
74-
return forms.Media(
75+
return media + forms.Media(
7576
js=(
7677
f"admin/js/vendor/jquery/jquery{extra}.js",
7778
"admin/js/jquery.init.js",

import_export/resources.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,13 @@ def _is_dry_run(self, kwargs):
11531153
return kwargs.get("dry_run", False)
11541154

11551155
def _check_import_id_fields(self, headers):
1156+
"""
1157+
Provides a safety check with a meaningful error message for cases where
1158+
the ``import_id_fields`` declaration contains a field which is not in the
1159+
dataset. For most use-cases this is an error, so we detect and raise.
1160+
There are conditions, such as 'dynamic fields' where this does not apply.
1161+
See issue 1834 for more information.
1162+
"""
11561163
import_id_fields = list()
11571164
missing_fields = list()
11581165
missing_headers = list()

tests/core/tests/test_forms.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import django.forms
2+
from core.models import Author
23
from django.test import TestCase
34

45
from import_export import forms, resources
@@ -33,6 +34,59 @@ def test_formbase_init_two_resources(self):
3334
)
3435

3536

37+
class ImportFormMediaTest(TestCase):
38+
def test_import_form_media(self):
39+
form = forms.ImportForm([CSV], [MyResource])
40+
media = form.media
41+
self.assertEqual(
42+
media._css,
43+
{},
44+
)
45+
self.assertEqual(
46+
media._js,
47+
[
48+
"admin/js/vendor/jquery/jquery.min.js",
49+
"admin/js/jquery.init.js",
50+
"import_export/guess_format.js",
51+
],
52+
)
53+
54+
def test_import_form_and_custom_widget_media(self):
55+
class TestMediaWidget(django.forms.TextInput):
56+
"""Dummy test widget with associated CSS and JS media."""
57+
58+
class Media:
59+
css = {
60+
"all": ["test.css"],
61+
}
62+
js = ["test.js"]
63+
64+
class CustomImportForm(forms.ImportForm):
65+
"""Dummy custom import form with a custom widget."""
66+
67+
author = django.forms.ModelChoiceField(
68+
queryset=Author.objects.none(),
69+
required=True,
70+
widget=TestMediaWidget,
71+
)
72+
73+
form = CustomImportForm([CSV], [MyResource])
74+
media = form.media
75+
self.assertEqual(
76+
media._css,
77+
{"all": ["test.css"]},
78+
)
79+
self.assertEqual(
80+
media._js,
81+
[
82+
"test.js",
83+
"admin/js/vendor/jquery/jquery.min.js",
84+
"admin/js/jquery.init.js",
85+
"import_export/guess_format.js",
86+
],
87+
)
88+
89+
3690
class SelectableFieldsExportFormTest(TestCase):
3791
@classmethod
3892
def setUpTestData(cls) -> None:

tests/core/tests/test_resources/test_import_export.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,27 @@ class Meta:
330330
str(e.exception),
331331
)
332332

333+
def test_dynamic_import_id_fields(self):
334+
# issue 1834
335+
class BookResource(resources.ModelResource):
336+
def before_import(self, dataset, **kwargs):
337+
# mimic a 'dynamic field' - i.e. append field which exists on
338+
# Book model, but not in dataset
339+
dataset.headers.append("price")
340+
super().before_import(dataset, **kwargs)
341+
342+
class Meta:
343+
model = Book
344+
import_id_fields = ("price",)
345+
346+
self.resource = BookResource()
347+
dataset = tablib.Dataset(
348+
*[(self.book.pk, "Goldeneye", "[email protected]")],
349+
headers=["id", "name", "author_email"],
350+
)
351+
self.resource.import_data(dataset, raise_errors=True)
352+
self.assertEqual("Goldeneye", Book.objects.latest("id").name)
353+
333354

334355
class ImportWithMissingFields(TestCase):
335356
# issue 1517

0 commit comments

Comments
 (0)