Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

newforms-admin: Fixed #6117 -- Implemented change history for the adm…

…in. This includes the ability to track changes on a newform. Model formsets now only return the changed/new objects saved. A big thanks to Karen Tracey and Alex Gaynor.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7507 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit abb7a7ff0fa9440d833434a52328f56ab0c425e3 1 parent 95dcdf4
Brian Rosner authored April 29, 2008
29  django/contrib/admin/options.py
@@ -389,17 +389,26 @@ def save_change(self, request, model, form, formsets=None):
389 389
             for formset in formsets:
390 390
                 formset.save()
391 391
 
392  
-        # Construct the change message. TODO: Temporarily commented-out,
393  
-        # as manipulator object doesn't exist anymore, and we don't yet
394  
-        # have a way to get fields_added, fields_changed, fields_deleted.
  392
+        # Construct the change message.                 
395 393
         change_message = []
396  
-        #if manipulator.fields_added:
397  
-            #change_message.append(_('Added %s.') % get_text_list(manipulator.fields_added, _('and')))
398  
-        #if manipulator.fields_changed:
399  
-            #change_message.append(_('Changed %s.') % get_text_list(manipulator.fields_changed, _('and')))
400  
-        #if manipulator.fields_deleted:
401  
-            #change_message.append(_('Deleted %s.') % get_text_list(manipulator.fields_deleted, _('and')))
402  
-        #change_message = ' '.join(change_message)
  394
+        if form.changed_data:
  395
+            change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
  396
+            
  397
+        if formsets:
  398
+            for formset in formsets:
  399
+                for added_object in formset.new_objects:
  400
+                    change_message.append(_('Added %s "%s".') 
  401
+                                          % (added_object._meta.verbose_name, added_object))
  402
+                for changed_object, changed_fields in formset.changed_objects:
  403
+                    change_message.append(_('Changed %s for %s "%s".') 
  404
+                                          % (get_text_list(changed_fields, _('and')), 
  405
+                                             changed_object._meta.verbose_name, 
  406
+                                             changed_object))
  407
+                for deleted_object in formset.deleted_objects:
  408
+                    change_message.append(_('Deleted %s "%s".') 
  409
+                                          % (deleted_object._meta.verbose_name, deleted_object))
  410
+            
  411
+        change_message = ' '.join(change_message)
403 412
         if not change_message:
404 413
             change_message = _('No fields changed.')
405 414
         LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), CHANGE, change_message)
33  django/newforms/forms.py
@@ -81,6 +81,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
81 81
         self.label_suffix = label_suffix
82 82
         self.empty_permitted = empty_permitted
83 83
         self._errors = None # Stores the errors after clean() has been called.
  84
+        self._changed_data = None
84 85
 
85 86
         # The base_fields class attribute is the *class-wide* definition of
86 87
         # fields. Because a particular *instance* of the class might want to
@@ -243,19 +244,25 @@ def has_changed(self):
243 244
         """
244 245
         Returns True if data differs from initial.
245 246
         """
246  
-        # XXX: For now we're asking the individual widgets whether or not the
247  
-        # data has changed. It would probably be more efficient to hash the
248  
-        # initial data, store it in a hidden field, and compare a hash of the
249  
-        # submitted data, but we'd need a way to easily get the string value
250  
-        # for a given field. Right now, that logic is embedded in the render
251  
-        # method of each widget.
252  
-        for name, field in self.fields.items():
253  
-            prefixed_name = self.add_prefix(name)
254  
-            data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
255  
-            initial_value = self.initial.get(name, field.initial)
256  
-            if field.widget._has_changed(initial_value, data_value):
257  
-                return True
258  
-        return False
  247
+        return bool(self.changed_data)
  248
+    
  249
+    def _get_changed_data(self):
  250
+        if self._changed_data is None:
  251
+            self._changed_data = []
  252
+            # XXX: For now we're asking the individual widgets whether or not the
  253
+            # data has changed. It would probably be more efficient to hash the
  254
+            # initial data, store it in a hidden field, and compare a hash of the
  255
+            # submitted data, but we'd need a way to easily get the string value
  256
+            # for a given field. Right now, that logic is embedded in the render
  257
+            # method of each widget.
  258
+            for name, field in self.fields.items():
  259
+                prefixed_name = self.add_prefix(name)
  260
+                data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
  261
+                initial_value = self.initial.get(name, field.initial)
  262
+                if field.widget._has_changed(initial_value, data_value):
  263
+                    self._changed_data.append(name)
  264
+        return self._changed_data
  265
+    changed_data = property(_get_changed_data)
259 266
 
260 267
     def _get_media(self):
261 268
         """
17  django/newforms/models.py
@@ -328,8 +328,11 @@ def save(self, commit=True):
328 328
         return self.save_existing_objects(commit) + self.save_new_objects(commit)
329 329
 
330 330
     def save_existing_objects(self, commit=True):
  331
+        self.changed_objects = []
  332
+        self.deleted_objects = []
331 333
         if not self.get_queryset():
332 334
             return []
  335
+
333 336
         # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
334 337
         existing_objects = {}
335 338
         for obj in self.get_queryset():
@@ -338,23 +341,25 @@ def save_existing_objects(self, commit=True):
338 341
         for form in self.initial_forms:
339 342
             obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
340 343
             if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
  344
+                self.deleted_objects.append(obj)
341 345
                 obj.delete()
342 346
             else:
343  
-                saved_instances.append(self.save_existing(form, obj, commit=commit))
  347
+                if form.changed_data:
  348
+                    self.changed_objects.append((obj, form.changed_data))
  349
+                    saved_instances.append(self.save_existing(form, obj, commit=commit))
344 350
         return saved_instances
345 351
 
346 352
     def save_new_objects(self, commit=True):
347  
-        new_objects = []
  353
+        self.new_objects = []
348 354
         for form in self.extra_forms:
349 355
             if not form.has_changed():
350 356
                 continue
351 357
             # If someone has marked an add form for deletion, don't save the
352  
-            # object. At some point it would be nice if we didn't display
353  
-            # the deletion widget for add forms.
  358
+            # object.
354 359
             if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
355 360
                 continue
356  
-            new_objects.append(self.save_new(form, commit=commit))
357  
-        return new_objects
  361
+            self.new_objects.append(self.save_new(form, commit=commit))
  362
+        return self.new_objects
358 363
 
359 364
     def add_fields(self, form, index):
360 365
         """Add a hidden field for the object's primary key."""
36  tests/modeltests/model_formsets/models.py
@@ -50,9 +50,9 @@ def __unicode__(self):
50 50
 
51 51
 
52 52
 Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing
53  
-authors with an extra form to add him. This time we'll use formset_for_queryset.
54  
-We *could* use formset_for_queryset to restrict the Author objects we edit,
55  
-but in that case we'll use it to display them in alphabetical order by name.
  53
+authors with an extra form to add him. We *could* pass in a queryset to
  54
+restrict the Author objects we edit, but in this case we'll use it to display
  55
+them in alphabetical order by name.
56 56
 
57 57
 >>> qs = Author.objects.order_by('name')
58 58
 >>> AuthorFormSet = _modelformset_factory(Author, extra=1, can_delete=False)
@@ -79,8 +79,9 @@ def __unicode__(self):
79 79
 >>> formset.is_valid()
80 80
 True
81 81
 
  82
+# Only changed or new objects are returned from formset.save()
82 83
 >>> formset.save()
83  
-[<Author: Arthur Rimbaud>, <Author: Charles Baudelaire>, <Author: Paul Verlaine>]
  84
+[<Author: Paul Verlaine>]
84 85
 
85 86
 >>> for author in Author.objects.order_by('name'):
86 87
 ...     print author.name
@@ -124,8 +125,9 @@ def __unicode__(self):
124 125
 >>> formset.is_valid()
125 126
 True
126 127
 
  128
+# No objects were changed or saved so nothing will come back.
127 129
 >>> formset.save()
128  
-[<Author: Arthur Rimbaud>, <Author: Charles Baudelaire>, <Author: Paul Verlaine>]
  130
+[]
129 131
 
130 132
 >>> for author in Author.objects.order_by('name'):
131 133
 ...     print author.name
@@ -133,6 +135,28 @@ def __unicode__(self):
133 135
 Charles Baudelaire
134 136
 Paul Verlaine
135 137
 
  138
+Let's edit a record to ensure save only returns that one record.
  139
+
  140
+>>> data = {
  141
+...     'form-TOTAL_FORMS': '4', # the number of forms rendered
  142
+...     'form-INITIAL_FORMS': '3', # the number of forms with initial data
  143
+...     'form-0-id': '2',
  144
+...     'form-0-name': 'Walt Whitman',
  145
+...     'form-1-id': '1',
  146
+...     'form-1-name': 'Charles Baudelaire',
  147
+...     'form-2-id': '3',
  148
+...     'form-2-name': 'Paul Verlaine',
  149
+...     'form-3-name': '',
  150
+...     'form-3-DELETE': '',
  151
+... }
  152
+
  153
+>>> formset = AuthorFormSet(data=data, queryset=qs)
  154
+>>> formset.is_valid()
  155
+True
  156
+
  157
+# One record has changed.
  158
+>>> formset.save()
  159
+[<Author: Walt Whitman>]
136 160
 
137 161
 # Inline Formsets ############################################################
138 162
 
@@ -199,7 +223,7 @@ def __unicode__(self):
199 223
 True
200 224
 
201 225
 >>> formset.save()
202  
-[<Book: Les Fleurs du Mal>, <Book: Le Spleen de Paris>]
  226
+[<Book: Le Spleen de Paris>]
203 227
 
204 228
 As you can see, 'Le Spleen de Paris' is now a book belonging to Charles Baudelaire.
205 229
 

0 notes on commit abb7a7f

Please sign in to comment.
Something went wrong with that request. Please try again.