Ticket #10505: admin-actions.diff

File admin-actions.diff, 34.7 KB (added by Alex Gaynor, 15 years ago)
  • 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 69f52aa..096960d 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
     419    def get_actions(self, request=None):
     420        actions = {}
     421        for klass in self.__class__.mro():
     422            for action in getattr(klass, 'actions', []):
     423                func, name, description = self.get_action(action)
     424                actions[name] = (func, name, description)
     425        return actions
     426
     427    def get_action_choices(self, request=None, default_choices=BLANK_CHOICE_DASH):
     428        choices = [] + default_choices
     429        for func, name, description in self.get_actions(request).itervalues():
     430            choice = (name, description % model_format_dict(self.opts))
     431            choices.append(choice)
     432        return choices
     433
     434    def get_action(self, action):
     435        if callable(action):
     436            func = action
     437            action = action.__name__
     438        elif hasattr(self, action):
     439            func = getattr(self, action)
     440        if hasattr(func, 'short_description'):
     441            description = func.short_description
     442        else:
     443            description = capfirst(action.replace('_', ' '))
     444        return func, action, description
     445
     446    def delete_selected(self, request, selected):
     447        """
     448        Default action which deletes the selected objects.
     449        """
     450        opts = self.model._meta
     451        app_label = opts.app_label
     452
     453        if not self.has_delete_permission(request):
     454            raise PermissionDenied
     455
     456        # Populate deleted_objects, a data structure of all related objects that
     457        # will also be deleted.
     458
     459        # deleted_objects must be a list if we want to use '|unordered_list' in the template
     460        deleted_objects = []
     461        perms_needed = set()
     462        i = 0
     463        for obj in selected:
     464            deleted_objects.append([mark_safe(u'%s: <a href="%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), obj.pk, escape(obj))), []])
     465            get_deleted_objects(deleted_objects[i], perms_needed, request.user, obj, opts, 1, self.admin_site, levels_to_root=2)
     466            i=i+1
     467
     468        # The user has already confirmed the deletion.
     469        if request.POST.get('post'):
     470            if perms_needed:
     471                raise PermissionDenied
     472            n = selected.count()
     473            if n:
     474                for obj in selected:
     475                    obj_display = force_unicode(obj)
     476                    self.log_deletion(request, obj, obj_display)
     477                selected.delete()
     478                self.message_user(request, _("Successfully deleted %d %s.") % (
     479                    n, model_ngettext(self.opts, n)
     480                ))
     481            return None
     482
     483        context = {
     484            "title": _("Are you sure?"),
     485            "object_name": force_unicode(opts.verbose_name),
     486            "deleted_objects": deleted_objects,
     487            'selected': selected,
     488            "perms_lacking": perms_needed,
     489            "opts": opts,
     490            "root_path": self.admin_site.root_path,
     491            "app_label": app_label,
     492        }
     493        return render_to_response(self.delete_confirmation_template or [
     494            "admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.object_name.lower()),
     495            "admin/%s/delete_selected_confirmation.html" % app_label,
     496            "admin/delete_selected_confirmation.html"
     497        ], context, context_instance=template.RequestContext(request))
     498
     499    delete_selected.short_description = ugettext_lazy("Delete selected %(verbose_name_plural)s")
    393500
    394501    def construct_change_message(self, request, form, formsets):
    395502        """
    class ModelAdmin(BaseModelAdmin):  
    529636            self.message_user(request, msg)
    530637            return HttpResponseRedirect("../")
    531638
     639    def response_action(self, request, queryset):
     640        if request.method == 'POST':
     641            # There can be multiple action forms on the page (at the top
     642            # and bottom of the change list, for example). Get the action
     643            # whose button was pushed.
     644            try:
     645                action_index = int(request.POST.get('index', 0))
     646            except ValueError:
     647                action_index = 0
     648            data = {}
     649            for key in request.POST:
     650                if key not in (helpers.ACTION_CHECKBOX_NAME, 'index'):
     651                    data[key] = request.POST.getlist(key)[action_index]
     652            action_form = self.action_form(data, auto_id=None)
     653            action_form.fields['action'].choices = self.get_action_choices(request)
     654
     655            if action_form.is_valid():
     656                action = action_form.cleaned_data['action']
     657                func, name, description = self.get_actions(request)[action]
     658                selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
     659                results = queryset.filter(pk__in=selected)
     660                response = None
     661                if callable(func):
     662                    response = func(request, results)
     663                if isinstance(response, HttpResponse):
     664                    return response
     665                else:
     666                    redirect_to = request.META.get('HTTP_REFERER') or "."
     667                    return HttpResponseRedirect(redirect_to)
     668        else:
     669            action_form = self.action_form(auto_id=None)
     670            action_form.fields['action'].choices = self.get_action_choices(request)
     671        return action_form
     672
    532673    def add_view(self, request, form_url='', extra_context=None):
    533674        "The 'add' admin view for this model."
    534675        model = self.model
    class ModelAdmin(BaseModelAdmin):  
    721862                return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
    722863            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
    723864
     865        action_form_or_response = self.response_action(request, queryset=cl.get_query_set())
     866        if isinstance(action_form_or_response, HttpResponse):
     867            return action_form_or_response
     868
    724869        # If we're allowing changelist editing, we need to construct a formset
    725870        # for the changelist given all the fields to be edited. Then we'll
    726871        # use the formset to validate/process POSTed data.
    class ModelAdmin(BaseModelAdmin):  
    764909        if formset:
    765910            media = self.media + formset.media
    766911        else:
    767             media = None
     912            media = self.media
    768913
    769914        context = {
    770915            'title': cl.title,
    class ModelAdmin(BaseModelAdmin):  
    774919            'has_add_permission': self.has_add_permission(request),
    775920            'root_path': self.admin_site.root_path,
    776921            'app_label': app_label,
     922            'action_form': action_form_or_response,
     923            'actions_on_top': self.actions_on_top,
     924            'actions_on_bottom': self.actions_on_bottom,
    777925        }
    778926        context.update(extra_context or {})
    779927        return render_to_response(self.change_list_template or [
  • django/contrib/admin/sites.py

    diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
    index 5171e71..ebcf886 100644
    a b class AdminSite(object):  
    4444        else:
    4545            name += '_'
    4646        self.name = name
     47       
     48        self.actions = []
    4749   
    4850    def register(self, model_or_iterable, admin_class=None, **options):
    4951        """
    class AdminSite(object):  
    8183                options['__module__'] = __name__
    8284                admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)
    8385           
     86            for action in self.actions:
     87                admin_class.actions.append(action)
     88           
    8489            # Validate (which might be a no-op)
    8590            validate(admin_class, model)
    8691           
    class AdminSite(object):  
    100105                raise NotRegistered('The model %s is not registered' % model.__name__)
    101106            del self._registry[model]
    102107   
     108    def add_action(self, action):
     109        if not callable(action):
     110            raise TypeError("You can only register callable actions through an admin site")
     111        self.actions.append(action)
     112        for klass in self._registery.itervalues():
     113            klass.actions.append(action)
     114   
    103115    def has_permission(self, request):
    104116        """
    105117        Returns True if the given HttpRequest has permission to view
  • 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..38f86e3 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 _
     8from django.core.urlresolvers import reverse, NoReverseMatch
    89
    910def quote(s):
    1011    """
    def _nest_help(obj, depth, val):  
    6061        current = current[-1]
    6162    current.append(val)
    6263
    63 def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site):
    64     "Helper function that recursively populates deleted_objects."
     64def get_change_view_url(app_label, module_name, pk, admin_site, levels_to_root):
     65    """
     66    Returns the url to the admin change view for the given app_label,
     67    module_name and primary key.
     68    """
     69    try:
     70        return reverse('%sadmin_%s_%s_change' % (admin_site.name, app_label, module_name), None, (pk,))
     71    except NoReverseMatch:
     72        return '%s%s/%s/%s/' % ('../'*levels_to_root, app_label, module_name, pk)
     73
     74def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site, levels_to_root=4):
     75    """
     76    Helper function that recursively populates deleted_objects.
     77
     78    `levels_to_root` defines the number of directories (../) to reach the
     79    admin root path. In a change_view this is 4, in a change_list view 2.
     80
     81    This is for backwards compatibility since the options.delete_selected
     82    method uses this function also from a change_list view.
     83    This will not be used if we can reverse the URL.
     84    """
    6585    nh = _nest_help # Bind to local variable for performance
    6686    if current_depth > 16:
    6787        return # Avoid recursing too deep.
    def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_  
    91111                        [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []])
    92112                else:
    93113                    # Display a link to the admin page.
    94                     nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' %
     114                    nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' %
    95115                        (escape(capfirst(related.opts.verbose_name)),
    96                         related.opts.app_label,
    97                         related.opts.object_name.lower(),
    98                         sub_obj._get_pk_val(),
     116                        get_change_view_url(related.opts.app_label,
     117                                            related.opts.object_name.lower(),
     118                                            sub_obj._get_pk_val(),
     119                                            admin_site,
     120                                            levels_to_root),
    99121                        escape(sub_obj))), []])
    100122                get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
    101123        else:
    def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_  
    109131                        [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []])
    110132                else:
    111133                    # Display a link to the admin page.
    112                     nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' %
     134                    nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' %
    113135                        (escape(capfirst(related.opts.verbose_name)),
    114                         related.opts.app_label,
    115                         related.opts.object_name.lower(),
    116                         sub_obj._get_pk_val(),
     136                        get_change_view_url(related.opts.app_label,
     137                                            related.opts.object_name.lower(),
     138                                            sub_obj._get_pk_val(),
     139                                            admin_site,
     140                                            levels_to_root),
    117141                        escape(sub_obj))), []])
    118142                get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
    119143            # If there were related objects, and the user doesn't have
    def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_  
    147171                    # Display a link to the admin page.
    148172                    nh(deleted_objects, current_depth, [
    149173                        mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \
    150                         (u' <a href="../../../../%s/%s/%s/">%s</a>' % \
    151                             (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []])
     174                        (u' <a href="%s">%s</a>' % \
     175                            (get_change_view_url(related.opts.app_label,
     176                                                 related.opts.object_name.lower(),
     177                                                 sub_obj._get_pk_val(),
     178                                                 admin_site,
     179                                                 levels_to_root),
     180                            escape(sub_obj)))), []])
    152181        # If there were related objects, and the user doesn't have
    153182        # permission to change them, add the missing perm to perms_needed.
    154183        if has_admin and has_related_objs:
    155184            p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
    156185            if not user.has_perm(p):
    157186                perms_needed.add(related.opts.verbose_name)
     187
     188def model_format_dict(obj):
     189    """
     190    Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
     191    typically for use with string formatting.
     192
     193    `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
     194
     195    """
     196    if isinstance(obj, (models.Model, models.base.ModelBase)):
     197        opts = obj._meta
     198    elif isinstance(obj, models.query.QuerySet):
     199        opts = obj.model._meta
     200    else:
     201        opts = obj
     202    return {
     203        'verbose_name': force_unicode(opts.verbose_name),
     204        'verbose_name_plural': force_unicode(opts.verbose_name_plural)
     205    }
     206
     207def model_ngettext(obj, n=None):
     208    """
     209    Return the appropriate `verbose_name` or `verbose_name_plural` for `obj`
     210    depending on the count `n`.
     211
     212    `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
     213    If `obj` is a `QuerySet` instance, `n` is optional and the length of the
     214    `QuerySet` is used.
     215
     216    """
     217    if isinstance(obj, models.query.QuerySet):
     218        if n is None:
     219            n = obj.count()
     220        obj = obj.model
     221    d = model_format_dict(obj)
     222    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..316e750
    - +  
     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.externalsubscriber">
     12        <field type="CharField" name="name">John Doe</field>
     13        <field type="CharField" name="email">john@example.org</field>
     14    </object>
     15</django-objects>
  • tests/regressiontests/admin_views/models.py

    diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
    index eeaf039..e5e112f 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 ExternalSubscriber(Subscriber):
     222    pass
     223
     224def external_mail(request, selected):
     225    EmailMessage(
     226        'Greetings from a function action',
     227        'This is the test email from a function action',
     228        'from@example.com',
     229        ['to@example.com']
     230    ).send()
     231
     232def redirect_to(request, selected):
     233    from django.http import HttpResponseRedirect
     234    return HttpResponseRedirect('/some-where-else/')
     235
     236class ExternalSubscriberAdmin(admin.ModelAdmin):
     237    actions = [external_mail, redirect_to]
    202238
    203239admin.site.register(Article, ArticleAdmin)
    204240admin.site.register(CustomArticle, CustomArticleAdmin)
    admin.site.register(Color)  
    208244admin.site.register(Thing, ThingAdmin)
    209245admin.site.register(Person, PersonAdmin)
    210246admin.site.register(Persona, PersonaAdmin)
     247admin.site.register(Subscriber, SubscriberAdmin)
     248admin.site.register(ExternalSubscriber, ExternalSubscriberAdmin)
    211249
    212250# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
    213251# 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..49e0628 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, ExternalSubscriber
    1516
    1617try:
    1718    set
    class AdminViewStringPrimaryKeyTest(TestCase):  
    516517    def test_changelist_to_changeform_link(self):
    517518        "The link from the changelist referring to the changeform of the object should be quoted"
    518519        response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/')
    519         should_contain = """<tr class="row1"><th><a href="%s/">%s</a></th></tr>""" % (quote(self.pk), escape(self.pk))
     520        should_contain = """<th><a href="%s/">%s</a></th></tr>""" % (quote(self.pk), escape(self.pk))
    520521        self.assertContains(response, should_contain)
    521522
    522523    def test_recentactions_link(self):
    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_function_mail_action(self):
     922        "Tests a custom action defined in a function"
     923        action_data = {
     924            ACTION_CHECKBOX_NAME: [1],
     925            'action' : 'external_mail',
     926            'index': 0,
     927        }
     928        response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
     929        self.assertEquals(len(mail.outbox), 1)
     930        self.assertEquals(mail.outbox[0].subject, 'Greetings from a function action')
     931
     932    def test_custom_function_action_with_redirect(self):
     933        "Tests a custom action defined in a function"
     934        action_data = {
     935            ACTION_CHECKBOX_NAME: [1],
     936            'action' : 'redirect_to',
     937            'index': 0,
     938        }
     939        response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
     940        self.failUnlessEqual(response.status_code, 302)
Back to Top