Commit 7bc08789 authored by Jacob Kaplan-Moss's avatar Jacob Kaplan-Moss
Browse files

Fixed #8939: added a `list_editable` option to `ModelAdmin`; fields declared...

Fixed #8939: added a `list_editable` option to `ModelAdmin`; fields declared `list_editable` may be edited, in bulk, on the changelist page. Thanks, Alex Gaynor.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@10077 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent a7d1c73a
Loading
Loading
Loading
Loading
+71 −3
Original line number Diff line number Diff line
from django import forms, template
from django.forms.formsets import all_valid
from django.forms.models import modelform_factory, inlineformset_factory
from django.forms.models import modelform_factory, modelformset_factory, inlineformset_factory
from django.forms.models import BaseInlineFormSet
from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets
@@ -16,6 +16,7 @@ from django.utils.safestring import mark_safe
from django.utils.functional import curry
from django.utils.text import capfirst, get_text_list
from django.utils.translation import ugettext as _
from django.utils.translation import ngettext
from django.utils.encoding import force_unicode
try:
    set
@@ -177,6 +178,7 @@ class ModelAdmin(BaseModelAdmin):
    list_filter = ()
    list_select_related = False
    list_per_page = 100
    list_editable = ()
    search_fields = ()
    date_hierarchy = None
    save_as = False
@@ -313,6 +315,29 @@ class ModelAdmin(BaseModelAdmin):
        defaults.update(kwargs)
        return modelform_factory(self.model, **defaults)
    
    def get_changelist_form(self, request, **kwargs):
        """
        Returns a Form class for use in the Formset on the changelist page.
        """
        defaults = {
            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
        }
        defaults.update(kwargs)
        return modelform_factory(self.model, **defaults)
    
    def get_changelist_formset(self, request, **kwargs):
        """
        Returns a FormSet class for use on the changelist page if list_editable 
        is used.
        """
        defaults = {
            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
        }
        defaults.update(kwargs)
        return modelformset_factory(self.model, 
            self.get_changelist_form(request), extra=0, 
            fields=self.list_editable, **defaults)
    
    def get_formsets(self, request, obj=None):
        for inline in self.inline_instances:
            yield inline.get_formset(request, obj)
@@ -685,7 +710,7 @@ class ModelAdmin(BaseModelAdmin):
            raise PermissionDenied
        try:
            cl = ChangeList(request, self.model, self.list_display, self.list_display_links, self.list_filter,
                self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self)
                self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self.list_editable, self)
        except IncorrectLookupParameters:
            # Wacky lookup parameters were given, so redirect to the main
            # changelist page, without parameters, and pass an 'invalid=1'
@@ -696,10 +721,53 @@ class ModelAdmin(BaseModelAdmin):
                return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
        
        # If we're allowing changelist editing, we need to construct a formset
        # for the changelist given all the fields to be edited. Then we'll
        # use the formset to validate/process POSTed data.
        formset = cl.formset = None
        
        # Handle POSTed bulk-edit data.
        if request.method == "POST" and self.list_editable:
            FormSet = self.get_changelist_formset(request)
            formset = cl.formset = FormSet(request.POST, request.FILES, queryset=cl.result_list)
            if formset.is_valid():
                changecount = 0
                for form in formset.forms:
                    if form.has_changed():
                        obj = self.save_form(request, form, change=True)
                        self.save_model(request, obj, form, change=True)
                        form.save_m2m()
                        change_msg = self.construct_change_message(request, form, None)
                        self.log_change(request, obj, change_msg)
                        changecount += 1
                
                if changecount:
                    msg = ngettext("%(count)s %(singular)s was changed successfully.", 
                                   "%(count)s %(plural)s were changed successfully.", 
                                   changecount) % {'count': changecount,
                                                   'singular': force_unicode(opts.verbose_name), 
                                                   'plural': force_unicode(opts.verbose_name_plural),
                                                   'obj': force_unicode(obj)}
                    self.message_user(request, msg)
            
                return HttpResponseRedirect(request.get_full_path())
        
        # Handle GET -- construct a formset for display.
        elif self.list_editable:
            FormSet = self.get_changelist_formset(request)
            formset = cl.formset = FormSet(queryset=cl.result_list)
        
        # Build the list of media to be used by the formset.
        if formset:
            media = self.media + formset.media
        else:
            media = None

        context = {
            'title': cl.title,
            'is_popup': cl.is_popup,
            'cl': cl,
            'media': media,
            'has_add_permission': self.has_add_permission(request),
            'root_path': self.admin_site.root_path,
            'app_label': app_label,
+63 −25
Original line number Diff line number Diff line
{% extends "admin/base_site.html" %}
{% load adminmedia admin_list i18n %}

{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/changelists.css" />{% endblock %}
{% block extrastyle %}
  {{ block.super }}
  <link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/changelists.css" />
  {% if cl.formset %}
    <link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />
    <script type="text/javascript" src="../../jsi18n/"></script>
    {{ media }}
  {% endif %}
{% endblock %}

{% block bodyclass %}change-list{% endblock %}

{% 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 %}
{% 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 %}

{% block coltype %}flex{% endblock %}

@@ -13,9 +35,21 @@
  <div id="content-main">
    {% block object-tools %}
      {% if has_add_permission %}
<ul class="object-tools"><li><a href="add/{% if is_popup %}?_popup=1{% endif %}" class="addlink">{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}</a></li></ul>
        <ul class="object-tools">
          <li>
            <a href="add/{% if is_popup %}?_popup=1{% endif %}" class="addlink">
              {% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
            </a>
          </li>
        </ul>
      {% endif %}
    {% endblock %}
    {% if cl.formset.errors %}
        <p class="errornote">
        {% blocktrans count cl.formset.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
        </p>
        <ul class="errorlist">{% for error in cl.formset.non_field_errors %}<li>{{ error }}</li>{% endfor %}</ul>
    {% endif %}
    <div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist">
      {% block search %}{% search_form cl %}{% endblock %}
      {% block date_hierarchy %}{% date_hierarchy cl %}{% endblock %}
@@ -24,15 +58,19 @@
        {% if cl.has_filters %}
          <div id="changelist-filter">
            <h2>{% trans 'Filter' %}</h2>
{% for spec in cl.filter_specs %}
   {% admin_list_filter cl spec %}
{% endfor %}
            {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
          </div>
        {% endif %}
      {% endblock %}
      
      {% if cl.formset %}
        <form action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}>
        {{ cl.formset.management_form }}
      {% endif %}

      {% block result_list %}{% result_list cl %}{% endblock %}
      {% block pagination %}{% pagination cl %}{% endblock %}
      {% if cl.formset %}</form>{% endif %}
    </div>
  </div>
{% endblock %}
+1 −0
Original line number Diff line number Diff line
@@ -8,4 +8,5 @@
{% endif %}
{{ cl.result_count }} {% ifequal cl.result_count 1 %}{{ cl.opts.verbose_name }}{% else %}{{ cl.opts.verbose_name_plural }}{% endifequal %}
{% if show_all_url %}&nbsp;&nbsp;<a href="{{ show_all_url }}" class="showall">{% trans 'Show all' %}</a>{% endif %}
{% if cl.formset %}<input type="submit" name="_save" class="default" value="Save"/>{% endif %}
</p>
+18 −4
Original line number Diff line number Diff line
@@ -133,7 +133,7 @@ def _boolean_icon(field_val):
    BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
    return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val))

def items_for_result(cl, result):
def items_for_result(cl, result, form):
    first = True
    pk = cl.lookup_opts.pk.attname
    for field_name in cl.list_display:
@@ -227,11 +227,25 @@ def items_for_result(cl, result):
            yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \
                (table_tag, row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), conditional_escape(result_repr), table_tag))
        else:
            yield mark_safe(u'<td%s>%s</td>' % (row_class, conditional_escape(result_repr)))
            # By default the fields come from ModelAdmin.list_editable, but if we pull
            # the fields out of the form instead of list_editable custom admins
            # can provide fields on a per request basis
            if form and field_name in form.fields:
                bf = form[field_name]
                result_repr = mark_safe(force_unicode(bf.errors) + force_unicode(bf))
            else:
                result_repr = conditional_escape(result_repr)
            yield mark_safe(u'<td%s>%s</td>' % (row_class, result_repr))
    if form:
        yield mark_safe(force_unicode(form[cl.model._meta.pk.attname]))

def results(cl):
    if cl.formset:
        for res, form in zip(cl.result_list, cl.formset.forms):
            yield list(items_for_result(cl, res, form))
    else:
        for res in cl.result_list:
        yield list(items_for_result(cl,res))
            yield list(items_for_result(cl, res, None))

def result_list(cl):
    return {'cl': cl,
+23 −0
Original line number Diff line number Diff line
@@ -64,6 +64,29 @@ def validate(cls, model):
        raise ImproperlyConfigured("'%s.list_per_page' should be a integer."
                % cls.__name__)
    
    # list_editable
    if hasattr(cls, 'list_editable') and cls.list_editable:
        check_isseq(cls, 'list_editable', cls.list_editable)
        if not (opts.ordering or cls.ordering):
            raise ImproperlyConfigured("'%s.list_editable' cannot be used "
                "without a default ordering. Please define ordering on either %s or %s."
                % (cls.__name__, cls.__name__, model.__name__))
        for idx, field in enumerate(cls.list_editable):
            try:
                opts.get_field_by_name(field)
            except models.FieldDoesNotExist:
                raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a "
                    "field, '%s', not defiend on %s." % (cls.__name__, idx, field, model.__name__))
            if field not in cls.list_display:
                raise ImproperlyConfigured("'%s.list_editable[%d]' refers to "
                    "'%s' which is not defined in 'list_display'."
                    % (cls.__name__, idx, field))
            if field in cls.list_display_links:
                raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'"
                    " and '%s.list_display_links'"
                    % (field, cls.__name__, cls.__name__))
                

    # search_fields = ()
    if hasattr(cls, 'search_fields'):
        check_isseq(cls, 'search_fields', cls.search_fields)
Loading