AdvancedSearch: trac_whoosh_integration_20090321c.diff
| File trac_whoosh_integration_20090321c.diff, 30.5 KB (added by Chris Mulligan <chris.mulligan@…>, 3 years ago) |
|---|
-
setup.py
70 70 71 71 install_requires = [ 72 72 'setuptools>=0.6b1', 73 'Genshi>=0.6dev-r960' 73 'Genshi>=0.6dev-r960', 74 'whoosh>=0.1.11', 74 75 ], 75 76 extras_require = { 76 77 'Babel': ['Babel>=0.9.4'], … … 102 103 trac.mimeview.txtl = trac.mimeview.txtl[Textile] 103 104 trac.prefs = trac.prefs.web_ui 104 105 trac.search = trac.search.web_ui 106 trac.search.admin = trac.search.admin 105 107 trac.ticket.admin = trac.ticket.admin 106 108 trac.ticket.query = trac.ticket.query 107 109 trac.ticket.report = trac.ticket.report -
trac/attachment.py
35 35 from trac.mimeview import * 36 36 from trac.perm import PermissionError, PermissionSystem, IPermissionPolicy 37 37 from trac.resource import * 38 from trac.search import search_to_sql, shorten_result39 38 from trac.util import get_reporter_id, create_unique_file, content_disposition 40 39 from trac.util.datefmt import format_datetime, to_timestamp, utc 41 40 from trac.util.text import exception_to_unicode, pretty_size, print_table, \ … … 481 480 _(" attached to "), tag.em(name, title=title)) 482 481 elif field == 'description': 483 482 return format_to(self.env, None, context(attachment.parent), descr) 484 485 def get_search_results(self, req, resource_realm, terms):486 """Return a search result generator suitable for ISearchSource.487 488 Search results are attachments on resources of the given489 `resource_realm.realm` whose filename, description or author match490 the given terms.491 """492 db = self.env.get_db_cnx()493 sql_query, args = search_to_sql(db, ['filename', 'description',494 'author'], terms)495 cursor = db.cursor()496 cursor.execute("SELECT id,time,filename,description,author "497 "FROM attachment "498 "WHERE type = %s "499 "AND " + sql_query, (resource_realm.realm, ) + args)500 501 for id, time, filename, desc, author in cursor:502 attachment = resource_realm(id=id).child('attachment', filename)503 if 'ATTACHMENT_VIEW' in req.perm(attachment):504 yield (get_resource_url(self.env, attachment, req.href),505 get_resource_shortname(self.env, attachment),506 datetime.fromtimestamp(time, utc), author,507 shorten_result(desc, terms))508 483 509 484 # IResourceManager methods 510 485 -
trac/ticket/api.py
166 166 self._fields_lock = threading.RLock() 167 167 168 168 # Public API 169 170 def get_tickets(self): 171 """Returns a list of all tickets.""" 172 db = self.env.get_db_cnx() 173 cursor = db.cursor() 174 cursor.execute("SELECT DISTINCT id FROM ticket") 175 for (ticket,) in cursor: 176 yield ticket 177 169 178 170 179 def get_available_actions(self, req, ticket): 171 180 """Returns a sorted list of available actions""" -
trac/ticket/web_ui.py
31 31 from trac.mimeview.api import Mimeview, IContentConverter, Context 32 32 from trac.resource import Resource, get_resource_url, \ 33 33 render_resource_link, get_resource_shortname 34 from trac.search import ISearchSource, search_to_sql, shorten_result35 34 from trac.ticket.api import TicketSystem, ITicketManipulator, \ 36 35 ITicketActionController 37 36 from trac.ticket.model import Milestone, Ticket, group_milestones … … 59 58 class TicketModule(Component): 60 59 61 60 implements(IContentConverter, INavigationContributor, IRequestHandler, 62 I SearchSource, ITemplateProvider, ITimelineEventProvider)61 ITemplateProvider, ITimelineEventProvider) 63 62 64 63 ticket_manipulators = ExtensionPoint(ITicketManipulator) 65 64 … … 198 197 def get_templates_dirs(self): 199 198 return [pkg_resources.resource_filename('trac.ticket', 'templates')] 200 199 201 # ISearchSource methods202 200 203 def get_search_filters(self, req):204 if 'TICKET_VIEW' in req.perm:205 yield ('ticket', 'Tickets')206 207 def get_search_results(self, req, terms, filters):208 if not 'ticket' in filters:209 return210 ticket_realm = Resource('ticket')211 db = self.env.get_db_cnx()212 sql, args = search_to_sql(db, ['b.newvalue'], terms)213 sql2, args2 = search_to_sql(db, ['summary', 'keywords', 'description',214 'reporter', 'cc',215 db.cast('id', 'text')], terms)216 sql3, args3 = search_to_sql(db, ['c.value'], terms)217 cursor = db.cursor()218 cursor.execute("SELECT DISTINCT a.summary,a.description,a.reporter, "219 "a.type,a.id,a.time,a.status,a.resolution "220 "FROM ticket a "221 "LEFT JOIN ticket_change b ON a.id = b.ticket "222 "LEFT OUTER JOIN ticket_custom c ON (a.id = c.ticket) "223 "WHERE (b.field='comment' AND %s) OR %s OR %s" %224 (sql, sql2, sql3), args + args2 + args3)225 ticketsystem = TicketSystem(self.env)226 for summary, desc, author, type, tid, ts, status, resolution in cursor:227 t = ticket_realm(id=tid)228 if 'TICKET_VIEW' in req.perm(t):229 yield (req.href.ticket(tid),230 tag(tag.span(get_resource_shortname(self.env, t),231 class_=status),232 ': ',233 ticketsystem.format_summary(summary, status,234 resolution, type)),235 datetime.fromtimestamp(ts, utc), author,236 shorten_result(desc, terms))237 238 # Attachments239 for result in AttachmentModule(self.env).get_search_results(240 req, ticket_realm, terms):241 yield result242 243 201 # ITimelineEventProvider methods 244 202 245 203 def get_timeline_filters(self, req): -
trac/ticket/__init__.py
1 1 from trac.ticket.api import * 2 2 from trac.ticket.default_workflow import * 3 3 from trac.ticket.model import * 4 from trac.ticket.search import * 5 No newline at end of file -
trac/ticket/roadmap.py
23 23 24 24 from trac import __version__ 25 25 from trac.attachment import AttachmentModule 26 from trac.config import ExtensionOption 26 from trac.config import ExtensionOption, ExtensionPoint 27 27 from trac.core import * 28 28 from trac.mimeview import Context 29 29 from trac.perm import IPermissionRequestor 30 30 from trac.resource import * 31 from trac.search import ISearch Source, search_to_sql, shorten_result31 from trac.search import ISearchParticipant, SearchSystem 32 32 from trac.util.datefmt import parse_date, utc, to_timestamp, to_datetime, \ 33 33 get_date_format_hint, get_datetime_format_hint, \ 34 34 format_date, format_datetime … … 50 50 This method returns a valid TicketGroupStats object. 51 51 """ 52 52 53 class IMilestoneChangeListener(Interface): 54 """Extension point interface for components that require notification 55 when tickets are created, modified, or deleted.""" 56 57 def milestone_created(milestone): 58 """Called when a ticket is created.""" 59 60 def milestone_changed(milestone, comment, author, old_values): 61 """Called when a milestone is modified. 62 63 `old_values` is a dictionary containing the previous values of the 64 fields that have changed. 65 """ 66 67 def milestone_deleted(milestone): 68 """Called when a milestone is deleted.""" 69 70 53 71 class TicketGroupStats(object): 54 72 """Encapsulates statistics on a group of tickets.""" 55 73 … … 286 304 for interval in stat.intervals]} 287 305 288 306 289 290 307 class RoadmapModule(Component): 291 308 292 implements(INavigationContributor, IPermissionRequestor, IRequestHandler) 309 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 310 ISearchParticipant) 293 311 stats_provider = ExtensionOption('roadmap', 'stats_provider', 294 312 ITicketGroupStatsProvider, 295 313 'DefaultTicketGroupStatsProvider', … … 468 486 write_prop('END', 'VCALENDAR') 469 487 470 488 489 # ISearchParticipant methods 490 def get_search_filters(self, req=None): 491 if not req or 'MILESTONE_VIEW' in req.perm: 492 return ('milestone', 'Milestones') 471 493 494 def build_search_index(self, s): 495 db = self.env.get_db_cnx() 496 milestones = Milestone.select(self.env, True, db) 497 for milestone in milestones: 498 _index_milestone(self.env, milestone) 499 500 def format_search_results(self, res): 501 return u'Milestone: %s' % res['id'] 502 503 504 505 472 506 class MilestoneModule(Component): 473 507 474 508 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 475 509 ITimelineEventProvider, IWikiSyntaxProvider, IResourceManager, 476 I SearchSource)510 IMilestoneChangeListener) 477 511 512 change_listeners = ExtensionPoint(IMilestoneChangeListener) 478 513 stats_provider = ExtensionOption('milestone', 'stats_provider', 479 514 ITicketGroupStatsProvider, 480 515 'DefaultTicketGroupStatsProvider', … … 590 625 retarget_to = req.args.get('target') or None 591 626 milestone.delete(retarget_to, req.authname) 592 627 db.commit() 628 for listener in self.change_listeners: 629 listener.milestone_deleted(milestone) 593 630 req.redirect(req.href.roadmap()) 594 631 595 632 def _do_save(self, req, db, milestone): … … 660 697 milestone.insert() 661 698 db.commit() 662 699 700 for listener in self.change_listeners: 701 listener.milestone_changed(milestone) 663 702 req.redirect(req.href.milestone(milestone.name)) 664 703 665 704 def _render_confirm(self, req, db, milestone): … … 820 859 else: 821 860 return desc 822 861 823 # I SearchSourcemethods862 # IMilestoneChangeListener methods 824 863 825 def get_search_filters(self, req): 826 if 'MILESTONE_VIEW' in req.perm: 827 yield ('milestone', _('Milestones')) 864 def milestone_changed(self, milestone): 865 _index_milestone(self.env, milestone) 828 866 829 def get_search_results(self, req, terms, filters): 830 if not 'milestone' in filters: 831 return 832 db = self.env.get_db_cnx() 833 sql_query, args = search_to_sql(db, ['name', 'description'], terms) 834 cursor = db.cursor() 835 cursor.execute("SELECT name,due,completed,description " 836 "FROM milestone " 837 "WHERE " + sql_query, args) 838 839 milestone_realm = Resource('milestone') 840 for name, due, completed, description in cursor: 841 milestone = milestone_realm(id=name) 842 if 'MILESTONE_VIEW' in req.perm(milestone): 843 yield (get_resource_url(self.env, milestone, req.href), 844 get_resource_name(self.env, milestone), 845 datetime.fromtimestamp( 846 completed or due or time(), utc), 847 '', shorten_result(description, terms)) 867 def milestone_deleted(self, milestone): 868 s = SearchSystem(self.env) 869 s.delete_doc(u'milestone', milestone.name) 848 870 849 # Attachments 850 for result in AttachmentModule(self.env).get_search_results( 851 req, milestone_realm, terms): 852 yield result 871 def _index_milestone(env, milestone, search_system=None): 872 if not search_system: 873 search_system = SearchSystem(env) 874 if milestone.is_completed: 875 status = u'completed' 876 else: 877 status = u'open' 878 contents = {'id' : milestone.name, 879 'content' : milestone.description, 880 'status' : status, 881 } 882 search_system.index_doc(u'milestone', contents) 883 No newline at end of file -
trac/versioncontrol/web_ui/changeset.py
33 33 from trac.mimeview import Mimeview, is_binary, Context 34 34 from trac.perm import IPermissionRequestor 35 35 from trac.resource import Resource, ResourceNotFound 36 from trac.search import ISearchSource, search_to_sql, shorten_result37 36 from trac.timeline.api import ITimelineEventProvider 38 37 from trac.util import embedded_numbers, content_disposition 39 38 from trac.util.compat import any … … 122 121 """ 123 122 124 123 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 125 ITimelineEventProvider, IWikiSyntaxProvider , ISearchSource)124 ITimelineEventProvider, IWikiSyntaxProvider) 126 125 127 126 property_diff_renderers = ExtensionPoint(IPropertyDiffRenderer) 128 127 … … 955 954 956 955 # ISearchSource methods 957 956 958 def get_search_filters(self, req):959 if 'CHANGESET_VIEW' in req.perm:960 yield ('changeset', _('Changesets'))961 957 962 def get_search_results(self, req, terms, filters):963 if not 'changeset' in filters:964 return965 repos = self.env.get_repository(req.authname)966 db = self.env.get_db_cnx()967 sql, args = search_to_sql(db, ['rev', 'message', 'author'], terms)968 cursor = db.cursor()969 cursor.execute("SELECT rev,time,author,message "970 "FROM revision WHERE " + sql, args)971 for rev, ts, author, log in cursor:972 if not repos.authz.has_permission_for_changeset(rev):973 continue974 yield (req.href.changeset(rev),975 '[%s]: %s' % (rev, shorten_line(log)),976 datetime.fromtimestamp(ts, utc), author,977 shorten_result(log, terms))978 979 980 958 class AnyDiffModule(Component): 981 959 982 960 implements(IRequestHandler) -
trac/wiki/web_ui.py
29 29 from trac.mimeview.api import Mimeview, IContentConverter, Context 30 30 from trac.perm import IPermissionRequestor 31 31 from trac.resource import * 32 from trac.search import ISearchSource, search_to_sql, shorten_result33 32 from trac.timeline.api import ITimelineEventProvider 34 33 from trac.util import get_reporter_id 35 34 from trac.util.datefmt import to_timestamp, utc … … 54 53 class WikiModule(Component): 55 54 56 55 implements(IContentConverter, INavigationContributor, IPermissionRequestor, 57 IRequestHandler, ITimelineEventProvider, ISearchSource,56 IRequestHandler, ITimelineEventProvider, 58 57 ITemplateProvider) 59 58 60 59 page_manipulators = ExtensionPoint(IWikiPageManipulator) … … 608 607 wiki_page.id, version=wiki_page.version, action='diff') 609 608 markup = tag(markup, ' ', tag.a('(diff)', href=diff_href)) 610 609 return markup 611 612 # ISearchSource methods613 614 def get_search_filters(self, req):615 if 'WIKI_VIEW' in req.perm:616 yield ('wiki', _('Wiki'))617 618 def get_search_results(self, req, terms, filters):619 if not 'wiki' in filters:620 return621 db = self.env.get_db_cnx()622 sql_query, args = search_to_sql(db, ['w1.name', 'w1.author', 'w1.text'],623 terms)624 cursor = db.cursor()625 cursor.execute("SELECT w1.name,w1.time,w1.author,w1.text "626 "FROM wiki w1,"627 "(SELECT name,max(version) AS ver "628 "FROM wiki GROUP BY name) w2 "629 "WHERE w1.version = w2.ver AND w1.name = w2.name "630 "AND " + sql_query, args)631 632 wiki_realm = Resource('wiki')633 for name, ts, author, text in cursor:634 page = wiki_realm(id=name)635 if 'WIKI_VIEW' in req.perm(page):636 yield (get_resource_url(self.env, page, req.href),637 '%s: %s' % (name, shorten_line(text)),638 datetime.fromtimestamp(ts, utc), author,639 shorten_result(text, terms))640 641 # Attachments642 for result in AttachmentModule(self.env).get_search_results(643 req, wiki_realm, terms):644 yield result -
trac/wiki/__init__.py
3 3 from trac.wiki.intertrac import * 4 4 from trac.wiki.model import * 5 5 from trac.wiki.parser import * 6 from trac.wiki.search import * 7 No newline at end of file -
trac/search/admin.py
1 # -*- coding: utf-8 -*- 2 # 3 # Copyright (C) 2008 Edgewall Software 4 # All rights reserved. 5 # 6 # This software is licensed as described in the file COPYING, which 7 # you should have received as part of this distribution. The terms 8 # are also available at http://trac.edgewall.com/license.html. 9 # 10 # This software consists of voluntary contributions made by many 11 # individuals. For the exact contribution history, see the revision 12 # history and logs, available at http://trac.edgewall.org/. 13 14 from datetime import datetime 15 import os.path 16 import pkg_resources 17 import sys 18 import time 19 20 from trac.admin import * 21 from trac.core import * 22 from trac.search.api import * 23 24 25 class SearchAdmin(Component): 26 """Search administration component. Only rebuilds the index.""" 27 28 implements(IAdminCommandProvider) 29 30 # IAdminCommandProvider methods 31 32 def get_admin_commands(self): 33 yield ('search rebuild', '', 34 'Rebuild the search index', 35 None, SearchSystem(self.env).rebuild_index) 36 yield ('search optimize', '', 37 'Optimize the search index', 38 None, SearchSystem(self.env).optimize) 39 No newline at end of file -
trac/search/api.py
12 12 # history and logs, available at http://trac.edgewall.org/log/. 13 13 14 14 from trac.core import * 15 from trac.env import IEnvironmentSetupParticipant 15 16 17 from whoosh.fields import * 18 from whoosh import index 19 import os 16 20 17 class ISearchSource(Interface): 18 """Extension point interface for adding search sources to the search 19 system. 21 class ISearchParticipant(Interface): 22 """Extension point interface for components that should be searched. 20 23 """ 21 24 22 25 def get_search_filters(req): 23 """Return a list of filters that this search source supports. 24 25 Each filter must be a `(name, label[, default])` tuple, where `name` is 26 the internal name, `label` is a human-readable name for display and 27 `default` is an optional boolean for determining whether this filter 28 is searchable by default. 29 """ 26 """Called when we want to build the list of components with search. 27 Passes the request object to do permission checking.""" 30 28 31 def get_search_results(req, terms, filters):32 """ Return a list of search results matching each search term in `terms`.29 def build_search_index(search_system): 30 """Called when we want to rebuild the entire index.""" 33 31 34 The `filters` parameters is a list of the enabled filters, each item35 being the name of the tuples returned by `get_search_events`.32 def format_search_results(contents): 33 """Called to see if the module wants to format the search results.""" 36 34 37 The events returned by this function must be tuples of the form38 `(href, title, date, author, excerpt).`39 """40 35 36 class SearchSystem(Component): 37 """Implements the search system, provides methods for adding and 38 Deleting documents for indexing. 39 """ 41 40 42 def search_to_sql(db, columns, terms): 43 """Convert a search query into an SQL WHERE clause and corresponding 44 parameters. 41 implements(IEnvironmentSetupParticipant) 42 search_participants = ExtensionPoint(ISearchParticipant) 45 43 46 The result is returned as an `(sql, params)` tuple. 47 """ 48 assert columns and terms 44 def __init__(self): 45 if not self.env.path: 46 raise TracError(_("Environment.path is very much required...")) 47 self.searchpath = os.path.join(self.env.path, 'search_index') 49 48 50 likes = ['%s %s' % (i, db.like()) for i in columns] 51 c = ' OR '.join(likes) 52 sql = '(' + ') AND ('.join([c] * len(terms)) + ')' 53 args = [] 54 for t in terms: 55 args.extend(['%' + db.like_escape(t) + '%'] * len(columns)) 56 return sql, tuple(args) 49 SCHEMA = Schema( 50 unique_id=ID(stored=True, unique=True), 51 id=ID(stored=True), 52 type=ID(stored=True), 53 time=ID(stored=True), 54 author=ID(stored=True), 55 component=KEYWORD, 56 status=KEYWORD(stored=True), 57 resolution=KEYWORD(stored=True), 58 keywords=KEYWORD(scorable=True), 59 milestone=TEXT, 60 summary=TEXT(stored=True), 61 content=TEXT(stored=True), 62 changes=TEXT, 63 ) 57 64 65 def rebuild_index(self): 66 """Delete the index if it exists. Then create a new full index.""" 67 self.log.info('Rebuilding the search index.') 68 69 if not os.path.exists(self.searchpath): 70 os.mkdir(self.searchpath) 71 self.index = index.create_in(self.searchpath, schema=self.SCHEMA) 72 for participant in self.search_participants: 73 self.log.debug('Now building search index for: %s.' 74 % participant.get_search_filters()[0]) 75 participant.build_search_index(self) 76 self.index.commit() 77 self.index.optimize() 78 79 # IEnvironmentSetupParticipant methods 80 81 def environment_created(self): 82 """Create the initial search index.""" 83 self.rebuild_index() 84 85 def environment_needs_upgrade(self, db): 86 """Check if we need to make the index based on if the path is there""" 87 if self.env.path: 88 try: 89 ix = index.open_dir(self.searchpath) 90 #TODO: figure out schema equality testing and re-enable this 91 #return ix.schema != self.SCHEMA 92 return False 93 except: 94 return True 95 else: 96 return True 97 98 def upgrade_environment(self, db): 99 """Create the initial search index.""" 100 self.rebuild_index() 101 102 # Public APIs 103 104 def optimize(self): 105 """Optimize the index to improve search performance.""" 106 if 'index' not in self.__dict__: 107 try: 108 self.index = index.open_dir(self.searchpath) 109 except(IOError, index.EmptyIndexError): 110 raise TracError(_("""Search index missing. \ 111 Rebuild by running trac-admin %s search rebuild.""" % self.env.path)) 112 self.index.optimize() 113 114 def index_doc(self, type, contents): 115 """Add a document (of any type) to the search index. 116 117 The contents should be a dict with fields matching the search schema. 118 The only required fields are type and id, everything else is optional. 119 """ 120 if 'index' not in self.__dict__: 121 try: 122 self.index = index.open_dir(self.searchpath) 123 except(IOError, index.EmptyIndexError): 124 raise TracError(_("""Search index missing. \ 125 Rebuild by running trac-admin %s search rebuild.""" % self.env.path)) 126 if 'writer' not in self.__dict__: 127 self.writer = self.index.writer() 128 contents['type'] = type 129 contents['unique_id'] = type+contents['id'] 130 self.log.debug('Adding doc to the search index: %s' % contents['unique_id']) 131 self.writer.update_document(**contents) 132 self.writer.commit() 133 134 def delete_doc(self, type, id): 135 """Delete a document from the search index. 136 """ 137 if 'index' not in self.__dict__: 138 try: 139 self.index = index.open_dir(self.searchpath) 140 except(IOError, index.EmptyIndexError): 141 raise TracError(_("""Search index missing. \ 142 Rebuild by running trac-admin %s search rebuild.""" % self.env.path)) 143 self.log.debug('Removing doc from the search index: %s' % type+unicode(id)) 144 self.index.delete_by_term('unique_id', type+unicode(id)) 145 self.index.commit() 146 147 148 58 149 def shorten_result(text='', keywords=[], maxlen=240, fuzz=60): 59 150 if not text: 60 151 text = '' -
trac/search/web_ui.py
17 17 import pkg_resources 18 18 import re 19 19 import time 20 import os 20 21 21 22 from genshi.builder import tag, Element 22 23 … … 24 25 from trac.core import * 25 26 from trac.mimeview import Context 26 27 from trac.perm import IPermissionRequestor 27 from trac.search.api import ISearchSource 28 from trac.resource import Resource 29 from trac.search.api import * 28 30 from trac.util.datefmt import format_datetime 29 31 from trac.util.presentation import Paginator 30 32 from trac.util.translation import _ 31 33 from trac.web import IRequestHandler 32 34 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor, \ 33 35 ITemplateProvider 36 from trac.wiki import WikiSystem, WikiPage 34 37 from trac.wiki.api import IWikiSyntaxProvider 35 38 from trac.wiki.formatter import extract_link 36 39 40 from whoosh.fields import * 41 from whoosh import index 42 from whoosh.qparser import MultifieldParser 37 43 44 38 45 class SearchModule(Component): 39 46 40 47 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 41 ITemplateProvider, IWikiSyntaxProvider) 42 43 search_sources = ExtensionPoint(ISearchSource) 48 ITemplateProvider, IWikiSyntaxProvider) 44 49 50 search_participants = ExtensionPoint(ISearchParticipant) 51 45 52 RESULTS_PER_PAGE = 10 46 53 47 min_query_length = IntOption('search', 'min_query_length', 3,48 """Minimum length of query string allowed when performing a search.""")49 50 54 default_disabled_filters = ListOption('search', 'default_disabled_filters', 51 55 doc="""Specifies which search filters should be disabled by default 52 56 on the search page. This will also restrict the filters for the … … 80 84 return ('opensearch.xml', {}, 81 85 'application/opensearchdescription+xml') 82 86 83 available_filters = [] 84 for source in self.search_sources: 85 available_filters += source.get_search_filters(req) 87 available_filters = filter(None, [p.get_search_filters(req) for p 88 in self.search_participants]) 86 89 filters = [f[0] for f in available_filters if req.args.has_key(f[0])] 87 90 if not filters: 88 91 filters = [f[0] for f in available_filters … … 100 103 data['quickjump'] = self._check_quickjump(req, query) 101 104 if query.startswith('!'): 102 105 query = query[1:] 103 terms = self._get_search_terms(query) 106 107 titlers = dict([(x.get_search_filters(req)[0], x.format_search_results) 108 for x in self.search_participants if x.get_search_filters(req)]) 109 110 ix = index.open_dir(os.path.join(self.env.path, 'search_index')) 111 searcher = ix.searcher() 112 parser = MultifieldParser(["id", 'summary', "content", 'changes', 113 'keywords', 'component', 'milestone',], schema = ix.schema) 114 whoosh_results = searcher.search(parser.parse(query)) 115 whoosh_results = [r for r in whoosh_results if r['type'] in filters] 104 116 105 # Refuse queries that obviously would result in a huge result set106 if len(terms) == 1 and len(terms[0]) < self.min_query_length:107 raise TracError(_('Search query too short. Query must be at '108 'least %(num)s characters long.',109 num=self.min_query_length), _('Search Error'))110 111 results = []112 for source in self.search_sources:113 results += list(source.get_search_results(req, terms, filters))114 results.sort(lambda x,y: cmp(y[2], x[2]))115 116 117 page = int(req.args.get('page', '1')) 117 results = Paginator( results, page - 1, self.RESULTS_PER_PAGE)118 results = Paginator(whoosh_results, page - 1, self.RESULTS_PER_PAGE) 118 119 for idx, result in enumerate(results): 119 results[idx] = {'href': result[0], 'title': result[1], 120 'date': format_datetime(result[2]), 121 'author': result[3], 'excerpt': result[4]} 120 resdata = {'href': req.href(result['type'], result['id']), 121 'id' : result['id'],} 122 for k in ['summary', 'author', 'content', 'status',]: 123 if result.has_key(k): 124 resdata[k] = result[k] 125 if result.has_key('content'): 126 resdata['excerpt'] = shorten_result(result['content']) 127 if result.has_key('time'): 128 resdata['date'] = format_datetime(int(result['time'])) 129 130 resdata['title'] = titlers[result['type']](result) 131 results[idx] = resdata 122 132 123 133 pagedata = [] 124 134 data['results'] = results … … 156 166 add_stylesheet(req, 'common/css/search.css') 157 167 return 'search.html', data, None 158 168 169 159 170 # ITemplateProvider methods 160 171 161 172 def get_htdocs_dirs(self):
