I have some code hooked to the post-save signal for some of my models,
but due to interaction with some non-django subsystems I need some
fine-grained control and knowledge of what fields actually was
affected before save() was called.
I have solved this, in theory, by sub-classing models.Model and adding
a __setattr__() method that records what fields was changed. The
sub-classing works, but I can't figure out how to save the data about
the changes on the model object. Should I try to extend or
monkey-patch the Meta-class? If so, how do I get my own variables in
there, and initialized? Ideally, I would like to be able to do
something like:
class Meta:
trigger_fields = ['title', 'description']
.. and then later, in my __setattr__() I was hoping I could save data
about changes made (to the fields in _meta.trigger_fields) on my model
object to some other variable in the Meta object. This part of the
plan has proved a lot harder than I thought, as I couldn't quite
follow how the model object is constructed.
At the end of my rambling I find myself lonely, cold and going over
the same questions:
- Was I right in sublclassing models.Model or should this be done in
a subclass of ModelBase instead (due to it being where the objects are
constructed)?
- How do I extend the Meta class and have my own variables
initialized to the values I declare in the meta-class definition?
Anyone have any creative input on this?
Regards,
Daniel
Daniel, I was toying around with that kind of stuff back and forth and
found that Meta
(and Admin )
is hacked into django core in couple of places so extensions mean you
have to patch
django (which I also did but that's another story ;-) and I ultimately
also wanted ot
change some of the admin behavior. Analogous to your gripes, post_save
was not good enough for me
either since I wanted something that could fail on save when things beyond
model's save went wrong (but had to be done after database object was
written ;-).
So, I ended up kind of subclassing the Models to get some standard
attributes
and method into everything (someone else's snippet here).
class ConfigsFOO(object):
"""@brief pure metaclass, never use for anything but inheritance.
Anything that generates configFOO things needs to inherit via
django_extends
this thing. """
FOOconfigfileswritten = models.BooleanField(primary_key=False,
core=False, null=False, default=True, editable=False,
verbose_name = _("Set
when FOO config has been written out"))
def django_extends(base):
"""@brief Allows to 'inherit' things in django since models do not
support inheritance.
It fakes up the dictionary to include the base class in bases. """
class ProxyMetaClass(models.Model.__metaclass__):
def __new__(cls, name, bases, attrs):
"""@brief The following attributes must be moved *prior* to
deferring
execution to the models.Model's metaclass, because they
will be manipulated by __new__
\arg - models.fields.Field instances
\arg - objects (ModelManager)
"""
for key in dir(base):
if not key.startswith('_'):
obj = getattr(base, key)
if isinstance(obj, models.fields.Field) or \
key == 'objects':
attrs[key] = obj
delattr(base, key)
# Delete objects that have attribute
'contribute_to_class'
# for otherwise that will break in
# base.py:Model.add_to_class
# Eg: inner classes inherited from models.Manager
elif hasattr(obj, 'contribute_to_class'):
delattr(base, key)
return super(ProxyMetaClass,
cls).__new__(cls, name,
tuple([base]+list(bases)),
attrs)
frame = sys._getframe(1)
frame.f_locals['__metaclass__'] = ProxyMetaClass
class FOOInterfaceConfiguration(models.Model):
"""@brief configuration of an FOO interface """
django_extends(ConfigsFOO)
Was that a good solution ? Probably not much so, kind of twisted. I
should probably go and look for pyprotocols for that
purpose ;-) i.e. forcing Models to have protocols (albeit that doesn't
solve the common field problem
so in a sense pyprotocols is not the full enchilada).
I also added my own options, kind of like class Admin: That looks much
cleaner and I use it a lot.
Only the contribute_to_class is django magic but with some greps in
django code you get the hang of it ;-)
So
class FOOFormOptions(object):
def __init__(self)
self.manager = manager or Manager()
@classmethod
def contribute_to_class(self, cls, name):
""" complicated beast, takes in the contribute call from the
django base class construction, generates an instance
of this class and fills it with the parameters being globals in
the class definition in a model. """
cls._meta.fooformoptions = FOOFormOptions(**dict([(k, v) for k,
v in self.__dict__.items() if not k.startswith('_')]))
# Make sure our manager/form classes have access to the model
cls._meta.fooformoptions.manager.model = cls
and then simply in a model:
class Admin:
pass
class FOOOptions(FOOFormOptions):
whatever_you_like=[]
pass
for the thing to show up beside _meta.admin as _meta.fooformoptions
things. I like that and use it quite a lot. I think that
will give you a hammer to try to whack your nail.
good luck
-- tony
Funny timing on this one -- just yesterday I started thinking about
mechanisms for adding custom metadata to ``Meta``; I agree it'd be
very useful. In your case, you could add this ``trigger_fields``
argument and then listen to the ``class_prepared`` signal to handle
any model with that option.
I've not thought too much about the mechanism here, but in my mind you
shouldn't have to subclass Model just to do this -- that's silly.
Ideally, your app could somehow "register" extra options with
ModelBase. Those options could be used by a signal like I described
above, or perhaps they'd be registered with a callback...
Dunno, exactly -- like I said, I just started thinking about this
yesterday -- but I just wanted to chime in and let you know I, at
least, think it's a good idea. If you wanna run with the idea, be my
guest!
Jacob
I used prz's last approach to add data to the _meta object, and that
works nicely, although lifting the restriction in the Option-object on
what variables are valid in the Meta-class would probably be a better
solution, unless there is some side-effect I didn't notice while
reading through the code. I also got rid of my Model sub-class, and
added my __setattr__-method with a mix-in instead, wich seems cleaner.
So now I have some custom data monkey patched on to the Meta object,
and I'm able to modify that data from my __setattr__. The last thing I
need to get all I planned working is someway to clear the variables I
added when save() is called on the object. This is necessary for
situations when a model object is reused and changed in a different
way and saved again. I don't want information about changes made the
first time the object was used to stick around after save()..
I have explored some code found on djangosnippets[1] and other
places[2] to get a good clean solution to this, but I'm not sure what
would be a good solution to this..
What's by best option here? Something like what is described in the
examples or some other way entirely?
Thanks a lot!
// Daniel
[1] http://www.djangosnippets.org/snippets/329/ and
http://www.djangosnippets.org/snippets/529/
[2] http://blog.elsdoerfer.name/2008/02/13/revisited-common-fields-model-subclassing/