trac-ticketlinks / trac / search /

# -*- coding: utf-8 -*-
# Copyright (C) 2003-2008 Edgewall Software
# Copyright (C) 2003-2004 Jonas Borgström <>
# All rights reserved.
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at
# Author: Jonas Borgström <>

import pkg_resources
import re
import time

from genshi.builder import tag, Element

from trac.config import IntOption
from trac.core import *
from trac.mimeview import Context
from trac.perm import IPermissionRequestor
from import ISearchSource
from trac.util.datefmt import format_datetime
from trac.util.presentation import Paginator
from trac.util.translation import _
from trac.web import IRequestHandler
from import add_link, add_stylesheet, INavigationContributor, \
from import IWikiSyntaxProvider
from import extract_link

class SearchModule(Component):

    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
               ITemplateProvider, IWikiSyntaxProvider)

    search_sources = ExtensionPoint(ISearchSource)

    min_query_length = IntOption('search', 'min_query_length', 3,
        """Minimum length of query string allowed when performing a search.""")

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'search'

    def get_navigation_items(self, req):
        if 'SEARCH_VIEW' in req.perm:
            yield ('mainnav', 'search',
                   tag.a(_('Search'),, accesskey=4))

    # IPermissionRequestor methods

    def get_permission_actions(self):
        return ['SEARCH_VIEW']

    # IRequestHandler methods

    def match_request(self, req):
        return re.match(r'/search/?', req.path_info) is not None

    def process_request(self, req):

        if req.path_info == '/search/opensearch':
            return ('opensearch.xml', {},

        available_filters = []
        for source in self.search_sources:
            available_filters += source.get_search_filters(req)
        filters = [f[0] for f in available_filters if req.args.has_key(f[0])]
        if not filters:
            filters = [f[0] for f in available_filters
                       if len(f) < 3 or len(f) > 2 and f[2]]
        data = {'filters': [{'name': f[0], 'label': f[1],
                             'active': f[0] in filters}
                            for f in available_filters],
                'quickjump': None,
                'results': []}

        query = req.args.get('q')
        data['query'] = query
        if query:
            data['quickjump'] = self._check_quickjump(req, query)
            if query.startswith('!'):
                query = query[1:]
            terms = self._get_search_terms(query)

            # Refuse queries that obviously would result in a huge result set
            if len(terms) == 1 and len(terms[0]) < self.min_query_length:
                raise TracError(_('Search query too short. Query must be at '
                                  'least %(num)s characters long.',
                                  num=self.min_query_length), _('Search Error'))

            results = []
            for source in self.search_sources:
                results += list(source.get_search_results(req, terms, filters))
            results.sort(lambda x,y: cmp(y[2], x[2]))

            page = int(req.args.get('page', '1'))
            results = Paginator(results, page - 1, self.RESULTS_PER_PAGE)
            for idx, result in enumerate(results):
                results[idx] = {'href': result[0], 'title': result[1],
                                'date': format_datetime(result[2]),
                                'author': result[3], 'excerpt': result[4]}
            pagedata = []    
            data['results'] = results
            shown_pages = results.get_shown_pages(21)
            for shown_page in shown_pages:
                page_href =[(f, 'on') for f in filters],
                                            page=shown_page, noquickjump=1)
                pagedata.append([page_href, None, str(shown_page),
                                 'page ' + str(shown_page)])

            fields = ['href', 'class', 'string', 'title']
            results.shown_pages = [dict(zip(fields, p)) for p in pagedata]
            results.current_page = {'href': None, 'class': 'current',
                                    'string': str( + 1),

            if results.has_next_page:
                next_href =, ['on'] * len(filters)),
                                            q=req.args.get('q'), page=page + 1,
                add_link(req, 'next', next_href, _('Next Page'))

            if results.has_previous_page:
                prev_href =, ['on'] * len(filters)),
                                            q=req.args.get('q'), page=page - 1,
                add_link(req, 'prev', prev_href, _('Previous Page'))

            data['page_href'] =
                zip(filters, ['on'] * len(filters)), q=req.args.get('q'),

        add_stylesheet(req, 'common/css/search.css')
        return 'search.html', data, None

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        return []

    def get_templates_dirs(self):
        return [pkg_resources.resource_filename('', 'templates')]

    # IWikiSyntaxProvider methods

    def get_wiki_syntax(self):
        return []

    def get_link_resolvers(self):
        yield ('search', self._format_link)

    def _format_link(self, formatter, ns, target, label):
        path, query, fragment = formatter.split_link(target)
        if query:
            href = + query.replace(' ', '+')
            href =
        return tag.a(label, class_='search', href=href)

    # Internal methods

    def _check_quickjump(self, req, kwd):
        noquickjump = int(req.args.get('noquickjump', '0'))
        # Source quickjump
        quickjump_href = None
        if kwd[0] == '/':
            quickjump_href = req.href.browser(kwd)
            name = kwd
            description = _('Browse repository path %(path)s', path=kwd)
            link = extract_link(self.env, Context.from_request(req, 'search'),
            if isinstance(link, Element):
                quickjump_href = link.attrib.get('href')
                name = link.children
                description = link.attrib.get('title', '')
        if quickjump_href:
            if noquickjump:
                return {'href': quickjump_href, 'name': tag.EM(name),
                        'description': description}

    def _get_search_terms(self, query):
        """Break apart a search query into its various search terms.
        Terms are grouped implicitly by word boundary, or explicitly by (single
        or double) quotes.
        results = []
        for term in re.split('(".*?")|(\'.*?\')|(\s+)', query):
            if term != None and term.strip() != '':
                if term[0] == term[-1] == "'" or term[0] == term[-1] == '"':
                    term = term[1:-1]
        return results