bgneal@753: """This module contains a custom Haystack signal processing class to update the bgneal@753: search index in realtime. We update our search index by enqueuing edits and bgneal@753: deletes into a queue for batch processing. Our class ensures we only enqueue bgneal@753: content that should be in the search index. bgneal@469: bgneal@469: """ bgneal@753: from django.db.models import signals bgneal@753: import queued_search.signals bgneal@924: import haystack bgneal@469: bgneal@753: from bio.signals import profile_content_update bgneal@753: from forums.signals import topic_content_update, post_content_update bgneal@469: bgneal@753: import ygroup.models bgneal@753: from weblinks.models import Link bgneal@753: from podcast.models import Item bgneal@753: from news.models import Story bgneal@753: from downloads.models import Download bgneal@753: from forums.models import Forum, Topic, Post bgneal@753: from bio.models import UserProfile bgneal@753: bgneal@753: bgneal@753: UID = 'custom_search.signals' bgneal@753: bgneal@753: bgneal@753: class QueuedSignalProcessor(queued_search.signals.QueuedSignalProcessor): bgneal@469: """ bgneal@753: This customized version of queued_search's QueuedSignalProcessor bgneal@753: conditionally enqueues items to be indexed. bgneal@469: bgneal@469: """ bgneal@753: def __init__(self, *args, **kwargs): bgneal@753: bgneal@753: # We assume that it is okay to attempt to delete a model from the search bgneal@753: # index even if the model object is not in the index. In other words, bgneal@753: # attempting to delete an object from the index will not cause any bgneal@753: # errors if it is not in the index. Thus if we see an object that has an bgneal@753: # 'is_public' attribute, and it is false, we can safely enqueue a delete bgneal@753: # in case the 'is_public' attribute just went from True to False. We bgneal@753: # have no way of knowing that, it could have been False all along, but we bgneal@753: # just try the delete in case to be safe. bgneal@753: bgneal@753: # To make the code easier to read, use a table to drive our signal bgneal@753: # connecting and disconnecting: bgneal@753: self.signal_chain = [ bgneal@753: # Yahoo Group posts are always updated: bgneal@753: (signals.post_save, ygroup.models.Post, self.enqueue_save), bgneal@753: (signals.post_delete, ygroup.models.Post, self.enqueue_delete), bgneal@753: bgneal@753: # Weblink Links are updated if they are public: bgneal@753: (signals.post_save, Link, self.enqueue_public_save), bgneal@753: (signals.post_delete, Link, self.enqueue_delete), bgneal@753: bgneal@753: # Podcast Items are always updated: bgneal@753: (signals.post_save, Item, self.enqueue_save), bgneal@753: (signals.post_delete, Item, self.enqueue_delete), bgneal@753: bgneal@753: # News Stories are always updated: bgneal@753: (signals.post_save, Story, self.enqueue_save), bgneal@753: (signals.post_delete, Story, self.enqueue_delete), bgneal@753: bgneal@753: # Downloads are updated if they are public: bgneal@753: (signals.post_save, Download, self.enqueue_public_save), bgneal@753: (signals.post_delete, Download, self.enqueue_delete), bgneal@753: bgneal@753: # Forum Topics are updated if they belong to a public forum: bgneal@753: (topic_content_update, None, self.enqueue_topic_save), bgneal@753: (signals.post_delete, Topic, self.enqueue_delete), bgneal@753: bgneal@753: # Forum Posts are updated if they belong to a public forum: bgneal@753: (post_content_update, None, self.enqueue_post_save), bgneal@753: (signals.post_delete, Post, self.enqueue_delete), bgneal@753: bgneal@753: # UserProfiles are updated when we receive a special signal: bgneal@753: (profile_content_update, None, self.enqueue_profile), bgneal@753: (signals.post_delete, UserProfile, self.enqueue_delete), bgneal@753: ] bgneal@753: bgneal@753: super(QueuedSignalProcessor, self).__init__(*args, **kwargs) bgneal@753: bgneal@753: def setup(self): bgneal@753: """We override setup() so we can attach signal handlers to only the bgneal@753: models we search on. In some cases we have custom signals to tell us bgneal@753: when to update the search index. bgneal@469: bgneal@469: """ bgneal@753: for signal, sender, receiver in self.signal_chain: bgneal@753: signal.connect(receiver, sender=sender, dispatch_uid=UID) bgneal@469: bgneal@753: def teardown(self): bgneal@753: """Disconnect all signals we previously connected.""" bgneal@753: for signal, sender, receiver in self.signal_chain: bgneal@753: signal.disconnect(receiver, sender=sender, dispatch_uid=UID) bgneal@753: bgneal@753: def enqueue_public_save(self, sender, instance, **kwargs): bgneal@753: """Index only if the instance is_public. bgneal@753: bgneal@753: If not, enqueue a delete just in case the is_public flag got flipped bgneal@753: from True to False. bgneal@469: bgneal@469: """ bgneal@753: if instance.is_public: bgneal@753: self.enqueue_save(sender, instance, **kwargs) bgneal@753: else: bgneal@753: self.enqueue_delete(sender, instance, **kwargs) bgneal@677: bgneal@753: def enqueue_topic_save(self, sender, **kwargs): bgneal@753: """Enqueue only if the topic instance belongs to a public forum.""" bgneal@753: if sender.forum.id in Forum.objects.public_forum_ids(): bgneal@753: self.enqueue_save(Topic, sender, **kwargs) bgneal@677: bgneal@753: def enqueue_post_save(self, sender, **kwargs): bgneal@753: """Enqueue only if the post instance belongs to a public forum.""" bgneal@753: if sender.topic.forum.id in Forum.objects.public_forum_ids(): bgneal@753: self.enqueue_save(Post, sender, **kwargs) bgneal@677: bgneal@753: def enqueue_profile(self, sender, **kwargs): bgneal@753: """Forward the user profile instance on unconditionally.""" bgneal@753: self.enqueue_save(UserProfile, sender, **kwargs) bgneal@924: bgneal@924: bgneal@924: # Starting with Django 1.7, we'd see Django generate warnings if we defined bgneal@924: # a HAYSTACK_SIGNAL_PROCESSOR in our settings that referenced the class above. bgneal@924: # This is because Haystack creates an instance of our signal processor class bgneal@924: # (defined above) at import time, and thus imports this module very early in the bgneal@924: # application startup sequence. Warnings are then generated when this module bgneal@924: # imports our models, some of whose applications have not been imported yet. bgneal@924: # This problem will presumably go away when Haystack can fully support Django bgneal@924: # 1.7.x and implements an AppConfig with a ready() method. Until then, we don't bgneal@924: # use Haystack's signal processor object; we'll just create one here. This bgneal@924: # module will be imported when our custom_search app's ready() method runs. bgneal@924: bgneal@924: signal_processor = QueuedSignalProcessor(haystack.connections, bgneal@924: haystack.connection_router)