1
0
mirror of https://github.com/django/django.git synced 2025-07-06 10:49:17 +00:00

newforms-admin: Fixed #6117 -- Implemented change history for the admin. 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
This commit is contained in:
Brian Rosner 2008-04-29 05:45:46 +00:00
parent 95dcdf473e
commit abb7a7ff0f
4 changed files with 80 additions and 35 deletions

View File

@ -389,17 +389,26 @@ class ModelAdmin(BaseModelAdmin):
for formset in formsets: for formset in formsets:
formset.save() formset.save()
# Construct the change message. TODO: Temporarily commented-out, # Construct the change message.
# as manipulator object doesn't exist anymore, and we don't yet
# have a way to get fields_added, fields_changed, fields_deleted.
change_message = [] change_message = []
#if manipulator.fields_added: if form.changed_data:
#change_message.append(_('Added %s.') % get_text_list(manipulator.fields_added, _('and'))) change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
#if manipulator.fields_changed:
#change_message.append(_('Changed %s.') % get_text_list(manipulator.fields_changed, _('and'))) if formsets:
#if manipulator.fields_deleted: for formset in formsets:
#change_message.append(_('Deleted %s.') % get_text_list(manipulator.fields_deleted, _('and'))) for added_object in formset.new_objects:
#change_message = ' '.join(change_message) 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: if not change_message:
change_message = _('No fields changed.') 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) LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), CHANGE, change_message)

View File

@ -81,6 +81,7 @@ class BaseForm(StrAndUnicode):
self.label_suffix = label_suffix self.label_suffix = label_suffix
self.empty_permitted = empty_permitted self.empty_permitted = empty_permitted
self._errors = None # Stores the errors after clean() has been called. 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 # The base_fields class attribute is the *class-wide* definition of
# fields. Because a particular *instance* of the class might want to # fields. Because a particular *instance* of the class might want to
@ -243,19 +244,25 @@ class BaseForm(StrAndUnicode):
""" """
Returns True if data differs from initial. Returns True if data differs from initial.
""" """
# XXX: For now we're asking the individual widgets whether or not the return bool(self.changed_data)
# data has changed. It would probably be more efficient to hash the
# initial data, store it in a hidden field, and compare a hash of the def _get_changed_data(self):
# submitted data, but we'd need a way to easily get the string value if self._changed_data is None:
# for a given field. Right now, that logic is embedded in the render self._changed_data = []
# method of each widget. # XXX: For now we're asking the individual widgets whether or not the
for name, field in self.fields.items(): # data has changed. It would probably be more efficient to hash the
prefixed_name = self.add_prefix(name) # initial data, store it in a hidden field, and compare a hash of the
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) # submitted data, but we'd need a way to easily get the string value
initial_value = self.initial.get(name, field.initial) # for a given field. Right now, that logic is embedded in the render
if field.widget._has_changed(initial_value, data_value): # method of each widget.
return True for name, field in self.fields.items():
return False 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): def _get_media(self):
""" """

View File

@ -328,8 +328,11 @@ class BaseModelFormSet(BaseFormSet):
return self.save_existing_objects(commit) + self.save_new_objects(commit) return self.save_existing_objects(commit) + self.save_new_objects(commit)
def save_existing_objects(self, commit=True): def save_existing_objects(self, commit=True):
self.changed_objects = []
self.deleted_objects = []
if not self.get_queryset(): if not self.get_queryset():
return [] return []
# Put the objects from self.get_queryset into a dict so they are easy to lookup by pk # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
existing_objects = {} existing_objects = {}
for obj in self.get_queryset(): for obj in self.get_queryset():
@ -338,23 +341,25 @@ class BaseModelFormSet(BaseFormSet):
for form in self.initial_forms: for form in self.initial_forms:
obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]] obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]: if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
self.deleted_objects.append(obj)
obj.delete() obj.delete()
else: 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 return saved_instances
def save_new_objects(self, commit=True): def save_new_objects(self, commit=True):
new_objects = [] self.new_objects = []
for form in self.extra_forms: for form in self.extra_forms:
if not form.has_changed(): if not form.has_changed():
continue continue
# If someone has marked an add form for deletion, don't save the # If someone has marked an add form for deletion, don't save the
# object. At some point it would be nice if we didn't display # object.
# the deletion widget for add forms.
if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]: if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
continue continue
new_objects.append(self.save_new(form, commit=commit)) self.new_objects.append(self.save_new(form, commit=commit))
return new_objects return self.new_objects
def add_fields(self, form, index): def add_fields(self, form, index):
"""Add a hidden field for the object's primary key.""" """Add a hidden field for the object's primary key."""

View File

@ -50,9 +50,9 @@ Charles Baudelaire
Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing
authors with an extra form to add him. This time we'll use formset_for_queryset. authors with an extra form to add him. We *could* pass in a queryset to
We *could* use formset_for_queryset to restrict the Author objects we edit, restrict the Author objects we edit, but in this case we'll use it to display
but in that case we'll use it to display them in alphabetical order by name. them in alphabetical order by name.
>>> qs = Author.objects.order_by('name') >>> qs = Author.objects.order_by('name')
>>> AuthorFormSet = _modelformset_factory(Author, extra=1, can_delete=False) >>> AuthorFormSet = _modelformset_factory(Author, extra=1, can_delete=False)
@ -79,8 +79,9 @@ but in that case we'll use it to display them in alphabetical order by name.
>>> formset.is_valid() >>> formset.is_valid()
True True
# Only changed or new objects are returned from formset.save()
>>> formset.save() >>> formset.save()
[<Author: Arthur Rimbaud>, <Author: Charles Baudelaire>, <Author: Paul Verlaine>] [<Author: Paul Verlaine>]
>>> for author in Author.objects.order_by('name'): >>> for author in Author.objects.order_by('name'):
... print author.name ... print author.name
@ -124,8 +125,9 @@ deltetion, make sure we don't save that form.
>>> formset.is_valid() >>> formset.is_valid()
True True
# No objects were changed or saved so nothing will come back.
>>> formset.save() >>> formset.save()
[<Author: Arthur Rimbaud>, <Author: Charles Baudelaire>, <Author: Paul Verlaine>] []
>>> for author in Author.objects.order_by('name'): >>> for author in Author.objects.order_by('name'):
... print author.name ... print author.name
@ -133,6 +135,28 @@ Arthur Rimbaud
Charles Baudelaire Charles Baudelaire
Paul Verlaine Paul Verlaine
Let's edit a record to ensure save only returns that one record.
>>> data = {
... 'form-TOTAL_FORMS': '4', # the number of forms rendered
... 'form-INITIAL_FORMS': '3', # the number of forms with initial data
... 'form-0-id': '2',
... 'form-0-name': 'Walt Whitman',
... 'form-1-id': '1',
... 'form-1-name': 'Charles Baudelaire',
... 'form-2-id': '3',
... 'form-2-name': 'Paul Verlaine',
... 'form-3-name': '',
... 'form-3-DELETE': '',
... }
>>> formset = AuthorFormSet(data=data, queryset=qs)
>>> formset.is_valid()
True
# One record has changed.
>>> formset.save()
[<Author: Walt Whitman>]
# Inline Formsets ############################################################ # Inline Formsets ############################################################
@ -199,7 +223,7 @@ book.
True True
>>> formset.save() >>> 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. As you can see, 'Le Spleen de Paris' is now a book belonging to Charles Baudelaire.