Ticket #10505: admin-actions.7.diff
File admin-actions.7.diff, 34.4 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..649cff7 100644
a b 50 50 51 51 #changelist table thead th { 52 52 white-space: nowrap; 53 vertical-align: middle; 54 } 55 56 #changelist table thead th:first-child { 57 width: 1.5em; 58 text-align: center; 53 59 } 54 60 55 61 #changelist table tbody td { 56 62 border-left: 1px solid #ddd; 57 63 } 58 64 65 #changelist table tbody td:first-child { 66 border-left: 0; 67 border-right: 1px solid #ddd; 68 text-align: center; 69 } 70 59 71 #changelist table tfoot { 60 72 color: #666; 61 73 } … … 209 221 border-color: #036; 210 222 } 211 223 224 /* ACTIONS */ 225 226 .filtered .actions { 227 margin-right: 160px !important; 228 border-right: 1px solid #ddd; 229 } 230 231 #changelist .actions { 232 color: #666; 233 padding: 3px; 234 border-bottom: 1px solid #ddd; 235 background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; 236 } 237 238 #changelist .actions:last-child { 239 border-bottom: none; 240 } 241 242 #changelist .actions select { 243 border: 1px solid #aaa; 244 margin: 0 0.5em; 245 padding: 1px 2px; 246 } 247 248 #changelist .actions label { 249 font-size: 11px; 250 margin: 0 0.5em; 251 } 252 253 #changelist #action-toggle { 254 display: none; 255 } -
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 69f52aa..1f01660 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.safestring import mark_safe 16 17 from django.utils.functional import curry 17 18 from django.utils.text import capfirst, get_text_list 18 19 from django.utils.translation import ugettext as _ 19 from django.utils.translation import ngettext 20 from django.utils.translation import ngettext, ugettext_lazy 20 21 from django.utils.encoding import force_unicode 21 22 try: 22 23 set … … class BaseModelAdmin(object): 172 173 class ModelAdmin(BaseModelAdmin): 173 174 "Encapsulates all admin options and functionality for a given model." 174 175 __metaclass__ = forms.MediaDefiningClass 175 176 list_display = (' __str__',)176 177 list_display = ('action_checkbox', '__str__',) 177 178 list_display_links = () 178 179 list_filter = () 179 180 list_select_related = False … … class ModelAdmin(BaseModelAdmin): 192 193 delete_confirmation_template = None 193 194 object_history_template = None 194 195 196 # Actions 197 actions = ['delete_selected'] 198 action_form = helpers.ActionForm 199 actions_on_top = False 200 actions_on_bottom = True 201 195 202 def __init__(self, model, admin_site): 196 203 self.model = model 197 204 self.opts = model._meta … … class ModelAdmin(BaseModelAdmin): 200 207 for inline_class in self.inlines: 201 208 inline_instance = inline_class(self.model, self.admin_site) 202 209 self.inline_instances.append(inline_instance) 210 if 'action_checkbox' not in self.list_display: 211 self.list_display = list(self.list_display) 212 self.list_display.insert(0, 'action_checkbox') 213 if not self.list_display_links: 214 for name in self.list_display: 215 if name != 'action_checkbox': 216 self.list_display_links = [name] 217 break 203 218 super(ModelAdmin, self).__init__() 204 219 205 220 def get_urls(self): … … class ModelAdmin(BaseModelAdmin): 239 254 from django.conf import settings 240 255 241 256 js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] 257 if self.actions: 258 js.extend(['js/getElementsBySelector.js', 'js/actions.js']) 242 259 if self.prepopulated_fields: 243 260 js.append('js/urlify.js') 244 261 if self.opts.get_ordered_objects(): … … class ModelAdmin(BaseModelAdmin): 390 407 action_flag = DELETION 391 408 ) 392 409 410 def action_checkbox(self, obj): 411 """ 412 A list_display column containing a checkbox widget. 413 """ 414 return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_unicode(obj.pk)) 415 action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />') 416 action_checkbox.allow_tags = True 417 418 def get_action_choices(self, default_choices=BLANK_CHOICE_DASH): 419 choices = [] + default_choices 420 for action in getattr(self, 'actions', []): 421 func, name, description, instance_action = self.get_action(action) 422 choice = (name, description % model_format_dict(self.opts)) 423 choices.append(choice) 424 return choices 425 426 def get_action(self, action): 427 is_instance_action = False 428 if callable(action): 429 func = action 430 action = action.__name__ 431 elif hasattr(self, action): 432 func = getattr(self, action) 433 elif hasattr(self.model, action): 434 func = getattr(self.model, action) 435 is_instance_action = True 436 else: 437 callable_actions = {} 438 for item in getattr(self, 'actions', []): 439 if callable(item): 440 callable_actions[item.__name__] = item 441 if action in callable_actions: 442 return self.get_action(callable_actions[action]) 443 raise AttributeError, \ 444 "'%s' model or '%s' have no action '%s'" % \ 445 (self.opts.object_name, self.__class__.__name__, action) 446 if hasattr(func, 'short_description'): 447 description = func.short_description 448 else: 449 description = capfirst(action.replace('_', ' ')) 450 return func, action, description, is_instance_action 451 452 def delete_selected(self, request, selected): 453 """ 454 Default action which deletes the selected objects. 455 """ 456 opts = self.model._meta 457 app_label = opts.app_label 458 459 if not self.has_delete_permission(request): 460 raise PermissionDenied 461 462 # Populate deleted_objects, a data structure of all related objects that 463 # will also be deleted. 464 465 # deleted_objects must be a list if we want to use '|unordered_list' in the template 466 deleted_objects = [] 467 perms_needed = set() 468 i = 0 469 for obj in selected: 470 deleted_objects.append([mark_safe(u'%s: <a href="%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), obj.pk, escape(obj))), []]) 471 get_deleted_objects(deleted_objects[i], perms_needed, request.user, obj, opts, 1, self.admin_site, parents_to_home=2) 472 i=i+1 473 474 # The user has already confirmed the deletion. 475 if request.POST.get('post'): 476 if perms_needed: 477 raise PermissionDenied 478 n = selected.count() 479 if n: 480 for obj in selected: 481 obj_display = force_unicode(obj) 482 self.log_deletion(request, obj, obj_display) 483 selected.delete() 484 self.message_user(request, _("Successfully deleted %d %s.") % ( 485 n, model_ngettext(self.opts, n) 486 )) 487 return None 488 489 context = { 490 "title": _("Are you sure?"), 491 "object_name": force_unicode(opts.verbose_name), 492 "deleted_objects": deleted_objects, 493 'selected': selected, 494 "perms_lacking": perms_needed, 495 "opts": opts, 496 "root_path": self.admin_site.root_path, 497 "app_label": app_label, 498 } 499 return render_to_response(self.delete_confirmation_template or [ 500 "admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.object_name.lower()), 501 "admin/%s/delete_selected_confirmation.html" % app_label, 502 "admin/delete_selected_confirmation.html" 503 ], context, context_instance=template.RequestContext(request)) 504 505 delete_selected.short_description = ugettext_lazy("Delete selected %(verbose_name_plural)s") 393 506 394 507 def construct_change_message(self, request, form, formsets): 395 508 """ … … class ModelAdmin(BaseModelAdmin): 529 642 self.message_user(request, msg) 530 643 return HttpResponseRedirect("../") 531 644 645 def response_action(self, request, queryset): 646 if request.method == 'POST': 647 # There can be multiple action forms on the page (at the top 648 # and bottom of the change list, for example). Get the action 649 # whose button was pushed. 650 try: 651 action_index = int(request.POST.get('index', 0)) 652 except ValueError: 653 action_index = 0 654 data = {} 655 for key in request.POST: 656 if key not in (helpers.ACTION_CHECKBOX_NAME, 'index'): 657 data[key] = request.POST.getlist(key)[action_index] 658 action_form = self.action_form(data, auto_id=None) 659 action_form.fields['action'].choices = self.get_action_choices() 660 661 if action_form.is_valid(): 662 action = action_form.cleaned_data['action'] 663 func, name, description, instance_action = self.get_action(action) 664 selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) 665 results = queryset.filter(pk__in=selected) 666 response = None 667 if callable(func): 668 if instance_action: 669 for obj in results: 670 getattr(obj, name)(request) 671 else: 672 response = func(request, results) 673 if isinstance(response, HttpResponse): 674 return response 675 else: 676 redirect_to = request.META.get('HTTP_REFERER') or "." 677 return HttpResponseRedirect(redirect_to) 678 else: 679 action_form = self.action_form(auto_id=None) 680 action_form.fields['action'].choices = self.get_action_choices() 681 return action_form 682 532 683 def add_view(self, request, form_url='', extra_context=None): 533 684 "The 'add' admin view for this model." 534 685 model = self.model … … class ModelAdmin(BaseModelAdmin): 721 872 return render_to_response('admin/invalid_setup.html', {'title': _('Database error')}) 722 873 return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') 723 874 875 action_form_or_response = self.response_action(request, queryset=cl.get_query_set()) 876 if isinstance(action_form_or_response, HttpResponse): 877 return action_form_or_response 878 724 879 # If we're allowing changelist editing, we need to construct a formset 725 880 # for the changelist given all the fields to be edited. Then we'll 726 881 # use the formset to validate/process POSTed data. … … class ModelAdmin(BaseModelAdmin): 764 919 if formset: 765 920 media = self.media + formset.media 766 921 else: 767 media = None768 922 media = self.media 923 769 924 context = { 770 925 'title': cl.title, 771 926 'is_popup': cl.is_popup, … … class ModelAdmin(BaseModelAdmin): 774 929 'has_add_permission': self.has_add_permission(request), 775 930 'root_path': self.admin_site.root_path, 776 931 'app_label': app_label, 932 'action_form': action_form_or_response, 933 'actions_on_top': self.actions_on_top, 934 'actions_on_bottom': self.actions_on_bottom, 777 935 } 778 936 context.update(extra_context or {}) 779 937 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 dca5b80..63254b8 100644
a b 7 7 {% if cl.formset %} 8 8 <link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" /> 9 9 <script type="text/javascript" src="../../jsi18n/"></script> 10 {{ media }}11 10 {% endif %} 11 {{ media }} 12 12 {% endblock %} 13 13 14 14 {% block bodyclass %}change-list{% endblock %} … … 63 63 {% endif %} 64 64 {% endblock %} 65 65 66 <form action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}> 66 67 {% if cl.formset %} 67 <form action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}>68 68 {{ cl.formset.management_form }} 69 69 {% endif %} 70 70 71 {% block result_list %}{% result_list cl %}{% endblock %} 71 {% block result_list %} 72 {% if actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %} 73 {% result_list cl %} 74 {% if actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %} 75 {% endblock %} 72 76 {% block pagination %}{% pagination cl %}{% endblock %} 73 {% if cl.formset %}</form>{% endif %}77 </form> 74 78 </div> 75 79 </div> 76 80 {% endblock %} -
new file django/contrib/admin/templates/admin/delete_selected_confirmation.html
diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html new file mode 100644 index 0000000..183134d
- + 1 {% extends "admin/base_site.html" %} 2 {% load i18n %} 3 4 {% block breadcrumbs %} 5 <div class="breadcrumbs"> 6 <a href="../../">{% trans "Home" %}</a> › 7 <a href="../">{{ app_label|capfirst }}</a> › 8 <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> › 9 {% trans 'Delete multiple objects' %} 10 </div> 11 {% endblock %} 12 13 {% block content %} 14 {% if perms_lacking %} 15 <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> 16 <ul> 17 {% for obj in perms_lacking %} 18 <li>{{ obj }}</li> 19 {% endfor %} 20 </ul> 21 {% else %} 22 <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> 23 {% for d in deleted_objects %} 24 <ul>{{ d|unordered_list }}</ul> 25 {% endfor %} 26 <form action="" method="post"> 27 <div> 28 {% for s in selected %} 29 <input type="hidden" name="selected" value="{{ s.pk }}" /> 30 {% endfor %} 31 <input type="hidden" name="action" value="delete_selected" /> 32 <input type="hidden" name="post" value="yes" /> 33 <input type="submit" value="{% trans "Yes, I'm sure" %}" /> 34 </div> 35 </form> 36 {% endif %} 37 {% endblock %} 38 No newline at end of file -
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 063ef0e..a374bf5 100644
a b search_form = register.inclusion_tag('admin/search_form.html')(search_form) 325 325 def admin_list_filter(cl, spec): 326 326 return {'title': spec.title(), 'choices' : list(spec.choices(cl))} 327 327 admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter) 328 329 def admin_actions(context): 330 """ 331 Track the number of times the action field has been rendered on the page, 332 so we know which value to use. 333 """ 334 context['action_index'] = context.get('action_index', -1) + 1 335 return context 336 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..89455a4 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 ugettext as _ 7 from django.utils.translation import ungettext, ugettext as _ 8 from django.core.urlresolvers import reverse, NoReverseMatch 8 9 9 10 def quote(s): 10 11 """ … … def _nest_help(obj, depth, val): 60 61 current = current[-1] 61 62 current.append(val) 62 63 63 def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site): 64 "Helper function that recursively populates deleted_objects." 64 def get_change_view_url(app_label, module_name, pk, levels_to_root): 65 """ 66 Returns the url to the admin change view for the given app_label, 67 module_name and primary key. 68 """ 69 try: 70 return reverse('admin_%s_%s_change' % (app_label, module_name), None, (pk,)) 71 except NoReverseMatch: 72 return '%s%s/%s/%s/' % ('../'*levels_to_root, app_label, module_name, pk) 73 74 def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site, levels_to_root=4): 75 """ 76 Helper function that recursively populates deleted_objects. 77 78 `levels_to_root` defines the number of directories (../) to reach the 79 admin root path. In a change_view this is 4, in a change_list view 2. 80 81 This is for backwards compatibility since the options.delete_selected 82 method uses this function also from a change_list view. 83 This will not be used if we can reverse the URL. 84 """ 65 85 nh = _nest_help # Bind to local variable for performance 66 86 if current_depth > 16: 67 87 return # Avoid recursing too deep. … … def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ 91 111 [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []]) 92 112 else: 93 113 # Display a link to the admin page. 94 nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href=" ../../../../%s/%s/%s/">%s</a>' %114 nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' % 95 115 (escape(capfirst(related.opts.verbose_name)), 96 related.opts.app_label, 97 related.opts.object_name.lower(), 98 sub_obj._get_pk_val(), 116 get_change_view_url(related.opts.app_label, 117 related.opts.object_name.lower(), 118 sub_obj._get_pk_val(), 119 levels_to_root), 99 120 escape(sub_obj))), []]) 100 121 get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site) 101 122 else: … … def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ 109 130 [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []]) 110 131 else: 111 132 # Display a link to the admin page. 112 nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href=" ../../../../%s/%s/%s/">%s</a>' %133 nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' % 113 134 (escape(capfirst(related.opts.verbose_name)), 114 related.opts.app_label, 115 related.opts.object_name.lower(), 116 sub_obj._get_pk_val(), 135 get_change_view_url(related.opts.app_label, 136 related.opts.object_name.lower(), 137 sub_obj._get_pk_val(), 138 levels_to_root), 117 139 escape(sub_obj))), []]) 118 140 get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site) 119 141 # If there were related objects, and the user doesn't have … … def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ 147 169 # Display a link to the admin page. 148 170 nh(deleted_objects, current_depth, [ 149 171 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))}) + \ 150 (u' <a href="../../../../%s/%s/%s/">%s</a>' % \ 151 (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []]) 172 (u' <a href="%s">%s</a>' % \ 173 (get_change_view_url(related.opts.app_label, 174 related.opts.object_name.lower(), 175 sub_obj._get_pk_val(), 176 levels_to_root), 177 escape(sub_obj)))), []]) 152 178 # If there were related objects, and the user doesn't have 153 179 # permission to change them, add the missing perm to perms_needed. 154 180 if has_admin and has_related_objs: 155 181 p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission()) 156 182 if not user.has_perm(p): 157 183 perms_needed.add(related.opts.verbose_name) 184 185 def model_format_dict(obj): 186 """ 187 Return a `dict` with keys 'verbose_name' and 'verbose_name_plural', 188 typically for use with string formatting. 189 190 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. 191 192 """ 193 if isinstance(obj, (models.Model, models.base.ModelBase)): 194 opts = obj._meta 195 elif isinstance(obj, models.query.QuerySet): 196 opts = obj.model._meta 197 else: 198 opts = obj 199 return { 200 'verbose_name': force_unicode(opts.verbose_name), 201 'verbose_name_plural': force_unicode(opts.verbose_name_plural) 202 } 203 204 def model_ngettext(obj, n=None): 205 """ 206 Return the appropriate `verbose_name` or `verbose_name_plural` for `obj` 207 depending on the count `n`. 208 209 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. 210 If `obj` is a `QuerySet` instance, `n` is optional and the length of the 211 `QuerySet` is used. 212 213 """ 214 if isinstance(obj, models.query.QuerySet): 215 if n is None: 216 n = obj.count() 217 obj = obj.model 218 d = model_format_dict(obj) 219 return ungettext(d['verbose_name'], d['verbose_name_plural'], n or 0) -
tests/regressiontests/admin_registration/models.py
diff --git a/tests/regressiontests/admin_registration/models.py b/tests/regressiontests/admin_registration/models.py index fdfa369..35cf8af 100644
a b AlreadyRegistered: The model Person is already registered 49 49 >>> site._registry[Person].search_fields 50 50 ['name'] 51 51 >>> site._registry[Person].list_display 52 [' __str__']52 ['action_checkbox', '__str__'] 53 53 >>> site._registry[Person].save_on_top 54 54 True 55 55 -
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 eeaf039..0f53230 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): 199 200 BarAccountAdmin 200 201 ) 201 202 203 class Subscriber(models.Model): 204 name = models.CharField(blank=False, max_length=80) 205 email = models.EmailField(blank=False, max_length=175) 206 207 def __unicode__(self): 208 return "%s (%s)" % (self.name, self.email) 209 210 class SubscriberAdmin(admin.ModelAdmin): 211 actions = ['delete_selected', 'mail_admin'] 212 213 def mail_admin(self, request, selected): 214 EmailMessage( 215 'Greetings from a ModelAdmin action', 216 'This is the test email from a admin action', 217 'from@example.com', 218 ['to@example.com'] 219 ).send() 220 221 class DirectSubscriber(Subscriber): 222 paid = models.BooleanField(default=False) 223 224 def direct_mail(self, request): 225 EmailMessage( 226 'Greetings from a model action', 227 'This is the test email from a model action', 228 'from@example.com', 229 [self.email] 230 ).send() 231 232 class DirectSubscriberAdmin(admin.ModelAdmin): 233 actions = ['direct_mail'] 234 235 class ExternalSubscriber(Subscriber): 236 pass 237 238 def external_mail(request, selected): 239 EmailMessage( 240 'Greetings from a function action', 241 'This is the test email from a function action', 242 'from@example.com', 243 ['to@example.com'] 244 ).send() 245 246 def redirect_to(request, selected): 247 from django.http import HttpResponseRedirect 248 return HttpResponseRedirect('/some-where-else/') 249 250 class ExternalSubscriberAdmin(admin.ModelAdmin): 251 actions = [external_mail, redirect_to] 202 252 203 253 admin.site.register(Article, ArticleAdmin) 204 254 admin.site.register(CustomArticle, CustomArticleAdmin) … … admin.site.register(Color) 208 258 admin.site.register(Thing, ThingAdmin) 209 259 admin.site.register(Person, PersonAdmin) 210 260 admin.site.register(Persona, PersonaAdmin) 261 admin.site.register(Subscriber, SubscriberAdmin) 262 admin.site.register(DirectSubscriber, DirectSubscriberAdmin) 263 admin.site.register(ExternalSubscriber, ExternalSubscriberAdmin) 211 264 212 265 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. 213 266 # 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 33000d4..8be78a7 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, Person, Persona, FooAccount, BarAccount 15 from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Person, Persona, FooAccount, BarAccount, Subscriber, DirectSubscriber, ExternalSubscriber 15 16 16 17 try: 17 18 set … … class AdminViewListEditable(TestCase): 743 744 response = self.client.get('/test_admin/admin/admin_views/person/') 744 745 # 2 inputs per object(the field and the hidden id field) = 6 745 746 # 2 management hidden fields = 2 747 # 4 action inputs (3 regular checkboxes, 1 checkbox to select all) 746 748 # main form submit button = 1 747 749 # search field and search submit button = 2 748 750 # 6 + 2 + 1 + 2 = 11 inputs 749 self.failUnlessEqual(response.content.count("<input"), 1 1)751 self.failUnlessEqual(response.content.count("<input"), 15) 750 752 # 1 select per object = 3 selects 751 self.failUnlessEqual(response.content.count("<select"), 3)753 self.failUnlessEqual(response.content.count("<select"), 4) 752 754 753 755 def test_post_submission(self): 754 756 data = { … … class AdminInheritedInlinesTest(TestCase): 875 877 self.failUnlessEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user) 876 878 self.failUnlessEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user) 877 879 self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2) 880 881 from django.core import mail 882 883 class AdminActionsTest(TestCase): 884 fixtures = ['admin-views-users.xml', 'admin-views-actions.xml'] 885 886 def setUp(self): 887 self.client.login(username='super', password='secret') 888 889 def tearDown(self): 890 self.client.logout() 891 892 def test_model_admin_custom_action(self): 893 "Tests a custom action defined in a ModelAdmin method" 894 action_data = { 895 ACTION_CHECKBOX_NAME: [1], 896 'action' : 'mail_admin', 897 'index': 0, 898 } 899 response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) 900 self.assertEquals(len(mail.outbox), 1) 901 self.assertEquals(mail.outbox[0].subject, 'Greetings from a ModelAdmin action') 902 903 def test_model_admin_default_delete_action(self): 904 "Tests the default delete action defined as a ModelAdmin method" 905 action_data = { 906 ACTION_CHECKBOX_NAME: [1, 2], 907 'action' : 'delete_selected', 908 'index': 0, 909 } 910 delete_confirmation_data = { 911 ACTION_CHECKBOX_NAME: [1, 2], 912 'action' : 'delete_selected', 913 'index': 0, 914 'post': 'yes', 915 } 916 confirmation = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) 917 self.assertContains(confirmation, "Are you sure you want to delete the selected subscriber objects") 918 response = self.client.post('/test_admin/admin/admin_views/subscriber/', delete_confirmation_data) 919 self.failUnlessEqual(Subscriber.objects.count(), 0) 920 921 def test_custom_model_instance_action(self): 922 "Tests a custom action defined in a model method" 923 action_data = { 924 ACTION_CHECKBOX_NAME: [1], 925 'action' : 'direct_mail', 926 'index': 0, 927 'paid': 1, 928 } 929 response = self.client.post('/test_admin/admin/admin_views/directsubscriber/', action_data) 930 self.assertEquals(len(mail.outbox), 1) 931 self.assertEquals(mail.outbox[0].subject, 'Greetings from a model action') 932 self.assertEquals(mail.outbox[0].to, [u'john@example.org']) 933 934 def test_custom_function_mail_action(self): 935 "Tests a custom action defined in a function" 936 action_data = { 937 ACTION_CHECKBOX_NAME: [1], 938 'action' : 'external_mail', 939 'index': 0, 940 } 941 response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) 942 self.assertEquals(len(mail.outbox), 1) 943 self.assertEquals(mail.outbox[0].subject, 'Greetings from a function action') 944 945 def test_custom_function_action_with_redirect(self): 946 "Tests a custom action defined in a function" 947 action_data = { 948 ACTION_CHECKBOX_NAME: [1], 949 'action' : 'redirect_to', 950 'index': 0, 951 } 952 response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) 953 self.failUnlessEqual(response.status_code, 302)