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

Issue:
Declared fields are still imported/exported even if they are not defined in Meta.fields 
**Describe the bug**
Declared field in `ModelResource` will still be imported/exported whether they are in `Meta.fields` or not

**To Reproduce**
```py
class BookResource(ModelResource):
    author_name = fields.Field(attribute="author__name", column_name="author_name")

    class Meta:
        model = Book
        fields = ("id", "price",)

# `author_name` will still be exported even though it's not in `Meta.fields`
```

**Versions (please complete the following information):**
 - Django Import Export: 3.3.3
 - Python 3.10.12
 - Django 4.2.7

**Expected behavior**
if `fields` is defined in `Meta` only those fields should be imported/exported




Discussion:
Thanks for raising - are you able to supply a fix for this?

Resolving patch:
diff --git a/import_export/declarative.py b/import_export/declarative.py
--- a/import_export/declarative.py
+++ b/import_export/declarative.py
@@ -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):
@@ -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:


Test patch:
diff --git a/tests/core/tests/admin_integration/test_action.py b/tests/core/tests/admin_integration/test_action.py
--- a/tests/core/tests/admin_integration/test_action.py
+++ b/tests/core/tests/admin_integration/test_action.py
@@ -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(
diff --git a/tests/core/tests/test_resources/test_resources.py b/tests/core/tests/test_resources/test_resources.py
--- a/tests/core/tests/test_resources/test_resources.py
+++ b/tests/core/tests/test_resources/test_resources.py
@@ -1260,9 +1260,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:
@@ -1404,6 +1404,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
@@ -1660,8 +1834,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):