Index: django/contrib/admin/media/js/actions.min.js
===================================================================
--- django/contrib/admin/media/js/actions.min.js	(révision 13045)
+++ django/contrib/admin/media/js/actions.min.js	(copie de travail)
@@ -1,5 +1,7 @@
-(function(a){a.fn.actions=function(g){var b=a.extend({},a.fn.actions.defaults,g),e=a(this);checker=function(c){c?showQuestion():reset();a(e).attr("checked",c).parent().parent().toggleClass(b.selectedClass,c)};updateCounter=function(){var c=a(e).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},true));a(b.allToggle).attr("checked",function(){if(c==e.length){value=true;showQuestion()}else{value=
+(function(a){a.fn.actions=function(h){var b=a.extend({},a.fn.actions.defaults,h),e=a(this),f=false;checker=function(c){c?showQuestion():reset();a(e).attr("checked",c).parent().parent().toggleClass(b.selectedClass,c)};updateCounter=function(){var c=a(e).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},true));a(b.allToggle).attr("checked",function(){if(c==e.length){value=true;showQuestion()}else{value=
 false;clearAcross()}return value})};showQuestion=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()};showClear=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()};reset=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()};clearAcross=function(){reset();a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)};
 a(b.counterContainer).show();a(this).filter(":checked").each(function(){a(this).parent().parent().toggleClass(b.selectedClass);updateCounter();a(b.acrossInput).val()==1&&showClear()});a(b.allToggle).show().click(function(){checker(a(this).attr("checked"));updateCounter()});a("div.actions span.question a").click(function(c){c.preventDefault();a(b.acrossInput).val(1);showClear()});a("div.actions span.clear a").click(function(c){c.preventDefault();a(b.allToggle).attr("checked",false);clearAcross();checker(0);
-updateCounter()});lastChecked=null;a(e).click(function(c){if(!c)c=window.event;var d=c.target?c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(d)&&c.shiftKey==true){var f=false;a(lastChecked).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(e).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(d))f=f?false:true;f&&a(this).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,
-d.checked);lastChecked=d;updateCounter()})};a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery);
+updateCounter()});lastChecked=null;a(e).click(function(c){if(!c)c=window.event;var d=c.target?c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(d)&&c.shiftKey==true){var g=false;a(lastChecked).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(e).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(d))g=g?false:true;g&&a(this).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,
+d.checked);lastChecked=d;updateCounter()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){f=true});a('form#changelist-form button[name="index"]').click(function(){if(f)return(answer=confirm(gettext("You have unsaved changes on individual editable fields. If you run this action, your unsaved changes will be lost.")))?true:false});a('form#changelist-form input[name="_save"]').click(function(){var c=false;a("div.actions select option:selected").each(function(){if(a(this).val()!=
+"")c=true});if(c&&!f)return(answer=confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button.")))?true:false;if(c&&f)return(answer=confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")))?true:false})};a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",
+allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery);
Index: django/contrib/admin/media/js/actions.js
===================================================================
--- django/contrib/admin/media/js/actions.js	(révision 13045)
+++ django/contrib/admin/media/js/actions.js	(copie de travail)
@@ -2,6 +2,7 @@
 	$.fn.actions = function(opts) {
 		var options = $.extend({}, $.fn.actions.defaults, opts);
 		var actionCheckboxes = $(this);
+		var list_editable_changed = false;
 		checker = function(checked) {
 			if (checked) {
 				showQuestion();
@@ -100,6 +101,34 @@
 			lastChecked = target;
 			updateCounter();
 		});
+		$('form#changelist-form table#result_list tr').find('td:gt(0) :input').change(function() {
+		    list_editable_changed = true;
+		});
+		$('form#changelist-form button[name="index"]').click(function(event) {
+		    if (list_editable_changed) {
+		        answer = confirm(gettext("You have unsaved changes on individual editable fields. If you run this action, your unsaved changes will be lost."));
+		        if (answer) { return true }
+		        else { return false };
+		    }
+		});
+		$('form#changelist-form input[name="_save"]').click(function(event) {
+		    var action_changed = false;
+		    $('div.actions select option:selected').each(function() {
+		        if ($(this).val() != "") {
+		            action_changed = true;
+		        }
+		    });
+		    if (action_changed && !list_editable_changed) {
+		        answer = confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."));
+		        if (answer) { return true }
+		        else { return false };
+		    }
+		    if (action_changed && list_editable_changed) {
+		        answer = confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action."));
+		        if (answer) { return true }
+		        else { return false };
+		    }
+		});
 	}
 	/* Setup plugin defaults */
 	$.fn.actions.defaults = {
Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py	(révision 13045)
+++ django/contrib/admin/options.py	(copie de travail)
@@ -737,7 +737,7 @@
 
             # Get the list of selected PKs. If nothing's selected, we can't
             # perform an action on it, so bail. Except we want to perform
-            # the action explicitely on all objects.
+            # the action explicitly on all objects.
             selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
             if not selected and not select_across:
                 # Reminder that something needs to be selected or nothing will happen
@@ -761,6 +761,7 @@
         else:
             msg = _("No action selected.")
             self.message_user(request, msg)
+            return None
 
     @csrf_protect_m
     @transaction.commit_on_success
@@ -979,20 +980,41 @@
             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 or confirmation first, but if this isn't an
-        # action the POST will fall through to the bulk edit check, below.
-        if actions and request.method == 'POST' and (helpers.ACTION_CHECKBOX_NAME in request.POST or 'index' in request.POST):
-            response = self.response_action(request, queryset=cl.get_query_set())
-            if response:
-                return response
+        # Try to look up an action or confirmation first, but if this isn't an action the POST
+        # will fall through to the bulk edit check, below.
 
+        action_failed = False
+
+        # Actions with no confirmation
+        if actions and request.method == 'POST' and 'index' in request.POST and '_save' not in request.POST:
+            if len(request.POST.get(helpers.ACTION_CHECKBOX_NAME, [])):
+                response = self.response_action(request, queryset=cl.get_query_set())
+                if response:
+                    return response
+                else:
+                    action_failed = True
+            else:
+                msg = _("Items must be selected in order to perform actions on them. No items have been changed.")
+                self.message_user(request, msg)
+                action_failed = True
+
+        # Actions with confirmation
+        if actions and request.method == 'POST' and helpers.ACTION_CHECKBOX_NAME in request.POST and 'index' not in request.POST and '_save' not in request.POST:
+            if len(request.POST.get(helpers.ACTION_CHECKBOX_NAME, [])):
+                response = self.response_action(request, queryset=cl.get_query_set())
+                if response:
+                    return response
+                else:
+                    action_failed = True
+
+
         # If we're allowing changelist editing, we need to construct a formset
         # for the changelist given all the fields to be edited. Then we'll
         # use the formset to validate/process POSTed data.
         formset = cl.formset = None
 
         # Handle POSTed bulk-edit data.
-        if request.method == "POST" and self.list_editable:
+        if request.method == "POST" and self.list_editable and '_save' in request.POST and not action_failed:
             FormSet = self.get_changelist_formset(request)
             formset = cl.formset = FormSet(request.POST, request.FILES, queryset=cl.result_list)
             if formset.is_valid():
Index: django/contrib/admin/templates/admin/change_list.html
===================================================================
--- django/contrib/admin/templates/admin/change_list.html	(révision 13045)
+++ django/contrib/admin/templates/admin/change_list.html	(copie de travail)
@@ -85,7 +85,7 @@
         {% endif %}
       {% endblock %}
       
-      <form action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}>{% csrf_token %}
+      <form id="changelist-form" action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}>{% csrf_token %}
       {% if cl.formset %}
         {{ cl.formset.management_form }}
       {% endif %}
Index: django/contrib/admin/templates/admin/change_list_results.html
===================================================================
--- django/contrib/admin/templates/admin/change_list_results.html	(révision 13045)
+++ django/contrib/admin/templates/admin/change_list_results.html	(copie de travail)
@@ -1,5 +1,5 @@
 {% if results %}
-<table cellspacing="0">
+<table cellspacing="0" id="result_list">
 <thead>
 <tr>
 {% for header in result_headers %}<th{{ header.class_attrib }}>
Index: tests/regressiontests/admin_views/tests.py
===================================================================
--- tests/regressiontests/admin_views/tests.py	(révision 13045)
+++ tests/regressiontests/admin_views/tests.py	(copie de travail)
@@ -1091,6 +1091,8 @@
             "form-2-alive": "checked",
             "form-2-gender": "1",
             "form-2-id": "3",
+
+            "_save": "Save",
         }
         response = self.client.post('/test_admin/admin/admin_views/person/',
                                     data, follow=True)
@@ -1111,6 +1113,8 @@
             "form-2-alive": "checked",
             "form-2-gender": "1",
             "form-2-id": "3",
+
+            "_save": "Save",
         }
         self.client.post('/test_admin/admin/admin_views/person/', data)
 
@@ -1130,6 +1134,8 @@
             "form-1-id": "3",
             "form-1-gender": "1",
             "form-1-alive": "checked",
+
+            "_save": "Save",
         }
         self.client.post('/test_admin/admin/admin_views/person/?gender__exact=1', data)
 
@@ -1142,7 +1148,9 @@
             "form-MAX_NUM_FORMS": "0",
 
             "form-0-id": "1",
-            "form-0-gender": "1"
+            "form-0-gender": "1",
+
+            "_save": "Save",
         }
         self.client.post('/test_admin/admin/admin_views/person/?q=mauchly', data)
 
@@ -1158,6 +1166,10 @@
             "form-0-id": "2",
             "form-0-alive": "1",
             "form-0-gender": "2",
+
+            # Ensure that the form processing understands this as a list_editable "Save"
+            # and not an action "Go".
+            "_save": "Save",
         }
         response = self.client.post('/test_admin/admin/admin_views/person/', data)
         self.assertContains(response, "Grace is not a Zombie")
@@ -1172,6 +1184,8 @@
             "form-0-id": "2",
             "form-0-alive": "1",
             "form-0-gender": "2",
+
+            "_save": "Save",
         }
         response = self.client.post('/test_admin/admin/admin_views/person/', data)
         non_form_errors = response.context['cl'].formset.non_form_errors()
@@ -1207,6 +1221,10 @@
             "form-3-order": "0",
             "form-3-id": "4",
             "form-3-collector": "1",
+
+            # Ensure that the form processing understands this as a list_editable "Save"
+            # and not an action "Go".
+            "_save": "Save",
         }
         response = self.client.post('/test_admin/admin/admin_views/category/', data)
         # Successful post will redirect
@@ -1218,6 +1236,63 @@
         self.failUnlessEqual(Category.objects.get(id=3).order, 1)
         self.failUnlessEqual(Category.objects.get(id=4).order, 0)
 
+    def test_list_editable_action_submit(self):
+        # List editable changes should not be executed if the action "Go" button is
+        # used to submit the form.
+        data = {
+            "form-TOTAL_FORMS": "3",
+            "form-INITIAL_FORMS": "3",
+            "form-MAX_NUM_FORMS": "0",
+
+            "form-0-gender": "1",
+            "form-0-id": "1",
+
+            "form-1-gender": "2",
+            "form-1-id": "2",
+
+            "form-2-alive": "checked",
+            "form-2-gender": "1",
+            "form-2-id": "3",
+
+            "index": "0",
+            "_selected_action": [u'3'],
+            "action": [u'', u'delete_selected'],
+        }
+        self.client.post('/test_admin/admin/admin_views/person/', data)
+
+        self.failUnlessEqual(Person.objects.get(name="John Mauchly").alive, True)
+        self.failUnlessEqual(Person.objects.get(name="Grace Hopper").gender, 1)
+
+    def test_list_editable_action_choices(self):
+        # List editable changes should be executed if the "Save" button is
+        # used to submit the form - any action choices should be ignored.
+        data = {
+            "form-TOTAL_FORMS": "3",
+            "form-INITIAL_FORMS": "3",
+            "form-MAX_NUM_FORMS": "0",
+
+            "form-0-gender": "1",
+            "form-0-id": "1",
+
+            "form-1-gender": "2",
+            "form-1-id": "2",
+
+            "form-2-alive": "checked",
+            "form-2-gender": "1",
+            "form-2-id": "3",
+
+            "_save": "Save",
+            "_selected_action": [u'1'],
+            "action": [u'', u'delete_selected'],
+        }
+        self.client.post('/test_admin/admin/admin_views/person/', data)
+
+        self.failUnlessEqual(Person.objects.get(name="John Mauchly").alive, False)
+        self.failUnlessEqual(Person.objects.get(name="Grace Hopper").gender, 2)
+
+
+
+
 class AdminSearchTest(TestCase):
     fixtures = ['admin-views-users','multiple-child-classes']
 
