mirror of
https://github.com/django-import-export/django-import-export.git
synced 2025-12-11 19:53:58 +03:00
Merge branch 'rel/4-x'
This commit is contained in:
commit
3ba3043fa7
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
|
|
@ -7,6 +7,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- rel/4-x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|
|
||||||
13
RELEASE.md
13
RELEASE.md
|
|
@ -46,6 +46,19 @@ so if another branch is used, then the documentation will be incomplete.
|
||||||
|
|
||||||
The `release` github workflow will run and publish the release binaries to both test.pypi.org and pypi.org.
|
The `release` github workflow will run and publish the release binaries to both test.pypi.org and pypi.org.
|
||||||
|
|
||||||
|
#### Release from a different branch
|
||||||
|
|
||||||
|
Ensure that feature branches are merged and that the changelog is up-to-date as required.
|
||||||
|
|
||||||
|
To release from a feature branch (e.g. 'rel/4-x'):
|
||||||
|
|
||||||
|
1. Ensure that the branch is up-to-date locally (`git pull upstream rel/4-x`)
|
||||||
|
2. Tag the branch as required (`git tag -a 2.29.2 -m "v4.3.11"`)
|
||||||
|
3. Push tags (`git push --tags upstream`)
|
||||||
|
|
||||||
|
Now release as above but use the appropriate git tag.
|
||||||
|
Remember to merge the release branch into the `main` branch.
|
||||||
|
|
||||||
### Check readthedocs
|
### Check readthedocs
|
||||||
|
|
||||||
[readthedocs](https://readthedocs.org/projects/django-import-export/) integration is used to publish documentation.
|
[readthedocs](https://readthedocs.org/projects/django-import-export/) integration is used to publish documentation.
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ Changelog
|
||||||
- Added support for django 6.0a (`2112 <https://github.com/django-import-export/django-import-export/pull/2112>`_)
|
- Added support for django 6.0a (`2112 <https://github.com/django-import-export/django-import-export/pull/2112>`_)
|
||||||
- Removed the deprecated :meth:`~import_export.admin.ExportMixin.get_valid_export_item_pks` method in favour of :meth:`~import_export.admin.ExportMixin.get_queryset` (`1898 <https://github.com/django-import-export/django-import-export/pull/1898>`_)
|
- Removed the deprecated :meth:`~import_export.admin.ExportMixin.get_valid_export_item_pks` method in favour of :meth:`~import_export.admin.ExportMixin.get_queryset` (`1898 <https://github.com/django-import-export/django-import-export/pull/1898>`_)
|
||||||
|
|
||||||
|
4.3.11 (2025-10-18)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Fix for export not retaining URI query params (`2097 <https://github.com/django-import-export/django-import-export/pull/2097>`_)
|
||||||
|
|
||||||
4.3.10 (2025-09-26)
|
4.3.10 (2025-09-26)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -906,6 +907,12 @@ class ExportActionMixin(ExportMixin):
|
||||||
self.model._meta.model_name,
|
self.model._meta.model_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Preserve admin changelist filters by including request GET parameters
|
||||||
|
# This fixes issue #2097 where applied filters are lost during export
|
||||||
|
if request.GET:
|
||||||
|
export_url += "?" + urlencode(request.GET)
|
||||||
|
|
||||||
context["export_url"] = export_url
|
context["export_url"] = export_url
|
||||||
|
|
||||||
return render(request, "admin/import_export/export.html", context=context)
|
return render(request, "admin/import_export/export.html", context=context)
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,39 @@ class BookNameResource(ModelResource):
|
||||||
name = "Export/Import only book names"
|
name = "Export/Import only book names"
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorBirthdayListFilter(admin.SimpleListFilter):
|
||||||
|
"""Example filter which can be used in the Admin UI to filter books by
|
||||||
|
author birthday."""
|
||||||
|
|
||||||
|
title = "author's birthday"
|
||||||
|
parameter_name = "birthday"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
("before", "before 1900"),
|
||||||
|
("after", "after 1900"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def choices(self, cl):
|
||||||
|
for lookup, title in self.lookup_choices:
|
||||||
|
yield {
|
||||||
|
"selected": self.value() == lookup,
|
||||||
|
"query_string": cl.get_query_string({self.parameter_name: lookup}, []),
|
||||||
|
"display": title,
|
||||||
|
}
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == "before":
|
||||||
|
return queryset.filter(author__birthday__year__lt=1900)
|
||||||
|
if self.value() == "after":
|
||||||
|
return queryset.filter(author__birthday__year__gte=1900)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Book)
|
@admin.register(Book)
|
||||||
class BookAdmin(ImportExportModelAdmin):
|
class BookAdmin(ExportActionModelAdmin, ImportExportModelAdmin):
|
||||||
list_display = ("name", "author", "added")
|
list_display = ("name", "author", "added")
|
||||||
list_filter = ["categories", "author"]
|
list_filter = [AuthorBirthdayListFilter, "categories", "author"]
|
||||||
resource_classes = [BookResource, BookNameResource]
|
resource_classes = [BookResource, BookNameResource]
|
||||||
change_list_template = "core/admin/change_list.html"
|
change_list_template = "core/admin/change_list.html"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
from core.admin import CategoryAdmin
|
from core.admin import CategoryAdmin
|
||||||
from core.models import Book, Category, UUIDCategory
|
from core.models import Author, Book, Category, UUIDCategory
|
||||||
from core.tests.admin_integration.mixins import AdminTestMixin
|
from core.tests.admin_integration.mixins import AdminTestMixin
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin import AdminSite
|
from django.contrib.admin import AdminSite
|
||||||
|
|
@ -230,6 +230,134 @@ class ExportActionAdminIntegrationTest(AdminTestMixin, TestCase):
|
||||||
m.get_export_data("0", request, Book.objects.none())
|
m.get_export_data("0", request, Book.objects.none())
|
||||||
|
|
||||||
|
|
||||||
|
class TestExportFilterPreservation(AdminTestMixin, TestCase):
|
||||||
|
"""
|
||||||
|
Test cases for issue #2097: Admin filters are lost during export actions.
|
||||||
|
|
||||||
|
Tests that admin changelist filters are properly preserved when exporting
|
||||||
|
selected items through the export action using the AuthorBirthdayListFilter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
# Create authors from different eras to test the AuthorBirthdayListFilter
|
||||||
|
self.old_author1 = Author.objects.create(
|
||||||
|
name="Old Author 1", birthday=date(1850, 1, 1)
|
||||||
|
)
|
||||||
|
self.old_author2 = Author.objects.create(
|
||||||
|
name="Old Author 2", birthday=date(1880, 6, 15)
|
||||||
|
)
|
||||||
|
self.new_author1 = Author.objects.create(
|
||||||
|
name="New Author 1", birthday=date(1950, 3, 10)
|
||||||
|
)
|
||||||
|
self.new_author2 = Author.objects.create(
|
||||||
|
name="New Author 2", birthday=date(1970, 12, 25)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create books with authors from different eras
|
||||||
|
self.old_book1 = Book.objects.create(name="Old Book 1", author=self.old_author1)
|
||||||
|
self.old_book2 = Book.objects.create(name="Old Book 2", author=self.old_author2)
|
||||||
|
self.new_book1 = Book.objects.create(name="New Book 1", author=self.new_author1)
|
||||||
|
self.new_book2 = Book.objects.create(name="New Book 2", author=self.new_author2)
|
||||||
|
|
||||||
|
# fields payload for `BookResource` - for `SelectableFieldsExportForm`
|
||||||
|
self.resource_fields_payload = {
|
||||||
|
"bookresource_id": True,
|
||||||
|
"bookresource_name": True,
|
||||||
|
"bookresource_author": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_export_action_preserves_admin_filters(self):
|
||||||
|
"""
|
||||||
|
Test that admin filters are preserved when exporting selected items.
|
||||||
|
This reproduces issue #2097 where applied filters are lost during export.
|
||||||
|
|
||||||
|
Uses the AuthorBirthdayListFilter to test filter preservation with books.
|
||||||
|
The issue occurs when:
|
||||||
|
1. User applies filters in admin changelist (authors born before 1900)
|
||||||
|
2. User selects items from filtered results
|
||||||
|
3. User chooses "Export selected items" action
|
||||||
|
4. The export URL loses the filter context, causing unfiltered export
|
||||||
|
"""
|
||||||
|
# Step 1: Simulate POST action with AuthorBirthdayListFilter applied
|
||||||
|
data = {
|
||||||
|
"action": ["export_admin_action"],
|
||||||
|
"_selected_action": [
|
||||||
|
str(self.old_book1.id),
|
||||||
|
str(self.old_book2.id),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add filter parameters to simulate applied admin filters
|
||||||
|
filter_params = "?birthday=before"
|
||||||
|
url_with_filters = self.core_book_url + filter_params
|
||||||
|
|
||||||
|
# Make the request with filters applied
|
||||||
|
response = self._post_url_response(url_with_filters, data)
|
||||||
|
|
||||||
|
# Should get an export form
|
||||||
|
self.assertIn("form", response.context)
|
||||||
|
|
||||||
|
# Check that export_url preserves the filter parameters
|
||||||
|
export_url = response.context.get("export_url", "")
|
||||||
|
self.assertIn(
|
||||||
|
"birthday=before",
|
||||||
|
export_url,
|
||||||
|
f"Export URL should preserve AuthorBirthdayListFilter parameters. "
|
||||||
|
f"Got URL: '{export_url}'. Filter preservation is working!",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_export_action_filter_preservation_end_to_end(self):
|
||||||
|
"""
|
||||||
|
Test the complete filter preservation workflow from action to final export.
|
||||||
|
This test follows the complete flow: action -> form -> export with filters.
|
||||||
|
"""
|
||||||
|
# Step 1: First trigger the export action with filters
|
||||||
|
action_data = {
|
||||||
|
"action": ["export_admin_action"],
|
||||||
|
"_selected_action": [
|
||||||
|
str(self.old_book1.id),
|
||||||
|
str(self.old_book2.id),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# POST to changelist with filter to get export form
|
||||||
|
filter_params = "?birthday=before"
|
||||||
|
url_with_filters = self.core_book_url + filter_params
|
||||||
|
action_response = self._post_url_response(url_with_filters, action_data)
|
||||||
|
|
||||||
|
# Should get an export form with preserved filter URL
|
||||||
|
self.assertIn("form", action_response.context)
|
||||||
|
export_url = action_response.context.get("export_url", "")
|
||||||
|
self.assertIn("birthday=before", export_url)
|
||||||
|
|
||||||
|
# Step 2: Now submit the export form to the preserved URL
|
||||||
|
export_data = {
|
||||||
|
"format": "0",
|
||||||
|
"export_items": [str(self.old_book1.id), str(self.old_book2.id)],
|
||||||
|
**self.resource_fields_payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
# POST to the export URL that should have preserved filters
|
||||||
|
final_response = self._post_url_response(export_url, export_data)
|
||||||
|
|
||||||
|
# Should get CSV export that respects the filter context
|
||||||
|
self.assertEqual(final_response["Content-Type"], "text/csv")
|
||||||
|
content = final_response.content.decode()
|
||||||
|
|
||||||
|
# Verify the export contains the expected filtered data
|
||||||
|
lines = content.strip().split("\n")
|
||||||
|
if len(lines) > 1:
|
||||||
|
data_lines = lines[1:] # Remove header
|
||||||
|
# Should only contain the 2 selected books
|
||||||
|
self.assertEqual(
|
||||||
|
len(data_lines),
|
||||||
|
2,
|
||||||
|
f"Filter preservation working! Expected 2 books, got {len(data_lines)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestExportButtonOnChangeForm(AdminTestMixin, TestCase):
|
class TestExportButtonOnChangeForm(AdminTestMixin, TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user