Back to index

moin  1.9.0~rc2
results.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - search results processing
00004 
00005     @copyright: 2005 MoinMoin:FlorianFesti,
00006                 2005 MoinMoin:NirSoffer,
00007                 2005 MoinMoin:AlexanderSchremmer,
00008                 2006 MoinMoin:ThomasWaldmann,
00009                 2006 MoinMoin:FranzPletz
00010     @license: GNU GPL, see COPYING for details
00011 """
00012 
00013 import StringIO, time
00014 
00015 from MoinMoin import wikiutil
00016 from MoinMoin.Page import Page
00017 
00018 ############################################################################
00019 ### Results
00020 ############################################################################
00021 
00022 
00023 class Match(object):
00024     """ Base class for all Matches (found pieces of pages).
00025 
00026     This class represents a empty True value as returned from negated searches.
00027     """
00028     # Default match weight
00029     _weight = 1.0
00030 
00031     def __init__(self, start=0, end=0, re_match=None):
00032         self.re_match = re_match
00033         if not re_match:
00034             self._start = start
00035             self._end = end
00036         else:
00037             self._start = self._end = 0
00038 
00039     def __len__(self):
00040         return self.end - self.start
00041 
00042     def __eq__(self, other):
00043         equal = (self.__class__ == other.__class__ and
00044                  self.start == other.start and
00045                  self.end == other.end)
00046         return equal
00047 
00048     def __ne__(self, other):
00049         return not self.__eq__(other)
00050 
00051     def view(self):
00052         return ''
00053 
00054     def weight(self):
00055         return self._weight
00056 
00057     def _get_start(self):
00058         if self.re_match:
00059             return self.re_match.start()
00060         return self._start
00061 
00062     def _get_end(self):
00063         if self.re_match:
00064             return self.re_match.end()
00065         return self._end
00066 
00067     # object properties
00068     start = property(_get_start)
00069     end = property(_get_end)
00070 
00071 
00072 class TextMatch(Match):
00073     """ Represents a match in the page content """
00074     pass
00075 
00076 
00077 class TitleMatch(Match):
00078     """ Represents a match in the page title
00079 
00080     Has more weight than a match in the page content.
00081     """
00082     # Matches in titles are much more important in wikis. This setting
00083     # seems to make all pages that have matches in the title to appear
00084     # before pages that their title does not match.
00085     _weight = 100.0
00086 
00087 
00088 class AttachmentMatch(Match):
00089     """ Represents a match in a attachment content
00090 
00091     Not used yet.
00092     """
00093     pass
00094 
00095 
00096 class FoundPage(object):
00097     """ Represents a page in a search result """
00098 
00099     def __init__(self, page_name, matches=None, page=None, rev=0):
00100         self.page_name = page_name
00101         self.attachment = '' # this is not an attachment
00102         self.page = page
00103         self.rev = rev
00104         if matches is None:
00105             matches = []
00106         self._matches = matches
00107 
00108     def weight(self, unique=1):
00109         """ returns how important this page is for the terms searched for
00110 
00111         Summarize the weight of all page matches
00112 
00113         @param unique: ignore identical matches
00114         @rtype: int
00115         @return: page weight
00116         """
00117         weight = 0
00118         for match in self.get_matches(unique=unique):
00119             weight += match.weight()
00120             # More sophisticated things to be added, like increase
00121             # weight of near matches.
00122         if self.page.parse_processing_instructions().get('deprecated', False):
00123             weight = int(weight / 4) # rank it down
00124         return weight
00125 
00126     def add_matches(self, matches):
00127         """ Add found matches """
00128         self._matches.extend(matches)
00129 
00130     def get_matches(self, unique=1, sort='start', type=Match):
00131         """ Return all matches of type sorted by sort
00132 
00133         @param unique: return only unique matches (bool)
00134         @param sort: match attribute to sort by (string)
00135         @param type: type of match to return (Match or sub class)
00136         @rtype: list
00137         @return: list of matches
00138         """
00139         if unique:
00140             matches = self._unique_matches(type=type)
00141             if sort == 'start':
00142                 # matches already sorted by match.start, finished.
00143                 return matches
00144         else:
00145             matches = self._matches
00146 
00147         # Filter by type and sort by sort using fast schwartzian transform.
00148         if sort == 'start':
00149             tmp = [(match.start, match) for match in matches if isinstance(match, type)]
00150         else:
00151             tmp = [(match.weight(), match) for match in matches if isinstance(match, type)]
00152         tmp.sort()
00153         if sort == 'weight':
00154             tmp.reverse()
00155         matches = [item[1] for item in tmp]
00156 
00157         return matches
00158 
00159     def _unique_matches(self, type=Match):
00160         """ Get a list of unique matches of type
00161 
00162         The result is sorted by match.start, because its easy to remove
00163         duplicates like this.
00164 
00165         @param type: type of match to return
00166         @rtype: list
00167         @return: list of matches of type, sorted by match.start
00168         """
00169         # Filter by type and sort by match.start using fast schwartzian transform.
00170         tmp = [(match.start, match) for match in self._matches if isinstance(match, type)]
00171         tmp.sort()
00172 
00173         if not len(tmp):
00174             return []
00175 
00176         # Get first match into matches list
00177         matches = [tmp[0][1]]
00178 
00179         # Add the remaining ones of matches ignoring identical matches
00180         for item in tmp[1:]:
00181             if item[1] == matches[-1]:
00182                 continue
00183             matches.append(item[1])
00184 
00185         return matches
00186 
00187 
00188 class FoundAttachment(FoundPage):
00189     """ Represents an attachment in search results """
00190 
00191     def __init__(self, page_name, attachment, matches=None, page=None, rev=0):
00192         self.page_name = page_name
00193         self.attachment = attachment
00194         self.rev = rev
00195         self.page = page
00196         if matches is None:
00197             matches = []
00198         self._matches = matches
00199 
00200     def weight(self, unique=1):
00201         return 1
00202 
00203 
00204 class FoundRemote(FoundPage):
00205     """ Represents a remote search result """
00206 
00207     def __init__(self, wikiname, page_name, attachment, matches=None, page=None, rev=0):
00208         self.wikiname = wikiname
00209         self.page_name = page_name
00210         self.rev = rev
00211         self.attachment = attachment
00212         self.page = page
00213         if matches is None:
00214             matches = []
00215         self._matches = matches
00216 
00217     def weight(self, unique=1):
00218         return 1
00219 
00220     def get_matches(self, unique=1, sort='start', type=Match):
00221         return []
00222 
00223     def _unique_matches(self, type=Match):
00224         return []
00225 
00226 
00227 ############################################################################
00228 ### Search results formatting
00229 ############################################################################
00230 
00231 
00232 class SearchResults(object):
00233     """ Manage search results, supply different views
00234 
00235     Search results can hold valid search results and format them for
00236     many requests, until the wiki content changes.
00237 
00238     For example, one might ask for full page list sorted from A to Z,
00239     and then ask for the same list sorted from Z to A. Or sort results
00240     by name and then by rank.
00241     """
00242     # Public functions --------------------------------------------------
00243 
00244     def __init__(self, query, hits, pages, elapsed, sort, estimated_hits):
00245         self.query = query # the query
00246         self.hits = hits # hits list
00247         self.pages = pages # number of pages in the wiki
00248         self.elapsed = elapsed # search time
00249         self.estimated_hits = estimated_hits # about how much hits?
00250 
00251         if sort == 'weight':
00252             self._sortByWeight()
00253         elif sort == 'page_name':
00254             self._sortByPagename()
00255         self.sort = sort
00256 
00257     def _sortByWeight(self):
00258         """ Sorts found pages by the weight of the matches """
00259         tmp = [(hit.weight(), hit.page_name, hit.attachment, hit) for hit in self.hits]
00260         tmp.sort()
00261         tmp.reverse()
00262         self.hits = [item[3] for item in tmp]
00263 
00264     def _sortByPagename(self):
00265         """ Sorts a list of found pages alphabetical by page/attachment name """
00266         tmp = [(hit.page_name, hit.attachment, hit) for hit in self.hits]
00267         tmp.sort()
00268         self.hits = [item[2] for item in tmp]
00269 
00270     def stats(self, request, formatter, hitsFrom):
00271         """ Return search statistics, formatted with formatter
00272 
00273         @param request: current request
00274         @param formatter: formatter to use
00275         @param hitsFrom: current position in the hits
00276         @rtype: unicode
00277         @return formatted statistics
00278         """
00279         _ = request.getText
00280 
00281         if not self.estimated_hits:
00282             self.estimated_hits = ('', len(self.hits))
00283 
00284         output = [
00285             formatter.paragraph(1, attr={'class': 'searchstats'}),
00286             _("Results %(bs)s%(hitsFrom)d - %(hitsTo)d%(be)s "
00287                     "of %(aboutHits)s %(bs)s%(hits)d%(be)s results out of "
00288                     "about %(pages)d pages.") %
00289                 {'aboutHits': self.estimated_hits[0],
00290                     'hits': self.estimated_hits[1], 'pages': self.pages,
00291                     'hitsFrom': hitsFrom + 1,
00292                     'hitsTo': hitsFrom +
00293                             min(self.estimated_hits[1] - hitsFrom,
00294                                 request.cfg.search_results_per_page),
00295                     'bs': formatter.strong(1), 'be': formatter.strong(0)},
00296             u' (%s %s)' % (''.join([formatter.strong(1),
00297                 formatter.text("%.2f" % self.elapsed),
00298                 formatter.strong(0)]),
00299                 formatter.text(_("seconds"))),
00300             formatter.paragraph(0),
00301             ]
00302         return ''.join(output)
00303 
00304     def pageList(self, request, formatter, info=0, numbered=1,
00305             paging=True, hitsFrom=0, hitsInfo=0):
00306         """ Format a list of found pages
00307 
00308         @param request: current request
00309         @param formatter: formatter to use
00310         @param info: show match info in title
00311         @param numbered: use numbered list for display
00312         @param paging: toggle paging
00313         @param hitsFrom: current position in the hits
00314         @param hitsInfo: toggle hits info line
00315         @rtype: unicode
00316         @return formatted page list
00317         """
00318         self._reset(request, formatter)
00319         f = formatter
00320         write = self.buffer.write
00321         if numbered:
00322             lst = lambda on: f.number_list(on, start=hitsFrom+1)
00323         else:
00324             lst = f.bullet_list
00325 
00326         if paging and len(self.hits) <= request.cfg.search_results_per_page:
00327             paging = False
00328 
00329         # Add pages formatted as list
00330         if self.hits:
00331             write(lst(1))
00332 
00333             if paging:
00334                 hitsTo = hitsFrom + request.cfg.search_results_per_page
00335                 displayHits = self.hits[hitsFrom:hitsTo]
00336             else:
00337                 displayHits = self.hits
00338 
00339             for page in displayHits:
00340                 # TODO handle interwiki search hits
00341                 if page.attachment:
00342                     querydict = {
00343                         'action': 'AttachFile',
00344                         'do': 'view',
00345                         'target': page.attachment,
00346                     }
00347                 elif page.rev and page.rev != page.page.getRevList()[0]:
00348                     querydict = {
00349                         'rev': page.rev,
00350                     }
00351                 else:
00352                     querydict = None
00353                 querystr = self.querystring(querydict)
00354 
00355                 matchInfo = ''
00356                 if info:
00357                     matchInfo = self.formatInfo(f, page)
00358 
00359                 info_for_hits = u''
00360                 if hitsInfo:
00361                     info_for_hits = self.formatHitInfoBar(page)
00362 
00363                 item = [
00364                     f.listitem(1),
00365                     f.pagelink(1, page.page_name, querystr=querystr),
00366                     self.formatTitle(page),
00367                     f.pagelink(0, page.page_name),
00368                     matchInfo,
00369                     info_for_hits,
00370                     f.listitem(0),
00371                     ]
00372                 write(''.join(item))
00373             write(lst(0))
00374             if paging:
00375                 write(self.formatPageLinks(hitsFrom=hitsFrom,
00376                     hitsPerPage=request.cfg.search_results_per_page,
00377                     hitsNum=len(self.hits)))
00378 
00379         return self.getvalue()
00380 
00381     def pageListWithContext(self, request, formatter, info=1, context=180,
00382                             maxlines=1, paging=True, hitsFrom=0, hitsInfo=0):
00383         """ Format a list of found pages with context
00384 
00385         @param request: current request
00386         @param formatter: formatter to use
00387         @param info: show match info near the page link
00388         @param context: how many characters to show around each match.
00389         @param maxlines: how many contexts lines to show.
00390         @param paging: toggle paging
00391         @param hitsFrom: current position in the hits
00392         @param hitsInfo: toggle hits info line
00393         @rtype: unicode
00394         @return formatted page list with context
00395         """
00396         self._reset(request, formatter)
00397         f = formatter
00398         write = self.buffer.write
00399         _ = request.getText
00400 
00401         if paging and len(self.hits) <= request.cfg.search_results_per_page:
00402             paging = False
00403 
00404         # Add pages formatted as definition list
00405         if self.hits:
00406             write(f.definition_list(1))
00407 
00408             if paging:
00409                 hitsTo = hitsFrom + request.cfg.search_results_per_page
00410                 displayHits = self.hits[hitsFrom:hitsTo]
00411             else:
00412                 displayHits = self.hits
00413 
00414             for page in displayHits:
00415                 # TODO handle interwiki search hits
00416                 matchInfo = ''
00417                 if info:
00418                     matchInfo = self.formatInfo(f, page)
00419                 if page.attachment:
00420                     fmt_context = ""
00421                     querydict = {
00422                         'action': 'AttachFile',
00423                         'do': 'view',
00424                         'target': page.attachment,
00425                     }
00426                 elif page.page_name.startswith('FS/'): # XXX FS hardcoded
00427                     fmt_context = ""
00428                     querydict = None
00429                 else:
00430                     fmt_context = self.formatContext(page, context, maxlines)
00431                     if page.rev and page.rev != page.page.getRevList()[0]:
00432                         querydict = {
00433                             'rev': page.rev,
00434                         }
00435                     else:
00436                         querydict = None
00437                 querystr = self.querystring(querydict)
00438                 item = [
00439                     f.definition_term(1),
00440                     f.pagelink(1, page.page_name, querystr=querystr),
00441                     self.formatTitle(page),
00442                     f.pagelink(0, page.page_name),
00443                     matchInfo,
00444                     f.definition_term(0),
00445                     f.definition_desc(1),
00446                     fmt_context,
00447                     f.definition_desc(0),
00448                     self.formatHitInfoBar(page),
00449                     ]
00450                 write(''.join(item))
00451             write(f.definition_list(0))
00452             if paging:
00453                 write(self.formatPageLinks(hitsFrom=hitsFrom,
00454                     hitsPerPage=request.cfg.search_results_per_page,
00455                     hitsNum=len(self.hits)))
00456 
00457         return self.getvalue()
00458 
00459     # Private -----------------------------------------------------------
00460 
00461     # This methods are not meant to be used by clients and may change
00462     # without notice.
00463 
00464     def formatContext(self, page, context, maxlines):
00465         """ Format search context for each matched page
00466 
00467         Try to show first maxlines interesting matches context.
00468         """
00469         f = self.formatter
00470         if not page.page:
00471             page.page = Page(self.request, page.page_name)
00472         body = page.page.get_raw_body()
00473         last = len(body) - 1
00474         lineCount = 0
00475         output = []
00476 
00477         # Get unique text matches sorted by match.start, try to ignore
00478         # matches in page header, and show the first maxlines matches.
00479         # TODO: when we implement weight algorithm for text matches, we
00480         # should get the list of text matches sorted by weight and show
00481         # the first maxlines matches.
00482         matches = page.get_matches(unique=1, sort='start', type=TextMatch)
00483         i, start = self.firstInterestingMatch(page, matches)
00484 
00485         # Format context
00486         while i < len(matches) and lineCount < maxlines:
00487             match = matches[i]
00488 
00489             # Get context range for this match
00490             start, end = self.contextRange(context, match, start, last)
00491 
00492             # Format context lines for matches. Each complete match in
00493             # the context will be highlighted, and if the full match is
00494             # in the context, we increase the index, and will not show
00495             # same match again on a separate line.
00496 
00497             output.append(f.text(u'...'))
00498 
00499             # Get the index of the first match completely within the
00500             # context.
00501             for j in xrange(0, len(matches)):
00502                 if matches[j].start >= start:
00503                     break
00504 
00505             # Add all matches in context and the text between them
00506             while True:
00507                 match = matches[j]
00508                 # Ignore matches behind the current position
00509                 if start < match.end:
00510                     # Append the text before match
00511                     if start < match.start:
00512                         output.append(f.text(body[start:match.start]))
00513                     # And the match
00514                     output.append(self.formatMatch(body, match, start))
00515                     start = match.end
00516                 # Get next match, but only if its completely within the context
00517                 if j < len(matches) - 1 and matches[j + 1].end <= end:
00518                     j += 1
00519                 else:
00520                     break
00521 
00522             # Add text after last match and finish the line
00523             if match.end < end:
00524                 output.append(f.text(body[match.end:end]))
00525             output.append(f.text(u'...'))
00526             output.append(f.linebreak(preformatted=0))
00527 
00528             # Increase line and point to the next match
00529             lineCount += 1
00530             i = j + 1
00531 
00532         output = ''.join(output)
00533 
00534         if not output:
00535             # Return the first context characters from the page text
00536             output = f.text(page.page.getPageText(length=context))
00537             output = output.strip()
00538             if not output:
00539                 # This is a page with no text, only header, for example,
00540                 # a redirect page.
00541                 output = f.text(page.page.getPageHeader(length=context))
00542 
00543         return output
00544 
00545     def firstInterestingMatch(self, page, matches):
00546         """ Return the first interesting match
00547 
00548         This function is needed only because we don't have yet a weight
00549         algorithm for page text matches.
00550 
00551         Try to find the first match in the page text. If we can't find
00552         one, we return the first match and start=0.
00553 
00554         @rtype: tuple
00555         @return: index of first match, start of text
00556         """
00557         header = page.page.getPageHeader()
00558         start = len(header)
00559         # Find first match after start
00560         for i in xrange(len(matches)):
00561             if matches[i].start >= start and \
00562                     isinstance(matches[i], TextMatch):
00563                 return i, start
00564         return 0, 0
00565 
00566     def contextRange(self, context, match, start, last):
00567         """ Compute context range
00568 
00569         Add context around each match. If there is no room for context
00570         before or after the match, show more context on the other side.
00571 
00572         @param context: context length
00573         @param match: current match
00574         @param start: context should not start before that index, unless
00575                       end is past the last character.
00576         @param last: last character index
00577         @rtype: tuple
00578         @return: start, end of context
00579         """
00580         # Start by giving equal context on both sides of match
00581         contextlen = max(context - len(match), 0)
00582         cstart = match.start - contextlen / 2
00583         cend = match.end + contextlen / 2
00584 
00585         # If context start before start, give more context on end
00586         if cstart < start:
00587             cend += start - cstart
00588             cstart = start
00589 
00590         # But if end if after last, give back context to start
00591         if cend > last:
00592             cstart -= cend - last
00593             cend = last
00594 
00595         # Keep context start positive for very short texts
00596         cstart = max(cstart, 0)
00597 
00598         return cstart, cend
00599 
00600     def formatTitle(self, page):
00601         """ Format page title
00602 
00603         Invoke format match on all unique matches in page title.
00604 
00605         @param page: found page
00606         @rtype: unicode
00607         @return: formatted title
00608         """
00609         # Get unique title matches sorted by match.start
00610         matches = page.get_matches(unique=1, sort='start', type=TitleMatch)
00611 
00612         # Format
00613         pagename = page.page_name
00614         f = self.formatter
00615         output = []
00616         start = 0
00617         for match in matches:
00618             # Ignore matches behind the current position
00619             if start < match.end:
00620                 # Append the text before the match
00621                 if start < match.start:
00622                     output.append(f.text(pagename[start:match.start]))
00623                 # And the match
00624                 output.append(self.formatMatch(pagename, match, start))
00625                 start = match.end
00626         # Add text after match
00627         if start < len(pagename):
00628             output.append(f.text(pagename[start:]))
00629 
00630         if page.attachment: # show the attachment that matched
00631             output.extend([
00632                     " ",
00633                     f.strong(1),
00634                     f.text("(%s)" % page.attachment),
00635                     f.strong(0)])
00636 
00637         return ''.join(output)
00638 
00639     def formatMatch(self, body, match, location):
00640         """ Format single match in text
00641 
00642         Format the part of the match after the current location in the
00643         text. Matches behind location are ignored and an empty string is
00644         returned.
00645 
00646         @param body: text containing match
00647         @param match: search match in text
00648         @param location: current location in text
00649         @rtype: unicode
00650         @return: formatted match or empty string
00651         """
00652         start = max(location, match.start)
00653         if start < match.end:
00654             f = self.formatter
00655             output = [
00656                 f.strong(1),
00657                 f.text(body[start:match.end]),
00658                 f.strong(0),
00659                 ]
00660             return ''.join(output)
00661         return ''
00662 
00663     def formatPageLinks(self, hitsFrom, hitsPerPage, hitsNum):
00664         """ Format previous and next page links in page
00665 
00666         @param hitsFrom: current position in the hits
00667         @param hitsPerPage: number of hits per page
00668         @param hitsNum: number of hits
00669         @rtype: unicode
00670         @return: links to previous and next pages (if exist)
00671         """
00672         _ = self.request.getText
00673         f = self.formatter
00674         querydict = dict(wikiutil.parseQueryString(self.request.query_string))
00675 
00676         def page_url(n):
00677             querydict.update({'from': n * hitsPerPage})
00678             return self.request.page.url(self.request, querydict, escape=0)
00679 
00680         pages = hitsNum // hitsPerPage
00681         remainder = hitsNum % hitsPerPage
00682         if remainder:
00683             pages += 1
00684         cur_page = hitsFrom // hitsPerPage
00685 
00686         textlinks = []
00687 
00688         # previous page available
00689         if cur_page > 0:
00690             textlinks.append(''.join([
00691                         f.url(1, href=page_url(cur_page-1)),
00692                         f.text(_('Previous')),
00693                         f.url(0)]))
00694         else:
00695             textlinks.append('')
00696 
00697         # list of pages to be shown
00698         page_range = range(*(
00699             cur_page - 5 < 0 and
00700                 (0, pages > 10 and 10 or pages) or
00701                 (cur_page - 5, cur_page + 6 > pages and
00702                     pages or cur_page + 6)))
00703         textlinks.extend([''.join([
00704                 i != cur_page and f.url(1, href=page_url(i)) or '',
00705                 f.text(str(i+1)),
00706                 i != cur_page and f.url(0) or '',
00707             ]) for i in page_range])
00708 
00709         # next page available
00710         if cur_page < pages - 1:
00711             textlinks.append(''.join([
00712                 f.url(1, href=page_url(cur_page+1)),
00713                 f.text(_('Next')),
00714                 f.url(0)]))
00715         else:
00716             textlinks.append('')
00717 
00718         return ''.join([
00719             f.table(1, attrs={'tableclass': 'searchpages'}),
00720             f.table_row(1),
00721                 f.table_cell(1),
00722                 # textlinks
00723                 (f.table_cell(0) + f.table_cell(1)).join(textlinks),
00724                 f.table_cell(0),
00725             f.table_row(0),
00726             f.table(0),
00727         ])
00728 
00729     def formatHitInfoBar(self, page):
00730         """ Returns the code for the information below a search hit
00731 
00732         @param page: the FoundPage instance
00733         """
00734         request = self.request
00735         f = self.formatter
00736         _ = request.getText
00737         p = page.page
00738 
00739         rev = p.get_real_rev()
00740         if rev is None:
00741             rev = 0
00742 
00743         size_str = '%.1fk' % (p.size()/1024.0)
00744         revisions = p.getRevList()
00745         if len(revisions) and rev == revisions[0]:
00746             rev_str = '%s: %d (%s)' % (_('rev'), rev, _('current'))
00747         else:
00748             rev_str = '%s: %d' % (_('rev'), rev, )
00749         lastmod_str = _('last modified: %s') % p.mtime_printable(request)
00750 
00751         result = f.paragraph(1, attr={'class': 'searchhitinfobar'}) + \
00752                  f.text('%s - %s %s' % (size_str, rev_str, lastmod_str)) + \
00753                  f.paragraph(0)
00754         return result
00755 
00756     def querystring(self, querydict=None):
00757         """ Return query string, used in the page link
00758 
00759         @keyword querydict: use these parameters (default: None)
00760         """
00761         if querydict is None:
00762             querydict = {}
00763         if 'action' not in querydict or querydict['action'] == 'AttachFile':
00764             highlight = self.query.highlight_re()
00765             if highlight:
00766                 querydict.update({'highlight': highlight})
00767         querystr = wikiutil.makeQueryString(querydict)
00768         return querystr
00769 
00770     def formatInfo(self, formatter, page):
00771         """ Return formatted match info
00772 
00773         @param formatter: the formatter instance to use
00774         @param page: the current page instance
00775         """
00776         template = u' . . . %s %s'
00777         template = u"%s%s%s" % (formatter.span(1, css_class="info"),
00778                                 template,
00779                                 formatter.span(0))
00780         # Count number of unique matches in text of all types
00781         count = len(page.get_matches(unique=1))
00782         info = template % (count, self.matchLabel[count != 1])
00783         return info
00784 
00785     def getvalue(self):
00786         """ Return output in div with CSS class """
00787         value = [
00788             self.formatter.div(1, css_class='searchresults'),
00789             self.buffer.getvalue(),
00790             self.formatter.div(0),
00791             ]
00792         return '\n'.join(value)
00793 
00794     def _reset(self, request, formatter):
00795         """ Update internal state before new output
00796 
00797         Do not call this, it should be called only by the instance code.
00798 
00799         Each request might need different translations or other user preferences.
00800 
00801         @param request: current request
00802         @param formatter: the formatter instance to use
00803         """
00804         self.buffer = StringIO.StringIO()
00805         self.formatter = formatter
00806         self.request = request
00807         # Use 1 match, 2 matches...
00808         _ = request.getText
00809         self.matchLabel = (_('match'), _('matches'))
00810 
00811 
00812 def getSearchResults(request, query, hits, start, sort, estimated_hits):
00813     """ Return a SearchResults object with the specified properties
00814 
00815     @param request: current request
00816     @param query: the search query object tree
00817     @param hits: list of hits
00818     @param start: position to start showing the hits
00819     @param sort: sorting of the results, either 'weight' or 'page_name'
00820     @param estimated_hits: if true, use this estimated hit count
00821     """
00822     result_hits = []
00823     for wikiname, page, attachment, match, rev in hits:
00824         if wikiname in (request.cfg.interwikiname, 'Self'): # a local match
00825             if attachment:
00826                 result_hits.append(FoundAttachment(page.page_name, attachment, matches=match, page=page, rev=rev))
00827             else:
00828                 result_hits.append(FoundPage(page.page_name, matches=match, page=page, rev=rev))
00829         else:
00830             page_name = page # for remote wikis, we have the page_name, not the page obj
00831             result_hits.append(FoundRemote(wikiname, page_name, attachment, matches=match, rev=rev))
00832     elapsed = time.time() - start
00833     count = request.rootpage.getPageCount()
00834     return SearchResults(query, result_hits, count, elapsed, sort,
00835             estimated_hits)
00836