Ticket #10505: admin-actions.8.diff

File admin-actions.8.diff, 34.6 KB (added by bartTC, 6 years ago)

URL resolving now takes care of AdminSite's

  • 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..eceb94f 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 BaseModelAdmin(object): 
    172173class ModelAdmin(BaseModelAdmin):
    173174    "Encapsulates all admin options and functionality for a given model."
    174175    __metaclass__ = forms.MediaDefiningClass
    175 
    176     list_display = ('__str__',)
     176   
     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        perms_needed = set()
     468        i = 0
     469        for obj in selected:
     470            deleted_objects.append([mark_safe(u'%s: <a href="%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), obj.pk, escape(obj))), []])
     471            get_deleted_objects(deleted_objects[i], perms_needed, request.user, obj, opts, 1, self.admin_site, levels_to_root=2)
     472            i=i+1
     473
     474        # The user has already confirmed the deletion.
     475        if request.POST.get('post'):
     476            if perms_needed:
     477                raise PermissionDenied
     478            n = selected.count()
     479            if n:
     480                for obj in selected:
     481                    obj_display = force_unicode(obj)
     482                    self.log_deletion(request, obj, obj_display)
     483                selected.delete()
     484                self.message_user(request, _("Successfully deleted %d %s.") % (
     485                    n, model_ngettext(self.opts, n)
     486                ))
     487            return None
     488
     489        context = {
     490            "title": _("Are you sure?"),
     491            "object_name": force_unicode(opts.verbose_name),
     492            "deleted_objects": deleted_objects,
     493            'selected': selected,
     494            "perms_lacking": perms_needed,
     495            "opts": opts,
     496            "root_path": self.admin_site.root_path,
     497            "app_label": app_label,
     498        }
     499        return render_to_response(self.delete_confirmation_template or [
     500            "admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.object_name.lower()),
     501            "admin/%s/delete_selected_confirmation.html" % app_label,
     502            "admin/delete_selected_confirmation.html"
     503        ], context, context_instance=template.RequestContext(request))
     504
     505    delete_selected.short_description = ugettext_lazy("Delete selected %(verbose_name_plural)s")
    393506
    394507    def construct_change_message(self, request, form, formsets):
    395508        """
    class ModelAdmin(BaseModelAdmin): 
    529642            self.message_user(request, msg)
    530643            return HttpResponseRedirect("../")
    531644
     645    def response_action(self, request, queryset):
     646        if request.method == 'POST':
     647            # There can be multiple action forms on the page (at the top
     648            # and bottom of the change list, for example). Get the action
     649            # whose button was pushed.
     650            try:
     651                action_index = int(request.POST.get('index', 0))
     652            except ValueError:
     653                action_index = 0
     654            data = {}
     655            for key in request.POST:
     656                if key not in (helpers.ACTION_CHECKBOX_NAME, 'index'):
     657                    data[key] = request.POST.getlist(key)[action_index]
     658            action_form = self.action_form(data, auto_id=None)
     659            action_form.fields['action'].choices = self.get_action_choices()
     660
     661            if action_form.is_valid():
     662                action = action_form.cleaned_data['action']
     663                func, name, description, instance_action = self.get_action(action)
     664                selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
     665                results = queryset.filter(pk__in=selected)
     666                response = None
     667                if callable(func):
     668                    if instance_action:
     669                        for obj in results:
     670                            getattr(obj, name)(request)
     671                    else:
     672                        response = func(request, results)
     673                if isinstance(response, HttpResponse):
     674                    return response
     675                else:
     676                    redirect_to = request.META.get('HTTP_REFERER') or "."
     677                    return HttpResponseRedirect(redirect_to)
     678        else:
     679            action_form = self.action_form(auto_id=None)
     680            action_form.fields['action'].choices = self.get_action_choices()
     681        return action_form
     682
    532683    def add_view(self, request, form_url='', extra_context=None):
    533684        "The 'add' admin view for this model."
    534685        model = self.model
    class ModelAdmin(BaseModelAdmin): 
    721872                return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
    722873            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
    723874
     875        action_form_or_response = self.response_action(request, queryset=cl.get_query_set())
     876        if isinstance(action_form_or_response, HttpResponse):
     877            return action_form_or_response
     878
    724879        # If we're allowing changelist editing, we need to construct a formset
    725880        # for the changelist given all the fields to be edited. Then we'll
    726881        # use the formset to validate/process POSTed data.
    class ModelAdmin(BaseModelAdmin): 
    764919        if formset:
    765920            media = self.media + formset.media
    766921        else:
    767             media = None
    768 
     922            media = self.media
     923       
    769924        context = {
    770925            'title': cl.title,
    771926            'is_popup': cl.is_popup,
    class ModelAdmin(BaseModelAdmin): 
    774929            'has_add_permission': self.has_add_permission(request),
    775930            'root_path': self.admin_site.root_path,
    776931            '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,
    777935        }
    778936        context.update(extra_context or {})
    779937        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..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..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