Ticket #10505: admin-actions.1.diff

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

Initial patch, partly based on django-batchadmin

  • django/contrib/admin/helpers.py

    diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
    index aaa2e30..335a44e 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()
     15
     16checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
    917
    1018class AdminForm(object):
    1119    def __init__(self, form, fieldsets, prepopulated_fields):
  • django/contrib/admin/media/css/changelists.css

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

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

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

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

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

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