PageRenderTime 417ms CodeModel.GetById 31ms RepoModel.GetById 1ms app.codeStats 0ms

/models.py

https://bitbucket.org/syneus/dynamic-content-watchout
Python | 221 lines | 145 code | 17 blank | 59 comment | 14 complexity | e57458a19ca21975d11d66a2a14f97cd MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. '''
  3. When a referred object is saved, class will look the referring class for
  4. an attribute named in STATIC_DATA_UPDATER_ATTR which should be a callable
  5. with no parameters. If there is none, regular save() method is called.
  6. TODO:
  7. * It would be nice if this needn't be the first one in INSTALLED_APPS
  8. (well, at least the first one of the custom apps.) Tried but failed
  9. approaches include:
  10. * Listening to 'class_prepared' signals. Did not work properly right
  11. after DynamicContentReferral had been initialized.
  12. * A quick test with django.db.get_model/register_models but they didn't
  13. seem to offer anything -- didn't go all the way, though.
  14. * Calling the initialization in urls.py. Problem: the code isn't executed
  15. before the first request. Now it doesn't require any code in urls.py,
  16. though. On the other hand wouldn't have to worry about shell/sqlall errors.
  17. * The code should be able to handle saving cycles as well but the testing
  18. is far from complete.
  19. * Currently the deletions aren't listened to at all.
  20. * Registration of listeners could be done in urls.py as well but then
  21. the listeners would be registered only after the first request.
  22. License: FreeBSD, http://www.freebsd.org/copyright/freebsd-license.html
  23. Copyright: Tommi Penttinen, tommi@syneus.fi
  24. '''
  25. from django.db import models
  26. from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation
  27. from django.contrib.contenttypes.models import ContentType
  28. # Attribute of a django.db.models.Model used in determining the method for
  29. # updating the static content
  30. STATIC_DATA_UPDATER_ATTR = 'static_data_updater'
  31. # A marker attribute for django.db.models.Model to keep track of objects
  32. # which have their content being updated. Required to avoid cycles.
  33. # TODO: Existence of the attribute isn't controlled at the moment.
  34. # Problem is that the attributed is deleted after finishing the update
  35. # and it wouldn't be nice to end up deleting something which existed
  36. # previously.
  37. CONTENT_UPDATE_PENDING_ATTR = 'content_update_pending'
  38. class DynamicContentReferral(models.Model):
  39. """
  40. A model solely for the purpose of enabling a Generic Many to Many relation.
  41. """
  42. # FIXME: If GenericRelation fields are not used obsolete referral objects
  43. # won't get deleted by related_managers
  44. # We could remove them in class methods or in custom manager's get_query_set()
  45. # TODO: Should we define a custom manager for hiding the class when
  46. # called from other classes?
  47. # Check out http://adam.gomaa.us/blog/2009/feb/16/subclassing-django-querysets/
  48. referer_type = models.ForeignKey(ContentType, related_name='dynamic_content_referers')
  49. referer_id = models.IntegerField()
  50. referer = GenericForeignKey('referer_type', 'referer_id')
  51. referent_type = models.ForeignKey(ContentType, related_name='dynamic_content_referents')
  52. referent_id = models.IntegerField()
  53. referent = GenericForeignKey('referent_type', 'referent_id')
  54. @staticmethod
  55. def get_referents(referer):
  56. '''
  57. Returns a list of objects 'referer' refers to.
  58. Unfortunately we have to watch out for empty values.
  59. Note that the 'referer' itself isn't included
  60. '''
  61. # TODO: Remove referral to explicit class name
  62. return [referral.referent for referral in
  63. DynamicContentReferral.objects.filter(
  64. referer_type=ContentType.objects.get_for_model(referer),
  65. referer_id=referer.pk,
  66. )
  67. if referral.referent and not referral.referent == referer]
  68. @staticmethod
  69. def get_referers(referent):
  70. '''
  71. Returns a list of objects referring to 'referent'.
  72. Unfortunately we have to watch out for empty values.
  73. Note that the 'referent' itself isn't included.
  74. '''
  75. # TODO: Remove referral to explicit class name
  76. return [referral.referer for referral in
  77. DynamicContentReferral.objects.filter(
  78. referent_type=ContentType.objects.get_for_model(referent),
  79. referent_id=referent.pk,
  80. )
  81. if referral.referer and not referral.referer == referent]
  82. @staticmethod
  83. def set_referents(referer, referents):
  84. '''
  85. Removes old DynamicContentReferral objects for the 'referer'
  86. and creates new ones.
  87. '''
  88. old_referrals = DynamicContentReferral.objects.filter(referer_id=referer.pk, referer_type=ContentType.objects.get_for_model(type(referer)))
  89. old_referrals.delete()
  90. # Remove empty values and duplicates
  91. referents[:] = [r for r in referents if r != None]
  92. referents = set(referents)
  93. for r in referents:
  94. DynamicContentReferral.objects.create(referer=referer, referent=r)
  95. class Meta:
  96. unique_together = ('referer_type', 'referer_id', 'referent_type', 'referent_id')
  97. # Note: For some reason this doesn't work with the signal dispatcher.
  98. def __unicode__(self):
  99. return u"DynamicContentReferral: %s referring to %s" % (self.referer, self.referent)
  100. # TODO: How about the delete (and how about it for models)? pre_delete? post_delete?
  101. def save(self, force_insert=False, force_update=False):
  102. # Add a listener for the self.referrent
  103. # Note: that dispatch_uid must match the one for signals defined at
  104. # startup to maintain proper bookkeeping of signals.
  105. models.signals.post_save.connect(
  106. update_referrers, sender=self.referent.__class__, dispatch_uid='DynamicContentReferral',
  107. )
  108. super(type(self), self).save(force_insert=force_insert, force_update=force_update)
  109. static_data_updater = 'foo'
  110. def update_referrers(sender, instance, created, **kwargs):
  111. '''
  112. Checks if there were any objects referring to 'instance' and if, either
  113. call save() or the method defined in an attribute named as
  114. STATIC_DATA_UPDATER_ATTR variable of the referer class.
  115. for each referer.
  116. '''
  117. # TODO: Should we check the class or the instance
  118. # I.e. realtime manipulation and forcing vs. staticness
  119. # when resolving the call
  120. referers = DynamicContentReferral.get_referers(instance)
  121. # TODO: Can we end up in trouble by calling save on DynamicContentReferral
  122. # objects. Not that any page should actually refer to them.
  123. if referers:
  124. # Check that the attibute referenced by the name in
  125. # STATIC_DATA_UPDATER_ATTR exists and is callable
  126. for referer in referers:
  127. if hasattr(referer, STATIC_DATA_UPDATER_ATTR):
  128. func_name = getattr(referer, STATIC_DATA_UPDATER_ATTR)
  129. else:
  130. func_name = 'save'
  131. if hasattr(referer, func_name):
  132. to_be_called = getattr(referer, func_name)
  133. if not callable(to_be_called):
  134. raise TypeError( "Attribute %r set in %r of %s is not callable!" % (func_name, STATIC_DATA_UPDATER_ATTR, referer) )
  135. else:
  136. raise AttributeError("Attribute %r set in %r does not exist in %s" % (func_name, STATIC_DATA_UPDATER_ATTR, referer))
  137. # Ultimately, call the function.
  138. if not hasattr(referer, CONTENT_UPDATE_PENDING_ATTR):
  139. referer.content_update_pending = True
  140. to_be_called()
  141. # We should still have the attribute but who knows.
  142. if hasattr(referer, CONTENT_UPDATE_PENDING_ATTR):
  143. delattr(referer, CONTENT_UPDATE_PENDING_ATTR)
  144. def initialize_watchout():
  145. # FIXME: Well, this wouldn't work any other db
  146. # We need it anyway to avoid errors on
  147. # manage.py sqlall | shell | etc
  148. from _mysql_exceptions import ProgrammingError
  149. try:
  150. # Extract the referents and remove duplicates and
  151. # possibe null values
  152. referent_cls = [r.referent.__class__ for r in DynamicContentReferral.objects.all()]
  153. referent_cls = set(referent_cls)
  154. if None in referent_cls:
  155. referent_cls.pop(None)
  156. referent_cls = list(referent_cls)
  157. for c in referent_cls:
  158. # TODO: Do we need deletion listeners as well?
  159. # http://www.djangozen.com/blog/turning-off-django-signals
  160. # Setting the dispatch_uid we'll always have only one listener
  161. # per class.
  162. # print 'Adding a signal for class %s' % c
  163. models.signals.post_save.connect(update_referrers, sender=c, dispatch_uid='DynamicContentReferral')
  164. except ProgrammingError, e:
  165. pass
  166. # TODO: This is probably something similar to the registration of the admin site
  167. # so its code might offer some pointers.
  168. dynamic_contact_referral_initialized = False
  169. if dynamic_contact_referral_initialized == False:
  170. try:
  171. initialize_watchout()
  172. dynamic_contact_referral_initialized = True
  173. except Exception, e:
  174. # This particular AttributeError is usually attributed to
  175. # initialize_watchout() being executed before the model is properly
  176. # initialized.
  177. if type(e) == AttributeError and unicode(e) == "'NoneType' object has no attribute '_meta'":
  178. message = ' '.join(
  179. ["This error is probably due to some app listed before %r",
  180. "in INSTALLED_APPS probably refers to DynamicContentReferral.",
  181. "Please list %r before any other such apps.",
  182. "If the error persists after that as well, it's a bug.",
  183. "\nOriginal message: %r"]
  184. ) % (__package__, __package__, unicode(e))
  185. raise AttributeError, message
  186. else:
  187. raise e