Ticket #10505: admin-actions.6.diff

File admin-actions.6.diff, 29.9 KB (added by Jannis Leidel, 15 years ago)

Updated to work with merged list_editable code. Now only passes the queryset to response_action method and fixes some stylesheet issues.

  • 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..649cff7 100644
    a b  
    5050
    5151#changelist table thead th {
    5252    white-space: nowrap;
     53    vertical-align: middle;
     54}
     55
     56#changelist table thead th:first-child {
     57    width: 1.5em;
     58    text-align: center;
    5359}
    5460
    5561#changelist table tbody td {
    5662    border-left: 1px solid #ddd;
    5763}
    5864
     65#changelist table tbody td:first-child {
     66    border-left: 0;
     67    border-right: 1px solid #ddd;
     68    text-align: center;
     69}
     70
    5971#changelist table tfoot {
    6072    color: #666;
    6173}
     
    209221    border-color: #036;
    210222}
    211223
     224/* ACTIONS */
     225
     226.filtered .actions {
     227    margin-right: 160px !important;
     228    border-right: 1px solid #ddd;
     229}
     230
     231#changelist .actions {
     232    color: #666;
     233    padding: 3px;
     234    border-bottom: 1px solid #ddd;
     235    background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
     236}
     237
     238#changelist .actions:last-child {
     239    border-bottom: none;
     240}
     241
     242#changelist .actions select {
     243    border: 1px solid #aaa;
     244    margin: 0 0.5em;
     245    padding: 1px 2px;
     246}
     247
     248#changelist .actions label {
     249    font-size: 11px;
     250    margin: 0 0.5em;
     251}
     252
     253#changelist #action-toggle {
     254    display: none;
     255}
  • 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 c79523c..ef2c52d 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.safestring import mark_safe  
    1617from django.utils.functional import curry
    1718from django.utils.text import capfirst, get_text_list
    1819from django.utils.translation import ugettext as _
    19 from django.utils.translation import ngettext
     20from django.utils.translation import ngettext, ugettext_lazy
    2021from django.utils.encoding import force_unicode
    2122try:
    2223    set
    class ModelAdmin(BaseModelAdmin):  
    173174    "Encapsulates all admin options and functionality for a given model."
    174175    __metaclass__ = forms.MediaDefiningClass
    175176   
    176     list_display = ('__str__',)
     177    list_display = ('action_checkbox', '__str__',)
    177178    list_display_links = ()
    178179    list_filter = ()
    179180    list_select_related = False
    class ModelAdmin(BaseModelAdmin):  
    192193    delete_confirmation_template = None
    193194    object_history_template = None
    194195   
     196    # Actions
     197    actions = ['delete_selected']
     198    action_form = helpers.ActionForm
     199    actions_on_top = False
     200    actions_on_bottom = True
     201   
    195202    def __init__(self, model, admin_site):
    196203        self.model = model
    197204        self.opts = model._meta
    class ModelAdmin(BaseModelAdmin):  
    200207        for inline_class in self.inlines:
    201208            inline_instance = inline_class(self.model, self.admin_site)
    202209            self.inline_instances.append(inline_instance)
     210        if 'action_checkbox' not in self.list_display:
     211            self.list_display = list(self.list_display)
     212            self.list_display.insert(0, 'action_checkbox')
     213        if not self.list_display_links:
     214            for name in self.list_display:
     215                if name != 'action_checkbox':
     216                    self.list_display_links = [name]
     217                    break
    203218        super(ModelAdmin, self).__init__()
    204219       
    205220    def get_urls(self):
    class ModelAdmin(BaseModelAdmin):  
    239254        from django.conf import settings
    240255       
    241256        js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
     257        if self.actions:
     258            js.extend(['js/getElementsBySelector.js', 'js/actions.js'])
    242259        if self.prepopulated_fields:
    243260            js.append('js/urlify.js')
    244261        if self.opts.get_ordered_objects():
    class ModelAdmin(BaseModelAdmin):  
    390407            action_flag     = DELETION
    391408        )
    392409   
     410    def action_checkbox(self, obj):
     411        """
     412        A list_display column containing a checkbox widget.
     413        """
     414        return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_unicode(obj.pk))
     415    action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />')
     416    action_checkbox.allow_tags = True
     417   
     418    def get_action_choices(self, default_choices=BLANK_CHOICE_DASH):
     419        choices = [] + default_choices
     420        for action in getattr(self, 'actions', []):
     421            func, name, description, instance_action = self.get_action(action)
     422            choice = (name, description % model_format_dict(self.opts))
     423            choices.append(choice)
     424        return choices
     425
     426    def get_action(self, action):
     427        is_instance_action = False
     428        if callable(action):
     429            func = action
     430            action = action.__name__
     431        elif hasattr(self, action):
     432            func = getattr(self, action)
     433        elif hasattr(self.model, action):
     434            func = getattr(self.model, action)
     435            is_instance_action = True
     436        else:
     437            callable_actions = {}
     438            for item in getattr(self, 'actions', []):
     439                if callable(item):
     440                    callable_actions[item.__name__] = item
     441            if action in callable_actions:
     442                return self.get_action(callable_actions[action])
     443            raise AttributeError, \
     444                "'%s' model or '%s' have no action '%s'" % \
     445                    (self.opts.object_name, self.__class__.__name__, action)
     446        if hasattr(func, 'short_description'):
     447            description = func.short_description
     448        else:
     449            description = capfirst(action.replace('_', ' '))
     450        return func, action, description, is_instance_action
     451   
     452    def delete_selected(self, request, selected):
     453        """
     454        Default action which deletes the selected objects.
     455        """
     456        opts = self.model._meta
     457        app_label = opts.app_label
     458       
     459        if not self.has_delete_permission(request):
     460            raise PermissionDenied
     461       
     462        # Populate deleted_objects, a data structure of all related objects that
     463        # will also be deleted.
     464       
     465        # deleted_objects must be a list if we want to use '|unordered_list' in the template
     466        deleted_objects = []
     467        for obj in selected:
     468            deleted_objects.append([mark_safe(u'%s: <a href="%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), obj.pk, escape(obj))), []])
     469        perms_needed = set()
     470        i = 0
     471        for d in deleted_objects:
     472            # FIXME: the urlpath to the detail-view of the related objects are hardcoded as "../../../../"
     473            # which is wrong from this changelist_view. Is there a admin-reverse-urlconf refactor?
     474            get_deleted_objects(deleted_objects[i], perms_needed, request.user, selected[i], opts, 1, self.admin_site)
     475            i=i+1
     476       
     477        # The user has already confirmed the deletion.
     478        if request.POST.get('post'):
     479            if perms_needed:
     480                raise PermissionDenied
     481            n = selected.count()
     482            if n:
     483                for obj in selected:
     484                    obj_display = force_unicode(obj)
     485                    self.log_deletion(request, obj, obj_display)
     486                selected.delete()
     487                self.message_user(request, _("Successfully deleted %d %s.") % (
     488                    n, model_ngettext(self.opts, n)
     489                ))
     490            return None
     491       
     492        context = {
     493            "title": _("Are you sure?"),
     494            "object_name": force_unicode(opts.verbose_name),
     495            "deleted_objects": deleted_objects,
     496            'selected': selected,
     497            "perms_lacking": perms_needed,
     498            "opts": opts,
     499            "root_path": self.admin_site.root_path,
     500            "app_label": app_label,
     501        }
     502        return render_to_response(self.delete_confirmation_template or [
     503            "admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.object_name.lower()),
     504            "admin/%s/delete_selected_confirmation.html" % app_label,
     505            "admin/delete_selected_confirmation.html"
     506        ], context, context_instance=template.RequestContext(request))
     507   
     508    delete_selected.short_description = ugettext_lazy("Delete selected %(verbose_name_plural)s")
    393509   
    394510    def construct_change_message(self, request, form, formsets):
    395511        """
    class ModelAdmin(BaseModelAdmin):  
    528644        else:
    529645            self.message_user(request, msg)
    530646            return HttpResponseRedirect("../")
     647
     648    def response_action(self, request, queryset):
     649        if request.method == 'POST':
     650            # There can be multiple action forms on the page (at the top
     651            # and bottom of the change list, for example). Get the action
     652            # whose button was pushed.
     653            try:
     654                action_index = int(request.POST.get('index', 0))
     655            except ValueError:
     656                action_index = 0
     657            data = {}
     658            for key in request.POST:
     659                if key not in (helpers.ACTION_CHECKBOX_NAME, 'index'):
     660                    data[key] = request.POST.getlist(key)[action_index]
     661            action_form = self.action_form(data, auto_id=None)
     662            action_form.fields['action'].choices = self.get_action_choices()
     663           
     664            if action_form.is_valid():
     665                action = action_form.cleaned_data['action']
     666                func, name, description, instance_action = self.get_action(action)
     667                selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
     668                results = queryset.filter(pk__in=selected)
     669                response = None
     670                if callable(func):
     671                    if instance_action:
     672                        for obj in results:
     673                            getattr(obj, name)(request)
     674                    else:
     675                        response = func(request, results)
     676                if isinstance(response, HttpResponse):
     677                    return response
     678                else:
     679                    redirect_to = request.META.get('HTTP_REFERER') or "."
     680                    return HttpResponseRedirect(redirect_to)
     681        else:
     682            action_form = self.action_form(auto_id=None)
     683            action_form.fields['action'].choices = self.get_action_choices()
     684        return action_form
    531685   
    532686    def add_view(self, request, form_url='', extra_context=None):
    533687        "The 'add' admin view for this model."
    class ModelAdmin(BaseModelAdmin):  
    721875                return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
    722876            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
    723877       
     878        action_form_or_response = self.response_action(request, queryset=cl.get_query_set())
     879        if isinstance(action_form_or_response, HttpResponse):
     880            return action_form_or_response
     881       
    724882        # If we're allowing changelist editing, we need to construct a formset
    725883        # for the changelist given all the fields to be edited. Then we'll
    726884        # use the formset to validate/process POSTed data.
    class ModelAdmin(BaseModelAdmin):  
    761919        if formset:
    762920            media = self.media + formset.media
    763921        else:
    764             media = None
    765 
     922            media = self.media
     923       
    766924        context = {
    767925            'title': cl.title,
    768926            'is_popup': cl.is_popup,
    class ModelAdmin(BaseModelAdmin):  
    771929            'has_add_permission': self.has_add_permission(request),
    772930            'root_path': self.admin_site.root_path,
    773931            'app_label': app_label,
     932            'action_form': action_form_or_response,
     933            'actions_on_top': self.actions_on_top,
     934            'actions_on_bottom': self.actions_on_bottom,
    774935        }
    775936        context.update(extra_context or {})
    776937        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 dca5b80..63254b8 100644
    a b  
    77  {% if cl.formset %}
    88    <link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />
    99    <script type="text/javascript" src="../../jsi18n/"></script>
    10     {{ media }}
    1110  {% endif %}
     11  {{ media }}
    1212{% endblock %}
    1313
    1414{% block bodyclass %}change-list{% endblock %}
     
    6363        {% endif %}
    6464      {% endblock %}
    6565     
     66      <form action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}>
    6667      {% if cl.formset %}
    67         <form action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}>
    6868        {{ cl.formset.management_form }}
    6969      {% endif %}
    7070
    71       {% block result_list %}{% result_list cl %}{% endblock %}
     71      {% block result_list %}
     72          {% if actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %}
     73          {% result_list cl %}
     74          {% if actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %}
     75      {% endblock %}
    7276      {% block pagination %}{% pagination cl %}{% endblock %}
    73       {% if cl.formset %}</form>{% endif %}
     77      </form>
    7478    </div>
    7579  </div>
    7680{% endblock %}
  • new file django/contrib/admin/templates/admin/delete_selected_confirmation.html

    diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
    new file mode 100644
    index 0000000..183134d
    - +  
     1{% extends "admin/base_site.html" %}
     2{% load i18n %}
     3
     4{% block breadcrumbs %}
     5<div class="breadcrumbs">
     6     <a href="../../">{% trans "Home" %}</a> &rsaquo;
     7     <a href="../">{{ app_label|capfirst }}</a> &rsaquo;
     8     <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
     9     {% trans 'Delete multiple objects' %}
     10</div>
     11{% endblock %}
     12
     13{% block content %}
     14{% if perms_lacking %}
     15    <p>{% blocktrans %}Deleting the {{ object_name }} would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p>
     16    <ul>
     17    {% for obj in perms_lacking %}
     18        <li>{{ obj }}</li>
     19    {% endfor %}
     20    </ul>
     21{% else %}
     22    <p>{% blocktrans %}Are you sure you want to delete the selected {{ object_name }} objects? All of the following objects and it's related items will be deleted:{% endblocktrans %}</p>
     23    {% for d in deleted_objects %}
     24        <ul>{{ d|unordered_list }}</ul>
     25    {% endfor %}
     26    <form action="" method="post">
     27    <div>
     28    {% for s in selected %}
     29    <input type="hidden" name="selected" value="{{ s.pk }}" />
     30    {% endfor %}
     31    <input type="hidden" name="action" value="delete_selected" />
     32    <input type="hidden" name="post" value="yes" />
     33    <input type="submit" value="{% trans "Yes, I'm sure" %}" />
     34    </div>
     35    </form>
     36{% endif %}
     37{% endblock %}
     38 No newline at end of file
  • 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 063ef0e..a374bf5 100644
    a b search_form = register.inclusion_tag('admin/search_form.html')(search_form)  
    325325def admin_list_filter(cl, spec):
    326326    return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
    327327admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter)
     328
     329def admin_actions(context):
     330    """
     331    Track the number of times the action field has been rendered on the page,
     332    so we know which value to use.
     333    """
     334    context['action_index'] = context.get('action_index', -1) + 1
     335    return context
     336admin_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 eeaf039..0f53230 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):  
    199200        BarAccountAdmin
    200201    )
    201202
     203class Subscriber(models.Model):
     204    name = models.CharField(blank=False, max_length=80)
     205    email = models.EmailField(blank=False, max_length=175)
     206
     207    def __unicode__(self):
     208        return "%s (%s)" % (self.name, self.email)
     209
     210class SubscriberAdmin(admin.ModelAdmin):
     211    actions = ['delete_selected', 'mail_admin']
     212
     213    def mail_admin(self, request, selected):
     214        EmailMessage(
     215            'Greetings from a ModelAdmin action',
     216            'This is the test email from a admin action',
     217            'from@example.com',
     218            ['to@example.com']
     219        ).send()
     220
     221class DirectSubscriber(Subscriber):
     222    paid = models.BooleanField(default=False)
     223
     224    def direct_mail(self, request):
     225        EmailMessage(
     226            'Greetings from a model action',
     227            'This is the test email from a model action',
     228            'from@example.com',
     229            [self.email]
     230        ).send()
     231
     232class DirectSubscriberAdmin(admin.ModelAdmin):
     233    actions = ['direct_mail']
     234
     235class ExternalSubscriber(Subscriber):
     236    pass
     237
     238def external_mail(request, selected):
     239    EmailMessage(
     240        'Greetings from a function action',
     241        'This is the test email from a function action',
     242        'from@example.com',
     243        ['to@example.com']
     244    ).send()
     245
     246def redirect_to(request, selected):
     247    from django.http import HttpResponseRedirect
     248    return HttpResponseRedirect('/some-where-else/')
     249
     250class ExternalSubscriberAdmin(admin.ModelAdmin):
     251    actions = [external_mail, redirect_to]
    202252
    203253admin.site.register(Article, ArticleAdmin)
    204254admin.site.register(CustomArticle, CustomArticleAdmin)
    admin.site.register(Color)  
    208258admin.site.register(Thing, ThingAdmin)
    209259admin.site.register(Person, PersonAdmin)
    210260admin.site.register(Persona, PersonaAdmin)
     261admin.site.register(Subscriber, SubscriberAdmin)
     262admin.site.register(DirectSubscriber, DirectSubscriberAdmin)
     263admin.site.register(ExternalSubscriber, ExternalSubscriberAdmin)
    211264
    212265# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
    213266# 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 33000d4..8be78a7 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, Person, Persona, FooAccount, BarAccount
     15from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Person, Persona, FooAccount, BarAccount, Subscriber, DirectSubscriber, ExternalSubscriber
    1516
    1617try:
    1718    set
    class AdminViewListEditable(TestCase):  
    743744        response = self.client.get('/test_admin/admin/admin_views/person/')
    744745        # 2 inputs per object(the field and the hidden id field) = 6
    745746        # 2 management hidden fields = 2
     747        # 4 action inputs (3 regular checkboxes, 1 checkbox to select all)
    746748        # main form submit button = 1
    747749        # search field and search submit button = 2
    748750        # 6 + 2 + 1 + 2 = 11 inputs
    749         self.failUnlessEqual(response.content.count("<input"), 11)
     751        self.failUnlessEqual(response.content.count("<input"), 15)
    750752        # 1 select per object = 3 selects
    751         self.failUnlessEqual(response.content.count("<select"), 3)
     753        self.failUnlessEqual(response.content.count("<select"), 4)
    752754   
    753755    def test_post_submission(self):
    754756        data = {
    class AdminInheritedInlinesTest(TestCase):  
    875877        self.failUnlessEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user)
    876878        self.failUnlessEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user)
    877879        self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2)
     880
     881from django.core import mail
     882
     883class AdminActionsTest(TestCase):
     884    fixtures = ['admin-views-users.xml', 'admin-views-actions.xml']
     885
     886    def setUp(self):
     887        self.client.login(username='super', password='secret')
     888
     889    def tearDown(self):
     890        self.client.logout()
     891
     892    def test_model_admin_custom_action(self):
     893        "Tests a custom action defined in a ModelAdmin method"
     894        action_data = {
     895            ACTION_CHECKBOX_NAME: [1],
     896            'action' : 'mail_admin',
     897            'index': 0,
     898        }
     899        response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
     900        self.assertEquals(len(mail.outbox), 1)
     901        self.assertEquals(mail.outbox[0].subject, 'Greetings from a ModelAdmin action')
     902
     903    def test_model_admin_default_delete_action(self):
     904        "Tests the default delete action defined as a ModelAdmin method"
     905        action_data = {
     906            ACTION_CHECKBOX_NAME: [1, 2],
     907            'action' : 'delete_selected',
     908            'index': 0,
     909        }
     910        delete_confirmation_data = {
     911            ACTION_CHECKBOX_NAME: [1, 2],
     912            'action' : 'delete_selected',
     913            'index': 0,
     914            'post': 'yes',
     915        }
     916        confirmation = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
     917        self.assertContains(confirmation, "Are you sure you want to delete the selected subscriber objects")
     918        response = self.client.post('/test_admin/admin/admin_views/subscriber/', delete_confirmation_data)
     919        self.failUnlessEqual(Subscriber.objects.count(), 0)
     920
     921    def test_custom_model_instance_action(self):
     922        "Tests a custom action defined in a model method"
     923        action_data = {
     924            ACTION_CHECKBOX_NAME: [1],
     925            'action' : 'direct_mail',
     926            'index': 0,
     927            'paid': 1,
     928        }
     929        response = self.client.post('/test_admin/admin/admin_views/directsubscriber/', action_data)
     930        self.assertEquals(len(mail.outbox), 1)
     931        self.assertEquals(mail.outbox[0].subject, 'Greetings from a model action')
     932        self.assertEquals(mail.outbox[0].to, [u'john@example.org'])
     933
     934    def test_custom_function_mail_action(self):
     935        "Tests a custom action defined in a function"
     936        action_data = {
     937            ACTION_CHECKBOX_NAME: [1],
     938            'action' : 'external_mail',
     939            'index': 0,
     940        }
     941        response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
     942        self.assertEquals(len(mail.outbox), 1)
     943        self.assertEquals(mail.outbox[0].subject, 'Greetings from a function action')
     944
     945    def test_custom_function_action_with_redirect(self):
     946        "Tests a custom action defined in a function"
     947        action_data = {
     948            ACTION_CHECKBOX_NAME: [1],
     949            'action' : 'redirect_to',
     950            'index': 0,
     951        }
     952        response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
     953        self.failUnlessEqual(response.status_code, 302)
Back to Top