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

Issue:
Admin Export fails for models with a primary key which is not "id"
**Issue**
I have a simple Django model as follows:
```python
class Category(models.Model):
    name = models.CharField(primary_key=True, max_length=30)
```

Clicking on the "Export" button in the `ImportExportModelAdmin` for this Model, I get the following, for line 736 in `import_export/admin.py`:
```
AttributeError at /admin/products/category/export/
'Category' object has no attribute 'id'
```

**Versions**
 - Django Import Export: 4.0
 - Python: 3.12
 - Django: 5.0.4



Discussion:
Thanks for raising.  This is a valid bug.  I can reproduce in the example app by adding 

```python
class UUIDCategoryAdmin(ExportActionModelAdmin):
    pass
...
admin.site.register(UUIDCategory, UUIDCategoryAdmin)
```
Then adding a `UUIDCategory` and then exporting.

Resolving patch:
diff --git a/import_export/admin.py b/import_export/admin.py
--- a/import_export/admin.py
+++ b/import_export/admin.py
@@ -733,7 +733,7 @@ def export_action(self, request):
         form.fields["export_items"] = MultipleChoiceField(
             widget=MultipleHiddenInput,
             required=False,
-            choices=[(o.id, o.id) for o in self.model.objects.all()],
+            choices=[(o.pk, o.pk) for o in self.model.objects.all()],
         )
         if form.is_valid():
             file_format = formats[int(form.cleaned_data["format"])]()
@@ -876,7 +876,7 @@ def export_admin_action(self, request, queryset):
         form = form_type(
             formats=formats,
             resources=self.get_export_resource_classes(request),
-            initial={"export_items": list(queryset.values_list("id", flat=True))},
+            initial={"export_items": list(queryset.values_list("pk", flat=True))},
         )
         # selected items are to be stored as a hidden input on the form
         form.fields["export_items"] = MultipleChoiceField(


Test patch:
diff --git a/tests/core/admin.py b/tests/core/admin.py
--- a/tests/core/admin.py
+++ b/tests/core/admin.py
@@ -8,7 +8,7 @@
 from import_export.resources import ModelResource
 
 from .forms import CustomConfirmImportForm, CustomExportForm, CustomImportForm
-from .models import Author, Book, Category, Child, EBook
+from .models import Author, Book, Category, Child, EBook, UUIDCategory
 
 
 class ChildAdmin(ImportMixin, admin.ModelAdmin):
@@ -41,6 +41,10 @@ class CategoryAdmin(ExportActionModelAdmin):
     pass
 
 
+class UUIDCategoryAdmin(ExportActionModelAdmin):
+    pass
+
+
 class AuthorAdmin(ImportMixin, admin.ModelAdmin):
     pass
 
@@ -104,3 +108,4 @@ def get_export_resource_kwargs(self, request, **kwargs):
 admin.site.register(Author, AuthorAdmin)
 admin.site.register(Child, ChildAdmin)
 admin.site.register(EBook, CustomBookAdmin)
+admin.site.register(UUIDCategory, UUIDCategoryAdmin)
diff --git a/tests/core/models.py b/tests/core/models.py
--- a/tests/core/models.py
+++ b/tests/core/models.py
@@ -56,6 +56,9 @@ class Category(models.Model):
     def __str__(self):
         return self.name
 
+    class Meta:
+        verbose_name_plural = "categories"
+
 
 class BookManager(models.Manager):
     """
@@ -158,6 +161,9 @@ class UUIDCategory(models.Model):
     catid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
     name = models.CharField(max_length=32)
 
+    class Meta:
+        verbose_name_plural = "UUID categories"
+
 
 class UUIDBook(models.Model):
     """A model which uses a UUID pk (issue 1274)"""
diff --git a/tests/core/tests/admin_integration/mixins.py b/tests/core/tests/admin_integration/mixins.py
--- a/tests/core/tests/admin_integration/mixins.py
+++ b/tests/core/tests/admin_integration/mixins.py
@@ -8,8 +8,13 @@
 
 class AdminTestMixin(object):
     category_change_url = "/admin/core/category/"
+    uuid_category_change_url = "/admin/core/uuidcategory/"
+    category_export_url = "/admin/core/category/export/"
+    uuid_category_export_url = "/admin/core/uuidcategory/export/"
     book_import_url = "/admin/core/book/import/"
+    book_export_url = "/admin/core/book/export/"
     ebook_import_url = "/admin/core/ebook/import/"
+    ebook_export_url = "/admin/core/ebook/export/"
     book_process_import_url = "/admin/core/book/process_import/"
     legacybook_import_url = "/admin/core/legacybook/import/"
     legacybook_process_import_url = "/admin/core/legacybook/process_import/"
diff --git a/tests/core/tests/admin_integration/test_action.py b/tests/core/tests/admin_integration/test_action_export.py
similarity index 87%
rename from tests/core/tests/admin_integration/test_action.py
rename to tests/core/tests/admin_integration/test_action_export.py
--- a/tests/core/tests/admin_integration/test_action.py
+++ b/tests/core/tests/admin_integration/test_action_export.py
@@ -3,7 +3,7 @@
 from unittest.mock import MagicMock
 
 from core.admin import CategoryAdmin
-from core.models import Book, Category
+from core.models import Book, Category, UUIDCategory
 from core.tests.admin_integration.mixins import AdminTestMixin
 from django.contrib import admin
 from django.core.exceptions import PermissionDenied
@@ -44,7 +44,7 @@ def test_export_skips_export_ui_page(self):
             "action": ["export_admin_action"],
             "_selected_action": [str(self.cat1.id)],
         }
-        response = self.client.post("/admin/core/category/", data)
+        response = self.client.post(self.category_change_url, data)
         self._check_export_response(response)
 
     def test_export_displays_ui_select_page(self):
@@ -52,7 +52,7 @@ def test_export_displays_ui_select_page(self):
             "action": ["export_admin_action"],
             "_selected_action": [str(self.cat1.id)],
         }
-        response = self.client.post("/admin/core/category/", data)
+        response = self.client.post(self.category_change_url, data)
 
         self.assertEqual(response.status_code, 200)
         self.assertIn("form", response.context)
@@ -66,7 +66,7 @@ def test_export_displays_ui_select_page_multiple_items(self):
             "action": ["export_admin_action"],
             "_selected_action": [str(self.cat1.id), str(self.cat2.id)],
         }
-        response = self.client.post("/admin/core/category/", data)
+        response = self.client.post(self.category_change_url, data)
 
         self.assertEqual(response.status_code, 200)
         self.assertIn("form", response.context)
@@ -77,6 +77,22 @@ def test_export_displays_ui_select_page_multiple_items(self):
         )
         self.assertIn("Export 2 selected items.", str(response.content))
 
+    def test_action_export_model_with_custom_PK(self):
+        # issue 1800
+        cat = UUIDCategory.objects.create(name="UUIDCategory")
+        data = {
+            "action": ["export_admin_action"],
+            "_selected_action": [str(cat.pk)],
+        }
+        response = self.client.post(self.uuid_category_change_url, data)
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("form", response.context)
+        export_form = response.context["form"]
+        data = export_form.initial
+        self.assertEqual([cat.pk], data["export_items"])
+        self.assertIn("Export 1 selected item.", str(response.content))
+
     def test_export_post(self):
         # create a POST request with data selected from the 'action' export
         data = {
@@ -85,7 +101,7 @@ def test_export_post(self):
             **self.resource_fields_payload,
         }
         date_str = datetime.now().strftime("%Y-%m-%d")
-        response = self.client.post("/admin/core/category/export/", data)
+        response = self.client.post(self.category_export_url, data)
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.has_header("Content-Disposition"))
         self.assertEqual(response["Content-Type"], "text/csv")
diff --git a/tests/core/tests/admin_integration/test_export.py b/tests/core/tests/admin_integration/test_export.py
--- a/tests/core/tests/admin_integration/test_export.py
+++ b/tests/core/tests/admin_integration/test_export.py
@@ -6,7 +6,7 @@
 import chardet
 import tablib
 from core.admin import BookAdmin, BookResource
-from core.models import Author, Book
+from core.models import Author, Book, UUIDCategory
 from core.tests.admin_integration.mixins import AdminTestMixin
 from core.tests.utils import ignore_utcnow_deprecation_warning
 from django.contrib.admin.sites import AdminSite
@@ -36,7 +36,7 @@ def setUp(self) -> None:
         }
 
     def test_export(self):
-        response = self.client.get("/admin/core/book/export/")
+        response = self.client.get(self.book_export_url)
         self.assertEqual(response.status_code, 200)
         self.assertNotIn("Export 0 selected items.", str(response.content))
         form = response.context["form"]
@@ -46,7 +46,7 @@ def test_export(self):
         date_str = datetime.now().strftime("%Y-%m-%d")
         # Should not contain COUNT queries from ModelAdmin.get_results()
         with self.assertNumQueries(6):
-            response = self.client.post("/admin/core/book/export/", data)
+            response = self.client.post(self.book_export_url, data)
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.has_header("Content-Disposition"))
         self.assertEqual(response["Content-Type"], "text/csv")
@@ -65,7 +65,7 @@ def test_export_passes_export_resource_kwargs(
     ):
         # issue 1738
         mock_get_export_resource_kwargs.return_value = {"a": 1}
-        response = self.client.get("/admin/core/book/export/")
+        response = self.client.get(self.book_export_url)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(2, mock_get_export_resource_kwargs.call_count)
 
@@ -78,14 +78,14 @@ def test_export_passes_no_resource_constructor_params(self):
         # issue 1716
         # assert that the export call with a no-arg constructor
         # does not crash
-        response = self.client.get("/admin/core/book/export/")
+        response = self.client.get(self.book_export_url)
         self.assertEqual(response.status_code, 200)
 
     def test_get_export_queryset(self):
         model_admin = BookAdmin(Book, AdminSite())
 
         factory = RequestFactory()
-        request = factory.get("/admin/core/book/export/")
+        request = factory.get(self.book_export_url)
         request.user = User.objects.create_user("admin1")
 
         call_number = 0
@@ -112,7 +112,7 @@ def test_get_export_queryset_no_queryset_init(self):
         model_admin = BookAdmin(Book, AdminSite())
 
         factory = RequestFactory()
-        request = factory.get("/admin/core/book/export/")
+        request = factory.get(self.book_export_url)
         request.user = User.objects.create_user("admin1")
 
         call_number = 0
@@ -167,13 +167,13 @@ def test_get_export_FieldError(self):
                 "booknameresource_id": True,
                 "booknameresource_name": True,
             }
-            response = self.client.post("/admin/core/book/export/", data)
+            response = self.client.post(self.book_export_url, data)
         self.assertEqual(response.status_code, 200)
         target_msg = "Some unknown error"
         self.assertIn(target_msg, str(response.content))
 
     def test_export_second_resource(self):
-        response = self.client.get("/admin/core/book/export/")
+        response = self.client.get(self.book_export_url)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "Export/Import only book names")
 
@@ -185,7 +185,7 @@ def test_export_second_resource(self):
             "booknameresource_name": True,
         }
         date_str = datetime.now().strftime("%Y-%m-%d")
-        response = self.client.post("/admin/core/book/export/", data)
+        response = self.client.post(self.book_export_url, data)
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.has_header("Content-Disposition"))
         self.assertEqual(response["Content-Type"], "text/csv")
@@ -196,7 +196,7 @@ def test_export_second_resource(self):
         self.assertEqual(b"id,name\r\n", response.content)
 
     def test_export_displays_resources_fields(self):
-        response = self.client.get("/admin/core/book/export/")
+        response = self.client.get(self.book_export_url)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(
             response.context["fields_list"],
@@ -235,12 +235,12 @@ def test_export_empty_export_formats(self):
             self.client.get("/admin/core/category/export/")
 
     def test_returns_xlsx_export(self):
-        response = self.client.get("/admin/core/book/export/")
+        response = self.client.get(self.book_export_url)
         self.assertEqual(response.status_code, 200)
 
         xlsx_index = self._get_input_format_index("xlsx")
         data = {"format": str(xlsx_index), **self.bookresource_export_fields_payload}
-        response = self.client.post("/admin/core/book/export/", data)
+        response = self.client.post(self.book_export_url, data)
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.has_header("Content-Disposition"))
         self.assertEqual(
@@ -253,12 +253,12 @@ def test_returns_xlsx_export(self):
     def test_export_escape_formulae(self):
         Book.objects.create(id=1, name="=SUM(1+1)")
         Book.objects.create(id=2, name="<script>alert(1)</script>")
-        response = self.client.get("/admin/core/book/export/")
+        response = self.client.get(self.book_export_url)
         self.assertEqual(response.status_code, 200)
 
         xlsx_index = self._get_input_format_index("xlsx")
         data = {"format": str(xlsx_index), **self.bookresource_export_fields_payload}
-        response = self.client.post("/admin/core/book/export/", data)
+        response = self.client.post(self.book_export_url, data)
         self.assertEqual(response.status_code, 200)
         content = response.content
         wb = load_workbook(filename=BytesIO(content))
@@ -268,7 +268,7 @@ def test_export_escape_formulae(self):
     @override_settings(IMPORT_EXPORT_ESCAPE_FORMULAE_ON_EXPORT=True)
     def test_export_escape_formulae_csv(self):
         b1 = Book.objects.create(id=1, name="=SUM(1+1)")
-        response = self.client.get("/admin/core/book/export/")
+        response = self.client.get(self.book_export_url)
         self.assertEqual(response.status_code, 200)
 
         index = self._get_input_format_index("csv")
@@ -277,7 +277,7 @@ def test_export_escape_formulae_csv(self):
             "bookresource_id": True,
             "bookresource_name": True,
         }
-        response = self.client.post("/admin/core/book/export/", data)
+        response = self.client.post(self.book_export_url, data)
         self.assertIn(
             f"{b1.id},SUM(1+1)\r\n".encode(),
             response.content,
@@ -286,7 +286,7 @@ def test_export_escape_formulae_csv(self):
     @override_settings(IMPORT_EXPORT_ESCAPE_FORMULAE_ON_EXPORT=False)
     def test_export_escape_formulae_csv_false(self):
         b1 = Book.objects.create(id=1, name="=SUM(1+1)")
-        response = self.client.get("/admin/core/book/export/")
+        response = self.client.get(self.book_export_url)
         self.assertEqual(response.status_code, 200)
 
         index = self._get_input_format_index("csv")
@@ -295,12 +295,19 @@ def test_export_escape_formulae_csv_false(self):
             "bookresource_id": True,
             "bookresource_name": True,
         }
-        response = self.client.post("/admin/core/book/export/", data)
+        response = self.client.post(self.book_export_url, data)
         self.assertIn(
             f"{b1.id},=SUM(1+1)\r\n".encode(),
             response.content,
         )
 
+    def test_export_model_with_custom_PK(self):
+        # issue 1800
+        UUIDCategory.objects.create(name="UUIDCategory")
+        response = self.client.get(self.uuid_category_export_url)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, "UUIDCategoryResource")
+
 
 class FilteredExportAdminIntegrationTest(AdminTestMixin, TestCase):
     fixtures = ["category", "book", "author"]
@@ -311,7 +318,7 @@ def test_export_filters_by_form_param(self):
 
         data = {"format": "0", "author": str(author.id)}
         date_str = datetime.now().strftime("%Y-%m-%d")
-        response = self.client.post("/admin/core/ebook/export/", data)
+        response = self.client.post(self.ebook_export_url, data)
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.has_header("Content-Disposition"))
         self.assertEqual(response["Content-Type"], "text/csv")
@@ -416,7 +423,7 @@ def get_export_filename(self, request, queryset, file_format):
 
 class TestSelectableFieldsExportPage(AdminTestMixin, TestCase):
     def test_selectable_fields_rendered_with_resource_index_attribute(self) -> None:
-        response = self.client.get("/admin/core/book/export/")
+        response = self.client.get(self.book_export_url)
 
         self.assertEqual(response.status_code, 200)
         form_resources = response.context["form"].resources