kosta5
kosta5

Reputation: 1138

django historical object values

I know there is reversion out there, I know there is fullhistory branch in django. BUT I would VERY much like to stick with original history of django objects. I just simply need to make sure I also save OLD and NEW values.

By default django just saves WHAT happened (e.g. update) but doesnt store the values. Anybody can point me to something (snippet or app) that slightly enhances the default history of django and stores also the values of edited attributes not just the fact that it was edited.

Now My history says

9-01-2012 12:55:02 chnaged: time

I want it to say

9-01-2012 12:55:02 chnaged: time from 6 to 8

UPDATE: ==> for anyone interested in my solution (based on answer below)..

  1. Added HistoryModel(object) class to my models.py

       class HistoryModel(object):
             changed_fields = {}
    
  2. Inherited in any of my models

  3. Added a pre_save receiver to save old values:

        @receiver(pre_save, sender=Customer) 
        def save_old_values(sender,**kwargs):
            #dont delete using in eval
            db_obj = sender.objects.get(pk=kwargs['instance'].pk)
            for field in kwargs['instance']._meta.fields:
                if not eval("db_obj." + str(field.get_attname_column()[0])) == eval("kwargs['instance']." +  str(field.get_attname_column()[0])):
                         kwargs['instance'].changed_fields[field.get_attname_column()[0]] = "from "+str(eval("db_obj." + str(field.get_attname_column()[0]))) + " to " + str(eval("kwargs['instance']." +  str(field.get_attname_column()[0])))
    
  4. Overwrote the log_change method in my admin and collected object.changed_fields dict and stored it as a message

Upvotes: 2

Views: 1406

Answers (3)

adi
adi

Reputation: 61

Updated version of @DavideBrunato's snippet that works in Django 4:

def construct_change_message(self, request, form, formsets, add=False):
    change_message = []
    if add:
        change_message.append({"added": {}})
    elif form.changed_data:
        msg_list = []
        for field in form.changed_data:
            if form.initial[field] is not None and hasattr(form.fields[field], 'queryset'):
                old_value = form.fields[field].queryset.get(id=form.initial[field])
            else:
                old_value = form.initial[field]
            msg_list.append('\n"%s": "%s" > "%s"' % (field, old_value, form[field].value()))
        change_message.append({"changed": {"fields": msg_list}})

    if formsets:
        with translation_override(None):
            for formset in formsets:
                for added_object in formset.new_objects:
                    change_message.append(
                        {
                            "added": {
                                "name": str(added_object._meta.verbose_name),
                                "object": str(added_object),
                            }
                        }
                    )
                for changed_object, changed_fields in formset.changed_objects:
                    for form in formset.initial_forms:
                        if form.instance != changed_object:
                            continue
                        msg_list = []
                        for field in changed_fields:
                            if form.initial[field] is not None and hasattr(form.fields[field], 'queryset'):
                                old_value = form.fields[field].queryset.get(id=form.initial[field])
                            else:
                                old_value = form.initial[field]
                            msg_list.append('\n"%s": "%s" > "%s"' % (field, old_value, form[field].value()))
                        change_message.append(
                            {
                                "changed": {
                                    "name": str(changed_object._meta.verbose_name),
                                    "object": str(changed_object),
                                    "fields": msg_list,
                                }
                            }
                        )
                for deleted_object in formset.deleted_objects:
                    change_message.append(
                        {
                            "deleted": {
                                "name": str(deleted_object._meta.verbose_name),
                                "object": str(deleted_object),
                            }
                        }
                    )
    return change_message

Upvotes: 0

Davide Brunato
Davide Brunato

Reputation: 752

To extend information about changes is sufficient an override of only ModelAdmin.construct_change_message method. No need to add attributes (eg. changed_fields) on the models, because you can build the log messages from initial data of forms and formsets.

This is a version that records the previous value for every changed field:

def construct_change_message(self, request, form, formsets):
    change_message = []
    if form.changed_data:
        msg_list = u''
        for field in form.changed_data:
            if form.initial[field] is not None and hasattr(form.fields[field], 'queryset'):
                old_value = form.fields[field].queryset.get(id=form.initial[field]).__unicode__()
            else:
                old_value = form.initial[field]
            msg_list = _("{0}field '{1}' from \"{2}\", ").format(msg_list, field, old_value)
        change_message.append(capfirst(_(u'changed {0}.').format(msg_list[:-2])))

    if formsets:
        for formset in formsets:
            for added_object in formset.new_objects:
                change_message.append(_('Added %(name)s "%(object)s".')
                                      % {'name': force_unicode(added_object._meta.verbose_name),
                                         'object': force_unicode(added_object)})
            for changed_object, changed_fields in formset.changed_objects :
                for form in formset.initial_forms:
                    if form.instance != changed_object:
                        continue
                    msg_list = u''
                    for field in changed_fields:
                        if form.initial[field] is not None and hasattr(form.fields[field], 'queryset'):
                            old_value = form.fields[field].queryset.get(id=form.initial[field]).__unicode__()
                        else:
                            old_value = form.initial[field]
                        msg_list = _("{0}field '{1}' from \"{2}\", ").format(msg_list, field, old_value)
                    change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
                                          % {'list': msg_list[:-2],
                                             'name': force_unicode(changed_object._meta.verbose_name),
                                             'object': force_unicode(changed_object)})
            for deleted_object in formset.deleted_objects:
                change_message.append(_('Deleted %(name)s "%(object)s".')
                                      % {'name': force_unicode(deleted_object._meta.verbose_name),
                                         'object': force_unicode(deleted_object)})
    change_message = ' '.join(change_message)
    return change_message or _('No fields changed.')

Upvotes: 1

Timmy O'Mahony
Timmy O'Mahony

Reputation: 54000

It's not 'default django'. You are using a contributed django application (the admin) so it's an app like any other (i.e. django-reversion) and to that extent, it doesn't support the feature you have in mind out of the box.

Luckily, the django admin is quite configurable. You could try overwriting the log_change method of your ModelAdmin class and making the change_message more verbose by detecting which fields have changed (by comparing the form values against the database values). Presumably you want this functionality across all apps in your project, so could either write a mixin to support this, or fork the entire admin and hardcode the functionality.

Upvotes: 2

Related Questions