bgneal@469: """
bgneal@469: This module contains custom forms to tailor the Haystack search application to
bgneal@469: our needs.
bgneal@469: 
bgneal@469: """
bgneal@753: import logging
bgneal@753: 
bgneal@469: from django import forms
bgneal@753: from django.conf import settings
bgneal@469: from haystack.forms import ModelSearchForm
bgneal@469: 
bgneal@469: 
bgneal@469: MODEL_CHOICES = (
bgneal@469:     ('forums.topic', 'Forum Topics'),
bgneal@469:     ('forums.post', 'Forum Posts'),
bgneal@469:     ('news.story', 'News Stories'),
bgneal@469:     ('bio.userprofile', 'User Profiles'),
bgneal@469:     ('weblinks.link', 'Links'),
bgneal@469:     ('downloads.download', 'Downloads'),
bgneal@469:     ('podcast.item', 'Podcasts'),
bgneal@469:     ('ygroup.post', 'Yahoo Group Archives'),
bgneal@469: )
bgneal@469: 
bgneal@753: logger = logging.getLogger(__name__)
bgneal@753: 
bgneal@469: 
bgneal@469: class CustomModelSearchForm(ModelSearchForm):
bgneal@469:     """
bgneal@469:     This customized ModelSearchForm allows us to explictly label and order
bgneal@469:     the model choices.
bgneal@469: 
bgneal@753:     We also provide "all words", "exact phrase", and "exclude" text input boxes.
bgneal@753:     Haystack 2.1.0's auto_query() function did not seem to work right so we just
bgneal@753:     rolled our own.
bgneal@753: 
bgneal@763:     This form can optionally receive the user making the search as a keyword
bgneal@763:     argument ('user') to __init__. This will be used for logging search queries.
bgneal@763: 
bgneal@469:     """
bgneal@753:     q = forms.CharField(required=False, label='All these words',
bgneal@753:             widget=forms.TextInput(attrs={'type': 'search', 'class': 'search',
bgneal@753:                 'size': 48}))
bgneal@753:     exact = forms.CharField(required=False, label='This exact word or phrase',
bgneal@753:             widget=forms.TextInput(attrs={'type': 'search', 'class': 'search',
bgneal@753:                 'size': 48}))
bgneal@753:     exclude = forms.CharField(required=False, label='None of these words',
bgneal@753:             widget=forms.TextInput(attrs={'type': 'search', 'class': 'search',
bgneal@753:                 'size': 48}))
bgneal@469: 
bgneal@469:     def __init__(self, *args, **kwargs):
bgneal@763:         self.user = kwargs.pop('user', None)
bgneal@469:         super(CustomModelSearchForm, self).__init__(*args, **kwargs)
bgneal@469:         self.fields['models'] = forms.MultipleChoiceField(choices=MODEL_CHOICES,
bgneal@753:                 label='Search in', widget=forms.CheckboxSelectMultiple)
bgneal@753: 
bgneal@753:     def clean(self):
bgneal@956:         super(CustomModelSearchForm, self).clean()
bgneal@753:         if not settings.SEARCH_QUEUE_ENABLED:
bgneal@753:             raise forms.ValidationError("Our search function is offline for "
bgneal@753:                     "maintenance. Please try again later. "
bgneal@753:                     "We apologize for any inconvenience.")
bgneal@753: 
bgneal@956:         q = self.cleaned_data.get('q')
bgneal@956:         exact = self.cleaned_data.get('exact')
bgneal@956:         exclude = self.cleaned_data.get('exclude')
bgneal@956: 
bgneal@956:         if not (q or exact or exclude):
bgneal@753:             raise forms.ValidationError('Please supply some search terms')
bgneal@753: 
bgneal@753:         return self.cleaned_data
bgneal@753: 
bgneal@943:     def clean_exact(self):
bgneal@943:         exact_field = self.cleaned_data['exact']
bgneal@943:         if "'" in exact_field or '"' in exact_field:
bgneal@943:             raise forms.ValidationError("Quotes are not needed in this field")
bgneal@943:         return exact_field
bgneal@943: 
bgneal@753:     def search(self):
bgneal@753:         if not self.is_valid():
bgneal@753:             return self.no_query_found()
bgneal@753: 
bgneal@763:         if self.user is None:
bgneal@763:             username = 'UNKNOWN'
bgneal@763:         elif self.user.is_authenticated():
bgneal@763:             username = self.user.username
bgneal@763:         else:
bgneal@763:             username = 'ANONYMOUS'
bgneal@763: 
bgneal@763:         logger.info('Search executed: /%s/%s/%s/ in %s by %s',
bgneal@753:             self.cleaned_data['q'],
bgneal@753:             self.cleaned_data['exact'],
bgneal@753:             self.cleaned_data['exclude'],
bgneal@763:             self.cleaned_data['models'],
bgneal@763:             username)
bgneal@753: 
bgneal@753:         # Note that in Haystack 2.x content is untrusted and is automatically
bgneal@753:         # auto-escaped for us.
bgneal@753:         #
bgneal@943:         # Gather regular search terms
bgneal@1034:         terms = u' '.join(self.cleaned_data['q'].split())
bgneal@753: 
bgneal@753:         # Exact words or phrases:
bgneal@943:         exact = self.cleaned_data['exact'].strip()
bgneal@943:         if exact:
bgneal@1034:             exact = u'"{}"'.format(exact)
bgneal@753: 
bgneal@753:         # Exclude terms:
bgneal@1034:         exclude = [u"-{}".format(term) for term in self.cleaned_data['exclude'].split()]
bgneal@1034:         exclude = u' '.join(exclude)
bgneal@943: 
bgneal@1034:         query = u' '.join([terms, exact, exclude]).strip()
bgneal@943:         logger.debug("auto_query: %s", query)
bgneal@943: 
bgneal@943:         sqs = self.searchqueryset.auto_query(query)
bgneal@753: 
bgneal@753:         if self.load_all:
bgneal@753:             sqs = sqs.load_all()
bgneal@753: 
bgneal@753:         # Apply model filtering
bgneal@753:         sqs = sqs.models(*self.get_models())
bgneal@753: 
bgneal@753:         return sqs