diff --git a/scribeengine/config/routing.py b/scribeengine/config/routing.py index 9c3aaa0..87c4ec7 100644 --- a/scribeengine/config/routing.py +++ b/scribeengine/config/routing.py @@ -48,6 +48,8 @@ def make_map(): map.connect('/archive/{year}/{month}/{day}', controller='blog', action='archive') map.connect('/archive/{year}/{month}/{day}/{url}', controller='blog', action='view') + map.connect('/search', controller='blog', action='search') + map.connect('/tag/{id}', controller='blog', action='tag') map.connect('/{controller}/{action}') diff --git a/scribeengine/controllers/blog.py b/scribeengine/controllers/blog.py index 1de9482..28362ce 100644 --- a/scribeengine/controllers/blog.py +++ b/scribeengine/controllers/blog.py @@ -118,14 +118,11 @@ class BlogController(BaseController): if not c.querystring: h.flash.set_message(u'You didn\'t supply anything to search for.', u'error') h.redirect_to('/') + keywords = [or_(Post.body.contains(kw.strip()), Post.title.contains(kw.strip())) for kw in c.querystring.split(',')] c.page_title = u'Search' - c.posts = Session.query(Post)\ - .filter( - or_( - Post.body.contains(c.querystring), - Post.title.contains(c.querystring) - ) - )\ - .all() + c.posts = Session.query(Post) + for or_clause in keywords: + c.posts = c.posts.filter(or_clause) + c.posts = c.posts.all() return render(u'/blog/search.mako') diff --git a/scribeengine/lib/utils.py b/scribeengine/lib/utils.py index d8cca2f..7a90c2b 100644 --- a/scribeengine/lib/utils.py +++ b/scribeengine/lib/utils.py @@ -32,6 +32,76 @@ from turbomail import Message from scribeengine.lib.base import render, h +class KeywordProcessor(object): + """Process user-supplied keywords, tags, or search terms. + + This tries to be as flexible as possible while being efficient. + The vast majority of the work is done in the regular expression.""" + + def __init__(self, separators=' \t', quotes=['"', "'"], groups=[], group=False, normalize=None, sort=False, result=list): + """Configure the processor. + + separators: A list of acceptable separator characters. The first will be used for joins. + quotes: Pass a list or tuple of allowable quotes. E.g. ["\"", "'"] or None to disable. + groups: Pass a string, list, or tuple of allowable prefixes. E.g. '+-' or None to disable. + group: Pass in the type you want to group by, e.g. list, tuple, or dict. + normalize: Pass a function which will normalize the results. E.g. lambda s: s.lower().strip(' \"') + sort: Sort the resulting list (or lists) alphabeticlly. + result: The return type. One of set, tuple, list. + + If groups are defined, and group is not, the result will be a list/tuple/set of tuples, e.g. [('+', "foo"), ...] + """ + + separators = list(separators) + + self.pattern = ''.join(( + ('[\s%s]*' % (''.join(separators), )), # Trap possible leading space or separators. + '(', + ('[%s]%s' % (''.join([i for i in list(groups) if i is not None]), '?' if None in groups else '')) if groups else '', # Pass groups=('+','-') to handle optional leading + or -. + ''.join([(r'%s[^%s]+%s|' % (i, i, i)) for i in quotes]) if quotes else '', # Match any amount of text (that isn't a quote) inside quotes. + ('[^%s]+' % (''.join(separators), )), # Match any amount of text that isn't whitespace. + ')', + ('[%s]*' % (''.join(separators), )), # Match possible separator character. + )) + self.regex = re.compile(self.pattern) + + self.groups = list(groups) + self.group = dict if group is True else group + self.normalize = normalize + self.sort = sort + self.result = result + + def split(self, value): + if not isinstance(value, basestring): raise TypeError("Invalid type for argument 'value'.") + + matches = self.regex.findall(value) + + if callable(self.normalize): matches = [self.normalize(i) for i in matches] + if self.sort: matches.sort() + if not self.groups: return self.result(matches) + + groups = dict([(i, list()) for i in self.groups]) + if None not in groups.iterkeys(): groups[None] = list() # To prevent errors. + + for i in matches: + if i[0] in self.groups: + groups[i[0]].append(i[1:]) + else: + groups[None].append(i) + + if self.group is dict: return groups + + if self.group is False or self.group is None: + results = [] + + for group in self.groups: + results.extend([(group, match) for match in groups[group]]) + + return self.result(results) + + return self.group([[match for match in groups[group]] for group in self.groups]) + + def send_mail(template, mail_to, mail_from, subject, variables={}, attachments=[]): """ Sends an e-mail using the template ``template``. diff --git a/scribeengine/templates/blog/search.mako b/scribeengine/templates/blog/search.mako index 363dfa1..a60072b 100644 --- a/scribeengine/templates/blog/search.mako +++ b/scribeengine/templates/blog/search.mako @@ -1,6 +1,7 @@ <%inherit file="/base.mako"/> <%include file="/flash.mako"/>
Sorry, there seem to be no results for your search query.