diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index b35f100..812a24f 100644
a
|
b
|
from django.db.models.related import RelatedObject
|
22 | 22 | from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist |
23 | 23 | from django.db.models.sql.constants import QUERY_TERMS |
24 | 24 | from django.http import Http404, HttpResponse, HttpResponseRedirect |
| 25 | from django.http.response import HttpResponseBase |
25 | 26 | from django.shortcuts import get_object_or_404 |
26 | 27 | from django.template.response import SimpleTemplateResponse, TemplateResponse |
27 | 28 | from django.utils.decorators import method_decorator |
… |
… |
class ModelAdmin(BaseModelAdmin):
|
973 | 974 | |
974 | 975 | response = func(self, request, queryset) |
975 | 976 | |
976 | | # Actions may return an HttpResponse, which will be used as the |
977 | | # response from the POST. If not, we'll be a good little HTTP |
978 | | # citizen and redirect back to the changelist page. |
979 | | if isinstance(response, HttpResponse): |
| 977 | # Actions may return an HttpResponse-like object, which will be |
| 978 | # used as the response from the POST. If not, we'll be a good |
| 979 | # little HTTP citizen and redirect back to the changelist page. |
| 980 | if isinstance(response, HttpResponseBase): |
980 | 981 | return response |
981 | 982 | else: |
982 | 983 | return HttpResponseRedirect(request.get_full_path()) |
diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt
index 3e15f5b..3f6c453 100644
a
|
b
|
Minor features
|
202 | 202 | * Added ``BCryptSHA256PasswordHasher`` to resolve the password truncation issue |
203 | 203 | with bcrypt. |
204 | 204 | |
| 205 | * :doc:`Admin action </ref/contrib/admin/actions>` can now return any response |
| 206 | that is a subclass of ``HttpResponseBase``, including |
| 207 | :class:`~django.http.StreamingHttpResponse`. |
| 208 | |
205 | 209 | Backwards incompatible changes in 1.6 |
206 | 210 | ===================================== |
207 | 211 | |
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index cc7585c..e1f2ef2 100644
a
|
b
|
from django.core.mail import EmailMessage
|
12 | 12 | from django.conf.urls import patterns, url |
13 | 13 | from django.db import models |
14 | 14 | from django.forms.models import BaseModelFormSet |
15 | | from django.http import HttpResponse |
| 15 | from django.http import HttpResponse, StreamingHttpResponse |
16 | 16 | from django.contrib.admin import BooleanFieldListFilter |
17 | 17 | |
18 | 18 | from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, |
… |
… |
def redirect_to(modeladmin, request, selected):
|
238 | 238 | redirect_to.short_description = 'Redirect to (Awesome action)' |
239 | 239 | |
240 | 240 | |
| 241 | def download(modeladmin, request, selected): |
| 242 | import StringIO |
| 243 | from django.core.servers.basehttp import FileWrapper |
| 244 | buf = StringIO.StringIO('This is the content of the file') |
| 245 | return StreamingHttpResponse(FileWrapper(buf)) |
| 246 | download.short_description = 'Download subscription' |
| 247 | |
| 248 | |
| 249 | def no_perm(modeladmin, request, selected): |
| 250 | return HttpResponse(content='No permission to perform this action', |
| 251 | status=403) |
| 252 | no_perm.short_description = 'No permission to run' |
| 253 | |
| 254 | |
241 | 255 | class ExternalSubscriberAdmin(admin.ModelAdmin): |
242 | | actions = [redirect_to, external_mail] |
| 256 | actions = [redirect_to, external_mail, download, no_perm] |
243 | 257 | |
244 | 258 | |
245 | 259 | class Podcast(Media): |
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index e0000ff..1c78b60 100644
a
|
b
|
class AdminActionsTest(TestCase):
|
2413 | 2413 | response = self.client.post(url, action_data) |
2414 | 2414 | self.assertRedirects(response, url) |
2415 | 2415 | |
| 2416 | def test_custom_function_action_streaming_response(self): |
| 2417 | """Tests a custom action that returns a StreamingHttpResponse.""" |
| 2418 | action_data = { |
| 2419 | ACTION_CHECKBOX_NAME: [1], |
| 2420 | 'action': 'download', |
| 2421 | 'index': 0, |
| 2422 | } |
| 2423 | response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) |
| 2424 | content = ''.join([b for b in response.streaming_content]) |
| 2425 | self.assertEqual(content, 'This is the content of the file') |
| 2426 | self.assertEqual(response.status_code, 200) |
| 2427 | |
| 2428 | def test_custom_function_action_no_perm_response(self): |
| 2429 | """Tests a custom action that returns an HttpResponse with 403 code.""" |
| 2430 | action_data = { |
| 2431 | ACTION_CHECKBOX_NAME: [1], |
| 2432 | 'action': 'no_perm', |
| 2433 | 'index': 0, |
| 2434 | } |
| 2435 | response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) |
| 2436 | self.assertEqual(response.status_code, 403) |
| 2437 | self.assertEqual(response.content, 'No permission to perform this action') |
| 2438 | |
2416 | 2439 | def test_actions_ordering(self): |
2417 | 2440 | """ |
2418 | 2441 | Ensure that actions are ordered as expected. |
… |
… |
class AdminActionsTest(TestCase):
|
2421 | 2444 | response = self.client.get('/test_admin/admin/admin_views/externalsubscriber/') |
2422 | 2445 | self.assertContains(response, '''<label>Action: <select name="action"> |
2423 | 2446 | <option value="" selected="selected">---------</option> |
2424 | | <option value="delete_selected">Delete selected external subscribers</option> |
| 2447 | <option value="delete_selected">Delete selected external |
| 2448 | subscribers</option> |
2425 | 2449 | <option value="redirect_to">Redirect to (Awesome action)</option> |
2426 | | <option value="external_mail">External mail (Another awesome action)</option> |
| 2450 | <option value="external_mail">External mail (Another awesome |
| 2451 | action)</option> |
| 2452 | <option value="download">Download subscription</option> |
| 2453 | <option value="no_perm">No permission to run</option> |
2427 | 2454 | </select>''', html=True) |
2428 | 2455 | |
2429 | 2456 | def test_model_without_action(self): |