Ticket #10505: admin-actions.2.diff
File admin-actions.2.diff, 23.9 KB (added by , 16 years ago) |
---|
-
django/contrib/admin/helpers.py
diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index aaa2e30..aac8911 100644
a b from django.utils.safestring import mark_safe 6 6 from django.utils.encoding import force_unicode 7 7 from django.contrib.admin.util import flatten_fieldsets 8 8 from django.contrib.contenttypes.models import ContentType 9 from django.utils.translation import ugettext_lazy as _ 10 11 ACTION_CHECKBOX_NAME = 'selected' 12 13 class ActionForm(forms.Form): 14 action = forms.ChoiceField(label=_('Action:')) 15 16 checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False) 9 17 10 18 class AdminForm(object): 11 19 def __init__(self, form, fieldsets, prepopulated_fields): -
django/contrib/admin/media/css/changelists.css
diff --git a/django/contrib/admin/media/css/changelists.css b/django/contrib/admin/media/css/changelists.css index 40142f5..6294a28 100644
a b 50 50 51 51 #changelist table thead th { 52 52 white-space: nowrap; 53 vertical-align: middle; 53 54 } 54 55 55 56 #changelist table tbody td { … … 209 210 border-color: #036; 210 211 } 211 212 213 .filtered #action-form .actions { 214 margin-right: 160px !important; 215 border-right: 1px solid #ddd; 216 } 217 218 #action-form .actions { 219 color: #666; 220 padding: 3px; 221 font-weight: bold; 222 background: #efefef url(../img/admin/nav-bg.gif); 223 } 224 225 #action-form .actions:last-child { 226 border-bottom: none; 227 } 228 229 #action-form .actions select { 230 border: 1px solid #aaa; 231 margin: 0 0.5em; 232 } 233 234 #action-toggle { 235 display: none; 236 } 237 238 #action-form .actions label { 239 font-size: 11px; 240 margin-left: 0.5em; 241 } 242 243 #action-form tbody tr input.action-select { 244 margin: 0; 245 } 246 247 #action-form thead th:first-child { 248 width: 1.5em; 249 text-align: center; 250 } 251 252 #action-form tbody td:first-child { 253 border-left: 0; 254 border-right: 1px solid #ddd; 255 text-align: center; 256 } -
new file django/contrib/admin/media/js/actions.js
diff --git a/django/contrib/admin/media/js/actions.js b/django/contrib/admin/media/js/actions.js new file mode 100644 index 0000000..febb0c1
- + 1 var Actions = { 2 init: function() { 3 selectAll = document.getElementById('action-toggle'); 4 if (selectAll) { 5 selectAll.style.display = 'inline'; 6 addEvent(selectAll, 'change', function() { 7 Actions.checker(this.checked); 8 }); 9 } 10 }, 11 checker: function(checked) { 12 actionCheckboxes = document.getElementsBySelector('tr input.action-select'); 13 for(var i = 0; i < actionCheckboxes.length; i++) { 14 actionCheckboxes[i].checked = checked; 15 } 16 } 17 } 18 19 addEvent(window, 'load', Actions.init); -
django/contrib/admin/options.py
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 859229e..4e1df38 100644
a b from django.forms.models import BaseInlineFormSet 5 5 from django.contrib.contenttypes.models import ContentType 6 6 from django.contrib.admin import widgets 7 7 from django.contrib.admin import helpers 8 from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects 8 from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict 9 9 from django.core.exceptions import PermissionDenied 10 10 from django.db import models, transaction 11 from django.db.models.fields import BLANK_CHOICE_DASH 11 12 from django.http import Http404, HttpResponse, HttpResponseRedirect 12 13 from django.shortcuts import get_object_or_404, render_to_response 13 14 from django.utils.functional import update_wrapper … … from django.utils.html import escape 15 16 from django.utils.safestring import mark_safe 16 17 from django.utils.functional import curry 17 18 from django.utils.text import capfirst, get_text_list 18 from django.utils.translation import ugettext as _ 19 from django.utils.translation import ugettext as _, ugettext_lazy 19 20 from django.utils.encoding import force_unicode 20 21 try: 21 22 set … … class ModelAdmin(BaseModelAdmin): 172 173 "Encapsulates all admin options and functionality for a given model." 173 174 __metaclass__ = forms.MediaDefiningClass 174 175 175 list_display = (' __str__',)176 list_display = ('action_checkbox', '__str__',) 176 177 list_display_links = () 177 178 list_filter = () 178 179 list_select_related = False … … class ModelAdmin(BaseModelAdmin): 190 191 delete_confirmation_template = None 191 192 object_history_template = None 192 193 194 # Actions 195 actions = ['delete_selected'] 196 action_form = helpers.ActionForm 197 actions_on_top = False 198 actions_on_bottom = True 199 193 200 def __init__(self, model, admin_site): 194 201 self.model = model 195 202 self.opts = model._meta … … class ModelAdmin(BaseModelAdmin): 198 205 for inline_class in self.inlines: 199 206 inline_instance = inline_class(self.model, self.admin_site) 200 207 self.inline_instances.append(inline_instance) 208 if 'action_checkbox' not in self.list_display: 209 self.list_display = list(self.list_display) 210 self.list_display.insert(0, 'action_checkbox') 211 if not self.list_display_links: 212 for name in self.list_display: 213 if name != 'action_checkbox': 214 self.list_display_links = [name] 215 break 216 self.callable_actions = {} 217 for action in getattr(self, 'actions', []): 218 if callable(action): 219 self.callable_actions[action.__name__] = action 201 220 super(ModelAdmin, self).__init__() 202 221 203 222 def get_urls(self): … … class ModelAdmin(BaseModelAdmin): 237 256 from django.conf import settings 238 257 239 258 js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] 259 if self.actions: 260 js.extend(['js/getElementsBySelector.js', 'js/actions.js']) 240 261 if self.prepopulated_fields: 241 262 js.append('js/urlify.js') 242 263 if self.opts.get_ordered_objects(): … … class ModelAdmin(BaseModelAdmin): 365 386 action_flag = DELETION 366 387 ) 367 388 389 def action_checkbox(self, obj): 390 """ 391 A list_display column containing a checkbox widget. 392 """ 393 return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_unicode(obj.pk)) 394 action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />') 395 action_checkbox.allow_tags = True 396 397 def get_action_choices(self, default_choices=BLANK_CHOICE_DASH): 398 choices = [] + default_choices 399 for action in getattr(self, 'actions', []): 400 func, name, description, instance_action = self.get_action(action) 401 choice = (name, description % model_format_dict(self.opts)) 402 choices.append(choice) 403 return choices 404 405 def get_action(self, action): 406 is_instance_action = False 407 if callable(action): 408 func = action 409 action = action.__name__ 410 elif hasattr(self, action): 411 func = getattr(self, action) 412 elif hasattr(self.model, action): 413 func = getattr(self.model, action) 414 is_instance_action = True 415 else: 416 if action in [name for name in self.callable_actions]: 417 return self.get_action(self.callable_actions[action]) 418 raise AttributeError, \ 419 "'%s' model or '%s' have no action '%s'" % \ 420 (self.opts.object_name, self.__class__.__name__, action) 421 if hasattr(func, 'short_description'): 422 description = func.short_description 423 else: 424 description = capfirst(action.replace('_', ' ')) 425 return func, action, description, is_instance_action 426 427 def delete_selected(self, request, selected): 428 """ 429 Default action which deletes the selected objects. 430 """ 431 if self.has_delete_permission(request): 432 n = selected.count() 433 if n: 434 for obj in selected: 435 obj_display = force_unicode(obj) 436 self.log_deletion(request, obj, obj_display) 437 selected.delete() 438 self.message_user(request, _("Successfully deleted %d %s.") % ( 439 n, model_ngettext(self.opts, n) 440 )) 441 delete_selected.short_description = ugettext_lazy("Delete selected %(verbose_name_plural)s") 368 442 369 443 def construct_change_message(self, request, form, formsets): 370 444 """ … … class ModelAdmin(BaseModelAdmin): 503 577 else: 504 578 self.message_user(request, msg) 505 579 return HttpResponseRedirect("../") 580 581 def response_action(self, request, changelist): 582 if request.method == 'POST': 583 # There can be multiple action forms on the page (at the top 584 # and bottom of the change list, for example). Get the action 585 # whose button was pushed. 586 try: 587 action_index = int(request.POST.get('index', 0)) 588 except ValueError: 589 action_index = 0 590 data = {} 591 for key in request.POST: 592 if key not in (helpers.ACTION_CHECKBOX_NAME, 'index'): 593 data[key] = request.POST.getlist(key)[action_index] 594 action_form = self.action_form(data, auto_id=None) 595 action_form.fields['action'].choices = self.get_action_choices() 596 597 if action_form.is_valid(): 598 action = action_form.cleaned_data['action'] 599 func, name, description, instance_action = self.get_action(action) 600 selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) 601 results = changelist.get_query_set().filter(pk__in=selected) 602 response = None 603 if callable(func): 604 if instance_action: 605 for obj in results: 606 getattr(obj, name)(request) 607 else: 608 response = func(request, results) 609 if isinstance(response, HttpResponse): 610 return response 611 else: 612 redirect_to = request.META.get('HTTP_REFERER') or "." 613 return HttpResponseRedirect(redirect_to) 614 else: 615 action_form = self.action_form(auto_id=None) 616 action_form.fields['action'].choices = self.get_action_choices() 617 return action_form 506 618 507 619 def add_view(self, request, form_url='', extra_context=None): 508 620 "The 'add' admin view for this model." … … class ModelAdmin(BaseModelAdmin): 696 808 return render_to_response('admin/invalid_setup.html', {'title': _('Database error')}) 697 809 return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') 698 810 811 action_response = self.response_action(request, cl) 812 if isinstance(action_response, HttpResponse): 813 return action_response 814 699 815 context = { 700 816 'title': cl.title, 701 817 'is_popup': cl.is_popup, 702 818 'cl': cl, 819 'media': mark_safe(self.media), 703 820 'has_add_permission': self.has_add_permission(request), 704 821 'root_path': self.admin_site.root_path, 705 822 'app_label': app_label, 823 'action_form': action_response, 824 'actions_on_top': self.actions_on_top, 825 'actions_on_bottom': self.actions_on_bottom, 706 826 } 707 827 context.update(extra_context or {}) 708 828 return render_to_response(self.change_list_template or [ -
new file django/contrib/admin/templates/admin/actions.html
diff --git a/django/contrib/admin/templates/admin/actions.html b/django/contrib/admin/templates/admin/actions.html new file mode 100644 index 0000000..bf4b975
- + 1 {% load i18n %} 2 <div class="actions"> 3 {% for field in action_form %}<label>{{ field.label }} {{ field }}</label>{% endfor %} 4 <button type="submit" class="button" title="{% trans "Run the selected action" %}" name="index" value="{{ action_index|default:0 }}">{% trans "Go" %}</button> 5 </div> -
django/contrib/admin/templates/admin/change_list.html
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index 5f8a430..1f00318 100644
a b 3 3 4 4 {% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/changelists.css" />{% endblock %} 5 5 6 {% block extrahead %}{{ block.super }}{{ media }}{% endblock %} 7 6 8 {% block bodyclass %}change-list{% endblock %} 7 9 8 10 {% 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 %} … … 31 33 {% endif %} 32 34 {% endblock %} 33 35 34 {% block result_list %}{% result_list cl %}{% endblock %} 36 {% block result_list %} 37 <form id="action-form" method="post" action=""> 38 {% if actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %} 39 {% result_list cl %} 40 {% if actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %} 41 </form> 42 {% endblock %} 35 43 {% block pagination %}{% pagination cl %}{% endblock %} 36 44 </div> 37 45 </div> -
django/contrib/admin/templatetags/admin_list.py
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 37cdb91..59fc4ff 100644
a b search_form = register.inclusion_tag('admin/search_form.html')(search_form) 311 311 def admin_list_filter(cl, spec): 312 312 return {'title': spec.title(), 'choices' : list(spec.choices(cl))} 313 313 admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter) 314 315 def admin_actions(context): 316 """ 317 Track the number of times the action field has been rendered on the page, 318 so we know which value to use. 319 """ 320 context['action_index'] = context.get('action_index', -1) + 1 321 return context 322 admin_actions = register.inclusion_tag("admin/actions.html", takes_context=True)(admin_actions) -
django/contrib/admin/util.py
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 4164c8a..7c2b56b 100644
a b from django.utils.html import escape 4 4 from django.utils.safestring import mark_safe 5 5 from django.utils.text import capfirst 6 6 from django.utils.encoding import force_unicode 7 from django.utils.translation import u gettext as _7 from django.utils.translation import ungettext, ugettext as _ 8 8 9 9 def quote(s): 10 10 """ … … def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ 155 155 p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission()) 156 156 if not user.has_perm(p): 157 157 perms_needed.add(related.opts.verbose_name) 158 159 def model_format_dict(obj): 160 """ 161 Return a `dict` with keys 'verbose_name' and 'verbose_name_plural', 162 typically for use with string formatting. 163 164 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. 165 166 """ 167 if isinstance(obj, (models.Model, models.base.ModelBase)): 168 opts = obj._meta 169 elif isinstance(obj, models.query.QuerySet): 170 opts = obj.model._meta 171 else: 172 opts = obj 173 return { 174 'verbose_name': force_unicode(opts.verbose_name), 175 'verbose_name_plural': force_unicode(opts.verbose_name_plural) 176 } 177 178 def model_ngettext(obj, n=None): 179 """ 180 Return the appropriate `verbose_name` or `verbose_name_plural` for `obj` 181 depending on the count `n`. 182 183 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. 184 If `obj` is a `QuerySet` instance, `n` is optional and the length of the 185 `QuerySet` is used. 186 187 """ 188 if isinstance(obj, models.query.QuerySet): 189 if n is None: 190 n = obj.count() 191 obj = obj.model 192 d = model_format_dict(obj) 193 return ungettext(d['verbose_name'], d['verbose_name_plural'], n or 0) -
new file tests/regressiontests/admin_views/fixtures/admin-views-actions.xml
diff --git a/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml b/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml new file mode 100644 index 0000000..1f6cc7f
- + 1 <?xml version="1.0" encoding="utf-8"?> 2 <django-objects version="1.0"> 3 <object pk="1" model="admin_views.subscriber"> 4 <field type="CharField" name="name">John Doe</field> 5 <field type="CharField" name="email">john@example.org</field> 6 </object> 7 <object pk="2" model="admin_views.subscriber"> 8 <field type="CharField" name="name">Max Mustermann</field> 9 <field type="CharField" name="email">max@example.org</field> 10 </object> 11 <object pk="1" model="admin_views.directsubscriber"> 12 <field type="CharField" name="name">John Doe</field> 13 <field type="CharField" name="email">john@example.org</field> 14 <field type="BooleanField" name="paid">True</field> 15 </object> 16 <object pk="1" model="admin_views.externalsubscriber"> 17 <field type="CharField" name="name">John Doe</field> 18 <field type="CharField" name="email">john@example.org</field> 19 </object> 20 </django-objects> 21 No newline at end of file -
tests/regressiontests/admin_views/models.py
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index d849a7b..fa4faa3 100644
a b 1 1 # -*- coding: utf-8 -*- 2 2 from django.db import models 3 3 from django.contrib import admin 4 from django.core.mail import EmailMessage 4 5 5 6 class Section(models.Model): 6 7 """ … … class PersonaAdmin(admin.ModelAdmin): 177 178 BarAccountAdmin 178 179 ) 179 180 181 class Subscriber(models.Model): 182 name = models.CharField(blank=False, max_length=80) 183 email = models.EmailField(blank=False, max_length=175) 184 185 def __unicode__(self): 186 return "%s (%s)" % (self.name, self.email) 187 188 class SubscriberAdmin(admin.ModelAdmin): 189 actions = ['delete_selected', 'mail_admin'] 190 191 def mail_admin(self, request, selected): 192 EmailMessage( 193 'Greetings from a ModelAdmin action', 194 'This is the test email from a admin action', 195 'from@example.com', 196 ['to@example.com'] 197 ).send() 198 199 class DirectSubscriber(Subscriber): 200 paid = models.BooleanField(default=False) 201 202 def direct_mail(self, request): 203 EmailMessage( 204 'Greetings from a model action', 205 'This is the test email from a model action', 206 'from@example.com', 207 [self.email] 208 ).send() 209 210 class DirectSubscriberAdmin(admin.ModelAdmin): 211 actions = ['direct_mail'] 212 213 class ExternalSubscriber(Subscriber): 214 pass 215 216 def external_mail(request, selected): 217 EmailMessage( 218 'Greetings from a function action', 219 'This is the test email from a function action', 220 'from@example.com', 221 ['to@example.com'] 222 ).send() 223 224 def redirect_to(request, selected): 225 from django.http import HttpResponseRedirect 226 return HttpResponseRedirect('/some-where-else/') 227 228 class ExternalSubscriberAdmin(admin.ModelAdmin): 229 actions = [external_mail, redirect_to] 230 180 231 admin.site.register(Article, ArticleAdmin) 181 232 admin.site.register(CustomArticle, CustomArticleAdmin) 182 233 admin.site.register(Section, inlines=[ArticleInline]) … … admin.site.register(ModelWithStringPrimaryKey) 184 235 admin.site.register(Color) 185 236 admin.site.register(Thing, ThingAdmin) 186 237 admin.site.register(Persona, PersonaAdmin) 238 admin.site.register(Subscriber, SubscriberAdmin) 239 admin.site.register(DirectSubscriber, DirectSubscriberAdmin) 240 admin.site.register(ExternalSubscriber, ExternalSubscriberAdmin) 187 241 188 242 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. 189 243 # That way we cover all four cases: -
tests/regressiontests/admin_views/tests.py
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index bf198bc..612e1ec 100644
a b from django.contrib.contenttypes.models import ContentType 8 8 from django.contrib.admin.models import LogEntry 9 9 from django.contrib.admin.sites import LOGIN_FORM_KEY 10 10 from django.contrib.admin.util import quote 11 from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME 11 12 from django.utils.html import escape 12 13 13 14 # local test models 14 from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Persona, FooAccount, BarAccount 15 from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Persona, FooAccount, BarAccount, Subscriber, DirectSubscriber, ExternalSubscriber 15 16 16 17 try: 17 18 set … … class AdminInheritedInlinesTest(TestCase): 805 806 self.failUnlessEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user) 806 807 self.failUnlessEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user) 807 808 self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2) 809 810 from django.core import mail 811 812 class AdminActionsTest(TestCase): 813 fixtures = ['admin-views-users.xml', 'admin-views-actions.xml'] 814 815 def setUp(self): 816 self.client.login(username='super', password='secret') 817 818 def tearDown(self): 819 self.client.logout() 820 821 def test_model_admin_custom_action(self): 822 "Tests a custom action defined in a ModelAdmin method" 823 action_data = { 824 ACTION_CHECKBOX_NAME: [1], 825 'action' : 'mail_admin', 826 'index': 0, 827 } 828 response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) 829 self.assertEquals(len(mail.outbox), 1) 830 self.assertEquals(mail.outbox[0].subject, 'Greetings from a ModelAdmin action') 831 832 def test_model_admin_default_delete_action(self): 833 "Tests the default delete action defined as a ModelAdmin method" 834 action_data = { 835 ACTION_CHECKBOX_NAME: [1, 2], 836 'action' : 'delete_selected', 837 'index': 0, 838 } 839 response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) 840 self.failUnlessEqual(Subscriber.objects.count(), 0) 841 842 def test_custom_model_instance_action(self): 843 "Tests a custom action defined in a model method" 844 action_data = { 845 ACTION_CHECKBOX_NAME: [1], 846 'action' : 'direct_mail', 847 'index': 0, 848 'paid': 1, 849 } 850 response = self.client.post('/test_admin/admin/admin_views/directsubscriber/', action_data) 851 self.assertEquals(len(mail.outbox), 1) 852 self.assertEquals(mail.outbox[0].subject, 'Greetings from a model action') 853 self.assertEquals(mail.outbox[0].to, [u'john@example.org']) 854 855 def test_custom_function_mail_action(self): 856 "Tests a custom action defined in a function" 857 action_data = { 858 ACTION_CHECKBOX_NAME: [1], 859 'action' : 'external_mail', 860 'index': 0, 861 } 862 response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) 863 self.assertEquals(len(mail.outbox), 1) 864 self.assertEquals(mail.outbox[0].subject, 'Greetings from a function action') 865 866 def test_custom_function_action_with_redirect(self): 867 "Tests a custom action defined in a function" 868 action_data = { 869 ACTION_CHECKBOX_NAME: [1], 870 'action' : 'redirect_to', 871 'index': 0, 872 } 873 response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) 874 self.failUnlessEqual(response.status_code, 302)