Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for declared fields being processed when not in fields list #1702

3 changes: 3 additions & 0 deletions docs/advanced_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ Or the ``exclude`` option to blacklist fields::
model = Book
exclude = ('imported', )

If both ``fields`` and ``exclude`` are declared, the ``fields`` declaration takes precedence, and ``exclude`` is
ignored.

.. _field_ordering:

Field ordering
Expand Down
5 changes: 3 additions & 2 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ Please refer to :doc:`release notes<release_notes>`.
4.0.0-beta.2 (unreleased)
--------------------------

- fix declaring existing model field(s) in ModelResource altering export order (#1663)
- Fix declaring existing model field(s) in ModelResource altering export order (#1663)
- Updated `docker-compose` command with latest version syntax in `runtests.sh` (#1686)
- Support export from model change form (#1687)
- Updated Admin UI to track deleted and skipped Imports (#1691)
- Import form defaults to read-only field if only one format defined (#1690)
- Refactored :module:`~import_export.resources` into separate modules for ``declarative`` and ``options`` (#1695)
- fix multiple inheritance not setting options (#1696)
- Fix multiple inheritance not setting options (#1696)
- Fix issue where declared Resource fields not defined in `fields` are still imported (#1702)

4.0.0-beta.1 (2023-11-16)
--------------------------
Expand Down
13 changes: 11 additions & 2 deletions import_export/declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,16 @@ def __new__(cls, name, bases, attrs):

if opts.model:
model_opts = opts.model._meta
declared_fields = new_class.fields

# #1693 check the fields explicitly declared as attributes of the Resource
# class.
# if 'fields' property is defined, declared fields can only be included
# if they appear in the 'fields' iterable.
declared_fields = dict()
for field_name, field in new_class.fields.items():
if opts.fields is not None and field_name not in opts.fields:
continue
declared_fields[field_name] = field

field_list = []
for f in sorted(model_opts.fields + model_opts.many_to_many):
Expand All @@ -98,7 +107,7 @@ def __new__(cls, name, bases, attrs):
)

# Order as model fields first then declared fields by default
new_class.fields = OrderedDict([*field_list, *new_class.fields.items()])
new_class.fields = OrderedDict([*field_list, *declared_fields.items()])

# add fields that follow relationships
if opts.fields is not None:
Expand Down
2 changes: 1 addition & 1 deletion tests/core/tests/admin_integration/test_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def test_export_admin_action(self):
"_selected_action": "0",
},
)
assert 200 <= response.status_code <= 399
self.assertTrue(200 <= response.status_code <= 399)
mock_export_admin_action.assert_called()

def test_get_export_data_raises_PermissionDenied_when_no_export_permission_assigned(
Expand Down
181 changes: 177 additions & 4 deletions tests/core/tests/test_resources/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -1255,9 +1255,9 @@ class EntryResource(resources.ModelResource):

class Meta:
model = Entry
fields = ("id",)
fields = ("id", "username")

def after_save_instance(self, instance, row, **kwargs):
def after_save_instance(self, instance, row_, **kwargs):
using_transactions = kwargs.get("using_transactions", False)
dry_run = kwargs.get("dry_run", False)
if not using_transactions and dry_run:
Expand Down Expand Up @@ -1399,6 +1399,180 @@ class Meta:
self.assertFalse(related_field_widget.use_natural_foreign_keys)


class ModelResourceFieldDeclarations(TestCase):
class MyBookResource(resources.ModelResource):
author_email = fields.Field(
attribute="author_email", column_name="author_email"
)

class Meta:
model = Book
fields = ("id", "price")

def setUp(self):
self.book = Book.objects.create(name="Moonraker", price=".99")
self.resource = ModelResourceFieldDeclarations.MyBookResource()

@ignore_widget_deprecation_warning
def test_declared_field_not_imported(self):
self.assertEqual("", self.book.author_email)
rows = [
(self.book.id, "12.99", "jj@example.com"),
]
dataset = tablib.Dataset(*rows, headers=["id", "price", "author_email"])
self.resource.import_data(dataset, raise_errors=True)
self.book.refresh_from_db()
# email should not be updated
self.assertEqual("", self.book.author_email)

@ignore_widget_deprecation_warning
def test_declared_field_not_exported(self):
self.assertEqual("", self.book.author_email)
data = self.resource.export()
self.assertFalse("author_email" in data.dict[0])


class ModelResourceNoFieldDeclarations(TestCase):
# No `fields` declaration so all fields should be included
class MyBookResource(resources.ModelResource):
author_email = fields.Field(
attribute="author_email", column_name="author_email"
)

class Meta:
model = Book

def setUp(self):
self.book = Book.objects.create(name="Moonraker", price=".99")
self.resource = ModelResourceNoFieldDeclarations.MyBookResource()

@ignore_widget_deprecation_warning
def test_declared_field_imported(self):
self.assertEqual("", self.book.author_email)
rows = [
(self.book.id, "12.99", "jj@example.com"),
]
dataset = tablib.Dataset(*rows, headers=["id", "price", "author_email"])
self.resource.import_data(dataset, raise_errors=True)
self.book.refresh_from_db()
# email should be updated
self.assertEqual("jj@example.com", self.book.author_email)

@ignore_widget_deprecation_warning
def test_declared_field_not_exported(self):
self.assertEqual("", self.book.author_email)
data = self.resource.export()
self.assertTrue("author_email" in data.dict[0])


class ModelResourceExcludeDeclarations(TestCase):
class MyBookResource(resources.ModelResource):
author_email = fields.Field(
attribute="author_email", column_name="author_email"
)

class Meta:
model = Book
fields = ("id", "price")
exclude = ("author_email",)

def setUp(self):
self.book = Book.objects.create(name="Moonraker", price=".99")
self.resource = ModelResourceExcludeDeclarations.MyBookResource()

@ignore_widget_deprecation_warning
def test_excluded_field_not_imported(self):
self.assertEqual("", self.book.author_email)
rows = [
(self.book.id, "12.99", "jj@example.com"),
]
dataset = tablib.Dataset(*rows, headers=["id", "price", "author_email"])
self.resource.import_data(dataset, raise_errors=True)
self.book.refresh_from_db()
# email should not be updated
self.assertEqual("", self.book.author_email)

@ignore_widget_deprecation_warning
def test_declared_field_not_exported(self):
self.assertEqual("", self.book.author_email)
data = self.resource.export()
self.assertFalse("author_email" in data.dict[0])


class ModelResourceFieldsAndExcludeDeclarations(TestCase):
# Include the same field in both `fields` and `exclude`.
# `fields` should take precedence.
class MyBookResource(resources.ModelResource):
author_email = fields.Field(
attribute="author_email", column_name="author_email"
)

class Meta:
model = Book
fields = ("id", "price", "author_email")
exclude = ("author_email",)

def setUp(self):
self.book = Book.objects.create(name="Moonraker", price=".99")
self.resource = ModelResourceFieldsAndExcludeDeclarations.MyBookResource()

@ignore_widget_deprecation_warning
def test_excluded_field_not_imported(self):
self.assertEqual("", self.book.author_email)
rows = [
(self.book.id, "12.99", "jj@example.com"),
]
dataset = tablib.Dataset(*rows, headers=["id", "price", "author_email"])
self.resource.import_data(dataset, raise_errors=True)
self.book.refresh_from_db()
# email should be updated
self.assertEqual("jj@example.com", self.book.author_email)

@ignore_widget_deprecation_warning
def test_declared_field_not_exported(self):
self.assertEqual("", self.book.author_email)
data = self.resource.export()
self.assertTrue("author_email" in data.dict[0])


class ModelResourceDeclarationsNotInImportTest(TestCase):
# issue 1697
# Add a declared field to the Resource, which is not present in the import file.
# The import should succeed without issue.
class MyBookResource(resources.ModelResource):
author_email = fields.Field(
attribute="author_email", column_name="author_email"
)

class Meta:
model = Book
fields = (
"id",
"price",
)

def setUp(self):
self.resource = ModelResourceDeclarationsNotInImportTest.MyBookResource()

@ignore_widget_deprecation_warning
def test_excluded_field_not_imported(self):
rows = [
("1", "12.99"),
]
dataset = tablib.Dataset(*rows, headers=["id", "price"])
result = self.resource.import_data(dataset, raise_errors=True)
book = Book.objects.first()
self.assertEqual("", book.author_email)
self.assertEqual(1, result.totals["new"])

@ignore_widget_deprecation_warning
def test_excluded_field_not_exported(self):
self.book = Book.objects.create(name="Moonraker", price=".99")
self.assertEqual("", self.book.author_email)
data = self.resource.export()
self.assertFalse("author_email" in data.dict[0])


class ModelResourceTransactionTest(TransactionTestCase):
@skipUnlessDBFeature("supports_transactions")
@ignore_widget_deprecation_warning
Expand Down Expand Up @@ -1655,8 +1829,7 @@ def setUp(self):
def test_export_field_with_appropriate_format(self):
resource = resources.modelresource_factory(model=BookWithChapters)()
result = resource.export(BookWithChapters.objects.all())

assert result[0][3] == json.dumps(self.json_data)
self.assertEqual(result[0][3], json.dumps(self.json_data))

class TestImportJsonField(TestCase):
def setUp(self):
Expand Down