Package trac :: Package search :: Module web_ui

Source Code for Module trac.search.web_ui

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # Copyright (C) 2003-2020 Edgewall Software 
  4  # Copyright (C) 2003-2004 Jonas Borgström <[email protected]> 
  5  # All rights reserved. 
  6  # 
  7  # This software is licensed as described in the file COPYING, which 
  8  # you should have received as part of this distribution. The terms 
  9  # are also available at https://trac.edgewall.org/wiki/TracLicense. 
 10  # 
 11  # This software consists of voluntary contributions made by many 
 12  # individuals. For the exact contribution history, see the revision 
 13  # history and logs, available at https://trac.edgewall.org/log/. 
 14  # 
 15  # Author: Jonas Borgström <[email protected]> 
 16   
 17  import pkg_resources 
 18  import re 
 19   
 20  from genshi.builder import tag 
 21   
 22  from trac.config import IntOption, ListOption 
 23  from trac.core import * 
 24  from trac.perm import IPermissionRequestor 
 25  from trac.search.api import ISearchSource 
 26  from trac.util import as_int 
 27  from trac.util.datefmt import format_datetime, user_time 
 28  from trac.util.html import find_element 
 29  from trac.util.presentation import Paginator 
 30  from trac.util.text import quote_query_string 
 31  from trac.util.translation import _ 
 32  from trac.web.api import IRequestHandler 
 33  from trac.web.chrome import (INavigationContributor, ITemplateProvider, 
 34                               add_link, add_stylesheet, add_warning, 
 35                               web_context) 
 36  from trac.wiki.api import IWikiSyntaxProvider 
 37  from trac.wiki.formatter import extract_link 
 38   
 39   
40 -class SearchModule(Component):
41 """Controller for the search sub-system""" 42 43 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 44 ITemplateProvider, IWikiSyntaxProvider) 45 46 search_sources = ExtensionPoint(ISearchSource) 47 48 RESULTS_PER_PAGE = 10 49 50 min_query_length = IntOption('search', 'min_query_length', 3, 51 """Minimum length of query string allowed when performing a search.""") 52 53 default_disabled_filters = ListOption('search', 'default_disabled_filters', 54 doc="""Specifies which search filters should be disabled by 55 default on the search page. This will also restrict the 56 filters for the quick search function. The filter names 57 defined by default components are: `wiki`, `ticket`, 58 `milestone` and `changeset`. For plugins, look for 59 their implementation of the ISearchSource interface, in 60 the `get_search_filters()` method, the first member of 61 returned tuple. Once disabled, search filters can still 62 be manually enabled by the user on the search page. 63 (since 0.12)""") 64 65 # INavigationContributor methods 66
67 - def get_active_navigation_item(self, req):
68 return 'search'
69
70 - def get_navigation_items(self, req):
71 if 'SEARCH_VIEW' in req.perm: 72 yield ('mainnav', 'search', 73 tag.a(_('Search'), href=req.href.search(), accesskey=4))
74 75 # IPermissionRequestor methods 76
77 - def get_permission_actions(self):
78 return ['SEARCH_VIEW']
79 80 # IRequestHandler methods 81
82 - def match_request(self, req):
83 return re.match(r'/search(?:/opensearch)?$', req.path_info) is not None
84
85 - def process_request(self, req):
86 req.perm.assert_permission('SEARCH_VIEW') 87 88 if req.path_info == '/search/opensearch': 89 return ('opensearch.xml', {}, 90 'application/opensearchdescription+xml') 91 92 query = req.args.getfirst('q') 93 available_filters = [] 94 for source in self.search_sources: 95 available_filters.extend(source.get_search_filters(req) or []) 96 available_filters.sort(key=lambda f: f[1].lower()) 97 98 filters = self._get_selected_filters(req, available_filters) 99 data = self._prepare_data(req, query, available_filters, filters) 100 if query: 101 data['quickjump'] = self._check_quickjump(req, query) 102 if query.startswith('!'): 103 query = query[1:] 104 105 terms = self._parse_query(req, query) 106 if terms: 107 results = self._do_search(req, terms, filters) 108 if results: 109 data.update(self._prepare_results(req, filters, results)) 110 111 add_stylesheet(req, 'common/css/search.css') 112 return 'search.html', data, None
113 114 # ITemplateProvider methods 115
116 - def get_htdocs_dirs(self):
117 return []
118
119 - def get_templates_dirs(self):
120 return [pkg_resources.resource_filename('trac.search', 'templates')]
121 122 # IWikiSyntaxProvider methods 123
124 - def get_wiki_syntax(self):
125 return []
126 129 140 141 # IRequestHandler helper methods 142
143 - def _get_selected_filters(self, req, available_filters):
144 """Return selected filters or the default filters if none was selected. 145 """ 146 filters = [f[0] for f in available_filters if f[0] in req.args] 147 if not filters: 148 filters = [f[0] for f in available_filters 149 if f[0] not in self.default_disabled_filters and 150 (len(f) < 3 or len(f) > 2 and f[2])] 151 return filters
152
153 - def _prepare_data(self, req, query, available_filters, filters):
154 return {'filters': [{'name': f[0], 'label': f[1], 155 'active': f[0] in filters} 156 for f in available_filters], 157 'query': query, 'quickjump': None, 'results': []}
158
159 - def _check_quickjump(self, req, kwd):
160 """Look for search shortcuts""" 161 noquickjump = as_int(req.args.get('noquickjump'), 0) 162 # Source quickjump FIXME: delegate to ISearchSource.search_quickjump 163 quickjump_href = None 164 if kwd[0] == '/': 165 quickjump_href = req.href.browser(kwd) 166 name = kwd 167 description = _('Browse repository path %(path)s', path=kwd) 168 else: 169 context = web_context(req, 'search') 170 link = find_element(extract_link(self.env, context, kwd), 'href') 171 if link is not None: 172 quickjump_href = link.attrib.get('href') 173 name = link.children 174 description = link.attrib.get('title', '') 175 if quickjump_href: 176 # Only automatically redirect to local quickjump links 177 if not quickjump_href.startswith(req.base_path or '/'): 178 noquickjump = True 179 if noquickjump: 180 return {'href': quickjump_href, 'name': tag.em(name), 181 'description': description} 182 else: 183 req.redirect(quickjump_href)
184
185 - def _get_search_terms(self, query):
186 """Break apart a search query into its various search terms. 187 188 Terms are grouped implicitly by word boundary, or explicitly by (single 189 or double) quotes. 190 """ 191 terms = [] 192 for term in re.split('(".*?")|(\'.*?\')|(\s+)', query): 193 if term is not None and term.strip(): 194 if term[0] == term[-1] and term[0] in "'\"": 195 term = term[1:-1] 196 terms.append(term) 197 return terms
198
199 - def _parse_query(self, req, query):
200 """Parse query and refuse those which would result in a huge result set 201 """ 202 terms = self._get_search_terms(query) 203 if terms and (len(terms) > 1 or 204 len(terms[0]) >= self.min_query_length): 205 return terms 206 207 add_warning(req, _('Search query too short. ' 208 'Query must be at least %(num)s characters long.', 209 num=self.min_query_length))
210
211 - def _do_search(self, req, terms, filters):
212 results = [] 213 for source in self.search_sources: 214 results.extend(source.get_search_results(req, terms, filters) 215 or []) 216 return sorted(results, key=lambda x: x[2], reverse=True)
217
218 - def _prepare_results(self, req, filters, results):
219 page = req.args.get('page', 1) 220 page = as_int(page, default=1, min=1) 221 try: 222 results = Paginator(results, page - 1, self.RESULTS_PER_PAGE) 223 except TracError: 224 add_warning(req, _("Page %(page)s is out of range.", page=page)) 225 page = 1 226 results = Paginator(results, page - 1, self.RESULTS_PER_PAGE) 227 228 for idx, result in enumerate(results): 229 results[idx] = {'href': result[0], 'title': result[1], 230 'date': user_time(req, format_datetime, result[2]), 231 'author': result[3], 'excerpt': result[4]} 232 233 pagedata = [] 234 shown_pages = results.get_shown_pages(21) 235 for shown_page in shown_pages: 236 page_href = req.href.search([(f, 'on') for f in filters], 237 q=req.args.get('q'), 238 page=shown_page, noquickjump=1) 239 pagedata.append([page_href, None, str(shown_page), 240 _("Page %(num)d", num=shown_page)]) 241 242 fields = ['href', 'class', 'string', 'title'] 243 results.shown_pages = [dict(zip(fields, p)) for p in pagedata] 244 245 results.current_page = {'href': None, 'class': 'current', 246 'string': str(results.page + 1), 247 'title': None} 248 249 if results.has_next_page: 250 next_href = req.href.search(zip(filters, ['on'] * len(filters)), 251 q=req.args.get('q'), page=page + 1, 252 noquickjump=1) 253 add_link(req, 'next', next_href, _('Next Page')) 254 255 if results.has_previous_page: 256 prev_href = req.href.search(zip(filters, ['on'] * len(filters)), 257 q=req.args.get('q'), page=page - 1, 258 noquickjump=1) 259 add_link(req, 'prev', prev_href, _('Previous Page')) 260 261 page_href = req.href.search( 262 zip(filters, ['on'] * len(filters)), q=req.args.get('q'), 263 noquickjump=1) 264 return {'results': results, 'page_href': page_href}
265