summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorJacob Kaplan-Moss <jacob@jacobian.org>2009-03-17 20:51:47 +0000
committerJacob Kaplan-Moss <jacob@jacobian.org>2009-03-17 20:51:47 +0000
commit7bc0878922d9d93ab8f4ef8a5c5ba7a1c671279f (patch)
treeb7fe40fd4a316fea577f140bb3357a9b4f5274a1 /django
parenta7d1c73ad9eca3010768153e3e71453dbe32267a (diff)
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
Diffstat (limited to 'django')
-rw-r--r--django/contrib/admin/options.py74
-rw-r--r--django/contrib/admin/templates/admin/change_list.html88
-rw-r--r--django/contrib/admin/templates/admin/pagination.html1
-rw-r--r--django/contrib/admin/templatetags/admin_list.py22
-rw-r--r--django/contrib/admin/validation.py23
-rw-r--r--django/contrib/admin/views/main.py3
6 files changed, 178 insertions, 33 deletions
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 859229e430..c79523c2ff 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1,6 +1,6 @@
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
@@ -108,7 +109,7 @@ class BaseModelAdmin(object):
if db_field.__class__ in self.formfield_overrides:
kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
return db_field.formfield(**kwargs)
-
+
# For any other type of field, just call its formfield() method.
return db_field.formfield(**kwargs)
@@ -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,
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 5f8a43050c..dca5b80245 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/django/contrib/admin/templates/admin/change_list.html
@@ -1,38 +1,76 @@
{% 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 %}
{% block content %}
-<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>
-{% endif %}
-{% endblock %}
-<div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist">
-{% block search %}{% search_form cl %}{% endblock %}
-{% block date_hierarchy %}{% date_hierarchy cl %}{% endblock %}
+ <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>
+ {% 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 %}
-{% block filters %}
-{% if cl.has_filters %}
-<div id="changelist-filter">
-<h2>{% trans 'Filter' %}</h2>
-{% for spec in cl.filter_specs %}
- {% admin_list_filter cl spec %}
-{% endfor %}
-</div>
-{% endif %}
-{% endblock %}
+ {% block filters %}
+ {% if cl.has_filters %}
+ <div id="changelist-filter">
+ <h2>{% trans 'Filter' %}</h2>
+ {% 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 %}
-</div>
-</div>
+ {% block result_list %}{% result_list cl %}{% endblock %}
+ {% block pagination %}{% pagination cl %}{% endblock %}
+ {% if cl.formset %}</form>{% endif %}
+ </div>
+ </div>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/pagination.html b/django/contrib/admin/templates/admin/pagination.html
index 7694e4c5b0..58ade6ad0c 100644
--- a/django/contrib/admin/templates/admin/pagination.html
+++ b/django/contrib/admin/templates/admin/pagination.html
@@ -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>
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 37cdb91c3c..063ef0e4a1 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -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):
- for res in cl.result_list:
- yield list(items_for_result(cl,res))
+ 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, None))
def result_list(cl):
return {'cl': cl,
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index ccade8a3ef..fa6d7e3e60 100644
--- a/django/contrib/admin/validation.py
+++ b/django/contrib/admin/validation.py
@@ -63,6 +63,29 @@ def validate(cls, model):
if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
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'):
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index d70b6da1de..9580e912a4 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -32,7 +32,7 @@ ERROR_FLAG = 'e'
EMPTY_CHANGELIST_VALUE = '(None)'
class ChangeList(object):
- def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, model_admin):
+ def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin):
self.model = model
self.opts = model._meta
self.lookup_opts = self.opts
@@ -44,6 +44,7 @@ class ChangeList(object):
self.search_fields = search_fields
self.list_select_related = list_select_related
self.list_per_page = list_per_page
+ self.list_editable = list_editable
self.model_admin = model_admin
# Get search parameters from the query string.