Author: jacob
Date: 2009-03-23 15:22:56 -0500 (Mon, 23 Mar 2009)
New Revision: 10121
Added:
django/trunk/django/contrib/admin/media/js/actions.js
django/trunk/django/contrib/admin/templates/admin/actions.html
django/trunk/django/contrib/admin/templates/admin/delete_selected_confirmation.html
django/trunk/docs/ref/contrib/admin/
django/trunk/docs/ref/contrib/admin/_images/
django/trunk/docs/ref/contrib/admin/_images/article_actions.png
django/trunk/docs/ref/contrib/admin/_images/article_actions_message.png
django/trunk/docs/ref/contrib/admin/_images/flatfiles_admin.png
django/trunk/docs/ref/contrib/admin/_images/user_actions.png
django/trunk/docs/ref/contrib/admin/_images/users_changelist.png
django/trunk/docs/ref/contrib/admin/actions.txt
django/trunk/docs/ref/contrib/admin/index.txt
django/trunk/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml
Removed:
django/trunk/docs/ref/contrib/_images/flatfiles_admin.png
django/trunk/docs/ref/contrib/_images/users_changelist.png
django/trunk/docs/ref/contrib/admin.txt
Modified:
django/trunk/AUTHORS
django/trunk/django/contrib/admin/__init__.py
django/trunk/django/contrib/admin/helpers.py
django/trunk/django/contrib/admin/media/css/changelists.css
django/trunk/django/contrib/admin/options.py
django/trunk/django/contrib/admin/sites.py
django/trunk/django/contrib/admin/templates/admin/change_list.html
django/trunk/django/contrib/admin/templatetags/admin_list.py
django/trunk/django/contrib/admin/util.py
django/trunk/django/contrib/admin/validation.py
django/trunk/docs/index.txt
django/trunk/docs/ref/contrib/index.txt
django/trunk/tests/regressiontests/admin_registration/models.py
django/trunk/tests/regressiontests/admin_views/models.py
django/trunk/tests/regressiontests/admin_views/tests.py
Log:
Fixed #10505: added support for bulk admin actions, including a
globally-available "delete selected" action. See the documentation for details.
This work started life as Brian Beck's "django-batchadmin." It was rewritten
for inclusion in Django by Alex Gaynor, Jannis Leidel (jezdez), and Martin
Mahner (bartTC). Thanks, guys!
Modified: django/trunk/AUTHORS
===================================================================
--- django/trunk/AUTHORS 2009-03-23 16:37:25 UTC (rev 10120)
+++ django/trunk/AUTHORS 2009-03-23 20:22:56 UTC (rev 10121)
@@ -56,6 +56,7 @@
Ned Batchelder <http://www.nedbatchelder.com/>
[email protected]
Batman
+ Brian Beck <http://blog.brianbeck.com/>
Shannon -jj Behrens <http://jjinux.blogspot.com/>
Esdras Beleza <[email protected]>
Chris Bennett <[email protected]>
@@ -268,6 +269,7 @@
Daniel Lindsley <[email protected]>
Trey Long <[email protected]>
msaelices <[email protected]>
+ Martin Mahner <http://www.mahner.org/>
Matt McClanahan <http://mmcc.cx/>
Frantisek Malina <[email protected]>
Martin Maney <http://www.chipy.org/Martin_Maney>
Modified: django/trunk/django/contrib/admin/__init__.py
===================================================================
--- django/trunk/django/contrib/admin/__init__.py 2009-03-23 16:37:25 UTC
(rev 10120)
+++ django/trunk/django/contrib/admin/__init__.py 2009-03-23 20:22:56 UTC
(rev 10121)
@@ -1,3 +1,4 @@
+from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
from django.contrib.admin.options import StackedInline, TabularInline
from django.contrib.admin.sites import AdminSite, site
Modified: django/trunk/django/contrib/admin/helpers.py
===================================================================
--- django/trunk/django/contrib/admin/helpers.py 2009-03-23 16:37:25 UTC
(rev 10120)
+++ django/trunk/django/contrib/admin/helpers.py 2009-03-23 20:22:56 UTC
(rev 10121)
@@ -6,7 +6,15 @@
from django.utils.encoding import force_unicode
from django.contrib.admin.util import flatten_fieldsets
from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import ugettext_lazy as _
+ACTION_CHECKBOX_NAME = '_selected_action'
+
+class ActionForm(forms.Form):
+ action = forms.ChoiceField(label=_('Action:'))
+
+checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
+
class AdminForm(object):
def __init__(self, form, fieldsets, prepopulated_fields):
self.form, self.fieldsets = form, fieldsets
@@ -132,11 +140,11 @@
self.original.content_type_id =
ContentType.objects.get_for_model(original).pk
self.show_url = original and hasattr(original, 'get_absolute_url')
super(InlineAdminForm, self).__init__(form, fieldsets,
prepopulated_fields)
-
+
def __iter__(self):
for name, options in self.fieldsets:
yield InlineFieldset(self.formset, self.form, name, **options)
-
+
def field_count(self):
# tabular.html uses this function for colspan value.
num_of_fields = 1 # always has at least one field
@@ -149,7 +157,7 @@
def pk_field(self):
return AdminField(self.form, self.formset._pk_field.name, False)
-
+
def fk_field(self):
fk = getattr(self.formset, "fk", None)
if fk:
@@ -169,14 +177,14 @@
def __init__(self, formset, *args, **kwargs):
self.formset = formset
super(InlineFieldset, self).__init__(*args, **kwargs)
-
+
def __iter__(self):
fk = getattr(self.formset, "fk", None)
for field in self.fields:
if fk and fk.name == field:
continue
yield Fieldline(self.form, field)
-
+
class AdminErrorList(forms.util.ErrorList):
"""
Stores all errors for the form/formsets in an add/change stage view.
Modified: django/trunk/django/contrib/admin/media/css/changelists.css
===================================================================
--- django/trunk/django/contrib/admin/media/css/changelists.css 2009-03-23
16:37:25 UTC (rev 10120)
+++ django/trunk/django/contrib/admin/media/css/changelists.css 2009-03-23
20:22:56 UTC (rev 10121)
@@ -50,12 +50,24 @@
#changelist table thead th {
white-space: nowrap;
+ vertical-align: middle;
}
+#changelist table thead th:first-child {
+ width: 1.5em;
+ text-align: center;
+}
+
#changelist table tbody td {
border-left: 1px solid #ddd;
}
+#changelist table tbody td:first-child {
+ border-left: 0;
+ border-right: 1px solid #ddd;
+ text-align: center;
+}
+
#changelist table tfoot {
color: #666;
}
@@ -209,3 +221,35 @@
border-color: #036;
}
+/* ACTIONS */
+
+.filtered .actions {
+ margin-right: 160px !important;
+ border-right: 1px solid #ddd;
+}
+
+#changelist .actions {
+ color: #666;
+ padding: 3px;
+ border-bottom: 1px solid #ddd;
+ background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
+}
+
+#changelist .actions:last-child {
+ border-bottom: none;
+}
+
+#changelist .actions select {
+ border: 1px solid #aaa;
+ margin: 0 0.5em;
+ padding: 1px 2px;
+}
+
+#changelist .actions label {
+ font-size: 11px;
+ margin: 0 0.5em;
+}
+
+#changelist #action-toggle {
+ display: none;
+}
Added: django/trunk/django/contrib/admin/media/js/actions.js
===================================================================
--- django/trunk/django/contrib/admin/media/js/actions.js
(rev 0)
+++ django/trunk/django/contrib/admin/media/js/actions.js 2009-03-23
20:22:56 UTC (rev 10121)
@@ -0,0 +1,19 @@
+var Actions = {
+ init: function() {
+ selectAll = document.getElementById('action-toggle');
+ if (selectAll) {
+ selectAll.style.display = 'inline';
+ addEvent(selectAll, 'change', function() {
+ Actions.checker(this.checked);
+ });
+ }
+ },
+ checker: function(checked) {
+ actionCheckboxes = document.getElementsBySelector('tr
input.action-select');
+ for(var i = 0; i < actionCheckboxes.length; i++) {
+ actionCheckboxes[i].checked = checked;
+ }
+ }
+}
+
+addEvent(window, 'load', Actions.init);
Modified: django/trunk/django/contrib/admin/options.py
===================================================================
--- django/trunk/django/contrib/admin/options.py 2009-03-23 16:37:25 UTC
(rev 10120)
+++ django/trunk/django/contrib/admin/options.py 2009-03-23 20:22:56 UTC
(rev 10121)
@@ -5,9 +5,10 @@
from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets
from django.contrib.admin import helpers
-from django.contrib.admin.util import unquote, flatten_fieldsets,
get_deleted_objects
+from django.contrib.admin.util import unquote, flatten_fieldsets,
get_deleted_objects, model_ngettext, model_format_dict
from django.core.exceptions import PermissionDenied
from django.db import models, transaction
+from django.db.models.fields import BLANK_CHOICE_DASH
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render_to_response
from django.utils.functional import update_wrapper
@@ -16,7 +17,7 @@
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.translation import ngettext, ugettext_lazy
from django.utils.encoding import force_unicode
try:
set
@@ -192,6 +193,12 @@
delete_confirmation_template = None
object_history_template = None
+ # Actions
+ actions = ['delete_selected']
+ action_form = helpers.ActionForm
+ actions_on_top = True
+ actions_on_bottom = False
+
def __init__(self, model, admin_site):
self.model = model
self.opts = model._meta
@@ -200,6 +207,13 @@
for inline_class in self.inlines:
inline_instance = inline_class(self.model, self.admin_site)
self.inline_instances.append(inline_instance)
+ if 'action_checkbox' not in self.list_display:
+ self.list_display = ['action_checkbox'] + list(self.list_display)
+ if not self.list_display_links:
+ for name in self.list_display:
+ if name != 'action_checkbox':
+ self.list_display_links = [name]
+ break
super(ModelAdmin, self).__init__()
def get_urls(self):
@@ -239,6 +253,8 @@
from django.conf import settings
js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
+ if self.actions:
+ js.extend(['js/getElementsBySelector.js', 'js/actions.js'])
if self.prepopulated_fields:
js.append('js/urlify.js')
if self.opts.get_ordered_objects():
@@ -390,7 +406,122 @@
action_flag = DELETION
)
+ def action_checkbox(self, obj):
+ """
+ A list_display column containing a checkbox widget.
+ """
+ return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME,
force_unicode(obj.pk))
+ action_checkbox.short_description = mark_safe('<input type="checkbox"
id="action-toggle" />')
+ action_checkbox.allow_tags = True
+ def get_actions(self, request=None):
+ """
+ Return a dictionary mapping the names of all actions for this
+ ModelAdmin to a tuple of (callable, name, description) for each action.
+ """
+ actions = {}
+ for klass in [self.admin_site] + self.__class__.mro()[::-1]:
+ for action in getattr(klass, 'actions', []):
+ func, name, description = self.get_action(action)
+ actions[name] = (func, name, description)
+ return actions
+
+ def get_action_choices(self, request=None,
default_choices=BLANK_CHOICE_DASH):
+ """
+ Return a list of choices for use in a form object. Each choice is a
+ tuple (name, description).
+ """
+ choices = [] + default_choices
+ for func, name, description in self.get_actions(request).itervalues():
+ choice = (name, description % model_format_dict(self.opts))
+ choices.append(choice)
+ return choices
+
+ def get_action(self, action):
+ """
+ Return a given action from a parameter, which can either be a calable,
+ or the name of a method on the ModelAdmin. Return is a tuple of
+ (callable, name, description).
+ """
+ if callable(action):
+ func = action
+ action = action.__name__
+ elif hasattr(self, action):
+ func = getattr(self, action)
+ if hasattr(func, 'short_description'):
+ description = func.short_description
+ else:
+ description = capfirst(action.replace('_', ' '))
+ return func, action, description
+
+ def delete_selected(self, request, queryset):
+ """
+ Default action which deletes the selected objects.
+
+ In the first step, it displays a confirmation page whichs shows all
+ the deleteable objects or, if the user has no permission one of the
+ related childs (foreignkeys) it displays a "permission denied" message.
+
+ In the second step delete all selected objects and display the change
+ list again.
+ """
+ opts = self.model._meta
+ app_label = opts.app_label
+
+ # Check that the user has delete permission for the actual model
+ if not self.has_delete_permission(request):
+ raise PermissionDenied
+
+ # Populate deletable_objects, a data structure of all related objects
that
+ # will also be deleted.
+
+ # deletable_objects must be a list if we want to use '|unordered_list'
in the template
+ deletable_objects = []
+ perms_needed = set()
+ i = 0
+ for obj in queryset:
+ deletable_objects.append([mark_safe(u'%s: <a href="%s/">%s</a>' %
(escape(force_unicode(capfirst(opts.verbose_name))), obj.pk, escape(obj))), []])
+ get_deleted_objects(deletable_objects[i], perms_needed,
request.user, obj, opts, 1, self.admin_site, levels_to_root=2)
+ i=i+1
+
+ # The user has already confirmed the deletion.
+ # Do the deletion and return a None to display the change list view
again.
+ if request.POST.get('post'):
+ if perms_needed:
+ raise PermissionDenied
+ n = queryset.count()
+ if n:
+ for obj in queryset:
+ obj_display = force_unicode(obj)
+ self.log_deletion(request, obj, obj_display)
+ queryset.delete()
+ self.message_user(request, _("Successfully deleted %d %s.") % (
+ n, model_ngettext(self.opts, n)
+ ))
+ # Return None to display the change list page again.
+ return None
+
+ context = {
+ "title": _("Are you sure?"),
+ "object_name": force_unicode(opts.verbose_name),
+ "deletable_objects": deletable_objects,
+ 'queryset': queryset,
+ "perms_lacking": perms_needed,
+ "opts": opts,
+ "root_path": self.admin_site.root_path,
+ "app_label": app_label,
+ 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
+ }
+
+ # Display the confirmation page
+ return render_to_response(self.delete_confirmation_template or [
+ "admin/%s/%s/delete_selected_confirmation.html" % (app_label,
opts.object_name.lower()),
+ "admin/%s/delete_selected_confirmation.html" % app_label,
+ "admin/delete_selected_confirmation.html"
+ ], context, context_instance=template.RequestContext(request))
+
+ delete_selected.short_description = ugettext_lazy("Delete selected
%(verbose_name_plural)s")
+
def construct_change_message(self, request, form, formsets):
"""
Construct a change message from a changed object.
@@ -529,6 +660,48 @@
self.message_user(request, msg)
return HttpResponseRedirect("../")
+ def response_action(self, request, queryset):
+ """
+ Handle an admin action. This is called if a request is POSTed to the
+ changelist; it returns an HttpResponse if the action was handled, and
+ None otherwise.
+ """
+ # There can be multiple action forms on the page (at the top
+ # and bottom of the change list, for example). Get the action
+ # whose button was pushed.
+ try:
+ action_index = int(request.POST.get('index', 0))
+ except ValueError:
+ action_index = 0
+
+ # Construct the action form.
+ data = request.POST.copy()
+ data.pop(helpers.ACTION_CHECKBOX_NAME, None)
+ data.pop("index", None)
+ action_form = self.action_form(data, auto_id=None)
+ action_form.fields['action'].choices = self.get_action_choices(request)
+
+ # If the form's valid we can handle the action.
+ if action_form.is_valid():
+ action = action_form.cleaned_data['action']
+ func, name, description = self.get_actions(request)[action]
+
+ # Get the list of selected PKs. If nothing's selected, we can't
+ # perform an action on it, so bail.
+ selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
+ if not selected:
+ return None
+
+ response = func(request, queryset.filter(pk__in=selected))
+
+ # Actions may return an HttpResponse, which will be used as the
+ # response from the POST. If not, we'll be a good little HTTP
+ # citizen and redirect back to the changelist page.
+ if isinstance(response, HttpResponse):
+ return response
+ else:
+ return HttpResponseRedirect(".")
+
def add_view(self, request, form_url='', extra_context=None):
"The 'add' admin view for this model."
model = self.model
@@ -721,6 +894,14 @@
return render_to_response('admin/invalid_setup.html',
{'title': _('Database error')})
return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
+ # If the request was POSTed, this might be a bulk action or a bulk
edit.
+ # Try to look up an action first, but if this isn't an action the POST
+ # will fall through to the bulk edit check, below.
+ if request.method == 'POST':
+ response = self.response_action(request,
queryset=cl.get_query_set())
+ if response:
+ return response
+
# 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.
@@ -764,7 +945,11 @@
if formset:
media = self.media + formset.media
else:
- media = None
+ media = self.media
+
+ # Build the action form and populate it with available actions.
+ action_form = self.action_form(auto_id=None)
+ action_form.fields['action'].choices =
self.get_action_choices(request)
context = {
'title': cl.title,
@@ -774,6 +959,9 @@
'has_add_permission': self.has_add_permission(request),
'root_path': self.admin_site.root_path,
'app_label': app_label,
+ 'action_form': action_form,
+ 'actions_on_top': self.actions_on_top,
+ 'actions_on_bottom': self.actions_on_bottom,
}
context.update(extra_context or {})
return render_to_response(self.change_list_template or [
Modified: django/trunk/django/contrib/admin/sites.py
===================================================================
--- django/trunk/django/contrib/admin/sites.py 2009-03-23 16:37:25 UTC (rev
10120)
+++ django/trunk/django/contrib/admin/sites.py 2009-03-23 20:22:56 UTC (rev
10121)
@@ -28,11 +28,11 @@
register() method, and the root() method can then be used as a Django view
function
that presents a full admin interface for the collection of registered
models.
"""
-
+
index_template = None
login_template = None
app_index_template = None
-
+
def __init__(self, name=None):
self._registry = {} # model_class class -> admin_class instance
# TODO Root path is used to calculate urls under the old root() method
@@ -44,17 +44,19 @@
else:
name += '_'
self.name = name
-
+
+ self.actions = []
+
def register(self, model_or_iterable, admin_class=None, **options):
"""
Registers the given model(s) with the given admin class.
-
+
The model(s) should be Model classes, not instances.
-
+
If an admin class isn't given, it will use ModelAdmin (the default
admin options). If keyword arguments are given -- e.g., list_display --
they'll be applied as options to the admin class.
-
+
If a model is already registered, this will raise AlreadyRegistered.
"""
if not admin_class:
@@ -65,13 +67,13 @@
from django.contrib.admin.validation import validate
else:
validate = lambda model, adminclass: None
-
+
if isinstance(model_or_iterable, ModelBase):
model_or_iterable = [model_or_iterable]
for model in model_or_iterable:
if model in self._registry:
raise AlreadyRegistered('The model %s is already registered' %
model.__name__)
-
+
# If we got **options then dynamically construct a subclass of
# admin_class with those **options.
if options:
@@ -80,17 +82,17 @@
# which causes issues later on.
options['__module__'] = __name__
admin_class = type("%sAdmin" % model.__name__, (admin_class,),
options)
-
+
# Validate (which might be a no-op)
validate(admin_class, model)
-
+
# Instantiate the admin class to save in the registry
self._registry[model] = admin_class(model, self)
-
+
def unregister(self, model_or_iterable):
"""
Unregisters the given model(s).
-
+
If a model isn't already registered, this will raise NotRegistered.
"""
if isinstance(model_or_iterable, ModelBase):
@@ -99,44 +101,49 @@
if model not in self._registry:
raise NotRegistered('The model %s is not registered' %
model.__name__)
del self._registry[model]
-
+
+ def add_action(self, action):
+ if not callable(action):
+ raise TypeError("You can only register callable actions through an
admin site")
+ self.actions.append(action)
+
def has_permission(self, request):
"""
Returns True if the given HttpRequest has permission to view
*at least one* page in the admin site.
"""
return request.user.is_authenticated() and request.user.is_staff
-
+
def check_dependencies(self):
"""
Check that all things needed to run the admin have been correctly
installed.
-
+
The default implementation checks that LogEntry, ContentType and the
auth context processor are installed.
"""
from django.contrib.admin.models import LogEntry
from django.contrib.contenttypes.models import ContentType
-
+
if not LogEntry._meta.installed:
raise ImproperlyConfigured("Put 'django.contrib.admin' in your
INSTALLED_APPS setting in order to use the admin application.")
if not ContentType._meta.installed:
raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in
your INSTALLED_APPS setting in order to use the admin application.")
if 'django.core.context_processors.auth' not in
settings.TEMPLATE_CONTEXT_PROCESSORS:
raise ImproperlyConfigured("Put
'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS
setting in order to use the admin application.")
-
+
def admin_view(self, view):
"""
Decorator to create an "admin view attached to this ``AdminSite``. This
wraps the view and provides permission checking by calling
``self.has_permission``.
-
+
You'll want to use this from within ``AdminSite.get_urls()``:
-
+
class MyAdminSite(AdminSite):
-
+
def get_urls(self):
from django.conf.urls.defaults import patterns, url
-
+
urls = super(MyAdminSite, self).get_urls()
urls += patterns('',
url(r'^my_view/$', self.protected_view(some_view))
@@ -148,15 +155,15 @@
return self.login(request)
return view(request, *args, **kwargs)
return update_wrapper(inner, view)
-
+
def get_urls(self):
from django.conf.urls.defaults import patterns, url, include
-
+
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_view(view)(*args, **kwargs)
return update_wrapper(wrapper, view)
-
+
# Admin-site-wide views.
urlpatterns = patterns('',
url(r'^$',
@@ -180,7 +187,7 @@
wrap(self.app_index),
name='%sadmin_app_list' % self.name),
)
-
+
# Add in each model's views.
for model, model_admin in self._registry.iteritems():
urlpatterns += patterns('',
@@ -188,11 +195,11 @@
include(model_admin.urls))
)
return urlpatterns
-
+
def urls(self):
return self.get_urls()
urls = property(urls)
-
+
def password_change(self, request):
"""
Handles the "change password" task -- both form display and validation.
@@ -200,18 +207,18 @@
from django.contrib.auth.views import password_change
return password_change(request,
post_change_redirect='%spassword_change/done/' % self.root_path)
-
+
def password_change_done(self, request):
"""
Displays the "success" page after a password change.
"""
from django.contrib.auth.views import password_change_done
return password_change_done(request)
-
+
def i18n_javascript(self, request):
"""
Displays the i18n JavaScript that the Django admin requires.
-
+
This takes into account the USE_I18N setting. If it's set to False, the
generated JavaScript will be leaner and faster.
"""
@@ -220,23 +227,23 @@
else:
from django.views.i18n import null_javascript_catalog as
javascript_catalog
return javascript_catalog(request, packages='django.conf')
-
+
def logout(self, request):
"""
Logs out the user for the given HttpRequest.
-
+
This should *not* assume the user is already logged in.
"""
from django.contrib.auth.views import logout
return logout(request)
logout = never_cache(logout)
-
+
def login(self, request):
"""
Displays the login form for the given HttpRequest.
"""
from django.contrib.auth.models import User
-
+
# If this isn't already the login page, display it.
if not request.POST.has_key(LOGIN_FORM_KEY):
if request.POST:
@@ -244,14 +251,14 @@
else:
message = ""
return self.display_login_form(request, message)
-
+
# Check that the user accepts cookies.
if not request.session.test_cookie_worked():
message = _("Looks like your browser isn't configured to accept
cookies. Please enable cookies, reload this page, and try again.")
return self.display_login_form(request, message)
else:
request.session.delete_test_cookie()
-
+
# Check the password.
username = request.POST.get('username', None)
password = request.POST.get('password', None)
@@ -271,7 +278,7 @@
else:
message = _("Usernames cannot contain the '@'
character.")
return self.display_login_form(request, message)
-
+
# The user data is correct; log in the user in and continue.
else:
if user.is_active and user.is_staff:
@@ -280,7 +287,7 @@
else:
return self.display_login_form(request, ERROR_MESSAGE)
login = never_cache(login)
-
+
def index(self, request, extra_context=None):
"""
Displays the main admin index page, which lists all of the installed
@@ -291,14 +298,14 @@
for model, model_admin in self._registry.items():
app_label = model._meta.app_label
has_module_perms = user.has_module_perms(app_label)
-
+
if has_module_perms:
perms = {
'add': model_admin.has_add_permission(request),
'change': model_admin.has_change_permission(request),
'delete': model_admin.has_delete_permission(request),
}
-
+
# Check whether user has any perm for this module.
# If so, add the module to the model_list.
if True in perms.values():
@@ -316,15 +323,15 @@
'has_module_perms': has_module_perms,
'models': [model_dict],
}
-
+
# Sort the apps alphabetically.
app_list = app_dict.values()
app_list.sort(lambda x, y: cmp(x['name'], y['name']))
-
+
# Sort the models alphabetically within each app.
for app in app_list:
app['models'].sort(lambda x, y: cmp(x['name'], y['name']))
-
+
context = {
'title': _('Site administration'),
'app_list': app_list,
@@ -335,7 +342,7 @@
context_instance=template.RequestContext(request)
)
index = never_cache(index)
-
+
def display_login_form(self, request, error_message='',
extra_context=None):
request.session.set_test_cookie()
context = {
@@ -348,7 +355,7 @@
return render_to_response(self.login_template or 'admin/login.html',
context,
context_instance=template.RequestContext(request)
)
-
+
def app_index(self, request, app_label, extra_context=None):
user = request.user
has_module_perms = user.has_module_perms(app_label)
@@ -394,46 +401,46 @@
return render_to_response(self.app_index_template or
'admin/app_index.html', context,
context_instance=template.RequestContext(request)
)
-
+
def root(self, request, url):
"""
DEPRECATED. This function is the old way of handling URL resolution,
and
is deprecated in favor of real URL resolution -- see ``get_urls()``.
-
+
This function still exists for backwards-compatibility; it will be
removed in Django 1.3.
"""
import warnings
warnings.warn(
- "AdminSite.root() is deprecated; use include(admin.site.urls)
instead.",
+ "AdminSite.root() is deprecated; use include(admin.site.urls)
instead.",
PendingDeprecationWarning
)
-
+
#
# Again, remember that the following only exists for
# backwards-compatibility. Any new URLs, changes to existing URLs, or
# whatever need to be done up in get_urls(), above!
#
-
+
if request.method == 'GET' and not request.path.endswith('/'):
return http.HttpResponseRedirect(request.path + '/')
-
+
if settings.DEBUG:
self.check_dependencies()
-
+
# Figure out the admin base URL path and stash it for later use
self.root_path = re.sub(re.escape(url) + '$', '', request.path)
-
+
url = url.rstrip('/') # Trim trailing slash, if it exists.
-
+
# The 'logout' view doesn't require that the person is logged in.
if url == 'logout':
return self.logout(request)
-
+
# Check permission to continue or display login form.
if not self.has_permission(request):
return self.login(request)
-
+
if url == '':
return self.index(request)
elif url == 'password_change':
@@ -451,9 +458,9 @@
return self.model_page(request, *url.split('/', 2))
else:
return self.app_index(request, url)
-
+
raise http.Http404('The requested admin page does not exist.')
-
+
def model_page(self, request, app_label, model_name, rest_of_url=None):
"""
DEPRECATED. This is the old way of handling a model view on the admin
@@ -468,7 +475,7 @@
except KeyError:
raise http.Http404("This model exists but has not been registered
with the admin site.")
return admin_obj(request, rest_of_url)
- model_page = never_cache(model_page)
+ model_page = never_cache(model_page)
# This global object represents the default admin site, for the common case.
# You can instantiate AdminSite in your own code to create a custom admin site.
Added: django/trunk/django/contrib/admin/templates/admin/actions.html
===================================================================
--- django/trunk/django/contrib/admin/templates/admin/actions.html
(rev 0)
+++ django/trunk/django/contrib/admin/templates/admin/actions.html
2009-03-23 20:22:56 UTC (rev 10121)
@@ -0,0 +1,5 @@
+{% load i18n %}
+<div class="actions">
+ {% for field in action_form %}<label>{{ field.label }} {{ field
}}</label>{% endfor %}
+ <button type="submit" class="button" title="{% trans "Run the selected
action" %}" name="index" value="{{ action_index|default:0 }}">{% trans "Go"
%}</button>
+</div>
Modified: django/trunk/django/contrib/admin/templates/admin/change_list.html
===================================================================
--- django/trunk/django/contrib/admin/templates/admin/change_list.html
2009-03-23 16:37:25 UTC (rev 10120)
+++ django/trunk/django/contrib/admin/templates/admin/change_list.html
2009-03-23 20:22:56 UTC (rev 10121)
@@ -7,8 +7,8 @@
{% 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 %}
+ {{ media }}
{% endblock %}
{% block bodyclass %}change-list{% endblock %}
@@ -63,14 +63,18 @@
{% endif %}
{% endblock %}
+ <form action="" method="post"{% if cl.formset.is_multipart %}
enctype="multipart/form-data"{% endif %}>
{% 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 result_list %}
+ {% if actions_on_top and cl.full_result_count %}{% admin_actions
%}{% endif %}
+ {% result_list cl %}
+ {% if actions_on_bottom and cl.full_result_count %}{% admin_actions
%}{% endif %}
+ {% endblock %}
{% block pagination %}{% pagination cl %}{% endblock %}
- {% if cl.formset %}</form>{% endif %}
+ </form>
</div>
</div>
{% endblock %}
Added:
django/trunk/django/contrib/admin/templates/admin/delete_selected_confirmation.html
===================================================================
---
django/trunk/django/contrib/admin/templates/admin/delete_selected_confirmation.html
(rev 0)
+++
django/trunk/django/contrib/admin/templates/admin/delete_selected_confirmation.html
2009-03-23 20:22:56 UTC (rev 10121)
@@ -0,0 +1,37 @@
+{% extends "admin/base_site.html" %}
+{% load i18n %}
+
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+ <a href="../../">{% trans "Home" %}</a> ›
+ <a href="../">{{ app_label|capfirst }}</a> ›
+ <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> ›
+ {% trans 'Delete multiple objects' %}
+</div>
+{% endblock %}
+
+{% block content %}
+{% if perms_lacking %}
+ <p>{% blocktrans %}Deleting the {{ object_name }} would result in deleting
related objects, but your account doesn't have permission to delete the
following types of objects:{% endblocktrans %}</p>
+ <ul>
+ {% for obj in perms_lacking %}
+ <li>{{ obj }}</li>
+ {% endfor %}
+ </ul>
+{% else %}
+ <p>{% blocktrans %}Are you sure you want to delete the selected {{
object_name }} objects? All of the following objects and it's related items
will be deleted:{% endblocktrans %}</p>
+ {% for deleteable_object in deletable_objects %}
+ <ul>{{ deleteable_object|unordered_list }}</ul>
+ {% endfor %}
+ <form action="" method="post">
+ <div>
+ {% for obj in queryset %}
+ <input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk
}}" />
+ {% endfor %}
+ <input type="hidden" name="action" value="delete_selected" />
+ <input type="hidden" name="post" value="yes" />
+ <input type="submit" value="{% trans "Yes, I'm sure" %}" />
+ </div>
+ </form>
+{% endif %}
+{% endblock %}
\ No newline at end of file
Modified: django/trunk/django/contrib/admin/templatetags/admin_list.py
===================================================================
--- django/trunk/django/contrib/admin/templatetags/admin_list.py
2009-03-23 16:37:25 UTC (rev 10120)
+++ django/trunk/django/contrib/admin/templatetags/admin_list.py
2009-03-23 20:22:56 UTC (rev 10121)
@@ -325,3 +325,12 @@
def admin_list_filter(cl, spec):
return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
admin_list_filter =
register.inclusion_tag('admin/filter.html')(admin_list_filter)
+
+def admin_actions(context):
+ """
+ Track the number of times the action field has been rendered on the page,
+ so we know which value to use.
+ """
+ context['action_index'] = context.get('action_index', -1) + 1
+ return context
+admin_actions = register.inclusion_tag("admin/actions.html",
takes_context=True)(admin_actions)
Modified: django/trunk/django/contrib/admin/util.py
===================================================================
--- django/trunk/django/contrib/admin/util.py 2009-03-23 16:37:25 UTC (rev
10120)
+++ django/trunk/django/contrib/admin/util.py 2009-03-23 20:22:56 UTC (rev
10121)
@@ -4,7 +4,8 @@
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.encoding import force_unicode
-from django.utils.translation import ugettext as _
+from django.utils.translation import ungettext, ugettext as _
+from django.core.urlresolvers import reverse, NoReverseMatch
def quote(s):
"""
@@ -60,8 +61,27 @@
current = current[-1]
current.append(val)
-def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts,
current_depth, admin_site):
- "Helper function that recursively populates deleted_objects."
+def get_change_view_url(app_label, module_name, pk, admin_site,
levels_to_root):
+ """
+ Returns the url to the admin change view for the given app_label,
+ module_name and primary key.
+ """
+ try:
+ return reverse('%sadmin_%s_%s_change' % (admin_site.name, app_label,
module_name), None, (pk,))
+ except NoReverseMatch:
+ return '%s%s/%s/%s/' % ('../'*levels_to_root, app_label, module_name,
pk)
+
+def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts,
current_depth, admin_site, levels_to_root=4):
+ """
+ Helper function that recursively populates deleted_objects.
+
+ `levels_to_root` defines the number of directories (../) to reach the
+ admin root path. In a change_view this is 4, in a change_list view 2.
+
+ This is for backwards compatibility since the options.delete_selected
+ method uses this function also from a change_list view.
+ This will not be used if we can reverse the URL.
+ """
nh = _nest_help # Bind to local variable for performance
if current_depth > 16:
return # Avoid recursing too deep.
@@ -91,11 +111,13 @@
[u'%s: %s' % (capfirst(related.opts.verbose_name),
force_unicode(sub_obj)), []])
else:
# Display a link to the admin page.
- nh(deleted_objects, current_depth, [mark_safe(u'%s: <a
href="../../../../%s/%s/%s/">%s</a>' %
+ nh(deleted_objects, current_depth, [mark_safe(u'%s: <a
href="%s">%s</a>' %
(escape(capfirst(related.opts.verbose_name)),
- related.opts.app_label,
- related.opts.object_name.lower(),
- sub_obj._get_pk_val(),
+ get_change_view_url(related.opts.app_label,
+ related.opts.object_name.lower(),
+ sub_obj._get_pk_val(),
+ admin_site,
+ levels_to_root),
escape(sub_obj))), []])
get_deleted_objects(deleted_objects, perms_needed, user,
sub_obj, related.opts, current_depth+2, admin_site)
else:
@@ -109,11 +131,13 @@
[u'%s: %s' % (capfirst(related.opts.verbose_name),
force_unicode(sub_obj)), []])
else:
# Display a link to the admin page.
- nh(deleted_objects, current_depth, [mark_safe(u'%s: <a
href="../../../../%s/%s/%s/">%s</a>' %
+ nh(deleted_objects, current_depth, [mark_safe(u'%s: <a
href="%s">%s</a>' %
(escape(capfirst(related.opts.verbose_name)),
- related.opts.app_label,
- related.opts.object_name.lower(),
- sub_obj._get_pk_val(),
+ get_change_view_url(related.opts.app_label,
+ related.opts.object_name.lower(),
+ sub_obj._get_pk_val(),
+ admin_site,
+ levels_to_root),
escape(sub_obj))), []])
get_deleted_objects(deleted_objects, perms_needed, user,
sub_obj, related.opts, current_depth+2, admin_site)
# If there were related objects, and the user doesn't have
@@ -147,11 +171,52 @@
# Display a link to the admin page.
nh(deleted_objects, current_depth, [
mark_safe((_('One or more %(fieldname)s in %(name)s:')
% {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name':
escape(force_unicode(related.opts.verbose_name))}) + \
- (u' <a href="../../../../%s/%s/%s/">%s</a>' % \
- (related.opts.app_label, related.opts.module_name,
sub_obj._get_pk_val(), escape(sub_obj)))), []])
+ (u' <a href="%s">%s</a>' % \
+ (get_change_view_url(related.opts.app_label,
+
related.opts.object_name.lower(),
+ sub_obj._get_pk_val(),
+ admin_site,
+ levels_to_root),
+ escape(sub_obj)))), []])
# If there were related objects, and the user doesn't have
# permission to change them, add the missing perm to perms_needed.
if has_admin and has_related_objs:
p = u'%s.%s' % (related.opts.app_label,
related.opts.get_change_permission())
if not user.has_perm(p):
perms_needed.add(related.opts.verbose_name)
+
+def model_format_dict(obj):
+ """
+ Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
+ typically for use with string formatting.
+
+ `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
+
+ """
+ if isinstance(obj, (models.Model, models.base.ModelBase)):
+ opts = obj._meta
+ elif isinstance(obj, models.query.QuerySet):
+ opts = obj.model._meta
+ else:
+ opts = obj
+ return {
+ 'verbose_name': force_unicode(opts.verbose_name),
+ 'verbose_name_plural': force_unicode(opts.verbose_name_plural)
+ }
+
+def model_ngettext(obj, n=None):
+ """
+ Return the appropriate `verbose_name` or `verbose_name_plural` for `obj`
+ depending on the count `n`.
+
+ `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
+ If `obj` is a `QuerySet` instance, `n` is optional and the length of the
+ `QuerySet` is used.
+
+ """
+ if isinstance(obj, models.query.QuerySet):
+ if n is None:
+ n = obj.count()
+ obj = obj.model
+ d = model_format_dict(obj)
+ return ungettext(d['verbose_name'], d['verbose_name_plural'], n or 0)
Modified: django/trunk/django/contrib/admin/validation.py
===================================================================
--- django/trunk/django/contrib/admin/validation.py 2009-03-23 16:37:25 UTC
(rev 10120)
+++ django/trunk/django/contrib/admin/validation.py 2009-03-23 20:22:56 UTC
(rev 10121)
@@ -63,7 +63,7 @@
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)
@@ -76,7 +76,7 @@
field = opts.get_field_by_name(field_name)[0]
except models.FieldDoesNotExist:
raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a
"
- "field, '%s', not defiend on %s."
+ "field, '%s', not defiend on %s."
% (cls.__name__, idx, field_name, model.__name__))
if field_name not in cls.list_display:
raise ImproperlyConfigured("'%s.list_editable[%d]' refers to "
@@ -89,7 +89,7 @@
if not cls.list_display_links and cls.list_display[0] in
cls.list_editable:
raise ImproperlyConfigured("'%s.list_editable[%d]' refers to"
" the first field in list_display, '%s', which can't be"
- " used unless list_display_links is set."
+ " used unless list_display_links is set."
% (cls.__name__, idx, cls.list_display[0]))
if not field.editable:
raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a
"
@@ -127,6 +127,14 @@
continue
get_field(cls, model, opts, 'ordering[%d]' % idx, field)
+ if cls.actions:
+ check_isseq(cls, 'actions', cls.actions)
+ for idx, item in enumerate(cls.actions):
+ if (not callable(item)) and (not hasattr(cls, item)):
+ raise ImproperlyConfigured("'%s.actions[%d]' is neither a "
+ "callable nor a method on %s" % (cls.__name__, idx,
cls.__name__))
+
+
# list_select_related = False
# save_as = False
# save_on_top = False
@@ -135,6 +143,7 @@
raise ImproperlyConfigured("'%s.%s' should be a boolean."
% (cls.__name__, attr))
+
# inlines = []
if hasattr(cls, 'inlines'):
check_isseq(cls, 'inlines', cls.inlines)
Modified: django/trunk/docs/index.txt
===================================================================
--- django/trunk/docs/index.txt 2009-03-23 16:37:25 UTC (rev 10120)
+++ django/trunk/docs/index.txt 2009-03-23 20:22:56 UTC (rev 10121)
@@ -78,7 +78,7 @@
Other batteries included
========================
- * :ref:`Admin site <ref-contrib-admin>`
+ * :ref:`Admin site <ref-contrib-admin>` | :ref:`Admin actions
<ref-contrib-admin-actions>`
* :ref:`Authentication <topics-auth>`
* :ref:`Cache system <topics-cache>`
* :ref:`Conditional content processing <topics-conditional-processing>`
Deleted: django/trunk/docs/ref/contrib/_images/flatfiles_admin.png
===================================================================
--- django/trunk/docs/ref/contrib/_images/flatfiles_admin.png 2009-03-23
16:37:25 UTC (rev 10120)
+++ django/trunk/docs/ref/contrib/_images/flatfiles_admin.png 2009-03-23
20:22:56 UTC (rev 10121)
@@ -1,294 +0,0 @@
-‰PNG
-
-
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---