diff options
| author | Jacob Kaplan-Moss <jacob@jacobian.org> | 2009-03-17 20:51:47 +0000 |
|---|---|---|
| committer | Jacob Kaplan-Moss <jacob@jacobian.org> | 2009-03-17 20:51:47 +0000 |
| commit | 7bc0878922d9d93ab8f4ef8a5c5ba7a1c671279f (patch) | |
| tree | b7fe40fd4a316fea577f140bb3357a9b4f5274a1 /django | |
| parent | a7d1c73ad9eca3010768153e3e71453dbe32267a (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.py | 74 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/change_list.html | 88 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/pagination.html | 1 | ||||
| -rw-r--r-- | django/contrib/admin/templatetags/admin_list.py | 22 | ||||
| -rw-r--r-- | django/contrib/admin/validation.py | 23 | ||||
| -rw-r--r-- | django/contrib/admin/views/main.py | 3 |
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> › <a href="../">{{ app_label|capfirst }}</a> › {{ cl.opts.verbose_name_plural|capfirst }}</div>{% endblock %}{% endif %} +{% if not is_popup %} + {% block breadcrumbs %} + <div class="breadcrumbs"> + <a href="../../"> + {% trans "Home" %} + </a> + › + <a href="../"> + {{ app_label|capfirst }} + </a> + › + {{ 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 %} <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. |
