Skip to content

Commit e4fafdb

Browse files
Add 'export from model form' (#1687)
* initial check-in * updated * updated trans tag * added test * updated docs * added extra tests * test fix * updated changelog
1 parent 7d912b4 commit e4fafdb

File tree

13 files changed

+145
-39
lines changed

13 files changed

+145
-39
lines changed
21 KB
Loading

docs/advanced_usage.rst

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,26 @@ this to refer to your own model instances. In the example application, the 'Cat
849849

850850
When 'Go' is clicked for the selected items, the user will be directed to the
851851
:ref:`export 'confirm' page<export_confirm>`. It is possible to disable this extra step by setting the
852-
:ref:`import_export_skip_admin_action_export_ui` flag.
852+
:ref:`import_export_skip_admin_action_export_ui` flag
853+
854+
Export from model instance change form
855+
--------------------------------------
856+
857+
When :ref:`export via admin action<export_via_admin_action>` is enabled, then it is also possible to export from a
858+
model instance change form:
859+
860+
.. figure:: _static/images/change-form-export.png
861+
:alt: export from change form
862+
863+
Export from model instance change form
864+
865+
When 'Export' is clicked, the user will be directed to the :ref:`export 'confirm' page<export_confirm>`.
866+
867+
This button can be removed from the UI by setting the
868+
:attr:`~import_export.admin.ExportActionMixin.show_change_form_export` attribute, for example::
869+
870+
class CategoryAdmin(ExportActionModelAdmin):
871+
show_change_form_export = False
853872

854873
Customize admin import forms
855874
----------------------------

docs/changelog.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ Changelog
33

44
Please refer to :doc:`release notes<release_notes>`.
55

6+
4.0.0-beta.2 (unreleased)
7+
--------------------------
8+
9+
- Updated `docker-compose` command with latest version syntax in `runtests.sh` (#1686)
10+
- Support export from model change form (#1687)
11+
612
4.0.0-beta.1 (2023-11-16)
713
--------------------------
814

@@ -36,7 +42,6 @@ Enhancements
3642
Fixes
3743
#####
3844

39-
- Updated `docker-compose` command with latest version syntax in `runtests.sh` (#1686)
4045
- dynamic widget parameters for CharField fixes 'NOT NULL constraint' error in xlsx (#1485)
4146
- fix cooperation with adminsortable2 (#1633)
4247
- Removed unused method ``utils.original()``

import_export/admin.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,10 +778,33 @@ class ExportActionMixin(ExportMixin):
778778
Mixin with export functionality implemented as an admin action.
779779
"""
780780

781+
#: template for change form
782+
change_form_template = "admin/import_export/change_form.html"
783+
784+
#: Flag to indicate whether to show 'export' button on change form
785+
show_change_form_export = True
786+
781787
# This action will receive a selection of items as a queryset,
782788
# store them in the context, and then render the 'export' admin form page,
783789
# so that users can select file format and resource
784790

791+
def change_view(self, request, object_id, form_url="", extra_context=None):
792+
extra_context = extra_context or {}
793+
extra_context["show_change_form_export"] = self.show_change_form_export
794+
return super().change_view(
795+
request,
796+
object_id,
797+
form_url,
798+
extra_context=extra_context,
799+
)
800+
801+
def response_change(self, request, obj):
802+
if "_export-item" in request.POST:
803+
return self.export_admin_action(
804+
request, self.model.objects.filter(id=obj.id)
805+
)
806+
return super().response_change(request, obj)
807+
785808
def export_admin_action(self, request, queryset):
786809
"""
787810
Action runs on POST from instance action menu (if enabled).
@@ -820,7 +843,15 @@ def export_admin_action(self, request, queryset):
820843

821844
# this is necessary to render the FORM action correctly
822845
# i.e. so the POST goes to the correct URL
823-
context["export_suffix"] = "export/"
846+
export_url = reverse(
847+
"%s:%s_%s_export"
848+
% (
849+
self.admin_site.name,
850+
self.model._meta.app_label,
851+
self.model._meta.model_name,
852+
)
853+
)
854+
context["export_url"] = export_url
824855

825856
return render(request, "admin/import_export/export.html", context=context)
826857

import_export/templates/admin/import_export/base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
{% if not is_popup %}
99
{% block breadcrumbs %}
1010
<div class="breadcrumbs">
11-
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
11+
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
1212
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
1313
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
1414
&rsaquo; {% block breadcrumbs_last %}{% endblock %}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{% extends 'admin/change_form.html' %}
2+
{% load i18n %}
3+
4+
{% block submit_buttons_bottom %}
5+
{{ block.super }}
6+
{% if show_change_form_export %}
7+
<div class="submit-row">
8+
<input type="submit" value="{% translate 'Export' %}" class="default" name="_export-item">
9+
</div>
10+
{% endif %}
11+
{% endblock %}

import_export/templates/admin/import_export/change_list_export_item.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
{% load admin_urls %}
33

44
{% if has_export_permission %}
5-
<li><a href="{% url opts|admin_urlname:'export' %}{{cl.get_query_string}}" class="export_link">{% trans "Export" %}</a></li>
5+
<li><a href="{% url opts|admin_urlname:'export' %}{{cl.get_query_string}}" class="export_link">{% translate "Export" %}</a></li>
66
{% endif %}

import_export/templates/admin/import_export/change_list_import_item.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
{% load admin_urls %}
33

44
{% if has_import_permission %}
5-
<li><a href='{% url opts|admin_urlname:"import" %}' class="import_link">{% trans "Import" %}</a></li>
5+
<li><a href='{% url opts|admin_urlname:"import" %}' class="import_link">{% translate "Import" %}</a></li>
66
{% endif %}

import_export/templates/admin/import_export/export.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
{% endblock %}
1010

1111
{% block breadcrumbs_last %}
12-
{% trans "Export" %}
12+
{% translate "Export" %}
1313
{% endblock %}
1414

1515
{% block content %}
16-
<form action="{{ request.path }}{{ export_suffix }}" method="POST">
16+
<form action="{{ export_url }}" method="POST">
1717
{% csrf_token %}
1818
{# export request has originated from an Admin UI action #}
1919
{% if form.initial.export_items %}
@@ -53,7 +53,7 @@
5353
</fieldset>
5454

5555
<div class="submit-row">
56-
<input type="submit" class="default" value="{% trans "Submit" %}">
56+
<input type="submit" class="default" value="{% translate "Submit" %}">
5757
</div>
5858
</form>
5959
{% endblock %}

import_export/templates/admin/import_export/import.html

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
{% endblock %}
1717

1818
{% block breadcrumbs_last %}
19-
{% trans "Import" %}
19+
{% translate "Import" %}
2020
{% endblock %}
2121

2222
{% block content %}
@@ -27,10 +27,10 @@
2727
{% csrf_token %}
2828
{{ confirm_form.as_p }}
2929
<p>
30-
{% trans "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %}
30+
{% translate "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %}
3131
</p>
3232
<div class="submit-row">
33-
<input type="submit" class="default" name="confirm" value="{% trans "Confirm import" %}">
33+
<input type="submit" class="default" name="confirm" value="{% translate "Confirm import" %}">
3434
</div>
3535
</form>
3636
{% endblock %}
@@ -61,7 +61,7 @@
6161

6262
{% block form_submit_button %}
6363
<div class="submit-row">
64-
<input type="submit" class="default" value="{% trans "Submit" %}">
64+
<input type="submit" class="default" value="{% translate "Submit" %}">
6565
</div>
6666
{% endblock %}
6767
</form>
@@ -72,7 +72,7 @@
7272

7373
{% if result.has_errors %}
7474
{% block errors %}
75-
<h2>{% trans "Errors" %}</h2>
75+
<h2>{% translate "Errors" %}</h2>
7676
<ul>
7777
{% for error in result.base_errors %}
7878
<li>
@@ -83,7 +83,7 @@ <h2>{% trans "Errors" %}</h2>
8383
{% for line, errors in result.row_errors %}
8484
{% for error in errors %}
8585
<li>
86-
{% trans "Line number" %}: {{ line }} - {{ error.error }}
86+
{% translate "Line number" %}: {{ line }} - {{ error.error }}
8787
<div><code>{{ error.row.values|join:", " }}</code></div>
8888
<div class="traceback">{{ error.traceback|linebreaks }}</div>
8989
</li>
@@ -95,15 +95,15 @@ <h2>{% trans "Errors" %}</h2>
9595
{% elif result.has_validation_errors %}
9696

9797
{% block validation_errors %}
98-
<h2>{% trans "Some rows failed to validate" %}</h2>
98+
<h2>{% translate "Some rows failed to validate" %}</h2>
9999

100-
<p>{% trans "Please correct these errors in your data where possible, then reupload it using the form above." %}</p>
100+
<p>{% translate "Please correct these errors in your data where possible, then reupload it using the form above." %}</p>
101101

102102
<table class="import-preview">
103103
<thead>
104104
<tr>
105-
<th>{% trans "Row" %}</th>
106-
<th>{% trans "Errors" %}</th>
105+
<th>{% translate "Row" %}</th>
106+
<th>{% translate "Errors" %}</th>
107107
{% for field in result.diff_headers %}
108108
<th>{{ field }}</th>
109109
{% endfor %}
@@ -129,7 +129,7 @@ <h2>{% trans "Some rows failed to validate" %}</h2>
129129
{% endfor %}
130130
{% if row.non_field_specific_errors %}
131131
<li>
132-
<span class="validation-error-field-label">{% trans "Non field specific" %}</span>
132+
<span class="validation-error-field-label">{% translate "Non field specific" %}</span>
133133
<ul>
134134
{% for error in row.non_field_specific_errors %}
135135
<li>{{ error }}</li>
@@ -152,7 +152,7 @@ <h2>{% trans "Some rows failed to validate" %}</h2>
152152
{% else %}
153153

154154
{% block preview %}
155-
<h2>{% trans "Preview" %}</h2>
155+
<h2>{% translate "Preview" %}</h2>
156156

157157
<table class="import-preview">
158158
<thead>
@@ -167,13 +167,13 @@ <h2>{% trans "Preview" %}</h2>
167167
<tr class="{{ row.import_type }}">
168168
<td class="import-type">
169169
{% if row.import_type == 'new' %}
170-
{% trans "New" %}
170+
{% translate "New" %}
171171
{% elif row.import_type == 'skip' %}
172-
{% trans "Skipped" %}
172+
{% translate "Skipped" %}
173173
{% elif row.import_type == 'delete' %}
174-
{% trans "Delete" %}
174+
{% translate "Delete" %}
175175
{% elif row.import_type == 'update' %}
176-
{% trans "Update" %}
176+
{% translate "Update" %}
177177
{% endif %}
178178
</td>
179179
{% for field in row.diff %}

import_export/templates/admin/import_export/resource_fields_list.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
{% block fields_help %}
33
<p>
44
{% if import_or_export == "export" %}
5-
{% trans "This exporter will export the following fields: " %}
5+
{% translate "This exporter will export the following fields: " %}
66
{% elif import_or_export == "import" %}
7-
{% trans "This importer will import the following fields: " %}
7+
{% translate "This importer will import the following fields: " %}
88
{% endif %}
99

1010
{% if fields_list|length <= 1 %}

tests/core/admin.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ class BookAdmin(ImportExportModelAdmin):
3838

3939

4040
class CategoryAdmin(ExportActionModelAdmin):
41-
def export_admin_action(self, request, queryset):
42-
return super().export_admin_action(request, queryset)
41+
pass
4342

4443

4544
class AuthorAdmin(ImportMixin, admin.ModelAdmin):

tests/core/tests/admin_integration/test_action.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
from unittest import mock
33
from unittest.mock import MagicMock
44

5+
from core.admin import CategoryAdmin
56
from core.models import Book, Category
67
from core.tests.admin_integration.mixins import AdminTestMixin
78
from core.tests.utils import ignore_widget_deprecation_warning
9+
from django.contrib import admin
810
from django.core.exceptions import PermissionDenied
911
from django.http import HttpRequest
12+
from django.test import RequestFactory
1013
from django.test.testcases import TestCase
1114
from django.test.utils import override_settings
15+
from django.urls import reverse
1216

13-
from import_export.admin import ExportMixin, ImportExportActionModelAdmin
14-
from import_export.tmp_storages import TempFolderStorage
17+
from import_export.admin import ExportMixin
1518

1619

1720
class ExportActionAdminIntegrationTest(AdminTestMixin, TestCase):
@@ -120,13 +123,51 @@ def has_export_permission(self, request):
120123
m.get_export_data("0", Book.objects.none(), request=request)
121124

122125

123-
class TestImportExportActionModelAdmin(ImportExportActionModelAdmin):
124-
def __init__(self, mock_model, mock_site, error_instance):
125-
self.error_instance = error_instance
126-
super().__init__(mock_model, mock_site)
126+
class TestExportButtonOnChangeForm(AdminTestMixin, TestCase):
127+
def setUp(self):
128+
super().setUp()
129+
self.cat1 = Category.objects.create(name="Cat 1")
130+
self.change_url = reverse(
131+
"%s:%s_%s_change"
132+
% (
133+
"admin",
134+
"core",
135+
"category",
136+
),
137+
args=[self.cat1.id],
138+
)
139+
self.target_str = (
140+
'<input type="submit" value="Export" '
141+
'class="default" name="_export-item">'
142+
)
143+
144+
def test_export_button_on_change_form(self):
145+
response = self.client.get(self.change_url)
146+
self.assertIn(
147+
self.target_str,
148+
str(response.content),
149+
)
150+
response = self.client.post(
151+
self.change_url, data={"_export-item": "Export", "name": self.cat1.name}
152+
)
153+
self.assertIn("Export 1 selected item", str(response.content))
154+
155+
def test_export_button_on_change_form_disabled(self):
156+
class MockCategoryAdmin(CategoryAdmin):
157+
show_change_form_export = True
158+
159+
factory = RequestFactory()
160+
category_admin = MockCategoryAdmin(Category, admin.site)
161+
162+
request = factory.get(self.change_url)
163+
request.user = self.user
164+
165+
response = category_admin.change_view(request, str(self.cat1.id))
166+
response.render()
127167

128-
def write_to_tmp_storage(self, import_file, input_format):
129-
mock_storage = MagicMock(spec=TempFolderStorage)
168+
self.assertIn(self.target_str, str(response.content))
130169

131-
mock_storage.read.side_effect = self.error_instance
132-
return mock_storage
170+
category_admin.show_change_form_export = False
171+
response = category_admin.change_view(request, str(self.cat1.id))
172+
response.render()
173+
self.assertNotIn(self.target_str, str(response.content))

0 commit comments

Comments
 (0)