diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 29abf7a723..6202646988 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -24,6 +24,7 @@ from django.db.models.related import RelatedObject from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist from django.db.models.sql.constants import QUERY_TERMS from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.http.response import HttpResponseBase from django.shortcuts import get_object_or_404 from django.template.response import SimpleTemplateResponse, TemplateResponse from django.utils.decorators import method_decorator @@ -1026,10 +1027,10 @@ class ModelAdmin(BaseModelAdmin): response = func(self, request, queryset) - # Actions may return an HttpResponse, which will be used as the - # response from the POST. If not, we'll be a good little HTTP - # citizen and redirect back to the changelist page. - if isinstance(response, HttpResponse): + # Actions may return an HttpResponse-like object, which will be + # used as the response from the POST. If not, we'll be a good + # little HTTP citizen and redirect back to the changelist page. + if isinstance(response, HttpResponseBase): return response else: return HttpResponseRedirect(request.get_full_path()) diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index 4ab7844ef7..a6ad7cc0bc 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -9,11 +9,13 @@ from django.contrib import admin from django.contrib.admin.views.main import ChangeList from django.core.files.storage import FileSystemStorage from django.core.mail import EmailMessage +from django.core.servers.basehttp import FileWrapper from django.conf.urls import patterns, url from django.db import models from django.forms.models import BaseModelFormSet -from django.http import HttpResponse +from django.http import HttpResponse, StreamingHttpResponse from django.contrib.admin import BooleanFieldListFilter +from django.utils.six import StringIO from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, Widget, DooHickey, Grommet, Whatsit, FancyDoodad, Category, Link, @@ -238,8 +240,20 @@ def redirect_to(modeladmin, request, selected): redirect_to.short_description = 'Redirect to (Awesome action)' +def download(modeladmin, request, selected): + buf = StringIO('This is the content of the file') + return StreamingHttpResponse(FileWrapper(buf)) +download.short_description = 'Download subscription' + + +def no_perm(modeladmin, request, selected): + return HttpResponse(content='No permission to perform this action', + status=403) +no_perm.short_description = 'No permission to run' + + class ExternalSubscriberAdmin(admin.ModelAdmin): - actions = [redirect_to, external_mail] + actions = [redirect_to, external_mail, download, no_perm] class Podcast(Media): diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 106b0a706a..8c8a65318c 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -2432,6 +2432,29 @@ class AdminActionsTest(TestCase): response = self.client.post(url, action_data) self.assertRedirects(response, url) + def test_custom_function_action_streaming_response(self): + """Tests a custom action that returns a StreamingHttpResponse.""" + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action': 'download', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) + content = b''.join(response.streaming_content) + self.assertEqual(content, b'This is the content of the file') + self.assertEqual(response.status_code, 200) + + def test_custom_function_action_no_perm_response(self): + """Tests a custom action that returns an HttpResponse with 403 code.""" + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action': 'no_perm', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.content, b'No permission to perform this action') + def test_actions_ordering(self): """ Ensure that actions are ordered as expected. @@ -2440,9 +2463,13 @@ class AdminActionsTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/externalsubscriber/') self.assertContains(response, '''