/models.py
Python | 221 lines | 145 code | 17 blank | 59 comment | 14 complexity | e57458a19ca21975d11d66a2a14f97cd MD5 | raw file
- # -*- coding: utf-8 -*-
- '''
- When a referred object is saved, class will look the referring class for
- an attribute named in STATIC_DATA_UPDATER_ATTR which should be a callable
- with no parameters. If there is none, regular save() method is called.
- TODO:
- * It would be nice if this needn't be the first one in INSTALLED_APPS
- (well, at least the first one of the custom apps.) Tried but failed
- approaches include:
-
- * Listening to 'class_prepared' signals. Did not work properly right
- after DynamicContentReferral had been initialized.
- * A quick test with django.db.get_model/register_models but they didn't
- seem to offer anything -- didn't go all the way, though.
- * Calling the initialization in urls.py. Problem: the code isn't executed
- before the first request. Now it doesn't require any code in urls.py,
- though. On the other hand wouldn't have to worry about shell/sqlall errors.
-
- * The code should be able to handle saving cycles as well but the testing
- is far from complete.
- * Currently the deletions aren't listened to at all.
- * Registration of listeners could be done in urls.py as well but then
- the listeners would be registered only after the first request.
- License: FreeBSD, http://www.freebsd.org/copyright/freebsd-license.html
- Copyright: Tommi Penttinen, tommi@syneus.fi
- '''
- from django.db import models
- from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation
- from django.contrib.contenttypes.models import ContentType
- # Attribute of a django.db.models.Model used in determining the method for
- # updating the static content
- STATIC_DATA_UPDATER_ATTR = 'static_data_updater'
- # A marker attribute for django.db.models.Model to keep track of objects
- # which have their content being updated. Required to avoid cycles.
- # TODO: Existence of the attribute isn't controlled at the moment.
- # Problem is that the attributed is deleted after finishing the update
- # and it wouldn't be nice to end up deleting something which existed
- # previously.
- CONTENT_UPDATE_PENDING_ATTR = 'content_update_pending'
- class DynamicContentReferral(models.Model):
- """
- A model solely for the purpose of enabling a Generic Many to Many relation.
- """
- # FIXME: If GenericRelation fields are not used obsolete referral objects
- # won't get deleted by related_managers
- # We could remove them in class methods or in custom manager's get_query_set()
-
- # TODO: Should we define a custom manager for hiding the class when
- # called from other classes?
- # Check out http://adam.gomaa.us/blog/2009/feb/16/subclassing-django-querysets/
-
- referer_type = models.ForeignKey(ContentType, related_name='dynamic_content_referers')
- referer_id = models.IntegerField()
- referer = GenericForeignKey('referer_type', 'referer_id')
-
- referent_type = models.ForeignKey(ContentType, related_name='dynamic_content_referents')
- referent_id = models.IntegerField()
- referent = GenericForeignKey('referent_type', 'referent_id')
-
- @staticmethod
- def get_referents(referer):
- '''
- Returns a list of objects 'referer' refers to.
- Unfortunately we have to watch out for empty values.
-
- Note that the 'referer' itself isn't included
- '''
- # TODO: Remove referral to explicit class name
- return [referral.referent for referral in
- DynamicContentReferral.objects.filter(
- referer_type=ContentType.objects.get_for_model(referer),
- referer_id=referer.pk,
- )
- if referral.referent and not referral.referent == referer]
-
- @staticmethod
- def get_referers(referent):
- '''
- Returns a list of objects referring to 'referent'.
- Unfortunately we have to watch out for empty values.
-
- Note that the 'referent' itself isn't included.
- '''
- # TODO: Remove referral to explicit class name
- return [referral.referer for referral in
- DynamicContentReferral.objects.filter(
- referent_type=ContentType.objects.get_for_model(referent),
- referent_id=referent.pk,
- )
- if referral.referer and not referral.referer == referent]
-
- @staticmethod
- def set_referents(referer, referents):
- '''
- Removes old DynamicContentReferral objects for the 'referer'
- and creates new ones.
- '''
- old_referrals = DynamicContentReferral.objects.filter(referer_id=referer.pk, referer_type=ContentType.objects.get_for_model(type(referer)))
- old_referrals.delete()
-
- # Remove empty values and duplicates
- referents[:] = [r for r in referents if r != None]
- referents = set(referents)
- for r in referents:
- DynamicContentReferral.objects.create(referer=referer, referent=r)
-
- class Meta:
- unique_together = ('referer_type', 'referer_id', 'referent_type', 'referent_id')
- # Note: For some reason this doesn't work with the signal dispatcher.
- def __unicode__(self):
- return u"DynamicContentReferral: %s referring to %s" % (self.referer, self.referent)
-
- # TODO: How about the delete (and how about it for models)? pre_delete? post_delete?
- def save(self, force_insert=False, force_update=False):
- # Add a listener for the self.referrent
- # Note: that dispatch_uid must match the one for signals defined at
- # startup to maintain proper bookkeeping of signals.
- models.signals.post_save.connect(
- update_referrers, sender=self.referent.__class__, dispatch_uid='DynamicContentReferral',
- )
- super(type(self), self).save(force_insert=force_insert, force_update=force_update)
-
- static_data_updater = 'foo'
- def update_referrers(sender, instance, created, **kwargs):
- '''
- Checks if there were any objects referring to 'instance' and if, either
- call save() or the method defined in an attribute named as
- STATIC_DATA_UPDATER_ATTR variable of the referer class.
- for each referer.
- '''
- # TODO: Should we check the class or the instance
- # I.e. realtime manipulation and forcing vs. staticness
- # when resolving the call
- referers = DynamicContentReferral.get_referers(instance)
-
- # TODO: Can we end up in trouble by calling save on DynamicContentReferral
- # objects. Not that any page should actually refer to them.
- if referers:
- # Check that the attibute referenced by the name in
- # STATIC_DATA_UPDATER_ATTR exists and is callable
- for referer in referers:
- if hasattr(referer, STATIC_DATA_UPDATER_ATTR):
- func_name = getattr(referer, STATIC_DATA_UPDATER_ATTR)
- else:
- func_name = 'save'
-
- if hasattr(referer, func_name):
- to_be_called = getattr(referer, func_name)
- if not callable(to_be_called):
- raise TypeError( "Attribute %r set in %r of %s is not callable!" % (func_name, STATIC_DATA_UPDATER_ATTR, referer) )
- else:
- raise AttributeError("Attribute %r set in %r does not exist in %s" % (func_name, STATIC_DATA_UPDATER_ATTR, referer))
-
- # Ultimately, call the function.
- if not hasattr(referer, CONTENT_UPDATE_PENDING_ATTR):
- referer.content_update_pending = True
- to_be_called()
-
- # We should still have the attribute but who knows.
- if hasattr(referer, CONTENT_UPDATE_PENDING_ATTR):
- delattr(referer, CONTENT_UPDATE_PENDING_ATTR)
- def initialize_watchout():
- # FIXME: Well, this wouldn't work any other db
- # We need it anyway to avoid errors on
- # manage.py sqlall | shell | etc
- from _mysql_exceptions import ProgrammingError
- try:
- # Extract the referents and remove duplicates and
- # possibe null values
- referent_cls = [r.referent.__class__ for r in DynamicContentReferral.objects.all()]
- referent_cls = set(referent_cls)
- if None in referent_cls:
- referent_cls.pop(None)
- referent_cls = list(referent_cls)
-
- for c in referent_cls:
- # TODO: Do we need deletion listeners as well?
- # http://www.djangozen.com/blog/turning-off-django-signals
-
- # Setting the dispatch_uid we'll always have only one listener
- # per class.
- # print 'Adding a signal for class %s' % c
- models.signals.post_save.connect(update_referrers, sender=c, dispatch_uid='DynamicContentReferral')
- except ProgrammingError, e:
- pass
- # TODO: This is probably something similar to the registration of the admin site
- # so its code might offer some pointers.
- dynamic_contact_referral_initialized = False
- if dynamic_contact_referral_initialized == False:
- try:
- initialize_watchout()
- dynamic_contact_referral_initialized = True
- except Exception, e:
- # This particular AttributeError is usually attributed to
- # initialize_watchout() being executed before the model is properly
- # initialized.
- if type(e) == AttributeError and unicode(e) == "'NoneType' object has no attribute '_meta'":
- message = ' '.join(
- ["This error is probably due to some app listed before %r",
- "in INSTALLED_APPS probably refers to DynamicContentReferral.",
- "Please list %r before any other such apps.",
- "If the error persists after that as well, it's a bug.",
- "\nOriginal message: %r"]
- ) % (__package__, __package__, unicode(e))
- raise AttributeError, message
- else:
- raise e