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)