diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index d0cca0a..d6ee1ee 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -385,17 +385,26 @@ class ModelAdmin(BaseModelAdmin):
             for formset in formsets:
                 formset.save()
 
-        # Construct the change message. TODO: Temporarily commented-out,
-        # as manipulator object doesn't exist anymore, and we don't yet
-        # have a way to get fields_added, fields_changed, fields_deleted.
+        # Construct the change message.                 
         change_message = []
-        #if manipulator.fields_added:
-            #change_message.append(_('Added %s.') % get_text_list(manipulator.fields_added, _('and')))
-        #if manipulator.fields_changed:
-            #change_message.append(_('Changed %s.') % get_text_list(manipulator.fields_changed, _('and')))
-        #if manipulator.fields_deleted:
-            #change_message.append(_('Deleted %s.') % get_text_list(manipulator.fields_deleted, _('and')))
-        #change_message = ' '.join(change_message)
+        if form.changed_data:
+            change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
+            
+        if formsets:
+            for formset in formsets:
+                for added_object in formset.new_objects:
+                    change_message.append(_('Added %s "%s".') 
+                                          % (added_object._meta.verbose_name, added_object))
+                for changed_object, changed_fields in formset.changed_objects:
+                    change_message.append(_('Changed %s for %s "%s".') 
+                                          % (get_text_list(changed_fields, _('and')), 
+                                             changed_object._meta.verbose_name, 
+                                             changed_object))
+                for deleted_object in formset.deleted_objects:
+                    change_message.append(_('Deleted %s "%s".') 
+                                          % (deleted_object._meta.verbose_name, deleted_object))
+            
+        change_message = ' '.join(change_message)
         if not change_message:
             change_message = _('No fields changed.')
         LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), CHANGE, change_message)
diff --git a/django/newforms/forms.py b/django/newforms/forms.py
index 42dacec..4c4c0e0 100644
--- a/django/newforms/forms.py
+++ b/django/newforms/forms.py
@@ -81,6 +81,7 @@ class BaseForm(StrAndUnicode):
         self.label_suffix = label_suffix
         self.empty_permitted = empty_permitted
         self._errors = None # Stores the errors after clean() has been called.
+        self._changed_data = None
 
         # The base_fields class attribute is the *class-wide* definition of
         # fields. Because a particular *instance* of the class might want to
@@ -256,6 +257,18 @@ class BaseForm(StrAndUnicode):
             if field.widget._has_changed(initial_value, data_value):
                 return True
         return False
+    
+    def _get_changed_data(self):
+        if self._changed_data is None:
+            self._changed_data = []
+            for name, field in self.fields.items():
+                prefixed_name = self.add_prefix(name)
+                data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
+                initial_value = self.initial.get(name, field.initial)
+                if field.widget._has_changed(initial_value, data_value):
+                    self._changed_data.append(name)
+        return self._changed_data
+    changed_data = property(_get_changed_data)
 
     def _get_media(self):
         """
diff --git a/django/newforms/models.py b/django/newforms/models.py
index f12b53d..b1332a7 100644
--- a/django/newforms/models.py
+++ b/django/newforms/models.py
@@ -328,8 +328,11 @@ class BaseModelFormSet(BaseFormSet):
         return self.save_existing_objects(commit) + self.save_new_objects(commit)
 
     def save_existing_objects(self, commit=True):
+        self.changed_objects = []
+        self.deleted_objects = []
         if not self.get_queryset():
             return []
+
         # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
         existing_objects = {}
         for obj in self.get_queryset():
@@ -338,13 +341,16 @@ class BaseModelFormSet(BaseFormSet):
         for form in self.initial_forms:
             obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
             if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+                self.deleted_objects.append(obj)
                 obj.delete()
             else:
-                saved_instances.append(self.save_existing(form, obj, commit=commit))
+                if form.changed_data:
+                    self.changed_objects.append((obj, form.changed_data))
+                    saved_instances.append(self.save_existing(form, obj, commit=commit))
         return saved_instances
 
     def save_new_objects(self, commit=True):
-        new_objects = []
+        self.new_objects = []
         for form in self.extra_forms:
             if not form.has_changed():
                 continue
@@ -353,8 +359,8 @@ class BaseModelFormSet(BaseFormSet):
             # the deletion widget for add forms.
             if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
                 continue
-            new_objects.append(self.save_new(form, commit=commit))
-        return new_objects
+            self.new_objects.append(self.save_new(form, commit=commit))
+        return self.new_objects
 
     def add_fields(self, form, index):
         """Add a hidden field for the object's primary key."""
diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
index 386cd04..e6ee82e 100644
--- a/tests/modeltests/model_formsets/models.py
+++ b/tests/modeltests/model_formsets/models.py
@@ -80,7 +80,7 @@ but in that case we'll use it to display them in alphabetical order by name.
 True
 
 >>> formset.save()
-[<Author: Arthur Rimbaud>, <Author: Charles Baudelaire>, <Author: Paul Verlaine>]
+[<Author: Paul Verlaine>]
 
 >>> for author in Author.objects.order_by('name'):
 ...     print author.name
@@ -125,7 +125,7 @@ deltetion, make sure we don't save that form.
 True
 
 >>> formset.save()
-[<Author: Arthur Rimbaud>, <Author: Charles Baudelaire>, <Author: Paul Verlaine>]
+[]
 
 >>> for author in Author.objects.order_by('name'):
 ...     print author.name
@@ -199,7 +199,7 @@ book.
 True
 
 >>> formset.save()
-[<Book: Les Fleurs du Mal>, <Book: Le Spleen de Paris>]
+[<Book: Le Spleen de Paris>]
 
 As you can see, 'Le Spleen de Paris' is now a book belonging to Charles Baudelaire.
 
diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py
index a865e91..96c871e 100644
--- a/tests/regressiontests/forms/forms.py
+++ b/tests/regressiontests/forms/forms.py
@@ -1738,4 +1738,43 @@ initial that returns False on a boolean call needs to be treated literally.
 >>> form.is_valid()
 True
 
+>>> from django import newforms as forms
+>>> class Person(forms.Form):
+...     first_name = forms.CharField()
+...     last_name = forms.CharField()
+...     age = forms.IntegerField()
+...     happy = forms.BooleanField()
+...     resume = forms.FileField()
+
+>>> p = Person()
+>>> p.changed_data
+[]
+
+>>> p = Person({'first_name': 'John'})
+>>> p.changed_data
+['first_name']
+
+>>> p = Person({'first_name': 'John', 'last_name': 'Boothe'})
+>>> p.changed_data
+['first_name', 'last_name']
+
+>>> p = Person({'first_name': 'John', 'last_name': 'Boothe'}, initial={'last_name': 'Boothe'})
+>>> p.changed_data
+['first_name']
+
+>>> p = Person({'age': '20'}, initial={'age': 20})
+>>> p.changed_data
+[]
+
+>>> p = Person({'happy': True}, initial={'happy': False})
+>>> p.changed_data
+['happy']
+
+>>> p = Person({'happy': 1}, initial={'happy': True})
+>>> p.changed_data
+[]
+
+>>> p = Person({'happy': '1'}, initial={'happy': True})
+>>> p.changed_data
+[]
 """
diff --git a/tests/regressiontests/forms/widgets.py b/tests/regressiontests/forms/widgets.py
index 0e69602..849224d 100644
--- a/tests/regressiontests/forms/widgets.py
+++ b/tests/regressiontests/forms/widgets.py
@@ -929,4 +929,33 @@ u'<input type="text" name="date" value="2007-09-17 12:51:34" />'
 u'<input type="text" name="date" value="2007-09-17 12:51:34" />'
 >>> w.render('date', datetime.datetime(2007, 9, 17, 12, 51))
 u'<input type="text" name="date" value="2007-09-17 12:51:00" />'
+
+>>> from django import newforms as forms
+>>> w =  forms.TextInput()
+>>> w._has_changed('John', 'John')
+False
+
+>>> w._has_changed('John', '')
+True
+
+>>> w._has_changed('', 'John')
+True
+
+>>> w._has_changed('', '')
+False
+
+# This test is for Integer Fields, which uses the TextInput for a widget.
+>>> w._has_changed(20, '20')
+False
+
+>>> w =  forms.CheckboxInput()
+>>> w._has_changed(True, '1')
+False
+
+>>> w._has_changed(True, '0')
+False
+
+>>> w._has_changed(True, 0)
+True
+
 """
