changeset 1275:e083ea8c934e

Merge with main and xapian.
author Alexander Schremmer <alex AT alexanderweb DOT de>
date Mon, 14 Aug 2006 22:29:44 +0200
parents 11778954b99c (current diff) a9b155a92bc2 (diff)
children ed68b5d6f47e 9608758dca9a
files docs/CHANGES.config
diffstat 52 files changed, 923 insertions(+), 521 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/action/SpellCheck.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/action/SpellCheck.py	Mon Aug 14 22:29:44 2006 +0200
@@ -99,7 +99,6 @@
 
 
 def _addLocalWords(request):
-    import types
     from MoinMoin.PageEditor import PageEditor
 
     # get the new words as a string (if any are marked at all)
--- a/MoinMoin/action/fullsearch.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/action/fullsearch.py	Mon Aug 14 22:29:44 2006 +0200
@@ -42,6 +42,7 @@
     needle = request.form.get(fieldname, [''])[0]
     case = int(request.form.get('case', [0])[0])
     regex = int(request.form.get('regex', [0])[0]) # no interface currently
+    hitsFrom = int(request.form.get('from', [0])[0])
 
     max_context = 1 # only show first `max_context` contexts XXX still unused
 
@@ -54,11 +55,19 @@
         Page(request, pagename).send_page(request, msg=err)
         return
 
+    # Setup for type of search
+    if titlesearch:
+        title = _('Title Search: "%s"')
+        sort = 'page_name'
+    else:
+        title = _('Full Text Search: "%s"')
+        sort = 'weight'
+
     # search the pages
     from MoinMoin.search import searchPages, QueryParser
     query = QueryParser(case=case, regex=regex,
             titlesearch=titlesearch).parse_query(needle)
-    results = searchPages(request, query)
+    results = searchPages(request, query, sort)
 
     # directly show a single hit
     # XXX won't work with attachment search
@@ -78,29 +87,22 @@
     # This action generate data using the user language
     request.setContentLanguage(request.lang)
 
-    # Setup for type of search
-    if titlesearch:
-        title = _('Title Search: "%s"')
-        results.sortByPagename()
-    else:
-        title = _('Full Text Search: "%s"')
-        results.sortByWeight()
-
     request.theme.send_title(title % needle, form=request.form, pagename=pagename)
 
     # Start content (important for RTL support)
     request.write(request.formatter.startContent("content"))
 
     # First search stats
-    request.write(results.stats(request, request.formatter))
+    request.write(results.stats(request, request.formatter, hitsFrom))
 
     # Then search results
     info = not titlesearch
     if context:
-        output = results.pageListWithContext(request, request.formatter, info=info,
-                                             context=context)
+        output = results.pageListWithContext(request, request.formatter,
+                info=info, context=context, hitsFrom=hitsFrom)
     else:
-        output = results.pageList(request, request.formatter, info=info)
+        output = results.pageList(request, request.formatter, info=info,
+                hitsFrom=hitsFrom)
     request.write(output)
 
     request.write(request.formatter.endContent())
--- a/MoinMoin/action/newpage.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/action/newpage.py	Mon Aug 14 22:29:44 2006 +0200
@@ -29,7 +29,7 @@
         @return: error message
         """
         _ = self.request.getText
-        need_replace = self.nametemplate.find('%s') != -1
+        need_replace = '%s' in self.nametemplate
         if not self.pagename and need_replace:
             return _("Cannot create a new page without a page name."
                      "  Please specify a page name.")
@@ -38,7 +38,7 @@
         # template variable
             repl = 'A@'
             i = 0
-            while self.nametemplate.find(repl) != -1:
+            while repl in self.nametemplate:
                 repl += ['#', '&', '$', 'x', 'X', ':', '@'][i]
                 i += 1
                 i = i % 7
--- a/MoinMoin/action/rss_rc.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/action/rss_rc.py	Mon Aug 14 22:29:44 2006 +0200
@@ -54,7 +54,7 @@
     for line in log.reverse():
         if not request.user.may.read(line.pagename):
             continue
-        if ((line.action[:4] != 'SAVE') or
+        if (not line.action.startswith('SAVE') or
             ((line.pagename in pages) and unique)): continue
         #if log.dayChanged() and log.daycount > _MAX_DAYS: break
         line.editor = line.getInterwikiEditorData(request)
--- a/MoinMoin/config/multiconfig.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/config/multiconfig.py	Mon Aug 14 22:29:44 2006 +0200
@@ -300,6 +300,7 @@
     xapian_search = False # disabled until xapian is finished
     xapian_index_dir = None
     xapian_stemming = True
+    search_results_per_page = 10
 
     mail_login = None # or "user pwd" if you need to use SMTP AUTH
     mail_sendmail = None # "/usr/sbin/sendmail -t -i" to not use SMTP, but sendmail
--- a/MoinMoin/filter/EXIF.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/filter/EXIF.py	Mon Aug 14 22:29:44 2006 +0200
@@ -1004,7 +1004,7 @@
             return
 
         # Olympus
-        if make[:7] == 'OLYMPUS':
+        if make.startswith('OLYMPUS'):
             self.dump_IFD(note.field_offset+8, 'MakerNote',
                           dict=MAKERNOTE_OLYMPUS_TAGS)
             return
--- a/MoinMoin/formatter/__init__.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/formatter/__init__.py	Mon Aug 14 22:29:44 2006 +0200
@@ -7,12 +7,13 @@
     @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>
     @license: GNU GPL, see COPYING for details.
 """
+import re
+
 from MoinMoin.util import pysupport
+from MoinMoin import wikiutil
 
 modules = pysupport.getPackageModules(__file__)
 
-from MoinMoin import wikiutil
-import re, types
 
 class FormatterBase:
     """ This defines the output interface used all over the rest of the code.
@@ -37,7 +38,7 @@
         self._base_depth = 0
 
     def set_highlight_re(self, hi_re=None):
-        if type(hi_re) in [types.StringType, types.UnicodeType]:
+        if isinstance(hi_re, (str, unicode)):
             try:
                 self._highlight_re = re.compile(hi_re, re.U + re.IGNORECASE)
             except re.error:
@@ -96,7 +97,7 @@
         """
         wikitag, wikiurl, wikitail, wikitag_bad = wikiutil.resolve_wiki(self.request, '%s:"%s"' % (interwiki, pagename))
         if wikitag == 'Self' or wikitag == self.request.cfg.interwikiname:
-            if wikitail.find('#') > -1:
+            if '#' in wikitail:
                 wikitail, kw['anchor'] = wikitail.split('#', 1)
                 wikitail = wikiutil.url_unquote(wikitail)
             return self.pagelink(on, wikitail, **kw)
@@ -295,7 +296,7 @@
         return macro_obj.execute(name, args)
 
     def _get_bang_args(self, line):
-        if line[:2] == '#!':
+        if line.startswith('#!'):
             try:
                 name, args = line[2:].split(None, 1)
             except ValueError:
--- a/MoinMoin/formatter/dom_xml.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/formatter/dom_xml.py	Mon Aug 14 22:29:44 2006 +0200
@@ -191,7 +191,7 @@
         return self._set_tag('lang', on, value=lang_name)
 
     def pagelink(self, on, pagename='', page=None, **kw):
-        apply(FormatterBase.pagelink, (self, pagename, page), kw)
+        FormatterBase.pagelink(self, pagename, page, **kw)
         if not pagename and page is not None:
             pagename = page.page_name
         kw['pagename'] = pagename
--- a/MoinMoin/formatter/text_gedit.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/formatter/text_gedit.py	Mon Aug 14 22:29:44 2006 +0200
@@ -40,7 +40,7 @@
 
             See wikiutil.link_tag() for possible keyword parameters.
         """
-        apply(FormatterBase.pagelink, (self, on, pagename, page), kw)
+        FormatterBase.pagelink(self, on, pagename, page, **kw)
         if page is None:
             page = Page(self.request, pagename, formatter=self)
         return page.link_to(self.request, on=on, **kw)
--- a/MoinMoin/formatter/text_html.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/formatter/text_html.py	Mon Aug 14 22:29:44 2006 +0200
@@ -173,7 +173,7 @@
     indentspace = ' '
 
     def __init__(self, request, **kw):
-        apply(FormatterBase.__init__, (self, request), kw)
+        FormatterBase.__init__(self, request, **kw)
 
         # inline tags stack. When an inline tag is called, it goes into
         # the stack. When a block element starts, all inline tags in
@@ -484,7 +484,7 @@
 
             See wikiutil.link_tag() for possible keyword parameters.
         """
-        apply(FormatterBase.pagelink, (self, on, pagename, page), kw)
+        FormatterBase.pagelink(self, on, pagename, page, **kw)
         if page is None:
             page = Page(self.request, pagename, formatter=self)
         if self.request.user.show_nonexist_qm and on and not page.exists():
@@ -506,13 +506,13 @@
         wikiurl = wikiutil.mapURL(self.request, wikiurl)
         if wikitag == 'Self': # for own wiki, do simple links
             if on:
-                if wikitail.find('#') > -1:
+                if '#' in wikitail:
                     wikitail, kw['anchor'] = wikitail.split('#', 1)
                 wikitail = wikiutil.url_unquote(wikitail)
                 try: # XXX this is the only place where we access self.page - do we need it? Crashes silently on actions!
-                    return apply(self.pagelink, (on, wikiutil.AbsPageName(self.request, self.page.page_name, wikitail)), kw)
+                    return self.pagelink(on, wikiutil.AbsPageName(self.request, self.page.page_name, wikitail), **kw)
                 except:
-                    return apply(self.pagelink, (on, wikitail), kw)
+                    return self.pagelink(on, wikitail, **kw)
             else:
                 return self.pagelink(0)
         else: # return InterWiki hyperlink
--- a/MoinMoin/formatter/text_plain.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/formatter/text_plain.py	Mon Aug 14 22:29:44 2006 +0200
@@ -16,7 +16,7 @@
     hardspace = u' '
 
     def __init__(self, request, **kw):
-        apply(FormatterBase.__init__, (self, request), kw)
+        FormatterBase.__init__(self, request, **kw)
         self._in_code_area = 0
         self._in_code_line = 0
         self._code_area_state = [0, -1, -1, 0]
@@ -36,7 +36,7 @@
         return (u'\n\n*** ', u' ***\n\n')[not on]
 
     def pagelink(self, on, pagename='', page=None, **kw):
-        apply(FormatterBase.pagelink, (self, on, pagename, page), kw)
+        FormatterBase.pagelink(self, on, pagename, page, **kw)
         return (u">>", u"<<") [not on]
 
     def interwikilink(self, on, interwiki='', pagename='', **kw):
--- a/MoinMoin/formatter/text_xml.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/formatter/text_xml.py	Mon Aug 14 22:29:44 2006 +0200
@@ -19,7 +19,7 @@
     hardspace = '&nbsp;'
 
     def __init__(self, request, **kw):
-        apply(FormatterBase.__init__, (self, request), kw)
+        FormatterBase.__init__(self, request, **kw)
         self._current_depth = 1
         self._base_depth = 0
         self.in_pre = 0
@@ -49,7 +49,7 @@
         return '<![CDATA[' + markup.replace(']]>', ']]>]]&gt;<![CDATA[') + ']]>'
 
     def pagelink(self, on, pagename='', page=None, **kw):
-        apply(FormatterBase.pagelink, (self, on, pagename, page), kw)
+        FormatterBase.pagelink(self, on, pagename, page, **kw)
         if page is None:
             page = Page(self.request, pagename, formatter=self)
         return page.link_to(self.request, on=on, **kw)
@@ -202,7 +202,7 @@
         for key, value in kw.items():
             if key in valid_attrs:
                 attrs[key] = value
-        return apply(FormatterBase.image, (self,), attrs) + '</img>'
+        return FormatterBase.image(self, **attrs) + '</img>'
 
     def code_area(self, on, code_id, code_type='code', show=0, start=-1, step=-1):
         return ('<codearea id="%s">' % code_id, '</codearea')[not on]
--- a/MoinMoin/macro/FullSearch.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/macro/FullSearch.py	Mon Aug 14 22:29:44 2006 +0200
@@ -32,13 +32,63 @@
 
 Dependencies = ["pages"]
 
+
+def search_box(type, macro):
+    """ Make a search box
+
+    Make both Title Search and Full Search boxes, according to type.
+
+    @param type: search box type: 'titlesearch' or 'fullsearch'
+    @rtype: unicode
+    @return: search box html fragment
+    """
+    _ = macro._
+    if macro.form.has_key('value'):
+        default = wikiutil.escape(macro.form["value"][0], quote=1)
+    else:
+        default = ''
+
+    # Title search settings
+    boxes = ''
+    button = _("Search Titles")
+
+    # Special code for fullsearch
+    if type == "fullsearch":
+        boxes = [
+            u'<br>',
+            u'<input type="checkbox" name="context" value="160" checked="checked">',
+            _('Display context of search results'),
+            u'<br>',
+            u'<input type="checkbox" name="case" value="1">',
+            _('Case-sensitive searching'),
+            ]
+        boxes = u'\n'.join(boxes)
+        button = _("Search Text")
+
+    # Format
+    type = (type == "titlesearch")
+    html = [
+        u'<form method="get" action="">',
+        u'<div>',
+        u'<input type="hidden" name="action" value="fullsearch">',
+        u'<input type="hidden" name="titlesearch" value="%i">' % type,
+        u'<input type="text" name="value" size="30" value="%s">' % default,
+        u'<input type="submit" value="%s">' % button,
+        boxes,
+        u'</div>',
+        u'</form>',
+        ]
+    html = u'\n'.join(html)
+    return macro.formatter.rawHTML(html)
+
+
 def execute(macro, needle):
     request = macro.request
     _ = request.getText
 
     # if no args given, invoke "classic" behavior
     if needle is None:
-        return macro._m_search("fullsearch")
+        return search_box("fullsearch", macro)
 
     # With empty arguments, simulate title click (backlinks to page)
     elif needle == '':
@@ -57,6 +107,6 @@
     results = search.searchPages(request, needle)
     results.sortByPagename()
 
-    return results.pageList(request, macro.formatter)
+    return results.pageList(request, macro.formatter, paging=False)
 
 
--- a/MoinMoin/macro/NewPage.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/macro/NewPage.py	Mon Aug 14 22:29:44 2006 +0200
@@ -79,7 +79,7 @@
         if parent == '@ME' and self.request.user.valid:
             parent = self.request.user.name
 
-        requires_input = nametemplate.find('%s') != -1
+        requires_input = '%s' in nametemplate
 
         if label:
             # Try to get a translation, this will probably not work in
--- a/MoinMoin/macro/RecentChanges.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/macro/RecentChanges.py	Mon Aug 14 22:29:44 2006 +0200
@@ -38,7 +38,7 @@
         elif line.action == 'ATTDRW':
             comment = _("Drawing '%(filename)s' saved.") % {
                 'filename': filename}
-    elif line.action.find('/REVERT') != -1:
+    elif '/REVERT' in line.action:
         rev = int(line.extra)
         comment = _("Revert to revision %(rev)d.") % {'rev': rev}
 
--- a/MoinMoin/macro/SystemInfo.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/macro/SystemInfo.py	Mon Aug 14 22:29:44 2006 +0200
@@ -112,12 +112,20 @@
     row(_('Local extension parsers'),
         ', '.join(wikiutil.wikiPlugins('parser', Macro.cfg)) or nonestr)
 
-    state = (_('Disabled'), _('Enabled'))
     from MoinMoin.search.builtin import Search
-    row(_('Xapian search'), '%s, %sactive' % (state[request.cfg.xapian_search],
-                not Search._xapianIndex(request) and 'not ' or ''))
+    xapState = (_('Disabled'), _('Enabled'))
+    idxState = (_('index available'), _('index unavailable'))
+    idx = Search._xapianIndex(request)
+    available = idx and idxState[0] or idxState[1]
+    mtime = _('last modified: %s') % (idx and
+            request.user.getFormattedDateTime(
+                wikiutil.version2timestamp(idx.mtime())) or
+                _('N/A'))
+    row(_('Xapian search'), '%s, %s, %s'
+            % (xapState[request.cfg.xapian_search], available, mtime))
+    row(_('Xapian stemming'), xapState[request.cfg.xapian_stemming])
 
-    row(_('Active threads'), t_count or 'N/A')
+    row(_('Active threads'), t_count or _('N/A'))
     buf.write(u'</dl>')
 
     return Macro.formatter.rawHTML(buf.getvalue())
--- a/MoinMoin/macro/TableOfContents.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/macro/TableOfContents.py	Mon Aug 14 22:29:44 2006 +0200
@@ -67,7 +67,7 @@
         if self.include_macro is None:
             self.include_macro = wikiutil.importPlugin(self.macro.request.cfg,
                                                        'macro', "Include")
-        return self.pre_re.sub('', apply(self.include_macro, args, kwargs)).split('\n')
+        return self.pre_re.sub('', self.include_macro(*args, **kwargs)).split('\n')
 
     def run(self):
         _ = self._
--- a/MoinMoin/macro/__init__.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/macro/__init__.py	Mon Aug 14 22:29:44 2006 +0200
@@ -143,55 +143,8 @@
             return self.defaultDependency
 
     def _macro_TitleSearch(self, args):
-        return self._m_search("titlesearch")
-
-    def _m_search(self, type):
-        """ Make a search box
-
-        Make both Title Search and Full Search boxes, according to type.
-
-        @param type: search box type: 'titlesearch' or 'fullsearch'
-        @rtype: unicode
-        @return: search box html fragment
-        """
-        _ = self._
-        if self.form.has_key('value'):
-            default = wikiutil.escape(self.form["value"][0], quote=1)
-        else:
-            default = ''
-
-        # Title search settings
-        boxes = ''
-        button = _("Search Titles")
-
-        # Special code for fullsearch
-        if type == "fullsearch":
-            boxes = [
-                u'<br>',
-                u'<input type="checkbox" name="context" value="160" checked="checked">',
-                _('Display context of search results'),
-                u'<br>',
-                u'<input type="checkbox" name="case" value="1">',
-                _('Case-sensitive searching'),
-                ]
-            boxes = u'\n'.join(boxes)
-            button = _("Search Text")
-
-        # Format
-        type = (type == "titlesearch")
-        html = [
-            u'<form method="get" action="">',
-            u'<div>',
-            u'<input type="hidden" name="action" value="fullsearch">',
-            u'<input type="hidden" name="titlesearch" value="%i">' % type,
-            u'<input type="text" name="value" size="30" value="%s">' % default,
-            u'<input type="submit" value="%s">' % button,
-            boxes,
-            u'</div>',
-            u'</form>',
-            ]
-        html = u'\n'.join(html)
-        return self.formatter.rawHTML(html)
+        from FullSearch import search_box
+        return search_box("titlesearch", self)
 
     def _macro_GoTo(self, args):
         """ Make a goto box
@@ -330,7 +283,7 @@
         results = search.searchPages(self.request, needle,
                 titlesearch=1, case=case)
         results.sortByPagename()
-        return results.pageList(self.request, self.formatter)
+        return results.pageList(self.request, self.formatter, paging=False)
 
     def _macro_InterWiki(self, args):
         from StringIO import StringIO
@@ -342,7 +295,7 @@
         for tag, url in list:
             buf.write('<dt><tt><a href="%s">%s</a></tt></dt>' % (
                 wikiutil.join_wiki(url, 'RecentChanges'), tag))
-            if url.find('$PAGE') == -1:
+            if '$PAGE' not in url:
                 buf.write('<dd><tt><a href="%s">%s</a></tt></dd>' % (url, url))
             else:
                 buf.write('<dd><tt>%s</tt></dd>' % url)
@@ -462,7 +415,7 @@
         from MoinMoin.mail.sendmail import decodeSpamSafeEmail
 
         args = args or ''
-        if args.find(',') == -1:
+        if ',' not in args:
             email = args
             text = ''
         else:
--- a/MoinMoin/parser/text_moin_wiki.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/parser/text_moin_wiki.py	Mon Aug 14 22:29:44 2006 +0200
@@ -758,7 +758,7 @@
     
     def _parser_repl(self, word):
         """Handle parsed code displays."""
-        if word[:3] == '{{{':
+        if word.startswith('{{{'):
             word = word[3:]
 
         self.parser = None
@@ -770,7 +770,7 @@
             word = ''
             self.in_pre = 3
             return self._closeP() + self.formatter.preformatted(1)
-        elif s_word[:2] == '#!':
+        elif s_word.startswith('#!'):
             # First try to find a parser for this (will go away in 2.0)
             parser_name = s_word[2:].split()[0]
             self.setParser(parser_name)
@@ -972,7 +972,7 @@
                 if self.in_pre == 1:
                     self.parser = None
                     parser_name = ''
-                    if (line.strip()[:2] == "#!"):
+                    if line.strip().startswith("#!"):
                         parser_name = line.strip()[2:].split()[0]
                         self.setParser(parser_name)
 
@@ -1054,7 +1054,7 @@
                 # Table mode
                 # TODO: move into function?                
                 if (not self.in_table and line[indlen:indlen + 2] == "||"
-                    and line[-3:] == "|| " and len(line) >= 5 + indlen):
+                    and line.endswith("|| ") and len(line) >= 5 + indlen):
                     # Start table
                     if self.list_types and not self.in_li:
                         self.request.write(self.formatter.listitem(1, style="list-style-type:none"))
@@ -1071,9 +1071,9 @@
                     self.in_table = True # self.lineno
                 elif (self.in_table and not
                       # intra-table comments should not break a table
-                      (line[:2] == "##" or  
+                      (line.startswith("##") or
                        line[indlen:indlen + 2] == "||" and
-                       line[-3:] == "|| " and
+                       line.endswith("|| ") and
                        len(line) >= 5 + indlen)):
                     
                     # Close table
--- a/MoinMoin/parser/text_rst.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/parser/text_rst.py	Mon Aug 14 22:29:44 2006 +0200
@@ -15,7 +15,6 @@
 import __builtin__
 import sys
 
-import types
 import os
 
 # docutils imports are below
@@ -227,7 +226,7 @@
     def append(self, text):
         f = sys._getframe()
         if f.f_back.f_code.co_filename.endswith('html4css1.py'):
-            if isinstance(text, types.StringType) or isinstance(text, types.UnicodeType):
+            if isinstance(text, (str, unicode)):
                 text = self.formatter.rawHTML(text)
         list.append(self, text)
 
@@ -256,7 +255,7 @@
         # Make all internal lists RawHTMLLists, see RawHTMLList class
         # comment for more information.
         for i in self.__dict__:
-            if isinstance(getattr(self, i), types.ListType):
+            if isinstance(getattr(self, i), list):
                 setattr(self, i, RawHTMLList(formatter))
 
     def depart_docinfo(self, node):
@@ -383,7 +382,7 @@
                 # Default case - make a link to a wiki page.
                 pagename = refuri
                 anchor = ''
-                if refuri.find('#') != -1:
+                if '#' in refuri:
                     pagename, anchor = refuri.split('#', 1)
                     anchor = '#' + anchor
                 page = Page(self.request, pagename)
--- a/MoinMoin/request/__init__.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/request/__init__.py	Mon Aug 14 22:29:44 2006 +0200
@@ -220,13 +220,16 @@
 
     def surge_protect(self):
         """ check if someone requesting too much from us """
+        limits = self.cfg.surge_action_limits
+        if not limits:
+            return False
+                                    
         validuser = self.user.valid
         current_id = validuser and self.user.name or self.remote_addr
         if not validuser and current_id.startswith('127.'): # localnet
             return False
         current_action = self.action
 
-        limits = self.cfg.surge_action_limits
         default_limit = self.cfg.surge_action_limits.get('default', (30, 60))
 
         now = int(time.time())
@@ -327,7 +330,7 @@
             accept_charset = accept_charset.lower()
             # Add iso-8859-1 if needed
             if (not '*' in accept_charset and
-                accept_charset.find('iso-8859-1') < 0):
+                'iso-8859-1' not in accept_charset):
                 accept_charset += ',iso-8859-1'
 
             # Make a list, sorted by quality value, using Schwartzian Transform
@@ -433,7 +436,7 @@
         """
         # Fix the script_name when using Apache on Windows.
         server_software = env.get('SERVER_SOFTWARE', '')
-        if os.name == 'nt' and server_software.find('Apache/') != -1:
+        if os.name == 'nt' and 'Apache/' in server_software:
             # Removes elements ending in '.' from the path.
             self.script_name = '/'.join([x for x in self.script_name.split('/')
                                          if not x.endswith('.')])
--- a/MoinMoin/script/account/check.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/script/account/check.py	Mon Aug 14 22:29:44 2006 +0200
@@ -189,12 +189,11 @@
                 self.process(uids)
 
     def make_WikiNames(self):
-        import string
         for uid, u in self.users.items():
             if u.disabled:
                 continue
             if not wikiutil.isStrictWikiname(u.name):
-                newname = string.capwords(u.name).replace(" ", "").replace("-", "")
+                newname = u.name.capwords().replace(" ", "").replace("-", "")
                 if not wikiutil.isStrictWikiname(newname):
                     print " %-20s %-30r - no WikiName, giving up" % (uid, u.name)
                 else:
--- a/MoinMoin/search/Xapian.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/search/Xapian.py	Mon Aug 14 22:29:44 2006 +0200
@@ -170,7 +170,10 @@
                        #  the D term, and changing the last digit to a '2' if it's a '3')
                        #X   longer prefix for user-defined use
         'linkto': 'XLINKTO', # this document links to that document
-        'stem_lang': 'XSTEMLANG', # ISO Language code this document was stemmed in 
+        'stem_lang': 'XSTEMLANG', # ISO Language code this document was stemmed in
+        'category': 'XCAT', # category this document belongs to
+        'full_title': 'XFT', # full title (for regex)
+        'domain': 'XDOMAIN', # standard or underlay
                        #Y   year (four digits)
     }
 
@@ -192,7 +195,7 @@
         """ Check if the Xapian index exists """
         return BaseIndex.exists(self) and os.listdir(self.dir)
 
-    def _search(self, query):
+    def _search(self, query, sort=None):
         """ read lock must be acquired """
         while True:
             try:
@@ -207,12 +210,22 @@
                 timestamp = self.mtime()
                 break
         
-        hits = searcher.search(query, valuesWanted=['pagename', 'attachment', 'mtime', 'wikiname'])
+        kw = {}
+        if sort == 'weight':
+            # XXX: we need real weight here, like _moinSearch
+            # (TradWeight in xapian)
+            kw['sortByRelevence'] = True
+        if sort == 'page_name':
+            kw['sortKey'] = 'pagename'
+
+        hits = searcher.search(query, valuesWanted=['pagename',
+            'attachment', 'mtime', 'wikiname'], **kw)
         self.request.cfg.xapian_searchers.append((searcher, timestamp))
         return hits
     
     def _do_queued_updates(self, request, amount=5):
         """ Assumes that the write lock is acquired """
+        self.touch()
         writer = xapidx.Index(self.dir, True)
         writer.configure(self.prefixMap, self.indexValueMap)
         pages = self.queue.pages()[:amount]
@@ -249,7 +262,7 @@
             mtime = wikiutil.timestamp2version(mtime)
             if mode == 'update':
                 query = xapidx.RawQuery(xapdoc.makePairForWrite('itemid', itemid))
-                docs = writer.search(query, valuesWanted=['pagename', 'attachment', 'mtime', 'wikiname', ])
+                enq, mset, docs = writer.search(query, valuesWanted=['pagename', 'attachment', 'mtime', 'wikiname', ])
                 if docs:
                     doc = docs[0] # there should be only one
                     uid = doc['uid']
@@ -316,6 +329,28 @@
         # return actual lang and lang to stem in
         return (lang, default_lang)
 
+    def _get_categories(self, page):
+        body = page.get_raw_body()
+
+        prev, next = (0, 1)
+        pos = 0
+        while next:
+            if next != 1:
+                pos += next.end()
+            prev, next = next, re.search(r'----*\r?\n', body[pos:])
+
+        if not prev or prev == 1:
+            return []
+
+        return [cat.lower()
+                for cat in re.findall(r'Category([^\s]+)', body[pos:])]
+
+    def _get_domains(self, page):
+        if page.isUnderlayPage():
+            yield 'underlay'
+        if page.isStandardPage():
+            yield 'standard'
+
     def _index_page(self, writer, page, mode='update'):
         """ Index a page - assumes that the write lock is acquired
             @arg writer: the index writer object
@@ -331,6 +366,8 @@
         itemid = "%s:%s" % (wikiname, pagename)
         # XXX: Hack until we get proper metadata
         language, stem_language = self._get_languages(page)
+        categories = self._get_categories(page)
+        domains = tuple(self._get_domains(page))
         updated = False
 
         if mode == 'update':
@@ -338,7 +375,7 @@
             # you can just call database.replace_document(uid_term, doc)
             # -> done in xapwrap.index.Index.index()
             query = xapidx.RawQuery(xapdoc.makePairForWrite('itemid', itemid))
-            docs = writer.search(query, valuesWanted=['pagename', 'attachment', 'mtime', 'wikiname', ])
+            enq, mset, docs = writer.search(query, valuesWanted=['pagename', 'attachment', 'mtime', 'wikiname', ])
             if docs:
                 doc = docs[0] # there should be only one
                 uid = doc['uid']
@@ -359,9 +396,14 @@
             xtitle = xapdoc.TextField('title', pagename, True) # prefixed
             xkeywords = [xapdoc.Keyword('itemid', itemid),
                     xapdoc.Keyword('lang', language),
-                    xapdoc.Keyword('stem_lang', stem_language)]
+                    xapdoc.Keyword('stem_lang', stem_language),
+                    xapdoc.Keyword('full_title', pagename.lower())]
             for pagelink in page.getPageLinks(request):
                 xkeywords.append(xapdoc.Keyword('linkto', pagelink))
+            for category in categories:
+                xkeywords.append(xapdoc.Keyword('category', category))
+            for domain in domains:
+                xkeywords.append(xapdoc.Keyword('domain', domain))
             xcontent = xapdoc.TextField('content', page.get_raw_body())
             doc = xapdoc.Document(textFields=(xcontent, xtitle),
                                   keywords=xkeywords,
@@ -387,7 +429,7 @@
             mtime = wikiutil.timestamp2version(os.path.getmtime(filename))
             if mode == 'update':
                 query = xapidx.RawQuery(xapdoc.makePairForWrite('itemid', att_itemid))
-                docs = writer.search(query, valuesWanted=['pagename', 'attachment', 'mtime', ])
+                enq, mset, docs = writer.search(query, valuesWanted=['pagename', 'attachment', 'mtime', ])
                 if debug: request.log("##%r %r" % (filename, docs))
                 if docs:
                     doc = docs[0] # there should be only one
@@ -446,6 +488,7 @@
             mode = 'add'
 
         try:
+            self.touch()
             writer = xapidx.Index(self.dir, True)
             writer.configure(self.prefixMap, self.indexValueMap)
             pages = request.rootpage.getPageList(user='', exists=1)
--- a/MoinMoin/search/__init__.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/search/__init__.py	Mon Aug 14 22:29:44 2006 +0200
@@ -13,7 +13,7 @@
 from MoinMoin.search.queryparser import QueryParser
 from MoinMoin.search.builtin import Search
 
-def searchPages(request, query, **kw):
+def searchPages(request, query, sort='weight', **kw):
     """ Search the text of all pages for query.
     
     @param request: current request
@@ -23,5 +23,5 @@
     """
     if isinstance(query, str) or isinstance(query, unicode):
         query = QueryParser(**kw).parse_query(query)
-    return Search(request, query).run()
+    return Search(request, query, sort).run()
 
--- a/MoinMoin/search/builtin.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/search/builtin.py	Mon Aug 14 22:29:44 2006 +0200
@@ -10,12 +10,12 @@
     @license: GNU GPL, see COPYING for details
 """
 
-import time, sys, os, errno
+import time, sys, os, errno, codecs
 from MoinMoin import wikiutil, config
 from MoinMoin.Page import Page
 from MoinMoin.util import filesys, lock
 from MoinMoin.search.results import getSearchResults
-from MoinMoin.search.queryparser import TextMatch, TitleMatch
+from MoinMoin.search.queryparser import Match, TextMatch, TitleMatch
 
 ##############################################################################
 # Search Engine Abstraction
@@ -159,7 +159,7 @@
         ##    self.indexPagesInNewThread(request)
 
     def _main_dir(self):
-        raise NotImplemented
+        raise NotImplemented('...')
 
     def exists(self):
         """ Check if index exists """        
@@ -167,15 +167,18 @@
                 
     def mtime(self):
         return os.path.getmtime(self.dir)
+
+    def touch(self):
+        os.utime(self.dir, None)
     
     def _search(self, query):
-        raise NotImplemented
+        raise NotImplemented('...')
 
-    def search(self, query):
+    def search(self, query, *args, **kw):
         #if not self.read_lock.acquire(1.0):
         #    raise self.LockedException
         #try:
-        hits = self._search(query)
+        hits = self._search(query, *args, **kw)
         #finally:
         #    self.read_lock.release()
         return hits
@@ -240,7 +243,7 @@
         When called in a new thread, lock is acquired before the call,
         and this method must release it when it finishes or fails.
         """
-        raise NotImplemented
+        raise NotImplemented('...')
 
     def _do_queued_updates_InNewThread(self):
         """ do queued index updates in a new thread
@@ -251,7 +254,7 @@
             self.request.log("can't index: can't acquire lock")
             return
         try:
-            def lockedDecorator(self, f):
+            def lockedDecorator(f):
                 def func(*args, **kwargs):
                     try:
                         return f(*args, **kwargs)
@@ -280,10 +283,10 @@
             raise
 
     def _do_queued_updates(self, request, amount=5):
-        raise NotImplemented
+        raise NotImplemented('...')
 
     def optimize(self):
-        raise NotImplemented
+        raise NotImplemented('...')
 
     def contentfilter(self, filename):
         """ Get a filter for content of filename and return unicode content. """
@@ -308,7 +311,7 @@
         return mt.mime_type(), data
 
     def test(self, request):
-        raise NotImplemented
+        raise NotImplemented('...')
 
     def _indexingRequest(self, request):
         """ Return a new request that can be used for index building.
@@ -349,9 +352,10 @@
 class Search:
     """ A search run """
     
-    def __init__(self, request, query):
+    def __init__(self, request, query, sort='weight'):
         self.request = request
         self.query = query
+        self.sort = sort
         self.filtered = False
         self.fs_rootpage = "FS" # XXX FS hardcoded
 
@@ -367,7 +371,20 @@
         if not self.filtered:
             hits = self._filter(hits)
 
-        return getSearchResults(self.request, self.query, hits, start)
+        # when xapian was used, we won't need to sort manually
+        if self.request.cfg.xapian_search:
+            self.sort = None
+            mset = self._xapianMset
+            estimated_hits = (
+                (mset.get_matches_estimated() == mset.get_matches_upper_bound() and
+                    mset.get_matches_estimated() == mset.get_matches_lower_bound()) and
+                '' or 'about',
+                mset.get_matches_estimated())
+        else:
+            estimated_hits = None
+
+        return getSearchResults(self.request, self.query, hits, start,
+                self.sort, estimated_hits)
         
 
     # ----------------------------------------------------------------
@@ -391,36 +408,52 @@
         Get a list of pages using fast xapian search and
         return moin search in those pages.
         """
+        clock = self.request.clock
         pages = None
         index = self._xapianIndex(self.request)
-        if index: #and self.query.xapian_wanted():
-            self.request.clock.start('_xapianSearch')
+        if index and self.query.xapian_wanted():
+            clock.start('_xapianSearch')
             try:
                 from MoinMoin.support import xapwrap
+                clock.start('_xapianQuery')
                 query = self.query.xapian_term(self.request, index.allterms)
                 self.request.log("xapianSearch: query = %r" %
                         query.get_description())
                 query = xapwrap.index.QObjQuery(query)
-                enq, hits = index.search(query)
-                self.request.log("xapianSearch: finds: %r" % hits)
+                enq, mset, hits = index.search(query, sort=self.sort)
+                clock.stop('_xapianQuery')
+                #self.request.log("xapianSearch: finds: %r" % hits)
                 def dict_decode(d):
                     """ decode dict values to unicode """
                     for k, v in d.items():
                         d[k] = d[k].decode(config.charset)
                     return d
-                pages = [{'uid': hit['uid'], 'values': dict_decode(hit['values'])}
-                        for hit in hits]
+                #pages = [{'uid': hit['uid'], 'values': dict_decode(hit['values'])}
+                #        for hit in hits]
+                pages = [dict_decode(hit['values']) for hit in hits]
                 self.request.log("xapianSearch: finds pages: %r" % pages)
                 self._xapianEnquire = enq
+                self._xapianMset = mset
                 self._xapianIndex = index
             except BaseIndex.LockedException:
                 pass
             #except AttributeError:
             #    pages = []
-            self.request.clock.stop('_xapianSearch')
-            return self._getHits(hits, self._xapianMatch)
+
+            try:
+                if not self.query.xapian_need_postproc():
+                    clock.start('_xapianProcess')
+                    try:
+                        return self._getHits(hits, self._xapianMatch)
+                    finally:
+                        clock.stop('_xapianProcess')
+            finally:
+                clock.stop('_xapianSearch')
         else:
-            return self._moinSearch(pages)
+            # we didn't use xapian in this request
+            self.request.cfg.xapian_search = 0
+        
+        return self._moinSearch(pages)
 
     def _xapianMatchDecider(self, term, pos):
         if term[0] == 'S':      # TitleMatch
@@ -439,9 +472,14 @@
                         len(positions[pos]) < len(term_name):
                     positions[pos] = term_name
             term.next()
-        return [self._xapianMatchDecider(term, pos) for pos, term
+        matches = [self._xapianMatchDecider(term, pos) for pos, term
             in positions.iteritems()]
 
+        if not matches:
+            return [Match()]    # dummy for metadata, we got a match!
+
+        return matches
+
     def _moinSearch(self, pages=None):
         """ Search pages using moin's built-in full text search 
         
--- a/MoinMoin/search/queryparser.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/search/queryparser.py	Mon Aug 14 22:29:44 2006 +0200
@@ -10,7 +10,7 @@
     @license: GNU GPL, see COPYING for details
 """
 
-import re, string
+import re
 from MoinMoin import config
 from MoinMoin.search.results import Match, TitleMatch, TextMatch
 
@@ -177,6 +177,12 @@
             wanted = wanted and term.xapian_wanted()
         return wanted
 
+    def xapian_need_postproc(self):
+        for term in self._subterms:
+            if term.xapian_need_postproc():
+                return True
+        return False
+
     def xapian_term(self, request, allterms):
         # sort negated terms
         terms = []
@@ -266,9 +272,10 @@
         matches = []
 
         # Search in page name
-        results = self.titlesearch.search(page)
-        if results:
-            matches.extend(results)
+        if self.titlesearch:
+            results = self.titlesearch.search(page)
+            if results:
+                matches.extend(results)
 
         # Search in page body
         body = page.get_raw_body()
@@ -301,15 +308,19 @@
             return []
 
     def xapian_wanted(self):
+        # XXX: Add option for term-based matching
         return not self.use_re
 
+    def xapian_need_postproc(self):
+        return self.case
+
     def xapian_term(self, request, allterms):
         if self.use_re:
             # basic regex matching per term
             terms = [term for term in allterms() if
                     self.search_re.match(term)]
             if not terms:
-                return None
+                return Query()
             queries = [Query(Query.OP_OR, terms)]
         else:
             analyzer = Xapian.WikiAnalyzer(request=request,
@@ -325,16 +336,18 @@
                     tmp = []
                     for w, s, pos in analyzer.tokenize(t, flat_stemming=False):
                         tmp.append(UnicodeQuery(Query.OP_OR, (w, s)))
-                        stemmed.append(w)
+                        stemmed.append(s)
                     t = tmp
                 else:
                     # just not stemmed
                     t = [UnicodeQuery(w) for w, pos in analyzer.tokenize(t)]
                 queries.append(Query(Query.OP_AND, t))
 
-            if stemmed:
-                self._build_re(' '.join(stemmed), use_re=False,
-                        case=self.case, stemmed=True)
+            if not self.case and stemmed:
+                new_pat = ' '.join(stemmed)
+                self._pattern = new_pat
+                self._build_re(new_pat, use_re=False, case=self.case,
+                        stemmed=True)
 
         # titlesearch OR parsed wikiwords
         return Query(Query.OP_OR,
@@ -383,7 +396,8 @@
         for match in self.search_re.finditer(page.page_name):
             if page.request.cfg.xapian_stemming:
                 # somewhere in regular word
-                if page.page_name[match.start()] not in config.chars_upper and \
+                if not self.case and \
+                        page.page_name[match.start()] not in config.chars_upper and \
                         page.page_name[match.start()-1] in config.chars_lower:
                     continue
 
@@ -408,15 +422,25 @@
             return []
 
     def xapian_wanted(self):
-        return not self.use_re
+        return True             # only easy regexps possible
+
+    def xapian_need_postproc(self):
+        return self.case
 
     def xapian_term(self, request, allterms):
         if self.use_re:
             # basic regex matching per term
-            terms = [term for term in allterms() if
-                    self.search_re.match(term)]
+            terms = []
+            found = False
+            for term in allterms():
+                if term[:4] == 'XFT:':
+                    found = True
+                    if self.search_re.findall(term[4:]):
+                        terms.append(Query(term, 100))
+                elif found:
+                    break
             if not terms:
-                return None
+                return Query()
             queries = [Query(Query.OP_OR, terms)]
         else:
             analyzer = Xapian.WikiAnalyzer(request=request,
@@ -432,21 +456,27 @@
                     # stemmed OR not stemmed
                     tmp = []
                     for w, s, pos in analyzer.tokenize(t, flat_stemming=False):
-                        tmp.append(UnicodeQuery(Query.OP_OR,
-                            ['%s%s' % (Xapian.Index.prefixMap['title'], j)
+                        tmp.append(Query(Query.OP_OR,
+                            [UnicodeQuery('%s%s' %
+                                    (Xapian.Index.prefixMap['title'], j),
+                                    100)
                                 for j in (w, s)]))
-                        stemmed.append(w)
+                        stemmed.append(s)
                     t = tmp
                 else:
                     # just not stemmed
-                    t = [UnicodeQuery('%s%s' % (Xapian.Index.prefixMap['title'], w))
-                        for w, pos in analyzer.tokenize(t)]
+                    t = [UnicodeQuery(
+                                '%s%s' % (Xapian.Index.prefixMap['title'], w),
+                                100)
+                            for w, pos in analyzer.tokenize(t)]
 
                 queries.append(Query(Query.OP_AND, t))
 
-            if stemmed:
-                self._build_re(' '.join(stemmed), use_re=False,
-                        case=self.case, stemmed=True)
+            if not self.case and stemmed:
+                new_pat = ' '.join(stemmed)
+                self._pattern = new_pat
+                self._build_re(new_pat, use_re=False, case=self.case,
+                        stemmed=True)
 
         return Query(Query.OP_AND, queries)
 
@@ -522,7 +552,10 @@
             return []
 
     def xapian_wanted(self):
-        return not self.use_re
+        return True             # only easy regexps possible
+
+    def xapian_need_postproc(self):
+        return self.case
 
     def xapian_term(self, request, allterms):
         prefix = Xapian.Index.prefixMap['linkto']
@@ -540,7 +573,7 @@
                     continue
 
             if not terms:
-                return None
+                return Query()
             return Query(Query.OP_OR, terms)
         else:
             return UnicodeQuery('%s:%s' % (prefix, self.pattern))
@@ -560,7 +593,7 @@
         self._pattern = pattern.lower()
         self.negated = 0
         self.use_re = use_re
-        self.case = case
+        self.case = False       # not case-sensitive!
         self.xapian_called = False
         self._build_re(self._pattern, use_re=use_re, case=case)
 
@@ -582,7 +615,10 @@
             return [Match()]
 
     def xapian_wanted(self):
-        return not self.use_re
+        return True             # only easy regexps possible
+
+    def xapian_need_postproc(self):
+        return False            # case-sensitivity would make no sense
 
     def xapian_term(self, request, allterms):
         self.xapian_called = True
@@ -601,13 +637,65 @@
                     continue
 
             if not terms:
-                return None
+                return Query()
             return Query(Query.OP_OR, terms)
         else:
             pattern = self.pattern
             return UnicodeQuery('%s%s' % (prefix, pattern))
 
 
+class CategorySearch(TextSearch):
+    """ Search the pages belonging to a category """
+
+    def __init__(self, *args, **kwargs):
+        TextSearch.__init__(self, *args, **kwargs)
+        self.titlesearch = None
+
+    def _build_re(self, pattern, **kwargs):
+        kwargs['use_re'] = True
+        TextSearch._build_re(self,
+                r'(----(-*)(\r)?\n)(.*)Category%s\b' % pattern, **kwargs)
+
+    def costs(self):
+        return 5000 # cheaper than a TextSearch
+
+    def __unicode__(self):
+        neg = self.negated and '-' or ''
+        return u'%s!"%s"' % (neg, unicode(self._pattern))
+
+    def highlight_re(self):
+        return u'(Category%s)' % self._pattern
+
+    def xapian_wanted(self):
+        return True             # only easy regexps possible
+
+    def xapian_need_postproc(self):
+        return self.case
+
+    def xapian_term(self, request, allterms):
+        self.xapian_called = True
+        prefix = Xapian.Index.prefixMap['category']
+        if self.use_re:
+            # basic regex matching per term
+            terms = []
+            found = None
+            n = len(prefix)
+            for term in allterms():
+                if prefix == term[:n]:
+                    found = True
+                    if self.search_re.match(term[n+1:]):
+                        terms.append(term)
+                elif found:
+                    continue
+
+            if not terms:
+                return Query()
+            return Query(Query.OP_OR, terms)
+        else:
+            pattern = self._pattern.lower()
+            return UnicodeQuery('%s:%s' % (prefix, pattern))
+
+
 ##############################################################################
 ### Parse Query
 ##############################################################################
@@ -693,6 +781,7 @@
         case = self.case
         linkto = False
         lang = False
+        category = False
 
         for m in modifiers:
             if "title".startswith(m):
@@ -705,8 +794,21 @@
                 linkto = True
             elif "language".startswith(m):
                 lang = True
+            elif "category".startswith(m):
+                category = True
 
-        if lang:
+        # oh, let's better call xapian if we encouter this nasty regexp ;)
+        if not category:
+            cat_re = re.compile(r'----\(-\*\)\(\\r\)\?\\n\)\(\.\*\)Category(.*)\\b', re.U)
+            cat_match = cat_re.search(text)
+            if cat_match:
+                text = cat_match.groups()[0]
+                category = True
+                regex = False
+
+        if category:
+            obj = CategorySearch(text, use_re=regex, case=case)
+        elif lang:
             obj = LanguageSearch(text, use_re=regex, case=False)
         elif linkto:
             obj = LinkSearch(text, use_re=regex, case=case)
--- a/MoinMoin/search/results.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/search/results.py	Mon Aug 14 22:29:44 2006 +0200
@@ -10,7 +10,7 @@
     @license: GNU GPL, see COPYING for details
 """
 
-import StringIO, time
+import StringIO, time, re
 from MoinMoin import config, wikiutil
 from MoinMoin.Page import Page
 
@@ -244,53 +244,76 @@
     """
     # Public functions --------------------------------------------------
     
-    def __init__(self, query, hits, pages, elapsed):
+    def __init__(self, query, hits, pages, elapsed, sort, estimated_hits):
         self.query = query # the query
         self.hits = hits # hits list
-        self.sort = None # hits are unsorted initially
         self.pages = pages # number of pages in the wiki
         self.elapsed = elapsed # search time
+        self.estimated_hits = estimated_hits # about how much hits?
 
-    def sortByWeight(self):
+        if sort == 'weight':
+            self._sortByWeight()
+        elif sort == 'page_name':
+            self._sortByPagename()
+        self.sort = sort
+
+    def _sortByWeight(self):
         """ Sorts found pages by the weight of the matches """
         tmp = [(hit.weight(), hit.page_name, hit) for hit in self.hits]
         tmp.sort()
         tmp.reverse()
         self.hits = [item[2] for item in tmp]
-        self.sort = 'weight'
         
-    def sortByPagename(self):
+    def _sortByPagename(self):
         """ Sorts a list of found pages alphabetical by page name """
         tmp = [(hit.page_name, hit) for hit in self.hits]
         tmp.sort()
         self.hits = [item[1] for item in tmp]
-        self.sort = 'page_name'
         
-    def stats(self, request, formatter):
+    def stats(self, request, formatter, hitsFrom):
         """ Return search statistics, formatted with formatter
 
         @param request: current request
         @param formatter: formatter to use
+        @param hitsFrom: current position in the hits
         @rtype: unicode
         @return formatted statistics
         """
         _ = request.getText
+
+        if not self.estimated_hits:
+            self.estimated_hits = ('', len(self.hits))
+
         output = [
-            formatter.paragraph(1),
-            formatter.text(_("%(hits)d results out of about %(pages)d pages.") %
-                   {'hits': len(self.hits), 'pages': self.pages}),
-            u' (%s)' % formatter.text(_("%.2f seconds") % self.elapsed),
+            formatter.paragraph(1, attr={'class': 'searchstats'}),
+            _("Results %(bs)s%(hitsFrom)d - %(hitsTo)d%(be)s "
+                    "of %(aboutHits)s %(bs)s%(hits)d%(be)s results out of"
+                    "about %(pages)d pages.") %
+                {'aboutHits': self.estimated_hits[0],
+                    'hits': self.estimated_hits[1], 'pages': self.pages,
+                    'hitsFrom': hitsFrom + 1,
+                    'hitsTo': hitsFrom +
+                            min(self.estimated_hits[1] - hitsFrom,
+                                request.cfg.search_results_per_page),
+                    'bs': formatter.strong(1), 'be': formatter.strong(0)},
+            u' (%s %s)' % (''.join([formatter.strong(1),
+                formatter.text("%.2f" % self.elapsed),
+                formatter.strong(0)]),
+                formatter.text(_("seconds"))),
             formatter.paragraph(0),
             ]
         return ''.join(output)
 
-    def pageList(self, request, formatter, info=0, numbered=1):
+    def pageList(self, request, formatter, info=0, numbered=1,
+            paging=True, hitsFrom=0):
         """ Format a list of found pages
 
         @param request: current request
         @param formatter: formatter to use
         @param info: show match info in title
         @param numbered: use numbered list for display
+        @param paging: toggle paging
+        @param hitsFrom: current position in the hits
         @rtype: unicode
         @return formatted page list
         """
@@ -298,15 +321,25 @@
         f = formatter
         write = self.buffer.write
         if numbered:
-            list = f.number_list
+            list = lambda on: f.number_list(on, start=hitsFrom+1)
         else:
             list = f.bullet_list
 
+        if paging and len(self.hits) <= request.cfg.search_results_per_page:
+            paging = False
+
         # Add pages formatted as list
         if self.hits:
             write(list(1))
+            
+            # XXX: Do some xapian magic here
+            if paging:
+                hitsTo = hitsFrom + request.cfg.search_results_per_page
+                displayHits = self.hits[hitsFrom:hitsTo]
+            else:
+                displayHits = self.hits
 
-            for page in self.hits:
+            for page in displayHits:
                 if page.attachment:
                     querydict = {
                         'action': 'AttachFile',
@@ -326,15 +359,20 @@
                     self.formatTitle(page),
                     f.pagelink(0, page.page_name),
                     matchInfo,
+                    self.formatHitInfoBar(page),
                     f.listitem(0),
                     ]
                 write(''.join(item))
             write(list(0))
+            if paging:
+                write(self.formatPrevNextPageLinks(hitsFrom=hitsFrom,
+                    hitsPerPage=request.cfg.search_results_per_page,
+                    hitsNum=len(self.hits)))
 
         return self.getvalue()
 
     def pageListWithContext(self, request, formatter, info=1, context=180,
-                            maxlines=1):
+                            maxlines=1, paging=True, hitsFrom=0):
         """ Format a list of found pages with context
 
         The default parameter values will create Google-like search
@@ -345,20 +383,33 @@
         @param request: current request
         @param formatter: formatter to use
         @param info: show match info near the page link
-        @param context: how many characters to show around each match. 
-        @param maxlines: how many contexts lines to show. 
+        @param context: how many characters to show around each match.
+        @param maxlines: how many contexts lines to show.
+        @param paging: toggle paging
+        @param hitsFrom: current position in the hits
         @rtype: unicode
         @return formatted page list with context
         """
         self._reset(request, formatter)
         f = formatter
         write = self.buffer.write
+        _ = request.getText
+
+        if paging and len(self.hits) <= request.cfg.search_results_per_page:
+            paging = False
         
         # Add pages formatted as definition list
         if self.hits:
             write(f.definition_list(1))
 
-            for page in self.hits:
+            # XXX: Do some xapian magic here
+            if paging:
+                hitsTo = hitsFrom+request.cfg.search_results_per_page
+                displayHits = self.hits[hitsFrom:hitsTo]
+            else:
+                displayHits = self.hits
+
+            for page in displayHits:
                 matchInfo = ''
                 if info:
                     matchInfo = self.formatInfo(f, page)
@@ -386,9 +437,14 @@
                     f.definition_desc(1),
                     fmt_context,
                     f.definition_desc(0),
+                    self.formatHitInfoBar(page),
                     ]
                 write(''.join(item))
             write(f.definition_list(0))
+            if paging:
+                write(self.formatPrevNextPageLinks(hitsFrom=hitsFrom,
+                    hitsPerPage=request.cfg.search_results_per_page,
+                    hitsNum=len(self.hits)))
         
         return self.getvalue()
 
@@ -596,6 +652,127 @@
             return ''.join(output)
         return ''
 
+    def _img_url(self, img):
+        cfg = self.request.cfg
+        return '%s/%s/img/%s.png' % (cfg.url_prefix, self.request.theme.name, img)
+
+    def formatPrevNextPageLinks(self, hitsFrom, hitsPerPage, hitsNum):
+        """ Format previous and next page links in page
+
+        @param hitsFrom: current position in the hits
+        @param hitsPerPage: number of hits per page
+        @param hitsNum: number of hits
+        @rtype: unicode
+        @return: links to previous and next pages (if exist)
+        """
+        _ = self.request.getText
+        f = self.formatter
+
+        querydict = wikiutil.parseQueryString(self.request.query_string)
+        def page_url(n):
+            querydict.update({'from': n * hitsPerPage})
+            return self.request.page.url(self.request, querydict, escape=0)
+        
+        pages = float(hitsNum) / hitsPerPage
+        if pages - int(pages) > 0.0:
+            pages = int(pages) + 1
+        cur_page = hitsFrom / hitsPerPage
+        l = []
+
+        # previous page available
+        if cur_page > 0:
+            l.append(''.join([
+                f.url(1, href=page_url(cur_page-1)),
+                f.text(_('Previous')),
+                f.url(0)
+            ]))
+        else:
+            l.append('')
+
+        # list of pages to be shown
+        page_range = range(*(
+            cur_page - 4 < 0 and
+                (0, pages >= 10 and 10 or pages) or
+                (cur_page - 4, cur_page + 6 >= pages and
+                    pages or cur_page + 6)))
+        l.extend([''.join([
+                i != cur_page and f.url(1, href=page_url(i)) or '',
+                f.text(str(i+1)),
+                i != cur_page and f.url(0) or '',
+            ]) for i in page_range])
+
+        # next page available
+        if cur_page < pages-1:
+            l.append(''.join([
+                f.url(1, href=page_url(cur_page+1)),
+                f.text(_('Next')),
+                f.url(0)
+            ]))
+        else:
+            l.append('')
+
+        return ''.join([
+            f.table(1, attrs={'tableclass': 'searchpages'}),
+            f.table_row(1),
+                f.table_cell(1, attrs={'class': 'prev'}),
+                # first image, previous page
+                l[0] and
+                    ''.join([
+                        f.url(1, href=page_url(cur_page-1)),
+                        f.image(self._img_url('nav_prev')),
+                        f.url(0),
+                    ]) or
+                    f.image(self._img_url('nav_first')),
+                f.table_cell(0),
+                # images for ooos, highlighted current page
+                ''.join([
+                    ''.join([
+                        f.table_cell(1),
+                        i != cur_page and f.url(1, href=page_url(i)) or '',
+                        f.image(self._img_url(i == cur_page and
+                            'nav_current' or 'nav_page')),
+                        i != cur_page and f.url(0) or '',
+                        f.table_cell(0),
+                    ]) for i in page_range
+                ]),
+                f.table_cell(1, attrs={'class': 'next'}),
+                # last image, next page
+                l[-1] and
+                    ''.join([
+                        f.url(1, href=page_url(cur_page+1)),
+                        f.image(self._img_url('nav_next')),
+                        f.url(0),
+                    ]) or
+                    f.image(self._img_url('nav_last')),
+                f.table_cell(0),
+            f.table_row(0),
+            f.table_row(1),
+                f.table_cell(1),
+                # textlinks
+                (f.table_cell(0) + f.table_cell(1)).join(l),
+                f.table_cell(0),
+            f.table_row(0),
+            f.table(0),
+        ])
+
+    def formatHitInfoBar(self, page):
+        f = self.formatter
+        _ = self.request.getText
+        return ''.join([
+            f.paragraph(1, attr={'class': 'searchhitinfobar'}),
+            f.text('%.1fk - ' % (page.page.size()/1024.0)),
+            f.text('rev: %d %s- ' % (page.page.get_real_rev(),
+                not page.page.rev and '(%s) ' % _('current') or '')),
+            f.text('last modified: %(time)s' % page.page.lastEditInfo()),
+            # XXX: proper metadata
+            #f.text('lang: %s - ' % page.page.language),
+            #f.url(1, href='#'),
+            #f.text(_('Similar pages')),
+            #f.url(0),
+            f.paragraph(0),
+        ])
+
+
     def querystring(self, querydict=None):
         """ Return query string, used in the page link """
         if querydict is None:
@@ -641,17 +818,20 @@
         self.matchLabel = (_('match'), _('matches'))
 
 
-def getSearchResults(request, query, hits, start):
+def getSearchResults(request, query, hits, start, sort, estimated_hits):
     result_hits = []
     for wikiname, page, attachment, match in hits:
         if wikiname in (request.cfg.interwikiname, 'Self'): # a local match
             if attachment:
-                result_hits.append(FoundAttachment(page.page_name, attachment))
+                result_hits.append(FoundAttachment(page.page_name,
+                    attachment, page=page))
             else:
-                result_hits.append(FoundPage(page.page_name, match))
+                result_hits.append(FoundPage(page.page_name, match, page))
         else:
-            result_hits.append(FoundRemote(wikiname, page, attachment, match))
+            result_hits.append(FoundRemote(wikiname, page.page_name,
+                attachment, match, page))
     elapsed = time.time() - start
     count = request.rootpage.getPageCount()
-    return SearchResults(query, result_hits, count, elapsed)
+    return SearchResults(query, result_hits, count, elapsed, sort,
+            estimated_hits)
 
--- a/MoinMoin/support/BasicAuthTransport.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/support/BasicAuthTransport.py	Mon Aug 14 22:29:44 2006 +0200
@@ -1,13 +1,13 @@
 # taken from Amos' XML-RPC HowTo:
 
-import string, xmlrpclib, httplib
+import xmlrpclib, httplib
 from base64 import encodestring
 
 class BasicAuthTransport(xmlrpclib.Transport):
     def __init__(self, username=None, password=None):
-        self.username=username
-        self.password=password
-        self.verbose=0
+        self.username = username
+        self.password = password
+        self.verbose = 0
 
     def request(self, host, handler, request_body, **kw):
         # issue XML-RPC request
@@ -21,13 +21,10 @@
         h.putheader("User-Agent", self.user_agent)
         h.putheader("Content-Type", "text/xml")
         h.putheader("Content-Length", str(len(request_body)))
-        #h.putheader("Connection", "close") # TW XXX just trying if that cures twisted ...
 
         # basic auth
         if self.username is not None and self.password is not None:
-            authhdr = "Basic %s" % string.replace(
-                    encodestring("%s:%s" % (self.username, self.password)),
-                    "\012", "")
+            authhdr = "Basic %s" % encodestring("%s:%s" % (self.username, self.password)).replace("\012", "")
             h.putheader("Authorization", authhdr)
         h.endheaders()
 
@@ -43,5 +40,5 @@
                 headers
                 )
 
-        return self.parse_response(h.getfile()) 
+        return self.parse_response(h.getfile())
 
--- a/MoinMoin/support/__init__.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/support/__init__.py	Mon Aug 14 22:29:44 2006 +0200
@@ -4,13 +4,8 @@
 
     This package collects small third party utilities in order
     to reduce the necessary steps in installing MoinMoin. Each
-    source file is copyrighted by its respective author. I've done
-    my best to assure those files are freely redistributable.
-
-    Further details on the modules:
-
-    cgitb
-        from python 2.2 + patches (see XXX)
+    source file is copyrighted by its respective author. We've done
+    our best to assure those files are freely redistributable.
 
     @copyright: 2001-2004 by Jürgen Hermann <jh@web.de>
     @license: GNU GPL, see COPYING for details.
--- a/MoinMoin/support/cgitb.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/support/cgitb.py	Mon Aug 14 22:29:44 2006 +0200
@@ -16,7 +16,7 @@
 
 By default, tracebacks are displayed but not saved, the context is 5 lines
 and the output format is 'html' (for backwards compatibility with the
-original use of this module)
+original use of this module).
 
 Alternatively, if you have caught an exception and want cgitb to display it
 for you, call cgitb.handler().  The optional argument to handler() is a
@@ -30,19 +30,19 @@
  - Refactor html and text functions to View class, HTMLFormatter and
    TextFormatter. No more duplicate formating code.
  - Layout is done with minimal html and css, in a way it can't be
-   effected by souranding code.
- - Built to be easy to subclass and modify without duplicating code
+   affected by surrounding code.
+ - Built to be easy to subclass and modify without duplicating code.
  - Change layout, important details come first.
- - Factor frame analaizing and formatting into separate class
+ - Factor frame analyzing and formatting into separate class.
  - Add debug argument, can be used to change error display e.g. user
-   error view, developer error view
- - Add viewClass argument, make it easy to customize the traceback view
- - Easy to customize system details and application details
+   error view, developer error view.
+ - Add viewClass argument, make it easy to customize the traceback view.
+ - Easy to customize system details and application details.
 
 The main goal of this rewrite was to have a traceback that can render
-few tracebacks combined. Its needed when you wrap an expection and want
+few tracebacks combined. It's needed when you wrap an expection and want
 to print both the traceback up to the wrapper exception, and the
-original traceback. There is no code to support this here, but its easy
+original traceback. There is no code to support this here, but it's easy
 to add by using your own View sub class.
 """
 
@@ -58,14 +58,14 @@
     Return a string that resets the CGI and browser to a known state.
     TODO: probably some of this is not needed any more.
     """
-    return '''<!--: spam
+    return """<!--: spam
 Content-Type: text/html
 
 <body><font style="color: white; font-size: 1px"> -->
 <body><font style="color: white; font-size: 1px"> --> -->
 </font> </font> </font> </script> </object> </blockquote> </pre>
 </table> </table> </table> </table> </table> </font> </font> </font>
-'''
+"""
 
 __UNDEF__ = [] # a special sentinel object
 
@@ -77,17 +77,16 @@
 
 class HTMLFormatter:
     """ Minimal html formatter """
-    
+
     def attributes(self, attributes=None):
         if attributes:
-            result = [' %s="%s"' % (k, v) for k, v in attributes.items()]           
+            result = [' %s="%s"' % (k, v) for k, v in attributes.items()]
             return ''.join(result)
         return ''
-    
+
     def tag(self, name, text, attributes=None):
-        return '<%s%s>%s</%s>\n' % (name, self.attributes(attributes), 
-                                    text, name)
-    
+        return '<%s%s>%s</%s>\n' % (name, self.attributes(attributes), text, name)
+
     def section(self, text, attributes=None):
         return self.tag('div', text, attributes)
 
@@ -114,9 +113,9 @@
         if isinstance(items, (list, tuple)):
             items = '\n' + ''.join([self.listItem(i) for i in items])
         return self.tag(name, items, attributes)
-    
+
     def listItem(self, text, attributes=None):
-        return self.tag('li', text, attributes)        
+        return self.tag('li', text, attributes)
 
     def link(self, href, text, attributes=None):
         if attributes is None:
@@ -125,14 +124,14 @@
         return self.tag('a', text, attributes)
 
     def strong(self, text, attributes=None):
-        return self.tag('strong', text, attributes)        
+        return self.tag('strong', text, attributes)
 
     def em(self, text, attributes=None):
-        return self.tag('em', text, attributes)        
+        return self.tag('em', text, attributes)
 
     def repr(self, object):
         return pydoc.html.repr(object)
-        
+
 
 class TextFormatter:
     """ Plain text formatter """
@@ -170,20 +169,20 @@
         return items
 
     def listItem(self, text, attributes=None):
-        return ' * %s\n' % text       
+        return ' * %s\n' % text
 
     def link(self, href, text, attributes=None):
         return '[[%s]]' % text
 
     def strong(self, text, attributes=None):
         return text
-   
+
     def em(self, text, attributes=None):
         return text
-   
+
     def repr(self, object):
         return repr(object)
-        
+
 
 class Frame:
     """ Analyze and format single frame in a traceback """
@@ -207,19 +206,19 @@
 
     # -----------------------------------------------------------------
     # Private - formatting
-        
+
     def formatCall(self):
         call = '%s in %s%s' % (self.formatFile(),
                                self.formatter.strong(self.func),
                                self.formatArguments(),)
         return self.formatter.paragraph(call, {'class': 'call'})
-    
+
     def formatFile(self):
         """ Return formatted file link """
         if not self.file:
             return '?'
         file = pydoc.html.escape(os.path.abspath(self.file))
-        return self.formatter.link('file://' + file, file)        
+        return self.formatter.link('file://' + file, file)
 
     def formatArguments(self):
         """ Return formated arguments list """
@@ -250,11 +249,11 @@
         return self.formatter.orderedList(context, {'class': 'context'})
 
     def formatVariables(self, vars):
-        """ Return formatted variables """ 
+        """ Return formatted variables """
         done = {}
         dump = []
         for name, where, value in vars:
-            if name in done: 
+            if name in done:
                 continue
             done[name] = 1
             if value is __UNDEF__:
@@ -280,12 +279,12 @@
     def scan(self):
         """ Scan frame for vars while setting highlight line """
         highlight = {}
-        
+
         def reader(lnum=[self.lnum]):
             highlight[lnum[0]] = 1
-            try: 
+            try:
                 return linecache.getline(self.file, lnum[0])
-            finally: 
+            finally:
                 lnum[0] += 1
 
         vars = self.scanVariables(reader)
@@ -295,7 +294,7 @@
         """ Lookup variables in one logical Python line """
         vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__
         for ttype, token, start, end, line in tokenize.generate_tokens(reader):
-            if ttype == tokenize.NEWLINE: 
+            if ttype == tokenize.NEWLINE:
                 break
             if ttype == tokenize.NAME and token not in keyword.kwlist:
                 if lasttoken == '.':
@@ -341,14 +340,14 @@
 
 class View:
     """ Traceback view """
-    
+
     frameClass = Frame # analyze and format a frame
-    
+
     def __init__(self, info=None, debug=0):
         """ Save starting info or current exception info """
         self.info = info or sys.exc_info()
         self.debug = debug
-        
+
     def format(self, formatter, context=5):
         self.formatter = formatter
         self.context = context
@@ -411,20 +410,20 @@
 
     # -----------------------------------------------------------------
     # Head
-    
+
     def formatTitle(self):
         return self.formatter.title(self.exceptionTitle(self.info))
-        
+
     def formatMessage(self):
         return self.formatter.paragraph(self.exceptionMessage(self.info))
-        
+
     # -----------------------------------------------------------------
     # Traceback
 
     def formatTraceback(self):
         """ Return formatted traceback """
         return self.formatOneTraceback(self.info)
-    
+
     def formatOneTraceback(self, info):
         """ Format one traceback
         
@@ -435,7 +434,7 @@
                   self.formatter.orderedList(self.tracebackFrames(info),
                                             {'class': 'frames'}),
                   self.formatter.section(self.formatException(info),
-                                         {'class': 'exception'}),]
+                                         {'class': 'exception'}), ]
         return self.formatter.section(''.join(output), {'class': 'traceback'})
 
     def tracebackFrames(self, info):
@@ -458,12 +457,12 @@
     def formatException(self, info):
         items = [self.formatExceptionTitle(info),
                  self.formatExceptionMessage(info),
-                 self.formatExceptionAttributes(info),]
+                 self.formatExceptionAttributes(info), ]
         return ''.join(items)
 
     def formatExceptionTitle(self, info):
         return self.formatter.subSubTitle(self.exceptionTitle(info))
-        
+
     def formatExceptionMessage(self, info):
         return self.formatter.paragraph(self.exceptionMessage(info))
 
@@ -471,7 +470,7 @@
         attribtues = []
         for name, value in self.exceptionAttributes(info):
             value = self.formatter.repr(value)
-            attribtues.append('%s = %s' % (name, value))           
+            attribtues.append('%s = %s' % (name, value))
         return self.formatter.list(attribtues)
 
     def exceptionAttributes(self, info):
@@ -488,7 +487,7 @@
     def exceptionTitle(self, info):
         type = info[0]
         return getattr(type, '__name__', str(type))
-        
+
     def exceptionMessage(self, info):
         instance = info[1]
         return pydoc.html.escape(str(instance))
@@ -500,15 +499,14 @@
     def formatSystemDetails(self):
         details = ['Date: %s' % self.date(),
                    'Platform: %s' % self.platform(),
-                   'Python: %s' % self.python(),]
+                   'Python: %s' % self.python(), ]
         details += self.applicationDetails()
         return (self.formatter.subTitle('System Details') +
                 self.formatter.list(details, {'class': 'system'}))
 
     def date(self):
         import time
-        rfc2822Date = time.strftime("%a, %d %b %Y %H:%M:%S +0000",
-                                    time.gmtime())
+        rfc2822Date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
         return rfc2822Date
 
     def platform(self):
@@ -535,7 +533,7 @@
         """ Separate to enable formatting multiple tracebacks. """
         import traceback
         return ''.join(traceback.format_exception(*info))
-    
+
     def textTracebackTemplate(self):
         return '''
     
@@ -593,7 +591,7 @@
 
         if self.logdir is not None:
             import os, tempfile
-            suffix = ['.txt', '.html'][self.format=="html"]
+            suffix = ['.txt', '.html'][self.format == "html"]
             (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir)
             try:
                 file = os.fdopen(fd, 'w')
@@ -610,8 +608,7 @@
 
 handler = Hook().handle
 
-def enable(display=1, logdir=None, context=5, format="html",
-           viewClass=View, debug=0):
+def enable(display=1, logdir=None, context=5, format="html", viewClass=View, debug=0):
     """Install an exception handler that formats tracebacks as HTML.
 
     The optional argument 'display' can be set to 0 to suppress sending the
@@ -619,3 +616,4 @@
     tracebacks to be written to files there."""
     sys.excepthook = Hook(display=display, logdir=logdir, context=context,
                           format=format, viewClass=viewClass, debug=debug)
+
--- a/MoinMoin/support/thfcgi.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/support/thfcgi.py	Mon Aug 14 22:29:44 2006 +0200
@@ -1,6 +1,5 @@
 # -*- coding: iso-8859-1 -*-
 """
-    
     thfcgi.py - FastCGI communication with thread support
 
     Copyright Peter Åstrand <astrand@lysator.liu.se> 2001
@@ -10,6 +9,10 @@
     Added "external application" support, refactored code
         by Alexander Schremmer <alex AT alexanderweb DOT de>
 
+    Cleanup, fixed typos, PEP-8, support for limiting creation of threads,
+    limited number of requests lifetime, configurable backlog for socket
+    .listen() by Thomas Waldmann <tw AT waldmann-edv DOT de>
+
     For code base see:
     http://cvs.lysator.liu.se/viewcvs/viewcvs.cgi/webkom/thfcgi.py?cvsroot=webkom
 
@@ -33,21 +36,27 @@
 # CONTENT_LENGTH and abort the update if the two numbers are not equal.
 #
 
-# Imports
+debug = False
+
 import os
 import sys
 import select
-import string
 import socket
 import errno
 import cgi
 from cStringIO import StringIO
 import struct
 
+try:
+    import threading as _threading
+except ImportError:
+    import dummy_threading as _threading
+
 # Maximum number of requests that can be handled
 FCGI_MAX_REQS = 50
 FCGI_MAX_CONNS = 50
 FCGI_VERSION_1 = 1
+
 # Can this application multiplex connections?
 FCGI_MPXS_CONNS = 0
 
@@ -90,8 +99,15 @@
 FCGI_UnknownTypeBody = "!B7x"
 FCGI_EndRequestBody = "!IB3x"
 
+LOGFILE = sys.stderr
+
+def log(s):
+    if debug:
+        LOGFILE.write(s)
+        LOGFILE.write('\n')
+
 class SocketErrorOnWrite:
-    """ Is raised if a write fails in the socket code."""
+    """Is raised if a write fails in the socket code."""
     pass
 
 class Record:
@@ -139,7 +155,7 @@
         value = data[pos:pos+valuelen]
         pos += valuelen
 
-        return (name, value, pos)
+        return name, value, pos
 
     def write_pair(self, name, value):
         """Write a FastCGI key-value pair to the server."""
@@ -158,29 +174,27 @@
             data += struct.pack("!I", value | 0x80000000L)
 
         return data + name + value
-        
+
     def readRecord(self, sock):
         """Read a FastCGI record from the server."""
         data = sock.recv(8)
         if not data:
-            # No data recieved. This means EOF. 
+            # No data received. This means EOF. 
             return None
-        
-        fields = struct.unpack(FCGI_Record_header, data)
-        (self.version, self.rec_type, self.req_id,
-         contentLength, paddingLength) = fields
-        
+
+        self.version, self.rec_type, self.req_id, contentLength, paddingLength = \
+            struct.unpack(FCGI_Record_header, data)
+
         self.content = ""
         while len(self.content) < contentLength:
             data = sock.recv(contentLength - len(self.content))
             self.content = self.content + data
         if paddingLength != 0:
             sock.recv(paddingLength)
-        
+
         # Parse the content information
         if self.rec_type == FCGI_BEGIN_REQUEST:
-            (self.role, self.flags) = struct.unpack(FCGI_BeginRequestBody,
-                                                    self.content)
+            self.role, self.flags = struct.unpack(FCGI_BeginRequestBody, self.content)
             self.keep_conn = self.flags & FCGI_KEEP_CONN
 
         elif self.rec_type == FCGI_UNKNOWN_TYPE:
@@ -192,10 +206,9 @@
             while pos < len(self.content):
                 name, value, pos = self.read_pair(self.content, pos)
                 self.values[name] = value
+
         elif self.rec_type == FCGI_END_REQUEST:
-            (self.appStatus,
-             self.protocolStatus) = struct.unpack(FCGI_EndRequestBody,
-                                                  self.content)
+            self.appStatus, self.protocolStatus = struct.unpack(FCGI_EndRequestBody, self.content)
 
         return 1
 
@@ -214,16 +227,14 @@
                 content = content + self.write_pair(i, self.values[i])
 
         elif self.rec_type == FCGI_END_REQUEST:
-            content = struct.pack(FCGI_EndRequestBody, self.appStatus,
-                                  self.protocolStatus)
+            content = struct.pack(FCGI_EndRequestBody, self.appStatus, self.protocolStatus)
 
         # Align to 8-byte boundary
         clen = len(content)
         padlen = ((clen + 7) & 0xfff8) - clen
-        
-        hdr = struct.pack(FCGI_Record_header, self.version, self.rec_type,
-                          self.req_id, clen, padlen)
-        
+
+        hdr = struct.pack(FCGI_Record_header, self.version, self.rec_type, self.req_id, clen, padlen)
+
         try:
             sock.sendall(hdr + content + padlen*"\x00")
         except socket.error:
@@ -234,13 +245,13 @@
 class Request:
     """A request, corresponding to an accept():ed connection and
     a FCGI request."""
-    
-    def __init__(self, conn, req_handler, multi=1):
+
+    def __init__(self, conn, req_handler, inthread=False):
         """Initialize Request container."""
         self.conn = conn
         self.req_handler = req_handler
-        self.multi = multi
-        
+        self.inthread = inthread
+
         self.keep_conn = 0
         self.req_id = None
 
@@ -276,14 +287,13 @@
             else:
                 # EOF, connection closed. Break loop, end thread. 
                 return
-                
+
     def getFieldStorage(self):
         """Return a cgi FieldStorage constructed from the stdin and
         environ read from the server for this request."""
         self.stdin.reset()
         # cgi.FieldStorage will eat the input here...
-        r = cgi.FieldStorage(fp=self.stdin, environ=self.env,
-                             keep_blank_values=1)
+        r = cgi.FieldStorage(fp=self.stdin, environ=self.env, keep_blank_values=1)
         # hence, we reset here so we can obtain
         # the data again...
         self.stdin.reset()
@@ -301,7 +311,7 @@
         if not data:
             # Writing zero bytes would mean stream termination
             return
-        
+
         while data:
             chunk, data = self.getNextChunk(data)
             rec.content = chunk
@@ -362,10 +372,9 @@
         rec.writeRecord(self.conn)
         if not self.keep_conn:
             self.conn.close()
-            if self.multi:
-                import thread
-                thread.exit()
-    
+            if self.inthread:
+                raise SystemExit
+
     #
     # Record handlers
     #
@@ -384,8 +393,7 @@
         if rec_type in KNOWN_MANAGEMENT_TYPES:
             self._handle_known_man_types(rec)
         else:
-            # It's a management record of an unknown
-            # type. Signal the error.
+            # It's a management record of an unknown type. Signal the error.
             rec = Record()
             rec.rec_type = FCGI_UNKNOWN_TYPE
             rec.unknownType = rec_type
@@ -397,9 +405,10 @@
             reply_rec = Record()
             reply_rec.rec_type = FCGI_GET_VALUES_RESULT
 
-            params = {'FCGI_MAX_CONNS' : FCGI_MAX_CONNS,
-                      'FCGI_MAX_REQS' : FCGI_MAX_REQS,
-                      'FCGI_MPXS_CONNS' : FCGI_MPXS_CONNS}
+            params = {'FCGI_MAX_CONNS': FCGI_MAX_CONNS,
+                      'FCGI_MAX_REQS': FCGI_MAX_REQS,
+                      'FCGI_MPXS_CONNS': FCGI_MPXS_CONNS,
+                     }
 
             for name in rec.values.keys():
                 if params.has_key(name):
@@ -416,7 +425,7 @@
             self._handle_begin_request(rec)
             return
         elif rec.req_id != self.req_id:
-            #print >> sys.stderr, "Recieved unknown request ID", rec.req_id
+            log("Received unknown request ID %r" % rec.req_id)
             # Ignore requests that aren't active
             return
         if rec.rec_type == FCGI_ABORT_REQUEST:
@@ -437,7 +446,7 @@
             self._handle_data(rec)
         else:
             # Should never happen. 
-            #print >> sys.stderr, "Recieved unknown FCGI record type", rec.rec_type
+            log("Received unknown FCGI record type %r" % rec.rec_type)
             pass
 
         if self.env_complete and self.stdin_complete:
@@ -461,14 +470,14 @@
 
         self.req_id = rec.req_id
         self.keep_conn = rec.keep_conn
-        
+
     def _handle_params(self, rec):
         """Handle environment."""
         if self.env_complete:
             # Should not happen
-            #print >> sys.stderr, "Recieved FCGI_PARAMS more than once"
+            log("Received FCGI_PARAMS more than once")
             return
-        
+
         if not rec.content:
             self.env_complete = 1
 
@@ -479,9 +488,9 @@
         """Handle stdin."""
         if self.stdin_complete:
             # Should not happen
-            #print >> sys.stderr, "Recieved FCGI_STDIN more than once"
+            log("Received FCGI_STDIN more than once")
             return
-        
+
         if not rec.content:
             self.stdin_complete = 1
             self.stdin.reset()
@@ -493,12 +502,12 @@
         """Handle data."""
         if self.data_complete:
             # Should not happen
-            #print >> sys.stderr, "Recieved FCGI_DATA more than once"
+            log("Received FCGI_DATA more than once")
             return
 
         if not rec.content:
             self.data_complete = 1
-        
+
         self.data.write(rec.content)
 
     def getNextChunk(self, data):
@@ -507,31 +516,30 @@
         data = data[8192:]
         return chunk, data
 
-class FCGIbase:
-    """Base class for FCGI requests."""
-    
-    def __init__(self, req_handler, fd, port):
+class FCGI:
+    """FCGI requests"""
+
+    def __init__(self, req_handler, fd=sys.stdin, port=None, max_requests=-1, backlog=5, max_threads=5):
         """Initialize main loop and set request_handler."""
         self.req_handler = req_handler
         self.fd = fd
         self.__port = port
         self._make_socket()
+        # how many requests we have left before terminating this process, -1 means infinite lifetime:
+        self.requests_left = max_requests
+        # for socket.listen(backlog):
+        self.backlog = backlog
+        # how many threads we have at maximum (including the main program = 1. thread)
+        self.max_threads = max_threads
 
-    def run(self):
-        raise NotImplementedError
-
-    def accept_handler(self, conn, addr):
+    def accept_handler(self, conn, addr, inthread=False):
         """Construct Request and run() it."""
         self._check_good_addrs(addr)
         try:
-            req = Request(conn, self.req_handler, self.multi)
+            req = Request(conn, self.req_handler, inthread)
             req.run()
         except SocketErrorOnWrite:
-            if self.multi:
-                import thread
-                thread.exit()
-            #else:
-            #    raise SystemExit
+            raise SystemExit
 
     def _make_socket(self):
         """Create socket and verify FCGI environment."""
@@ -550,54 +558,45 @@
                     raise ValueError("FastCGI port is not setup correctly")
         except socket.error, (err, errmsg):
             if err != errno.ENOTCONN:
-                raise RuntimeError("No FastCGI environment: %s - %s" % (`err`, errmsg))
+                raise RuntimeError("No FastCGI environment: %s - %s" % (repr(err), errmsg))
 
         self.sock = s
-        
+
     def _check_good_addrs(self, addr):
         """Check if request is done from the right server."""
         # Apaches mod_fastcgi seems not to use FCGI_WEB_SERVER_ADDRS. 
         if os.environ.has_key('FCGI_WEB_SERVER_ADDRS'):
-            good_addrs = string.split(os.environ['FCGI_WEB_SERVER_ADDRS'], ',')
-            good_addrs = map(string.strip, good_addrs) # Remove whitespace
+            good_addrs = os.environ['FCGI_WEB_SERVER_ADDRS'].split(',')
+            good_addrs = [addr.strip() for addr in good_addrs] # Remove whitespace
         else:
             good_addrs = None
-        
-        # Check if the connection is from a legal address
-        if good_addrs != None and addr not in good_addrs:
-            raise RuntimeError("Connection from invalid server!")
 
-class THFCGI(FCGIbase):
-    """Multi-threaded main loop to handle FastCGI Requests."""
-    
-    def __init__(self, req_handler, fd=sys.stdin, port=None):
-        """Initialize main loop and set request_handler."""
-        self.multi = 1
-        FCGIbase.__init__(self, req_handler, fd, port)
+        # Check if the connection is from a legal address
+        if good_addrs is not None and addr not in good_addrs:
+            raise RuntimeError("Connection from invalid server!")
 
     def run(self):
-        """Wait & serve. Calls request_handler in new
-        thread on every request."""
-        import thread
-        self.sock.listen(50)
-        
-        while 1:
-            (conn, addr) = self.sock.accept()
-            thread.start_new_thread(self.accept_handler, (conn, addr))
-
-class unTHFCGI(FCGIbase):
-    """Single-threaded main loop to handle FastCGI Requests."""
+        """Wait & serve. Calls request_handler on every request."""
+        self.sock.listen(self.backlog)
+        log("Starting Process")
+        running = True
+        while running:
+            if not self.requests_left:
+                # self.sock.shutdown(RDWR) here does NOT help with backlog
+                running = False
+            elif self.requests_left > 0:
+                self.requests_left -= 1
+            if running:
+                conn, addr = self.sock.accept()
+                threadcount = _threading.activeCount()
+                if threadcount < self.max_threads:
+                    log("Accepted connection, starting thread...")
+                    t = _threading.Thread(target=self.accept_handler, args=(conn, addr, True))
+                    t.start()
+                else:
+                    log("Accepted connection, running in main-thread...")
+                    self.accept_handler(conn, addr, False)
+                log("Active Threads: %d" % _threading.activeCount())
+        self.sock.close()
+        log("Ending Process")
 
-    def __init__(self, req_handler, fd=sys.stdin, port=None):
-        """Initialize main loop and set request_handler."""
-        self.multi = 0
-        FCGIbase.__init__(self, req_handler, fd, port)
-    
-    def run(self):
-        """Wait & serve. Calls request handler for every request (blocking)."""
-        self.sock.listen(50)
-        
-        while 1:
-            (conn, addr) = self.sock.accept()
-            self.accept_handler(conn, addr)
-   
--- a/MoinMoin/support/xapwrap/document.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/support/xapwrap/document.py	Mon Aug 14 22:29:44 2006 +0200
@@ -1,7 +1,6 @@
 """
     xapwrap.document - Pythonic wrapper around Xapian's Document API
 """
-import string
 import datetime
 import re
 import cPickle
--- a/MoinMoin/support/xapwrap/index.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/support/xapwrap/index.py	Mon Aug 14 22:29:44 2006 +0200
@@ -635,7 +635,7 @@
                         valRes[valName] = xapDoc.get_value(valueIndex)
                     thisResult['values'] = valRes
                 results.append(thisResult)
-            return enq, results
+            return enq, mset, results
         except:
             del enq, mset
             raise
--- a/MoinMoin/userform.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/userform.py	Mon Aug 14 22:29:44 2006 +0200
@@ -6,7 +6,7 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import string, time, re
+import time, re
 from MoinMoin import user, util, wikiutil
 from MoinMoin.util import web, timefuncs
 from MoinMoin.widget import html
@@ -359,8 +359,8 @@
                 '%s [%s%s:%s]' % (
                     time.strftime(self.cfg.datetime_fmt, timefuncs.tmtuple(t)),
                     "+-"[offset < 0],
-                    string.zfill("%d" % (abs(offset) / 3600), 2),
-                    string.zfill("%d" % (abs(offset) % 3600 / 60), 2),
+                    "%02d" % (abs(offset) / 3600),
+                    "%02d" % (abs(offset) % 3600 / 60),
                 ),
             ))
 
--- a/MoinMoin/util/__init__.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/util/__init__.py	Mon Aug 14 22:29:44 2006 +0200
@@ -14,9 +14,9 @@
 #############################################################################
 
 g_xmlIllegalCharPattern = re.compile('[\x01-\x08\x0B-\x0D\x0E-\x1F\x80-\xFF]')
-g_undoUtf8Pattern       = re.compile('\xC2([^\xC2])')
-g_cdataCharPattern      = re.compile('[&<\'\"]')
-g_textCharPattern       = re.compile('[&<]')
+g_undoUtf8Pattern = re.compile('\xC2([^\xC2])')
+g_cdataCharPattern = re.compile('[&<\'\"]')
+g_textCharPattern = re.compile('[&<]')
 g_charToEntity = {
     '&': '&amp;',
     '<': '&lt;',
@@ -60,11 +60,11 @@
     for i in range(len(numbers)-1):
         if pattern[-1] == ',':
             pattern = pattern + str(numbers[i])
-            if numbers[i]+1 == numbers[i+1]:
+            if numbers[i] + 1 == numbers[i+1]:
                 pattern = pattern + '-'
             else:
                 pattern = pattern + ','
-        elif numbers[i]+1 != numbers[i+1]:
+        elif numbers[i] + 1 != numbers[i+1]:
             pattern = pattern + str(numbers[i]) + ','
 
     if pattern[-1] in ',-':
@@ -116,3 +116,4 @@
 def random_string(length):
     chars = ''.join([chr(random.randint(0, 255)) for x in xrange(length)])
     return chars
+
--- a/MoinMoin/util/web.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/util/web.py	Mon Aug 14 22:29:44 2006 +0200
@@ -6,7 +6,7 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import re, types
+import re
 from MoinMoin import config
 
 def getIntegerInput(request, fieldname, default=None, minval=None, maxval=None):
@@ -224,11 +224,11 @@
             another Color instance, a tuple containing 3 color values, 
             a Netscape color name or a HTML color ("#RRGGBB").
         """
-        if isinstance(color, types.TupleType) and len(color) == 3:
+        if isinstance(color, tuple) and len(color) == 3:
             self.r, self.g, self.b = map(int, color)
         elif isinstance(color, Color):
             self.r, self.g, self.b = color.r, color.g, color.b
-        elif not isinstance(color, types.StringType):
+        elif not isinstance(color, str):
             raise TypeError("Color() expects a Color instance, a RGB triple or a color string")
         elif color[0] == '#':
             color = long(color[1:], 16)
--- a/MoinMoin/wikiutil.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/wikiutil.py	Mon Aug 14 22:29:44 2006 +0200
@@ -11,7 +11,6 @@
 import os
 import re
 import time
-import types
 import urllib
 
 from MoinMoin import util, version, config
@@ -1131,7 +1130,7 @@
                 continue
             if hasattr(Parser, 'extensions'):
                 exts = Parser.extensions
-                if type(exts) == types.ListType:
+                if isinstance(exts, list):
                     for ext in Parser.extensions:
                         etp[ext] = Parser
                 elif str(exts) == '*':
@@ -1531,14 +1530,14 @@
 
 def createTicket(tm=None):
     """Create a ticket using a site-specific secret (the config)"""
-    import sha, time, types
+    import sha
     ticket = tm or "%010x" % time.time()
     digest = sha.new()
     digest.update(ticket)
 
     cfgvars = vars(config)
     for var in cfgvars.values():
-        if type(var) is types.StringType:
+        if isinstance(var, str):
             digest.update(repr(var))
 
     return "%s.%s" % (ticket, digest.hexdigest())
--- a/MoinMoin/wikixml/marshal.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/MoinMoin/wikixml/marshal.py	Mon Aug 14 22:29:44 2006 +0200
@@ -6,9 +6,6 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import types
-
-
 class Marshal:
     """ Serialize Python data structures to XML.
 
@@ -64,16 +61,16 @@
         if data is None:
             content = "<none/>"
 
-        elif isinstance(data, types.StringType):
+        elif isinstance(data, str):
             content = (data.replace("&", "&amp;") # Must be done first!
                            .replace("<", "&lt;")
                            .replace(">", "&gt;"))
 
-        elif isinstance(data, types.DictionaryType):
+        elif isinstance(data, dict):
             for key, value in data.items():
                 add_content(self.__toXML(key, value))
 
-        elif isinstance(data, types.ListType) or isinstance(data, types.TupleType):
+        elif isinstance(data, (list, tuple)):
             for item in data:
                 add_content(self.__toXML(self.ITEM_CONTAINER, item))
 
@@ -89,7 +86,7 @@
                                 .replace(">", "&gt;"))
 
         # Close container element
-        if isinstance(content, types.StringType):
+        if isinstance(content, str):
             # No Whitespace
             if element:
                 content = ['<%s>%s</%s>' % (element, content, element)]
--- a/README	Mon Aug 14 22:27:04 2006 +0200
+++ b/README	Mon Aug 14 22:29:44 2006 +0200
@@ -40,7 +40,7 @@
 
 See docs/CHANGES                 for a version history. READ THIS!
 See docs/INSTALL.html            for installation instructions.
-See docs/UPDATE.html             for updating instructions.
+See docs/README.migration        for data conversion instructions.
 
 Note that the code base contains some experimental or unfinished features.
 Use them at your own risk. Official features are described on the set of
--- a/contrib/auth_externalcookie/wikiconfig.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/contrib/auth_externalcookie/wikiconfig.py	Mon Aug 14 22:29:44 2006 +0200
@@ -4,7 +4,7 @@
 # See the XXX places for customizing it to your needs. You need to put this
 # code into your farmconfig.py or wikiconfig.py.
 
-# ...
+# HINT: this code is slightly outdated, if you fix it to work with 1.6, please send us a copy.
 
 class FarmConfig(DefaultConfig):
     def external_cookie(request, **kw):
@@ -13,7 +13,7 @@
         user = None
         try_next = True # if True, moin tries the next auth method
         cookiename = "whatever" # XXX external cookie name you want to use
-        
+
         try:
             cookie = Cookie.SimpleCookie(request.saved_cookie)
         except Cookie.CookieError:
@@ -27,7 +27,7 @@
             cookievalue = urllib.unquote(cookievalue) # cookie value is urlencoded, decode it
             cookievalue = cookievalue.decode('iso-8859-1') # decode cookie charset to unicode
             cookievalue = cookievalue.split('#') # cookie has format loginname#firstname#lastname#email
-            
+
             auth_username = cookievalue[0] # having this cookie means user auth has already been done!
             aliasname = email = ''
             try:
@@ -44,13 +44,13 @@
             from MoinMoin.user import User
             # giving auth_username to User constructor means that authentication has already been done.
             user = User(request, name=auth_username, auth_username=auth_username)
-            
+
             changed = False
             if aliasname != user.aliasname: # was the aliasname externally updated?
                 user.aliasname = aliasname ; changed = True # yes -> update user profile
             if email != user.email: # was the email addr externally updated?
                 user.email = email ; changed = True # yes -> update user profile
-            
+
             if user:
                 user.create_or_update(changed)
             if user and user.valid: # did we succeed making up a valid user?
--- a/docs/CHANGES	Mon Aug 14 22:27:04 2006 +0200
+++ b/docs/CHANGES	Mon Aug 14 22:29:44 2006 +0200
@@ -219,6 +219,16 @@
       You need to change this in your wikiconfig.py or farmconfig.py file.
       See MoinMoin/multiconfig.py for an alternative way if you can't do that.
 
+Version 1.5-current:
+   * moin.fcg improved - if you use FastCGI, you must use the new file:
+     * can self-terminate after some number of requests (default: -1, this means
+       "unlimited lifetime")
+     * the count of created threads is limited now (default: 5), you can use 1
+       to use non-threaded operation.
+     * configurable socket.listen() backlog (default: 5)
+  
+Version 1.5.4:
+    HINT: read docs/README.migration.
 Version 1.5.4-current:
     * increased maxlength of some input fields from 80 to 200
 
--- a/docs/CHANGES.config	Mon Aug 14 22:27:04 2006 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-# this is a part of multiconfig.py - the stuff that changed recently:
-    actions_excluded = [] # ['DeletePage', 'AttachFile', 'RenamePage']
-    auth = [moin_cookie]
-    cookie_domain = None # use '.domain.tld" for a farm with hosts in that domain
-    cookie_path = None   # use '/wikifarm" for a farm with pathes below that path
-    editor_default = 'text' # which editor is called when nothing is specified
-    editor_ui = 'freechoice' # which editor links are shown on user interface
-    editor_force = False
-    hacks = {} # { 'feature1': value1, ... }
-               # Configuration for features still in development.
-               # For boolean stuff just use config like this:
-               #   hacks = { 'feature': True, ...}
-               # and in the code use:
-               #   if cfg.hacks.get('feature', False): <doit>
-               # A non-existing hack key should ever mean False, None, "", [] or {}!
-    interwiki_preferred = [] # list of wiki names to show at top of interwiki list
-    language_default = 'en'
-    language_ignore_browser = False # ignore browser settings, use language_default
-                                    # or user prefs
-    lupy_search = False # disabled until lupy is finished
-    mail_sendmail = None # "/usr/sbin/sendmail -t -i" to not use SMTP, but sendmail
-    show_interwiki = 0 # show interwiki name (and link it to page_front_page)
-    superuser = [] # list of unicode user names that have super powers :)
-    user_email_unique = True # do we check whether a user's email is unique?
-
-    user_checkbox_fields = [
-        ('mailto_author', lambda _: _('Publish my email (not my wiki homepage) in author info')),
-        ('edit_on_doubleclick', lambda _: _('Open editor on double click')),
-        ('remember_last_visit', lambda _: _('Remember last page visited')),
-        ('show_nonexist_qm', lambda _: _('Show question mark for non-existing pagelinks')),
-        ('show_page_trail', lambda _: _('Show page trail')),
-        ('show_toolbar', lambda _: _('Show icon toolbar')),
-        ('show_topbottom', lambda _: _('Show top/bottom links in headings')),
-        ('show_fancy_diff', lambda _: _('Show fancy diffs')),
-        ('wikiname_add_spaces', lambda _: _('Add spaces to displayed wiki names')),
-        ('remember_me', lambda _: _('Remember login information')),
-        ('want_trivial', lambda _: _('Subscribe to trivial changes')),
-        
-        ('disabled', lambda _: _('Disable this account forever')),
-        # if an account is disabled, it may be used for looking up
-        # id -> username for page info and recent changes, but it
-        # is not usable for the user any more:
-    ]
-    
-    user_checkbox_defaults = {'mailto_author':       0,
-                              'edit_on_doubleclick': 0,
-                              'remember_last_visit': 0,
-                              'show_nonexist_qm':    nonexist_qm,
-                              'show_page_trail':     1,
-                              'show_toolbar':        1,
-                              'show_topbottom':      0,
-                              'show_fancy_diff':     1,
-                              'wikiname_add_spaces': 0,
-                              'remember_me':         1,
-                              'want_trivial':        0,
-                             }
-    # don't let the user change those
-    # user_checkbox_disable = ['disabled', 'want_trivial']
-    user_checkbox_disable = []
-    # remove those checkboxes:
-    user_checkbox_remove = ['edit_on_doubleclick', 'show_nonexist_qm', 'show_toolbar', 'show_topbottom',
-                            'show_fancy_diff', 'wikiname_add_spaces', 'remember_me', 'disabled',]
-    
-    user_form_fields = [
-        ('name', _('Name'), "text", "36", _("(Use Firstname''''''Lastname)")),
-        ('aliasname', _('Alias-Name'), "text", "36", ''),
-        ('password', _('Password'), "password", "36", ''),
-        ('password2', _('Password repeat'), "password", "36", _('(Only when changing passwords)')),
-        ('email', _('Email'), "text", "36", ''),
-        ('css_url', _('User CSS URL'), "text", "40", _('(Leave it empty for disabling user CSS)')),
-        ('edit_rows', _('Editor size'), "text", "3", ''),
-        ##('theme', _('Preferred theme'), [self._theme_select()])
-        ##('', _('Editor Preference'), [self._editor_default_select()])
-        ##('', _('Editor shown on UI'), [self._editor_ui_select()])
-        ##('', _('Time zone'), [self._tz_select()])
-        ##('', _('Date format'), [self._dtfmt_select()])
-        ##('', _('Preferred language'), [self._lang_select()])
-    ]
-    
-    user_form_defaults = { # key: default
-        'name': '',
-        'aliasname': '',
-        'password': '',
-        'password2': '',
-        'email': '',
-        'css_url': '',
-        'edit_rows': "20",
-    }
-    
-    # don't let the user change those, but show them:
-    user_form_disable = ['name', 'aliasname', 'email',]
-    
-    # remove those completely:
-    user_form_remove = ['password', 'password2', 'css_url', 'logout', 'create', 'account_sendmail',]
-    
-    user_homewiki = 'Self' # interwiki name for where user homepages are located
-
--- a/docs/CHANGES.fpletz	Mon Aug 14 22:27:04 2006 +0200
+++ b/docs/CHANGES.fpletz	Mon Aug 14 22:29:44 2006 +0200
@@ -4,35 +4,42 @@
   Known main issues:
     * Only term-based regex searching possible, modifier or heuristic to
       enable usage of _moinSearch for full compatibility?
-    * HACK: MoinMoin.Xapian.Index._get_languages (wait for proper metadata)
-    * Positions saved in Xapian aren't always correct, check. Code
-      generally needs some more love.
+    * HACK: MoinMoin.search.Xapian.Index._get_languages (wait for proper
+      metadata)
 
   ToDo:
     * Implement the new search UI
     * Write/update documentation for all the new search stuff
-    * Indexing and searching of categories (new term prefix)
     * Reevaluate Xapwrap, possibly drop it and rip out usable stuff
       (i.e. ExceptionTranslator)
-    * Add stemming support for highlighting stuff:
-        1. regexp for whole word (all lowercase), or
-        2. just the root of the word
+
+  ToDo (low priority):
+    * Case-sensitive searches / Regexp on multiple terms: Graceful
+      fallback to and/or merge with moinSearch based on nodes xapian can
+      handle in the search term tree
+      * currently, xapian will fetch relevant pages and feed those into
+        _moinSearch for doing the real hard stuff it can't handle
+      -> need for a query optimizer, after SoC?
 
   New Features:
     * Faster search thanks to Xapian
     * Searching for languages with new prefix lang/language, i.e. lang:de
       Note: Currently only available when Xapian is used
+    * CategorySearch with prefix category or with the regexp previously
+      used (autodetected as CategorySearch)
     * New config options:
         xapian_search        0      enables xapian-powered search
         xapian_index_dir     None   directory for xapian indices
-        xapian_stemming      True   Toggles usage of stemmer, fallback
+        xapian_stemming      True   toggles usage of stemmer, fallback
                                     to False if no stemmer installed
+        search_results_per_page 10  determines how many hits should be
+                                    shown on a fullsearch action
   
   Bugfixes (only stuff that is buggy in moin/1.6 main branch):
     * ...
 
   Other Changes:
-    * ...
+    * Some whitespace fixes in miscellaneous code
   
   Developer notes:
     * ...
@@ -152,4 +159,76 @@
 2006-07-17
     * SystemInfo macro now also shows if xapian is being used (index
       available) and more graceful fallback to moinSearch
+    * Explored and evaluated the current framework for macros,
+      formatters and stuff which we need to touch for the new search UI
 
+2006-07-18
+    * Fixed some bugs, whitespaces at EOL, better i18n for SystemInfo
+    * Implemented paging support for searches, needs some style
+      adjustments
+
+2006-07-19
+    * student didn't work on the project -- ThomasWaldmann
+
+2006-07-20
+    * Fixed some bugs found while testing regexp and case-sensitive searches
+    * Conclusion after tinkering with the current code to allow
+      cooperation between moinSearch and Xapian for case-sensitive
+      searches (code buried): We probably need a rather big rewrite!
+
+2006-07-21
+2006-07-22
+    * Final thoughts: No query optimizer for now. Case-sensitive
+      sensitive search is done by querying Xapian with the lowercased
+      terms and run _moinSearch over the relevant pages with the same
+      query.
+    * Indexing of categories
+
+2006-07-23
+    * CategorySearch is live
+    * Subpage issue does not need changes: Can be done with regex magic
+      I.e.: - subpages of MyPage: re:^MyPage/
+            - subpages called SubPage: re:/SubPage
+            - subpages called Subpage (1st level): re:[^/]*/SubPage
+            - subpages called Subpage (last level): re:/Subpage$
+
+2006-07-24
+    * SystemInfo macro update (mtime)
+    * nicer regexp support for TitleSearch
+
+2006-07-25 .. 2006-08-01
+    * student did not work on project
+
+2006-08-02
+    * Reformatted search statistics to use CSS and be more google-like
+      (only in modern theme for now)
+    * Added "search result info bar", showing revision, size, mtime,
+      links for further searches (-> ToDo) etc.
+
+2006-08-03 no work on project
+2006-08-04 no work on project
+
+2006-08-05 .. 2006-08-06
+    * (finally :)) Google-like paging, using images from google.com until
+      we get proper moin gfx
+    * index domains of a page (standard, underlay)
+
+2006-08-07
+     * info bar for titlesearches
+     * bugfix for results code: sometimes we never got a page instance
+       in Found{Page,Attachment,...} which yielded strange errors
+
+2006-08-08
+    * added some more timers for regression testing
+    * improved highlighting code to work better with stemming and
+      special searches, extended SystemInfo macro
+
+2006-08-09
+    * use xapian for sorting, first step not to fetch all results
+      -> still TODO: need real weight
+
+2006-08-10
+    * entry missing
+
+2006-08-10 .. 13 no work on project
+
--- a/setup.py	Mon Aug 14 22:27:04 2006 +0200
+++ b/setup.py	Mon Aug 14 22:29:44 2006 +0200
@@ -3,11 +3,12 @@
 """
     MoinMoin installer
 
-    @copyright: 2001-2005 by Jürgen Hermann <jh@web.de>
+    @copyright: 2001-2005 by Jürgen Hermann <jh@web.de>,
+                2006 by MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
-import glob, os, string, sys
+import os, sys, glob
 
 import distutils
 from distutils.core import setup
@@ -134,8 +135,6 @@
             raise Exception("You have to inherit build_scripts_create and"
                 " provide a package name")
 
-        to_module = string.maketrans('-/', '_.')
-
         self.mkpath(self.build_dir)
         for script in self.scripts:
             outfile = os.path.join(self.build_dir, os.path.basename(script))
@@ -149,7 +148,7 @@
                 continue
 
             module = os.path.splitext(os.path.basename(script))[0]
-            module = string.translate(module, to_module)
+            module = module.replace('-', '_').replace('/', '.')
             script_vars = {
                 'python': os.path.normpath(sys.executable),
                 'package': self.package_name,
@@ -188,7 +187,7 @@
         module files.
     """
     script = os.path.splitext(os.path.basename(path))[0]
-    script = string.replace(script, '_', '-')
+    script = script.replace('_', '-')
     if sys.platform == "win32":
         script = script + ".bat"
     return script
@@ -241,6 +240,7 @@
         'MoinMoin.script.old',
         'MoinMoin.script.old.migration',
         'MoinMoin.script.old.xmlrpc-tools',
+        'MoinMoin.script.xmlrpc',
         'MoinMoin.search',
         'MoinMoin.security',
         'MoinMoin.server',
--- a/wiki/htdocs/modern/css/common.css	Mon Aug 14 22:27:04 2006 +0200
+++ b/wiki/htdocs/modern/css/common.css	Mon Aug 14 22:29:44 2006 +0200
@@ -334,11 +334,55 @@
 
 .searchresults dt {
     margin-top: 1em;
-	font-weight: normal;
+    font-weight: normal;
 }
 
-.searchresults dd {
-	font-size: 0.85em;
+.searchresults dd, .searchresults p {
+    font-size: 0.85em;
+}
+
+.searchresults .searchhitinfobar {
+    color: #008000;
+    margin-left: 15px;
+    margin-top: 0;
+}
+
+p.searchstats {
+    font-size: 0.8em;
+    text-align: right;
+    width: 100%;
+    background-color: #E6EAF0;
+    border-top: 1px solid #9088DC;
+    padding: 2px;
+}
+
+.searchpages {
+    margin-left: auto;
+    margin-right: auto;
+}
+
+.searchpages tr, .searchpages td {
+    border: 0;
+    padding: 0;
+    margin: 0;
+    text-align: center;
+    vertical-align: middle;
+    color: #a90a08;
+    font-weight: bold;
+}
+
+.searchpages td.prev {
+    text-align: right;
+}
+
+.searchpage td.next {
+    text-align: left;
+}
+
+.searchpages td a, .searchpages td a:link {
+    color: #000000;
+    text-decoration: underline;
+    font-weight: normal;
 }
 
 /* MonthCalendar css */
Binary file wiki/htdocs/modern/img/nav_current.png has changed
Binary file wiki/htdocs/modern/img/nav_first.png has changed
Binary file wiki/htdocs/modern/img/nav_last.png has changed
Binary file wiki/htdocs/modern/img/nav_next.png has changed
Binary file wiki/htdocs/modern/img/nav_page.png has changed
Binary file wiki/htdocs/modern/img/nav_prev.png has changed
--- a/wiki/server/moin.fcg	Mon Aug 14 22:27:04 2006 +0200
+++ b/wiki/server/moin.fcg	Mon Aug 14 22:29:44 2006 +0200
@@ -26,8 +26,15 @@
 ## import os
 ## os.environ['MOIN_DEBUG'] = '1'
 
-# Use threaded version or non-threaded version (default 1)?
-use_threads = 1
+# how many requests shall be handled by a moin fcgi process before it dies,
+# -1 mean "unlimited lifetime":
+max_requests = -1
+
+# how many threads to use (1 means use only main program, non-threaded)
+max_threads = 5
+
+# backlog, use in socket.listen(backlog) call
+backlog = 5
 
 
 # Code ------------------------------------------------------------------
@@ -37,7 +44,7 @@
 
 # Set threads flag, so other code can use proper locking
 from MoinMoin import config
-config.use_threads = use_threads
+config.use_threads = max_threads > 1
 del config
 
 from MoinMoin.request import FCGI
@@ -48,10 +55,6 @@
     request.run()
 
 if __name__ == '__main__':
-    if use_threads:
-        fcg = thfcgi.THFCGI(handle_request)
-    else:
-        fcg = thfcgi.unTHFCGI(handle_request)    
-
+    fcg = thfcgi.FCGI(handle_request, max_requests=max_requests, backlog=backlog, max_threads=max_threads)
     fcg.run()