Simple Observation in Django: How Can I Correctly Modify The `attrs` sent to __new__ of a Django Mod
- by DGGenuine
Hello,
I'm a strong proponent of the observer pattern, and this is what I'd like to be able to do in my Django models.py:
class AModel(Model):
__metaclass__ = SomethingMagical
@post_save(AnotherModel)
@classmethod
def observe_another_model_saved(klass, sender, instance, created, **kwargs):
pass
@pre_init('YetAnotherModel')
@classmethod
def observe_yet_another_model_initializing(klass, sender, *args, **kwargs):
pass
@post_delete('DifferentApp.SomeModel')
@classmethod
def observe_some_model_deleted(klass, sender, **kwargs):
pass
This would connect a signal with sender = the decorator's argument and receiver = the decorated method. Right now my signal connection code all exists in __init__.py which is okay, but a little unmaintainable. I want this code all in one place, the models.py file.
Thanks to helpful feedback from the community I'm very close (I think.) (I'm using a metaclass solution instead of the class decorator solution in the previous question/answer because you can't set attributes on classmethods, which I need.)
I am having a strange error I don't understand. At the end of my post are the contents of a models.py that you can pop into a fresh project/application to see the error. Set your database to sqlite and add the application to installed apps.
This is the error:
Validating models...
Unhandled exception in thread started by
Traceback (most recent call last):
File "/Library/Python/2.6/site-packages//lib/python2.6/site-packages/django/core/management/commands/runserver.py", line 48, in inner_run
File "/Library/Python/2.6/site-packages/django/core/management/base.py", line 253, in validate
raise CommandError("One or more models did not validate:\n%s" % error_text)
django.core.management.base.CommandError: One or more models did not validate:
local.myothermodel: 'my_model' has a relation with model MyModel, which has either not been installed or is abstract.
I've indicated a few different things you can comment in/out to fix the error. First, if you don't modify the attrs sent to the metaclass's __new__, then the error does not arise. (Note even if you copy the dictionary element by element into a new dictionary, it still fails; only using the exact attrs dictionary works.) Second, if you reference the first model by class rather than by string, the error also doesn't arise regardless of what you do in __new__.
I appreciate your help. I'll be githubbing the solution if and when it works. Maybe other people would enjoy a simplified way to use Django signals to observe application happenings.
#models.py
from django.db import models
from django.db.models.base import ModelBase
from django.db.models import signals
import pdb
class UnconnectedMethodWrapper(object):
sender = None
method = None
signal = None
def __init__(self, signal, sender, method):
self.signal = signal
self.sender = sender
self.method = method
def post_save(sender):
return _make_decorator(signals.post_save, sender)
def _make_decorator(signal, sender):
def decorator(view):
return UnconnectedMethodWrapper(signal, sender, view)
return decorator
class ConnectableModel(ModelBase):
"""
A meta class for any class that will have static or class methods
that need to be connected to signals.
"""
def __new__(cls, name, bases, attrs):
unconnecteds = {}
## NO WORK
newattrs = {}
for name, attr in attrs.iteritems():
if isinstance(attr, UnconnectedMethodWrapper):
unconnecteds[name] = attr
newattrs[name] = attr.method #replace the UnconnectedMethodWrapper with the method it wrapped.
else:
newattrs[name] = attr
## NO WORK
# newattrs = {}
# for name, attr in attrs.iteritems():
# newattrs[name] = attr
## WORKS
# newattrs = attrs
new = super(ConnectableModel, cls).__new__(cls, name, bases, newattrs)
for name, unconnected in unconnecteds.iteritems():
_connect_signal(unconnected.signal, unconnected.sender, getattr(new, name), new._meta.app_label)
return new
def _connect_signal(signal, sender, receiver, default_app_label):
# full implementation also accepts basestring as sender and will look up model accordingly
signal.connect(sender=sender, receiver=receiver)
class MyModel(models.Model):
__metaclass__ = ConnectableModel
@post_save('In my application this string matters')
@classmethod
def observe_it(klass, sender, instance, created, **kwargs):
pass
@classmethod
def normal_class_method(klass):
pass
class MyOtherModel(models.Model):
## WORKS
# my_model = models.ForeignKey(MyModel)
## NO WORK
my_model = models.ForeignKey('MyModel')