Need to examine and act on old vs. new at .save() time

3,443 views
Skip to the first unread message

Jeff

unread,
9 Jan 2012, 20:39:3709/01/2012
to Django users
Hi all,

I need to be able to determine, at .save() time (before I call the
parent class' .save()), if a certain field on my model object was
changed, and act on that via some custom code. How can I do this,
bearing in mind that we call .save() from non-web code, etc? We use
Django's ORM from various other non-web interfaces and code. My point
is that we can't just inspect a web form's result data.

I'd appreciate any thoughts.

Andre Terra

unread,
10 Jan 2012, 08:14:0510/01/2012
to django...@googlegroups.com
You can override the model's save method?[1][2]

Cheers,
AT


[1] https://docs.djangoproject.com/en/dev/ref/models/instances/#saving-objects
[2] https://docs.djangoproject.com/en/dev/topics/db/models/#overriding-model-methods



--
You received this message because you are subscribed to the Google Groups "Django users" group.
To post to this group, send email to django...@googlegroups.com.
To unsubscribe from this group, send email to django-users...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/django-users?hl=en.


Jeff

unread,
10 Jan 2012, 09:58:0910/01/2012
to Django users
On Jan 10, 8:14 am, Andre Terra <andrete...@gmail.com> wrote:
> You can override the model's save method?[1][2]

Hi Andre,

Thanks for replying. Yes, I'm aware that I need to override .save().
See:

> > I need to be able to determine, at .save() time (before I call the
> > parent class' .save()), ...

I need to know how to, in my custom .save() for a model:

a) determine what fields changed (if any) (HOW?)
b) and take certain actions if field X was one of the changed fields.
(I know how)

Nahuel Defossé

unread,
10 Jan 2012, 10:09:3610/01/2012
to django...@googlegroups.com
Hi,
I think you should see at the forms's cleaned_data attribute and
compare it against the model current state, before saving.

Regards
Nahuel

2012/1/10 Jeff <jbl...@mitre.org>:

Kelly Nicholes

unread,
10 Jan 2012, 10:42:3410/01/2012
to Django users
Isn't there a form.changed_data ?

Jeff

unread,
10 Jan 2012, 11:23:1410/01/2012
to Django users
On Jan 10, 10:09 am, Nahuel Defossé <nahuel.defo...@gmail.com> wrote:
> I think you should see at the forms's cleaned_data attribute and
> compare it against the model current state, before saving.

As I indicated in the 1st post:

> >> > I need to ... ... How can I do this,

Jeff

unread,
10 Jan 2012, 11:24:3910/01/2012
to Django users
On Jan 10, 10:42 am, Kelly Nicholes <kelbolici...@gmail.com> wrote:
> Isn't there a form.changed_data ?

See 1st post below indicating we manipulate Django data from various
Django
codebases, not just a web form.

> > I need to.. ... How can I do this,

Jeff

unread,
10 Jan 2012, 15:15:3210/01/2012
to Django users
For example, altering 'pana.our.org''s netgroups field, the following
save() method results in this debug line which makes no sense to me:

CHANGED: Device pana.our.org had old netgroups [{'name':
u'testnetgroup', 'desc': u''}] and now has new netgroups [{'name':
u'testnetgroup', 'desc': u''}]

Both the new and old netgroups in that line are identical and reflect
the NEW value. Working code would state:

CHANGED: Device pana.our.org had old netgroups [{'name':
u'testnetgroup', 'desc': u''}] and now has new netgroups []

class Device(models.Model):
# ...
netgroups = models.ManyToManyField(Netgroup,
null=True,
verbose_name="Netgroups",
blank=True)
# ...
def save(self, *args, **kwargs):
try:
#
# Get the THEORETICALLY old data from database for this
device
#
oldnetgroups =
Device.objects.get(pk=self.pk).netgroups.values_list()
except:
# This is likely a brand new device object, so can't look
it up
oldnetgroups = None
#
# Compare the THEORETICAL old netgroups value with the
theoretical new value
#
if (oldnetgroups != None) and (oldnetgroups !=
self.netgroups.values_list()):
prefix = 'CHANGED'
else:
prefix = 'UNCHANGED'
if oldnetgroups != None:
fd = open('/tmp/test.log', 'a')
fd.write('%s: Device %s had old netgroups %s and now has
new netgroups %s\n' % (prefix, self.name, oldnetgroups,
self.netgroups.values()))
fd.close()
super(Device, self).save(*args, **kwargs)

Jeff

unread,
10 Jan 2012, 15:34:5810/01/2012
to Django users
Sigh. I hate Google Groups via web. Here is a readable version of
the .save() method below.

https://gist.github.com/1591028

Matt Schinckel

unread,
10 Jan 2012, 17:57:5410/01/2012
to django...@googlegroups.com
The way I generally do this type of thing is: https://gist.github.com/1591723

Lately, I've been using model validation: the clean() method instead of the save() method, but I can't remember if this is always called.


The other thing you can do is have a pre_save signal listener. That might be useful if you have several classes that need to have the same check done.


Regards,

Matt.

hank...@gmail.com

unread,
11 Jan 2012, 10:03:0111/01/2012
to Django users
I go about this a different way, which is to monkeypatch the object
with the relevant initial values at __init__:

https://gist.github.com/1595055

Saves you a database call.

Jeff

unread,
11 Jan 2012, 11:53:5111/01/2012
to Django users
Matt,

On Jan 10, 5:57 pm, Matt Schinckel <m...@schinckel.net> wrote:
> The way I generally do this type of thing
> is:https://gist.github.com/1591723

Thanks for the reply!

This looked awfully similar to my logic (although yours is 10x more
clean),
but I was excited to implement it this morning. After doing so, I am
seeing
the same behavior: old_version.foo_field is matching self.foo_field
no matter
what I do to foo_field.

https://gist.github.com/1595587

For example, device 'mote' in the database shows no values for field
netgroups.
Viewing 'mote' in the Django admin interface, I add a netgroup called
'testnetgroup'
from the list of netgroups available. I click save. Viewing 'mote'
again shows that
the modification took place -- mote references netgroup
'testnetgroup'.

However, the log from that 'save' shows the following bogus
information:

Device : in custom .save()
Device : old_version.netgroups.values(): []
Device : self.netgroups.values(): []
Device : old does NOT match new

1. The "new" value is showing incorrectly as an empty list.
2. The test shows them the same, but is reporting that they don't even
match

Viewing device 'mote' again, *removing* the only netgroup from it
(testnetgroup)
and re-saving reports similar bogus info:

Device : in custom .save()
Device : old_version.netgroups.values(): [{'name': u'testnetgroup',
'desc': u''}]
Device : self.netgroups.values(): [{'name': u'testnetgroup', 'desc':
u''}]
Device : old does NOT match new

1. The "new" value is showing incorrectly as [{'name':
u'testnetgroup', 'desc': u''}] and
should be an empty list.
2. The test shows them the same, but is reporting that they don't even
match

This should have said:

Device : in custom .save()
Device : old_version.netgroups.values(): [{'name': u'testnetgroup',
'desc': u''}]
Device : self.netgroups.values(): []
Device : old does NOT match new

Jeff

unread,
11 Jan 2012, 11:59:4111/01/2012
to Django users
But is costly when the field in question is foreign, no? Mine's a M2M.

Andre Terra

unread,
11 Jan 2012, 11:59:2211/01/2012
to django...@googlegroups.com
The important question here is, what are you trying to achieve, outside of the functionality itself? Are you trying to log changes to provide an audit trail? If so, there are tools for that.


Cheers,
AT

--
You received this message because you are subscribed to the Google Groups "Django users" group.

Jeff

unread,
11 Jan 2012, 12:22:0611/01/2012
to Django users
On Jan 11, 11:59 am, Andre Terra <andrete...@gmail.com> wrote:
> The important question here is, what are you trying to achieve, outside of
> the functionality itself? Are you trying to log changes to provide an audit
> trail? If so, there are tools for that.

I wish that's all I was doing. Then again, I also wish Google Groups
hadn't wrapped my lines at around 60 columns in the previous
post.

The 'audit' looking stuff you're seeing is merely my debugging code to
figure out what the heck is going wrong with my approach.

When Device.netgroups (a M2M field) changes, we need to perform
some python-ldap operations.

Tom Evans

unread,
11 Jan 2012, 12:26:4411/01/2012
to django...@googlegroups.com
On Wed, Jan 11, 2012 at 5:22 PM, Jeff <jbl...@mitre.org> wrote:
> When Device.netgroups (a M2M field) changes, we need to perform
> some python-ldap operations.

Have you considered simply going back to the database to check what
the values currently are? It would be inefficient, but clean and
concise.

Cheers

Tom

hank...@gmail.com

unread,
11 Jan 2012, 12:44:5911/01/2012
to Django users
> But is costly when the field in question is foreign, no?  Mine's a
M2M.

Sure. There's probably no way around that, though, except for
judicious use of select_related.

https://docs.djangoproject.com/en/dev/ref/models/querysets/#select-related

Jeff

unread,
11 Jan 2012, 13:58:2911/01/2012
to Django users
I've just found that the problem is related to my desired
field being M2M. Anyone know what is going on here? :(

My approach (and Matt's) works fine on a simple CharField field.

class Device(models.Model):
# ...
floor = models.CharField('Floor', max_length=10, blank=True)
# ...

Device : old floor:
Device : new floor: 444444

Device : old floor: 444444
Device : new floor: 5555555555

Device : old floor: 5555555555
Device : new floor: 22

Device : old floor: 22
Device : new floor:
Message has been deleted

Jeff

unread,
12 Jan 2012, 12:20:5912/01/2012
to Django users

On Jan 11, 9:23 pm, Dennis Lee Bieber <wlfr...@ix.netcom.com> wrote:
> On Wed, 11 Jan 2012 17:26:44 +0000, Tom Evans <tevans...@googlemail.com>
> wrote:
>
> >On Wed, Jan 11, 2012 at 5:22 PM, Jeff <jbla...@mitre.org> wrote:
> >> When Device.netgroups (a M2M field) changes, we need to perform
> >> some python-ldap operations.
>
> >Have you considered simply going back to the database to check what
> >the values currently are? It would be inefficient, but clean and
> >concise.
>
>         I've forgotten how many means of changing this data there are,
> but...

Dennis,

Several.

A web-only solution won't work.

I found the 'm2m_changed' signal yesterday, read that you can't
determine *what* changed by using it, and also ended up directed
to some open bug reports... etc... and threw up my hands.

To the best of my digging, there is no way to accurately tell
in SomeModel.save() whether the previous value of MyM2MField
and the new value are the same, and/or what the differences
are.

I ended up completely restructuring stuff (I'll spare you the
details) so that I can just say "whatever .netgroups looks
like AFTER save, make LDAP look exactly like that." This
is essentially what Tom Evans suggested in his reply. It's
inefficient, and clearly a workaround, but is at least doable.

It boils down in LDAP-terms to saying "replace all attribute
values for X with the following full data. I don't care what
your old data was." ... instead of saying, "remove value Y
from the 1000 values set on X" :|

Thank you all for your efforts to help.

Furbee

unread,
12 Jan 2012, 12:40:5512/01/2012
to django...@googlegroups.com
The only other way that I could think to do it would be to write your own M2M object. Just an object with two foreign key fields to reference X and Y. In that object you could overwrite the save method to check for which relations currently exist for a given reference to X and update accordingly. I'm not sure this would work for you, but it might be an alternative, although it violates the DRY principle.

Furbee

Torsten Bronger

unread,
13 Jan 2012, 02:25:5613/01/2012
to django...@googlegroups.com
Hall�chen!

Jeff writes:

> [...]


>
> I found the 'm2m_changed' signal yesterday, read that you can't
> determine *what* changed by using it, and also ended up directed
> to some open bug reports... etc... and threw up my hands.

But the "action" and "pk_set" arguments contain this information.

Tsch�,
Torsten.

--
Torsten Bronger Jabber ID: torsten...@jabber.rwth-aachen.de
or http://bronger-jmp.appspot.com

Jeff

unread,
13 Jan 2012, 14:45:3213/01/2012
to Django users
Remarkably, I've just today tripped over this same exact
problem again. Turns out even using a post_save signal
callback function does not provide the information about
the new M2M data.

# callback for post_save signal, debug code removed for brevity
def ldap_netgroup_save(sender, **kwargs):
instance = kwargs['instance']
newng = kwargs['created']
cn = 'cn=%s,ou=Netgroup,%s' % (instance.name, base_dn)
connection = ldap.initialize(ldap_uri)
connection.simple_bind_s(bind_dn, bind_password)
if newng:
# WORKS
connection.add_s(cn, [('objectClass', 'nisNetgroup'),])
ifnames = []
for i in instance.interfaces.all():
ifnames = ifnames + make_nisnetgrouptriples(i.fqdn)
# HERE 'ifnames' is INCORRECTLY []
connection.modify_s(cn, [(ldap.MOD_REPLACE, 'nisNetgroupTriple',
ifnames),])

[NEW NETGROUP]*click*

Netgroup: foobar
Interfaces: I select 3
[SAVE]

DEBUG LOG:
Made new netgroup in LDAP: mynewnetgroup
saving interfaces as []

I view the netgroup in the admin interface and sure enough, all 3
'interfaces' are associated properly.

If I just click 'save' then it reports what it should
have reported when I made the new netgroup all along:

DEBUG LOG:
Existing netgroup in LDAP: mynewnetgroup
saving interfaces as ['(agilent,,)', '(agilent.our.org,,)',
'(ape,,)', '(ape.our.org,,)', '(aprh6test,,)',
'(aprh6test.our.org,,)']

This is such nonsense :(
Reply all
Reply to author
Forward
0 new messages