' % (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'
%s
' % (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.
diff --git a/docs/ref/contrib/admin.txt b/docs/ref/contrib/admin.txt
index 35bfde217c..1813a191f6 100644
--- a/docs/ref/contrib/admin.txt
+++ b/docs/ref/contrib/admin.txt
@@ -403,6 +403,32 @@ the change list page::
Finally, note that in order to use ``list_display_links``, you must define
``list_display``, too.
+``list_editable``
+~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.1
+
+Set ``list_editable`` to a list of field names on the model which will allow
+editing on the change list page. That is, fields listed in ``list_editable``
+will be displayed as form widgets on the change list page, allowing users to
+edit and save multiple rows at once.
+
+.. note::
+
+ ``list_editable`` interacts with a couple of other options in particular
+ ways; you should note the following rules:
+
+ * To use ``list_editable`` you must have defined ``ordering`` defined on
+ either your model or your ``ModelAdmin``.
+
+ * Any field in ``list_editable`` must also be in ``list_display``. You
+ can't edit a field that's not displayed!
+
+ * The same field can't be listed in both ``list_editable`` and
+ ``list_display_links`` -- a field can't be both a form and a link.
+
+ You'll get a validation error if any of these rules are broken.
+
``list_filter``
~~~~~~~~~~~~~~~
diff --git a/tests/regressiontests/admin_views/fixtures/admin-views-person.xml b/tests/regressiontests/admin_views/fixtures/admin-views-person.xml
new file mode 100644
index 0000000000..77928a834b
--- /dev/null
+++ b/tests/regressiontests/admin_views/fixtures/admin-views-person.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
index d849a7b9c1..eeaf039444 100644
--- a/tests/regressiontests/admin_views/models.py
+++ b/tests/regressiontests/admin_views/models.py
@@ -134,6 +134,28 @@ class Thing(models.Model):
class ThingAdmin(admin.ModelAdmin):
list_filter = ('color',)
+class Person(models.Model):
+ GENDER_CHOICES = (
+ (1, "Male"),
+ (2, "Female"),
+ )
+ name = models.CharField(max_length=100)
+ gender = models.IntegerField(choices=GENDER_CHOICES)
+ alive = models.BooleanField()
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ ordering = ["id"]
+
+class PersonAdmin(admin.ModelAdmin):
+ list_display = ('name', 'gender', 'alive')
+ list_editable = ('gender', 'alive')
+ list_filter = ('gender',)
+ search_fields = ('name',)
+ ordering = ["id"]
+
class Persona(models.Model):
"""
A simple persona associated with accounts, to test inlining of related
@@ -177,12 +199,14 @@ class PersonaAdmin(admin.ModelAdmin):
BarAccountAdmin
)
+
admin.site.register(Article, ArticleAdmin)
admin.site.register(CustomArticle, CustomArticleAdmin)
admin.site.register(Section, inlines=[ArticleInline])
admin.site.register(ModelWithStringPrimaryKey)
admin.site.register(Color)
admin.site.register(Thing, ThingAdmin)
+admin.site.register(Person, PersonAdmin)
admin.site.register(Persona, PersonaAdmin)
# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
index bf198bc9a2..33000d4f5a 100644
--- a/tests/regressiontests/admin_views/tests.py
+++ b/tests/regressiontests/admin_views/tests.py
@@ -11,7 +11,7 @@ from django.contrib.admin.util import quote
from django.utils.html import escape
# local test models
-from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Persona, FooAccount, BarAccount
+from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Person, Persona, FooAccount, BarAccount
try:
set
@@ -729,6 +729,76 @@ class AdminViewUnicodeTest(TestCase):
response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict)
self.assertRedirects(response, '/test_admin/admin/admin_views/book/')
+
+class AdminViewListEditable(TestCase):
+ fixtures = ['admin-views-users.xml', 'admin-views-person.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_changelist_input_html(self):
+ response = self.client.get('/test_admin/admin/admin_views/person/')
+ # 2 inputs per object(the field and the hidden id field) = 6
+ # 2 management hidden fields = 2
+ # main form submit button = 1
+ # search field and search submit button = 2
+ # 6 + 2 + 1 + 2 = 11 inputs
+ self.failUnlessEqual(response.content.count("