zinnia.search
Covered: 109 lines
Missed: 0 lines
Skipped 19 lines
Percent: 100 %
  1
"""Search module with complex query parsing for Zinnia"""
  2
from pyparsing import Word
  3
from pyparsing import alphas
  4
from pyparsing import WordEnd
  5
from pyparsing import Combine
  6
from pyparsing import opAssoc
  7
from pyparsing import Optional
  8
from pyparsing import OneOrMore
  9
from pyparsing import StringEnd
 10
from pyparsing import printables
 11
from pyparsing import quotedString
 12
from pyparsing import removeQuotes
 13
from pyparsing import ParseResults
 14
from pyparsing import CaselessLiteral
 15
from pyparsing import operatorPrecedence
 17
from django.db.models import Q
 19
from zinnia.models import Entry
 22
def createQ(token):
 23
    """Creates the Q() object"""
 24
    meta = getattr(token, 'meta', None)
 25
    query = getattr(token, 'query', '')
 26
    wildcards = None
 28
    if isinstance(query, basestring):  # Unicode -> Quoted string
 29
        search = query
 30
    else:  # List -> No quoted string (possible wildcards)
 31
        if len(query) == 1:
 32
            search = query[0]
 33
        elif len(query) == 3:
 34
            wildcards = 'BOTH'
 35
            search = query[1]
 36
        elif len(query) == 2:
 37
            if query[0] == '*':
 38
                wildcards = 'START'
 39
                search = query[1]
 40
            else:
 41
                wildcards = 'END'
 42
                search = query[0]
 44
    if not meta:
 45
        return Q(content__icontains=search) | \
 46
               Q(excerpt__icontains=search) | \
 47
               Q(title__icontains=search)
 49
    if meta == 'category':
 50
        if wildcards == 'BOTH':
 51
            return Q(categories__title__icontains=search) | \
 52
                    Q(categories__slug__icontains=search)
 53
        elif wildcards == 'START':
 54
            return Q(categories__title__iendswith=search) | \
 55
                    Q(categories__slug__iendswith=search)
 56
        elif wildcards == 'END':
 57
            return Q(categories__title__istartswith=search) | \
 58
                    Q(categories__slug__istartswith=search)
 59
        else:
 60
            return Q(categories__title__iexact=search) | \
 61
                    Q(categories__slug__iexact=search)
 62
    elif meta == 'author':
 63
        if wildcards == 'BOTH':
 64
            return Q(authors__username__icontains=search)
 65
        elif wildcards == 'START':
 66
            return Q(authors__username__iendswith=search)
 67
        elif wildcards == 'END':
 68
            return Q(authors__username__istartswith=search)
 69
        else:
 70
            return Q(authors__username__iexact=search)
 71
    elif meta == 'tag':  # TODO: tags ignore wildcards
 72
        return Q(tags__icontains=search)
 75
def unionQ(token):
 76
    """Appends all the Q() objects"""
 77
    query = Q()
 78
    operation = 'and'
 79
    negation = False
 81
    for t in token:
 82
        if type(t) is ParseResults:  # See tokens recursively
 83
            query &= unionQ(t)
 84
        else:
 85
            if t in ('or', 'and'):  # Set the new op and go to next token
 86
                operation = t
 87
            elif t == '-':  # Next tokens needs to be negated
 88
                negation = True
 89
            else:  # Append to query the token
 90
                if negation:
 91
                    t = ~t
 92
                if operation == 'or':
 93
                    query |= t
 94
                else:
 95
                    query &= t
 96
    return query
 99
NO_BRTS = printables.replace('(', '').replace(')', '')
100
SINGLE = Word(NO_BRTS.replace('*', ''))
101
WILDCARDS = Optional('*') + SINGLE + Optional('*') + WordEnd(wordChars=NO_BRTS)
102
QUOTED = quotedString.setParseAction(removeQuotes)
104
OPER_AND = CaselessLiteral('and')
105
OPER_OR = CaselessLiteral('or')
106
OPER_NOT = '-'
108
TERM = Combine(Optional(Word(alphas).setResultsName('meta') + ':') +
109
               (QUOTED.setResultsName('query') |
110
                WILDCARDS.setResultsName('query')))
111
TERM.setParseAction(createQ)
113
EXPRESSION = operatorPrecedence(TERM, [
114
    (OPER_NOT, 1, opAssoc.RIGHT),
115
    (OPER_OR, 2, opAssoc.LEFT),
116
    (Optional(OPER_AND, default='and'), 2, opAssoc.LEFT)])
117
EXPRESSION.setParseAction(unionQ)
119
QUERY = OneOrMore(EXPRESSION) + StringEnd()
120
QUERY.setParseAction(unionQ)
123
def advanced_search(pattern):
124
    """Parse the grammar of a pattern
125
    and build a queryset with it"""
126
    query_parsed = QUERY.parseString(pattern)
127
    return Entry.published.filter(query_parsed[0]).distinct()