Ticket #10505: admin-actions.5.diff

File admin-actions.5.diff, 24.4 KB (added by jezdez, 6 years ago)

Isolated the logic to look for callable actions inside the get_action method

  • django/contrib/admin/helpers.py

    diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
    index aaa2e30..aac8911 100644
    a b from django.utils.safestring import mark_safe 
    66from django.utils.encoding import force_unicode
    77from django.contrib.admin.util import flatten_fieldsets
    88from django.contrib.contenttypes.models import ContentType
     9from django.utils.translation import ugettext_lazy as _
     10
     11ACTION_CHECKBOX_NAME = 'selected'
     12
     13class ActionForm(forms.Form):
     14    action = forms.ChoiceField(label=_('Action:'))
     15
     16checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
    917
    1018class AdminForm(object):
    1119    def __init__(self, form, fieldsets, prepopulated_fields):
  • django/contrib/admin/media/css/changelists.css

    diff --git a/django/contrib/admin/media/css/changelists.css b/django/contrib/admin/media/css/changelists.css
    index 40142f5..6294a28 100644
    a b  
    5050
    5151#changelist table thead th {
    5252    white-space: nowrap;
     53    vertical-align: middle;
    5354}
    5455
    5556#changelist table tbody td {
     
    209210    border-color: #036;
    210211}
    211212
     213.filtered #action-form .actions {
     214    margin-right: 160px !important;
     215    border-right: 1px solid #ddd;
     216}
     217
     218#action-form .actions {
     219    color: #666;
     220    padding: 3px;
     221    font-weight: bold;
     222    background: #efefef url(../img/admin/nav-bg.gif);
     223}
     224
     225#action-form .actions:last-child {
     226    border-bottom: none;
     227}
     228
     229#action-form .actions select {
     230    border: 1px solid #aaa;
     231    margin: 0 0.5em;
     232}
     233
     234#action-toggle {
     235    display: none;
     236}
     237
     238#action-form .actions label {
     239    font-size: 11px;
     240    margin-left: 0.5em;
     241}
     242
     243#action-form tbody tr input.action-select {
     244    margin: 0;
     245}
     246
     247#action-form thead th:first-child {
     248    width: 1.5em;
     249    text-align: center;
     250}
     251
     252#action-form tbody td:first-child {
     253    border-left: 0;
     254    border-right: 1px solid #ddd;
     255    text-align: center;
     256}
  • new file django/contrib/admin/media/js/actions.js

    diff --git a/django/contrib/admin/media/js/actions.js b/django/contrib/admin/media/js/actions.js
    new file mode 100644
    index 0000000..febb0c1
    - +  
     1var Actions = {
     2    init: function() {
     3        selectAll = document.getElementById('action-toggle');
     4        if (selectAll) {
     5            selectAll.style.display = 'inline';
     6            addEvent(selectAll, 'change', function() {
     7                Actions.checker(this.checked);
     8            });
     9        }
     10    },
     11    checker: function(checked) {
     12        actionCheckboxes = document.getElementsBySelector('tr input.action-select');
     13        for(var i = 0; i < actionCheckboxes.length; i++) {
     14            actionCheckboxes[i].checked = checked;
     15        }
     16    }
     17}
     18
     19addEvent(window, 'load', Actions.init);
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index 859229e..834ac6e 100644
    a b from django.forms.models import BaseInlineFormSet 
    55from django.contrib.contenttypes.models import ContentType
    66from django.contrib.admin import widgets
    77from django.contrib.admin import helpers
    8 from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects
     8from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict
    99from django.core.exceptions import PermissionDenied
    1010from django.db import models, transaction
     11from django.db.models.fields import BLANK_CHOICE_DASH
    1112from django.http import Http404, HttpResponse, HttpResponseRedirect
    1213from django.shortcuts import get_object_or_404, render_to_response
    1314from django.utils.functional import update_wrapper
    from django.utils.html import escape 
    1516from django.utils.safestring import mark_safe
    1617from django.utils.functional import curry
    1718from django.utils.text import capfirst, get_text_list
    18 from django.utils.translation import ugettext as _
     19from django.utils.translation import ugettext as _, ugettext_lazy
    1920from django.utils.encoding import force_unicode
    2021try:
    2122    set
    class ModelAdmin(BaseModelAdmin): 
    172173    "Encapsulates all admin options and functionality for a given model."
    173174    __metaclass__ = forms.MediaDefiningClass
    174175   
    175     list_display = ('__str__',)
     176    list_display = ('action_checkbox', '__str__',)
    176177    list_display_links = ()
    177178    list_filter = ()
    178179    list_select_related = False
    class ModelAdmin(BaseModelAdmin): 
    190191    delete_confirmation_template = None
    191192    object_history_template = None
    192193   
     194    # Actions
     195    actions = ['delete_selected']
     196    action_form = helpers.ActionForm
     197    actions_on_top = False
     198    actions_on_bottom = True
     199   
    193200    def __init__(self, model, admin_site):
    194201        self.model = model
    195202        self.opts = model._meta
    class ModelAdmin(BaseModelAdmin): 
    198205        for inline_class in self.inlines:
    199206            inline_instance = inline_class(self.model, self.admin_site)
    200207            self.inline_instances.append(inline_instance)
     208        if 'action_checkbox' not in self.list_display:
     209            self.list_display = list(self.list_display)
     210            self.list_display.insert(0, 'action_checkbox')
     211        if not self.list_display_links:
     212            for name in self.list_display:
     213                if name != 'action_checkbox':
     214                    self.list_display_links = [name]
     215                    break
    201216        super(ModelAdmin, self).__init__()
    202217       
    203218    def get_urls(self):
    class ModelAdmin(BaseModelAdmin): 
    237252        from django.conf import settings
    238253       
    239254        js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
     255        if self.actions:
     256            js.extend(['js/getElementsBySelector.js', 'js/actions.js'])
    240257        if self.prepopulated_fields:
    241258            js.append('js/urlify.js')
    242259        if self.opts.get_ordered_objects():
    class ModelAdmin(BaseModelAdmin): 
    365382            action_flag     = DELETION
    366383        )
    367384   
     385    def action_checkbox(self, obj):
     386        """
     387        A list_display column containing a checkbox widget.
     388        """
     389        return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_unicode(obj.pk))
     390    action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />')
     391    action_checkbox.allow_tags = True
     392   
     393    def get_action_choices(self, default_choices=BLANK_CHOICE_DASH):
     394        choices = [] + default_choices
     395        for action in getattr(self, 'actions', []):
     396            func, name, description, instance_action = self.get_action(action)
     397            choice = (name, description % model_format_dict(self.opts))
     398            choices.append(choice)
     399        return choices
     400
     401    def get_action(self, action):
     402        is_instance_action = False
     403        if callable(action):
     404            func = action
     405            action = action.__name__
     406        elif hasattr(self, action):
     407            func = getattr(self, action)
     408        elif hasattr(self.model, action):
     409            func = getattr(self.model, action)
     410            is_instance_action = True
     411        else:
     412            callable_actions = {}
     413            for item in getattr(self, 'actions', []):
     414                if callable(item):
     415                    callable_actions[item.__name__] = item
     416            if action in callable_actions:
     417                return self.get_action(callable_actions[action])
     418            raise AttributeError, \
     419                "'%s' model or '%s' have no action '%s'" % \
     420                    (self.opts.object_name, self.__class__.__name__, action)
     421        if hasattr(func, 'short_description'):
     422            description = func.short_description
     423        else:
     424            description = capfirst(action.replace('_', ' '))
     425        return func, action, description, is_instance_action
     426   
     427    def delete_selected(self, request, selected):
     428        """
     429        Default action which deletes the selected objects.
     430        """
     431        if self.has_delete_permission(request):
     432            n = selected.count()
     433            if n:
     434                for obj in selected:
     435                    obj_display = force_unicode(obj)
     436                    self.log_deletion(request, obj, obj_display)
     437                selected.delete()
     438                self.message_user(request, _("Successfully deleted %d %s.") % (
     439                    n, model_ngettext(self.opts, n)
     440                ))
     441    delete_selected.short_description = ugettext_lazy("Delete selected %(verbose_name_plural)s")
    368442   
    369443    def construct_change_message(self, request, form, formsets):
    370444        """
    class ModelAdmin(BaseModelAdmin): 
    503577        else:
    504578            self.message_user(request, msg)
    505579            return HttpResponseRedirect("../")
     580
     581    def response_action(self, request, changelist):
     582        if request.method == 'POST':
     583            # There can be multiple action forms on the page (at the top
     584            # and bottom of the change list, for example). Get the action
     585            # whose button was pushed.
     586            try:
     587                action_index = int(request.POST.get('index', 0))
     588            except ValueError:
     589                action_index = 0
     590            data = {}
     591            for key in request.POST:
     592                if key not in (helpers.ACTION_CHECKBOX_NAME, 'index'):
     593                    data[key] = request.POST.getlist(key)[action_index]
     594            action_form = self.action_form(data, auto_id=None)
     595            action_form.fields['action'].choices = self.get_action_choices()
     596           
     597            if action_form.is_valid():
     598                action = action_form.cleaned_data['action']
     599                func, name, description, instance_action = self.get_action(action)
     600                selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
     601                results = changelist.get_query_set().filter(pk__in=selected)
     602                response = None
     603                if callable(func):
     604                    if instance_action:
     605                        for obj in results:
     606                            getattr(obj, name)(request)
     607                    else:
     608                        response = func(request, results)
     609                if isinstance(response, HttpResponse):
     610                    return response
     611                else:
     612                    redirect_to = request.META.get('HTTP_REFERER') or "."
     613                    return HttpResponseRedirect(redirect_to)
     614        else:
     615            action_form = self.action_form(auto_id=None)
     616            action_form.fields['action'].choices = self.get_action_choices()
     617        return action_form
    506618   
    507619    def add_view(self, request, form_url='', extra_context=None):
    508620        "The 'add' admin view for this model."
    class ModelAdmin(BaseModelAdmin): 
    696808                return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
    697809            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
    698810       
     811        action_response = self.response_action(request, cl)
     812        if isinstance(action_response, HttpResponse):
     813            return action_response
     814       
    699815        context = {
    700816            'title': cl.title,
    701817            'is_popup': cl.is_popup,
    702818            'cl': cl,
     819            'media': mark_safe(self.media),
    703820            'has_add_permission': self.has_add_permission(request),
    704821            'root_path': self.admin_site.root_path,
    705822            'app_label': app_label,
     823            'action_form': action_response,
     824            'actions_on_top': self.actions_on_top,
     825            'actions_on_bottom': self.actions_on_bottom,
    706826        }
    707827        context.update(extra_context or {})
    708828        return render_to_response(self.change_list_template or [
  • new file django/contrib/admin/templates/admin/actions.html

    diff --git a/django/contrib/admin/templates/admin/actions.html b/django/contrib/admin/templates/admin/actions.html
    new file mode 100644
    index 0000000..bf4b975
    - +  
     1{% load i18n %}
     2<div class="actions">
     3    {% for field in action_form %}<label>{{ field.label }} {{ field }}</label>{% endfor %}
     4    <button type="submit" class="button" title="{% trans "Run the selected action" %}" name="index" value="{{ action_index|default:0 }}">{% trans "Go" %}</button>
     5</div>
  • django/contrib/admin/templates/admin/change_list.html

    diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
    index 5f8a430..1f00318 100644
    a b  
    33
    44{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/changelists.css" />{% endblock %}
    55
     6{% block extrahead %}{{ block.super }}{{ media }}{% endblock %}
     7
    68{% block bodyclass %}change-list{% endblock %}
    79
    810{% if not is_popup %}{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans "Home" %}</a> &rsaquo; <a href="../">{{ app_label|capfirst }}</a> &rsaquo; {{ cl.opts.verbose_name_plural|capfirst }}</div>{% endblock %}{% endif %}
     
    3133{% endif %}
    3234{% endblock %}
    3335
    34 {% block result_list %}{% result_list cl %}{% endblock %}
     36{% block result_list %}
     37<form id="action-form" method="post" action="">
     38    {% if actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %}
     39    {% result_list cl %}
     40    {% if actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %}
     41</form>
     42{% endblock %}
    3543{% block pagination %}{% pagination cl %}{% endblock %}
    3644</div>
    3745</div>
  • django/contrib/admin/templatetags/admin_list.py

    diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
    index 37cdb91..59fc4ff 100644
    a b search_form = register.inclusion_tag('admin/search_form.html')(search_form) 
    311311def admin_list_filter(cl, spec):
    312312    return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
    313313admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter)
     314
     315def admin_actions(context):
     316    """
     317    Track the number of times the action field has been rendered on the page,
     318    so we know which value to use.
     319    """
     320    context['action_index'] = context.get('action_index', -1) + 1
     321    return context
     322admin_actions = register.inclusion_tag("admin/actions.html", takes_context=True)(admin_actions)
  • django/contrib/admin/util.py

    diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
    index 4164c8a..7c2b56b 100644
    a b from django.utils.html import escape 
    44from django.utils.safestring import mark_safe
    55from django.utils.text import capfirst
    66from django.utils.encoding import force_unicode
    7 from django.utils.translation import ugettext as _
     7from django.utils.translation import ungettext, ugettext as _
    88
    99def quote(s):
    1010    """
    def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ 
    155155            p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
    156156            if not user.has_perm(p):
    157157                perms_needed.add(related.opts.verbose_name)
     158
     159def model_format_dict(obj):
     160    """
     161    Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
     162    typically for use with string formatting.
     163
     164    `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
     165
     166    """
     167    if isinstance(obj, (models.Model, models.base.ModelBase)):
     168        opts = obj._meta
     169    elif isinstance(obj, models.query.QuerySet):
     170        opts = obj.model._meta
     171    else:
     172        opts = obj
     173    return {
     174        'verbose_name': force_unicode(opts.verbose_name),
     175        'verbose_name_plural': force_unicode(opts.verbose_name_plural)
     176    }
     177
     178def model_ngettext(obj, n=None):
     179    """
     180    Return the appropriate `verbose_name` or `verbose_name_plural` for `obj`
     181    depending on the count `n`.
     182
     183    `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
     184    If `obj` is a `QuerySet` instance, `n` is optional and the length of the
     185    `QuerySet` is used.
     186
     187    """
     188    if isinstance(obj, models.query.QuerySet):
     189        if n is None:
     190            n = obj.count()
     191        obj = obj.model
     192    d = model_format_dict(obj)
     193    return ungettext(d['verbose_name'], d['verbose_name_plural'], n or 0)
  • tests/regressiontests/admin_registration/models.py

    diff --git a/tests/regressiontests/admin_registration/models.py b/tests/regressiontests/admin_registration/models.py
    index fdfa369..35cf8af 100644
    a b AlreadyRegistered: The model Person is already registered 
    4949>>> site._registry[Person].search_fields
    5050['name']
    5151>>> site._registry[Person].list_display
    52 ['__str__']
     52['action_checkbox', '__str__']
    5353>>> site._registry[Person].save_on_top
    5454True
    5555
  • new file tests/regressiontests/admin_views/fixtures/admin-views-actions.xml

    diff --git a/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml b/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml
    new file mode 100644
    index 0000000..1f6cc7f
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<django-objects version="1.0">
     3    <object pk="1" model="admin_views.subscriber">
     4        <field type="CharField" name="name">John Doe</field>
     5        <field type="CharField" name="email">john@example.org</field>
     6    </object>
     7    <object pk="2" model="admin_views.subscriber">
     8        <field type="CharField" name="name">Max Mustermann</field>
     9        <field type="CharField" name="email">max@example.org</field>
     10    </object>
     11    <object pk="1" model="admin_views.directsubscriber">
     12        <field type="CharField" name="name">John Doe</field>
     13        <field type="CharField" name="email">john@example.org</field>
     14        <field type="BooleanField" name="paid">True</field>
     15    </object>
     16    <object pk="1" model="admin_views.externalsubscriber">
     17        <field type="CharField" name="name">John Doe</field>
     18        <field type="CharField" name="email">john@example.org</field>
     19    </object>
     20</django-objects>
     21 No newline at end of file
  • tests/regressiontests/admin_views/models.py

    diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
    index d849a7b..fa4faa3 100644
    a b  
    11# -*- coding: utf-8 -*-
    22from django.db import models
    33from django.contrib import admin
     4from django.core.mail import EmailMessage
    45
    56class Section(models.Model):
    67    """
    class PersonaAdmin(admin.ModelAdmin): 
    177178        BarAccountAdmin
    178179    )
    179180
     181class Subscriber(models.Model):
     182    name = models.CharField(blank=False, max_length=80)
     183    email = models.EmailField(blank=False, max_length=175)
     184
     185    def __unicode__(self):
     186        return "%s (%s)" % (self.name, self.email)
     187
     188class SubscriberAdmin(admin.ModelAdmin):
     189    actions = ['delete_selected', 'mail_admin']
     190
     191    def mail_admin(self, request, selected):
     192        EmailMessage(
     193            'Greetings from a ModelAdmin action',
     194            'This is the test email from a admin action',
     195            'from@example.com',
     196            ['to@example.com']
     197        ).send()
     198
     199class DirectSubscriber(Subscriber):
     200    paid = models.BooleanField(default=False)
     201
     202    def direct_mail(self, request):
     203        EmailMessage(
     204            'Greetings from a model action',
     205            'This is the test email from a model action',
     206            'from@example.com',
     207            [self.email]
     208        ).send()
     209
     210class DirectSubscriberAdmin(admin.ModelAdmin):
     211    actions = ['direct_mail']
     212
     213class ExternalSubscriber(Subscriber):
     214    pass
     215
     216def external_mail(request, selected):
     217    EmailMessage(
     218        'Greetings from a function action',
     219        'This is the test email from a function action',
     220        'from@example.com',
     221        ['to@example.com']
     222    ).send()
     223
     224def redirect_to(request, selected):
     225    from django.http import HttpResponseRedirect
     226    return HttpResponseRedirect('/some-where-else/')
     227
     228class ExternalSubscriberAdmin(admin.ModelAdmin):
     229    actions = [external_mail, redirect_to]
     230
    180231admin.site.register(Article, ArticleAdmin)
    181232admin.site.register(CustomArticle, CustomArticleAdmin)
    182233admin.site.register(Section, inlines=[ArticleInline])
    admin.site.register(ModelWithStringPrimaryKey) 
    184235admin.site.register(Color)
    185236admin.site.register(Thing, ThingAdmin)
    186237admin.site.register(Persona, PersonaAdmin)
     238admin.site.register(Subscriber, SubscriberAdmin)
     239admin.site.register(DirectSubscriber, DirectSubscriberAdmin)
     240admin.site.register(ExternalSubscriber, ExternalSubscriberAdmin)
    187241
    188242# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
    189243# That way we cover all four cases:
  • tests/regressiontests/admin_views/tests.py

    diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
    index bf198bc..612e1ec 100644
    a b from django.contrib.contenttypes.models import ContentType 
    88from django.contrib.admin.models import LogEntry
    99from django.contrib.admin.sites import LOGIN_FORM_KEY
    1010from django.contrib.admin.util import quote
     11from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
    1112from django.utils.html import escape
    1213
    1314# local test models
    14 from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Persona, FooAccount, BarAccount
     15from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Persona, FooAccount, BarAccount, Subscriber, DirectSubscriber, ExternalSubscriber
    1516
    1617try:
    1718    set
    class AdminInheritedInlinesTest(TestCase): 
    805806        self.failUnlessEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user)
    806807        self.failUnlessEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user)
    807808        self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2)
     809
     810from django.core import mail
     811
     812class AdminActionsTest(TestCase):
     813    fixtures = ['admin-views-users.xml', 'admin-views-actions.xml']
     814
     815    def setUp(self):
     816        self.client.login(username='super', password='secret')
     817
     818    def tearDown(self):
     819        self.client.logout()
     820
     821    def test_model_admin_custom_action(self):
     822        "Tests a custom action defined in a ModelAdmin method"
     823        action_data = {
     824            ACTION_CHECKBOX_NAME: [1],
     825            'action' : 'mail_admin',
     826            'index': 0,
     827        }
     828        response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
     829        self.assertEquals(len(mail.outbox), 1)
     830        self.assertEquals(mail.outbox[0].subject, 'Greetings from a ModelAdmin action')
     831
     832    def test_model_admin_default_delete_action(self):
     833        "Tests the default delete action defined as a ModelAdmin method"
     834        action_data = {
     835            ACTION_CHECKBOX_NAME: [1, 2],
     836            'action' : 'delete_selected',
     837            'index': 0,
     838        }
     839        response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
     840        self.failUnlessEqual(Subscriber.objects.count(), 0)
     841
     842    def test_custom_model_instance_action(self):
     843        "Tests a custom action defined in a model method"
     844        action_data = {
     845            ACTION_CHECKBOX_NAME: [1],
     846            'action' : 'direct_mail',
     847            'index': 0,
     848            'paid': 1,
     849        }
     850        response = self.client.post('/test_admin/admin/admin_views/directsubscriber/', action_data)
     851        self.assertEquals(len(mail.outbox), 1)
     852        self.assertEquals(mail.outbox[0].subject, 'Greetings from a model action')
     853        self.assertEquals(mail.outbox[0].to, [u'john@example.org'])
     854
     855    def test_custom_function_mail_action(self):
     856        "Tests a custom action defined in a function"
     857        action_data = {
     858            ACTION_CHECKBOX_NAME: [1],
     859            'action' : 'external_mail',
     860            'index': 0,
     861        }
     862        response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
     863        self.assertEquals(len(mail.outbox), 1)
     864        self.assertEquals(mail.outbox[0].subject, 'Greetings from a function action')
     865
     866    def test_custom_function_action_with_redirect(self):
     867        "Tests a custom action defined in a function"
     868        action_data = {
     869            ACTION_CHECKBOX_NAME: [1],
     870            'action' : 'redirect_to',
     871            'index': 0,
     872        }
     873        response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
     874        self.failUnlessEqual(response.status_code, 302)
Back to Top