diff --git a/.github/workflows/django-import-export-ci.yml b/.github/workflows/django-import-export-ci.yml index fd4784e5c..722973246 100644 --- a/.github/workflows/django-import-export-ci.yml +++ b/.github/workflows/django-import-export-ci.yml @@ -7,6 +7,8 @@ on: pull_request: branches: - main + # this is a temporary addition - can be removed after 3.0 release + - release-3-x jobs: test: diff --git a/import_export/utils.py b/import_export/utils.py index 5cca9f8b4..4f0c6a5e7 100644 --- a/import_export/utils.py +++ b/import_export/utils.py @@ -1,3 +1,6 @@ +import functools +import warnings + from django.db import transaction @@ -34,3 +37,20 @@ def original(method): """ method.is_original = True return method + + +def ignore_utcnow_deprecation_warning(fn): + """ + Ignore the specific deprecation warning occurring due to openpyxl and python3.12. + """ + + @functools.wraps(fn) + def inner(*args, **kwargs): + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + ) + fn(*args, **kwargs) + + return inner diff --git a/tests/core/admin.py b/tests/core/admin.py index f3e3c4169..138931623 100644 --- a/tests/core/admin.py +++ b/tests/core/admin.py @@ -5,10 +5,20 @@ ImportExportModelAdmin, ImportMixin, ) +from import_export.fields import Field from import_export.resources import ModelResource from .forms import CustomConfirmImportForm, CustomExportForm, CustomImportForm -from .models import Author, Book, Category, Child, EBook, LegacyBook +from .models import ( + Author, + Book, + Category, + Child, + EBook, + LegacyBook, + UUIDBook, + UUIDCategory, +) class ChildAdmin(ImportMixin, admin.ModelAdmin): @@ -42,11 +52,23 @@ def export_admin_action(self, request, queryset): return super().export_admin_action(request, queryset) +class UUIDBookAdmin(ImportExportModelAdmin): + pass + + +class UUIDCategoryAdmin(ExportActionModelAdmin): + pass + + class AuthorAdmin(ImportMixin, admin.ModelAdmin): pass class EBookResource(ModelResource): + published = Field(attribute="published", column_name="published_date") + author_email = Field(attribute="author_email", column_name="Email of the author") + auteur_name = Field(attribute="author__name", column_name="Author Name") + def __init__(self, **kwargs): super().__init__() self.author_id = kwargs.get("author_id") @@ -56,6 +78,7 @@ def filter_export(self, queryset, *args, **kwargs): class Meta: model = EBook + fields = ("id", "author_email", "name", "published", "auteur_name") class CustomBookAdmin(ImportExportModelAdmin): @@ -86,7 +109,7 @@ def get_export_resource_kwargs(self, request, *args, **kwargs): # this is overridden to demonstrate that custom form fields can be used # to override the export query. # The dict returned here will be passed as kwargs to EBookResource - export_form = kwargs["export_form"] + export_form = kwargs.get("export_form") if export_form: return dict(author_id=export_form.cleaned_data["author"].id) return {} @@ -119,3 +142,5 @@ def get_export_form(self): admin.site.register(Child, ChildAdmin) admin.site.register(EBook, CustomBookAdmin) admin.site.register(LegacyBook, LegacyBookAdmin) +admin.site.register(UUIDBook, UUIDBookAdmin) +admin.site.register(UUIDCategory, UUIDCategoryAdmin) diff --git a/tests/core/models.py b/tests/core/models.py index 99cb936de..76b722ad3 100644 --- a/tests/core/models.py +++ b/tests/core/models.py @@ -169,6 +169,9 @@ class UUIDCategory(models.Model): catid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=32) + def __str__(self): + return self.name + class UUIDBook(models.Model): """A model which uses a UUID pk (issue 1274)""" @@ -176,3 +179,6 @@ class UUIDBook(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField("Book name", max_length=100) categories = models.ManyToManyField(UUIDCategory, blank=True) + + def __str__(self): + return self.name diff --git a/tests/core/tests/test_admin_integration.py b/tests/core/tests/test_admin_integration.py index 7978dc836..c95443066 100644 --- a/tests/core/tests/test_admin_integration.py +++ b/tests/core/tests/test_admin_integration.py @@ -1,6 +1,6 @@ import os.path import warnings -from datetime import datetime +from datetime import date, datetime from io import BytesIO from unittest import mock from unittest.mock import MagicMock, patch @@ -13,6 +13,7 @@ BookAdmin, BookResource, CustomBookAdmin, + EBookResource, ImportMixin, ) from core.models import Author, Book, Category, EBook, Parent @@ -932,10 +933,8 @@ def test_export_filters_by_form_param(self): 'attachment; filename="EBook-{}.csv"'.format(date_str), ) self.assertEqual( - b"id,name,author,author_email,imported,published," - b"published_time,price,added,categories\r\n" - b"5,The Man with the Golden Gun,5,ian@example.com," - b"0,1965-04-01,21:00:00,5.00,,2\r\n", + b"published_date,Email of the author,Author Name,id,name\r\n" + b"1965-04-01,ian@example.com,Ian Fleming,5,The Man with the Golden Gun\r\n", response.content, ) @@ -1623,3 +1622,60 @@ def test_import_action_binary(self): follow=True, str_in_response="Import finished, with 1 new and 0 updated books.", ) + + +class CustomColumnNameExportTest(AdminTestMixin, TestCase): + """Test export ok when column name is defined in fields list (issue 1828).""" + + ebook_export_url = "/admin/core/ebook/export/" + + def setUp(self): + super().setUp() + self.author = Author.objects.create(id=11, name="Ian Fleming") + self.book = Book.objects.create( + name="Moonraker", author=self.author, published=date(1955, 4, 5) + ) + EBookResource._meta.fields = ( + "id", + "author_email", + "name", + "published_date", + "auteur_name", + ) + + def tearDown(self): + super().tearDown() + EBookResource._meta.fields = ("id", "author_email", "name", "published") + + def test_export_with_custom_field(self): + data = { + "file_format": "0", + "author": self.author.id, + } + date_str = datetime.now().strftime("%Y-%m-%d") + 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") + self.assertEqual( + response["Content-Disposition"], + 'attachment; filename="EBook-{}.csv"'.format(date_str), + ) + s = ( + "published_date,Email of the author,Author Name,id,name\r\n" + f"1955-04-05,,Ian Fleming,{self.book.id},Moonraker\r\n" + ) + self.assertEqual(s, response.content.decode()) + + def test_export_with_custom_name(self): + # issue 1893 + data = { + "file_format": "0", + "author": self.author.id, + } + response = self.client.post(self.ebook_export_url, data) + s = ( + "published_date,Email of the author,Author Name,id,name\r\n" + f"1955-04-05,,Ian Fleming,{self.book.id},Moonraker\r\n" + ) + self.assertEqual(s, response.content.decode()) diff --git a/tests/core/tests/test_resources.py b/tests/core/tests/test_resources.py index df8b256aa..44eaf55e9 100644 --- a/tests/core/tests/test_resources.py +++ b/tests/core/tests/test_resources.py @@ -24,13 +24,15 @@ from django.utils.html import strip_tags from import_export import fields, resources, results, widgets +from import_export.fields import Field from import_export.instance_loaders import ModelInstanceLoader -from import_export.resources import Diff +from import_export.resources import Diff, ModelResource from ..models import ( Author, Book, Category, + EBook, Entry, Person, Profile, @@ -551,6 +553,22 @@ def test_export(self): dataset = self.resource.export(Book.objects.all()) self.assertEqual(len(dataset), 1) + def test_export_declared_field(self): + # test behaviour of export when no attribute is set + class EBookResource(ModelResource): + published = Field(column_name="published") + + class Meta: + model = EBook + fields = ("id", "published") + + resource = EBookResource() + + self.book.published = date(1955, 4, 5) + self.book.save() + dataset = resource.export() + self.assertEqual("", dataset.dict[0]["published"]) + def test_export_iterable(self): with self.assertNumQueries(2): dataset = self.resource.export(list(Book.objects.all())) @@ -2855,3 +2873,53 @@ def test_after_import_row_check_for_change(self): # issue 1583 - assert that `original` object is available to after_import_row() self.resource.import_data(self.dataset, raise_errors=True) self.assertTrue(self.resource.is_published) + + def test_import_row_with_no_defined_id_field(self): + """Ensure a row with no id field can be imported (issue 1812).""" + self.assertEqual(0, Author.objects.count()) + dataset = tablib.Dataset(*[("J. R. R. Tolkien",)], headers=["name"]) + self.resource = AuthorResource() + self.resource.import_data(dataset) + self.assertEqual(1, Author.objects.count()) + + +class ImportExportFieldOrderTest(TestCase): + def setUp(self): + super().setUp() + self.pk = Book.objects.create(name="Ulysses", price="1.99").pk + self.dataset = tablib.Dataset(headers=["id", "name", "price"]) + row = [self.pk, "Some book", "19.99"] + self.dataset.append(row) + + def test_export_fields_column_name(self): + """Test export with declared export_fields and custom column_name""" + + # issue 1846 + class DeclaredModelFieldBookResource(resources.ModelResource): + published = fields.Field( + attribute="published", + column_name="datePublished", + widget=widgets.DateWidget("%d.%m.%Y"), + ) + author = fields.Field(column_name="AuthorFooName") + + class Meta: + model = Book + fields = ( + "id", + "author", + "published", + ) + export_order = ( + "published", + "id", + "author", + ) + + def dehydrate_author(self, obj): + return obj.author + + self.resource = DeclaredModelFieldBookResource() + data = self.resource.export(export_fields=["published", "id", "author"]) + target = f"datePublished,id,AuthorFooName\r\n,{self.pk},\r\n" + self.assertEqual(target, data.csv) diff --git a/tests/core/tests/test_widgets.py b/tests/core/tests/test_widgets.py index 4b1253ca3..78a327191 100644 --- a/tests/core/tests/test_widgets.py +++ b/tests/core/tests/test_widgets.py @@ -4,13 +4,13 @@ from unittest import mock, skipUnless import django -import pytz from core.models import Author, Book, Category from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone from import_export import widgets +from import_export.utils import ignore_utcnow_deprecation_warning class WidgetTest(TestCase): @@ -161,15 +161,21 @@ def test_render_none(self): def test_clean(self): self.assertEqual(self.widget.clean("13.08.2012 18:00:00"), self.datetime) + @ignore_utcnow_deprecation_warning @override_settings(USE_TZ=True, TIME_ZONE="Europe/Ljubljana") def test_use_tz(self): + import pytz + utc_dt = timezone.make_aware(self.datetime, pytz.UTC) self.assertEqual(self.widget.render(utc_dt), "13.08.2012 20:00:00") self.assertEqual(self.widget.clean("13.08.2012 20:00:00"), utc_dt) + @ignore_utcnow_deprecation_warning @override_settings(USE_TZ=True, TIME_ZONE="Europe/Ljubljana") def test_clean_returns_tz_aware_datetime_when_naive_datetime_passed(self): # issue 1165 + import pytz + if django.VERSION >= (5, 0): from zoneinfo import ZoneInfo @@ -179,8 +185,11 @@ def test_clean_returns_tz_aware_datetime_when_naive_datetime_passed(self): target_dt = timezone.make_aware(self.datetime, tz) self.assertEqual(target_dt, self.widget.clean(self.datetime)) + @ignore_utcnow_deprecation_warning @override_settings(USE_TZ=True, TIME_ZONE="Europe/Ljubljana") def test_clean_handles_tz_aware_datetime(self): + import pytz + self.datetime = datetime(2012, 8, 13, 18, 0, 0, tzinfo=pytz.timezone("UTC")) self.assertEqual(self.datetime, self.widget.clean(self.datetime))