changeset 1023:80043704f37e

merge
author "Luis Henrique Fagundes <lhfagundes@gmail.com>"
date Sun, 30 Oct 2011 01:16:40 -0200
parents afe0afe4e1a6 (current diff) 2256d29e3cd5 (diff)
children 0849d0697d31
files MoinMoin/items/__init__.py
diffstat 40 files changed, 575 insertions(+), 252 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/_tests/__init__.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/_tests/__init__.py	Sun Oct 30 01:16:40 2011 -0200
@@ -71,7 +71,7 @@
     item.destroy()
 
 
-def test_connection(port, host='127.0.0.1'):
+def check_connection(port, host='127.0.0.1'):
     """
     Check if we can make a connection to host:port.
 
--- a/MoinMoin/app.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/app.py	Sun Oct 30 01:16:40 2011 -0200
@@ -168,7 +168,6 @@
     if app.cfg.create_storage:
         app.router.create()
     app.router.open()
-    app.router = app.router._get_backend('')[0] # XXX hack until router works correctly
     app.storage = indexing.IndexingMiddleware(app.cfg.index_dir, app.router,
                                               wiki_name=app.cfg.interwikiname,
                                               acl_rights_contents=app.cfg.acl_rights_contents)
--- a/MoinMoin/apps/admin/views.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/apps/admin/views.py	Sun Oct 30 01:16:40 2011 -0200
@@ -27,7 +27,7 @@
 
 @admin.route('/')
 def index():
-    return render_template('admin/index.html', item_name="+admin")
+    return render_template('admin/index.html', title_name=_(u"Admin"))
 
 
 @admin.route('/userbrowser')
@@ -45,7 +45,7 @@
                           groups=[groupname for groupname in groups if doc[NAME] in groups[groupname]],
                      )
                      for doc in docs]
-    return render_template('admin/userbrowser.html', user_accounts=user_accounts, item_name="+admin/Userbrowser")
+    return render_template('admin/userbrowser.html', user_accounts=user_accounts, title_name=_(u"User Browser"))
 
 
 @admin.route('/userprofile/<user_name>', methods=['GET', 'POST', ])
@@ -101,7 +101,7 @@
         action = 'syspages_upgrade'
         label = 'Upgrade System Pages'
         return render_template('admin/sysitems_upgrade.html',
-                               item_name="+admin/System items upgrade")
+                               title_name=_(u"System items upgrade"))
     if request.method == 'POST':
         xmlfile = request.files.get('xmlfile')
         try:
@@ -156,7 +156,7 @@
 
     found.sort()
     return render_template('admin/wikiconfig.html',
-                           item_name="+admin/wikiconfig",
+                           title_name=_(u"Wiki Configuration"),
                            found=found, settings=settings)
 
 
@@ -185,7 +185,7 @@
         groups.append((heading, desc, opts))
     groups.sort()
     return render_template('admin/wikiconfighelp.html',
-                           item_name="+admin/wikiconfighelp",
+                           title_name=_(u"Wiki Configuration Help"),
                            groups=groups)
 
 
@@ -202,7 +202,7 @@
     rows = sorted([[desc, ' '.join(names), ' '.join(patterns), ' '.join(mimetypes), ]
                    for desc, names, patterns, mimetypes in lexers])
     return render_template('admin/highlighterhelp.html',
-                           item_name="+admin/highlighterhelp",
+                           title_name=_(u"Highlighter Help"),
                            headings=headings,
                            rows=rows)
 
@@ -215,7 +215,7 @@
                ]
     rows = sorted(app.cfg.interwiki_map.items())
     return render_template('admin/interwikihelp.html',
-                           item_name="+admin/interwikihelp",
+                           title_name=_(u"Interwiki Help"),
                            headings=headings,
                            rows=rows)
 
@@ -226,11 +226,11 @@
     headings = [_('Size'),
                 _('Item name'),
                ]
-    rows = [(doc[SIZE], doc[NAME])
-            for doc in flaskg.storage.documents(wikiname=app.cfg.interwikiname)]
+    rows = [(rev.meta[SIZE], rev.meta[NAME])
+            for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)]
     rows = sorted(rows, reverse=True)
     return render_template('admin/itemsize.html',
-                           item_name="+admin/itemsize",
+                           title_name=_(u"Item Size"),
                            headings=headings,
                            rows=rows)
 
--- a/MoinMoin/apps/feed/_tests/test_feed.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/apps/feed/_tests/test_feed.py	Sun Oct 30 01:16:40 2011 -0200
@@ -7,6 +7,9 @@
 
 from MoinMoin._tests import wikiconfig
 
+from MoinMoin.items import Item
+from MoinMoin.config import CONTENTTYPE, COMMENT
+from MoinMoin._tests import update_item
 
 class TestFeeds(object):
     class Config(wikiconfig.Config):
@@ -23,3 +26,22 @@
             assert '<feed xmlns="http://www.w3.org/2005/Atom">' in rv.data
             assert '</feed>' in rv.data
 
+    def test_global_atom_with_an_item(self):
+        basename = u'Foo'
+        item = update_item(basename, {COMMENT: u"foo data for feed item"}, '')
+        with self.app.test_client() as c:
+            rv = c.get('/+feed/atom')
+            assert rv.status == '200 OK'
+            assert rv.headers['Content-Type'] == 'application/atom+xml'
+            assert rv.data.startswith('<?xml')
+            assert "foo data for feed item" in rv.data
+
+        # tests the cache invalidation
+        update_item(basename, {COMMENT: u"checking if the cache invalidation works"}, '')
+        with self.app.test_client() as c:
+            rv = c.get('/+feed/atom')
+            assert rv.status == '200 OK'
+            assert rv.headers['Content-Type'] == 'application/atom+xml'
+            assert rv.data.startswith('<?xml')
+            assert "checking if the cache invalidation works" in rv.data
+
--- a/MoinMoin/apps/feed/views.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/apps/feed/views.py	Sun Oct 30 01:16:40 2011 -0200
@@ -25,13 +25,14 @@
 from MoinMoin import wikiutil
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.apps.feed import feed
-from MoinMoin.config import NAME, NAME_EXACT, WIKINAME, ACL, ACTION, ADDRESS, HOSTNAME, USERID, COMMENT, MTIME, REVID, ALL_REVS
+from MoinMoin.config import (NAME, NAME_EXACT, WIKINAME, ACL, ACTION, ADDRESS,
+                            HOSTNAME, USERID, COMMENT, MTIME, REVID, ALL_REVS,
+                            PARENTID, LATEST_REVS)
 from MoinMoin.themes import get_editor_info
 from MoinMoin.items import Item
 from MoinMoin.util.crypto import cache_key
 from MoinMoin.util.interwiki import url_for_item
 
-
 @feed.route('/atom/<itemname:item_name>')
 @feed.route('/atom', defaults=dict(item_name=''))
 def atom(item_name):
@@ -40,8 +41,17 @@
     # - full item in html is nice
     # - diffs in textmode are OK, but look very simple
     # - full-item content in textmode is OK, but looks very simple
-    cid = cache_key(usage="atom", item_name=item_name)
-    content = app.cache.get(cid)
+    query = Term(WIKINAME, app.cfg.interwikiname)
+    if item_name:
+        query = And([query, Term(NAME_EXACT, item_name), ])
+    revs = list(flaskg.storage.search(query, idx_name=LATEST_REVS, sortedby=[MTIME], reverse=True, limit=1))
+    if revs:
+        rev = revs[0]
+        cid = cache_key(usage="atom", revid=rev.revid, item_name=item_name)
+        content = app.cache.get(cid)
+    else:
+        content = None
+        cid = None
     if content is None:
         title = app.cfg.sitename
         feed = AtomFeed(title=title, feed_url=request.url, url=request.host_url)
@@ -75,9 +85,10 @@
                      content=content, content_type=content_type,
                      author=get_editor_info(rev.meta, external=True),
                      url=url_for_item(name, rev=this_revid, _external=True),
-                     updated=rev.meta[MTIME],
+                     updated=datetime.fromtimestamp(rev.meta[MTIME]),
                     )
         content = feed.to_string()
-        app.cache.set(cid, content)
+        if cid is not None:
+            app.cache.set(cid, content)
     return Response(content, content_type='application/atom+xml')
 
--- a/MoinMoin/apps/frontend/views.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/apps/frontend/views.py	Sun Oct 30 01:16:40 2011 -0200
@@ -56,8 +56,9 @@
 from MoinMoin.util import crypto
 from MoinMoin.util.interwiki import url_for_item
 from MoinMoin.security.textcha import TextCha, TextChaizedForm, TextChaValid
-from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, AccessDeniedError
+from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError
 from MoinMoin.signalling import item_displayed, item_modified
+from MoinMoin.storage.middleware.protecting import AccessDenied
 
 
 @frontend.route('/+dispatch', methods=['GET', ])
@@ -146,6 +147,7 @@
 
 @frontend.route('/+search', methods=['GET', 'POST'])
 def search():
+    title_name = _("Search")
     search_form = SearchForm.from_flat(request.values)
     valid = search_form.validate()
     search_form['submit'].set_default() # XXX from_flat() kills all values
@@ -177,14 +179,14 @@
                                    content_suggestions=content_suggestions,
                                    query=query,
                                    medium_search_form=search_form,
-                                   item_name='+search', # XXX
+                                   title_name=title_name,
                                   )
             flaskg.clock.stop('search render')
     else:
         html = render_template('search.html',
                                query=query,
                                medium_search_form=search_form,
-                               item_name='+search', # XXX
+                               title_name=title_name,
                               )
     return html
 
@@ -197,7 +199,7 @@
                         item_name=item_name)
     try:
         item = Item.create(item_name, rev_id=rev)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
     show_revision = rev != CURRENT
     show_navigation = False # TODO
@@ -230,7 +232,7 @@
 def show_dom(item_name, rev):
     try:
         item = Item.create(item_name, rev_id=rev)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
     if isinstance(item, NonExistent):
         status = 404
@@ -258,8 +260,10 @@
 def highlight_item(item_name, rev):
     try:
         item = Item.create(item_name, rev_id=rev)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
+    if isinstance(item, NonExistent):
+        abort(404, item_name)
     return render_template('highlight.html',
                            item=item, item_name=item.name,
                            data_text=Markup(item._render_data_highlight()),
@@ -272,8 +276,10 @@
     flaskg.user.addTrail(item_name)
     try:
         item = Item.create(item_name, rev_id=rev)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
+    if isinstance(item, NonExistent):
+        abort(404, item_name)
     show_revision = rev != CURRENT
     show_navigation = False # TODO
     first_rev = None
@@ -307,8 +313,10 @@
                         item_name=item_name)
     try:
         item = Item.create(item_name, rev_id=rev)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
+    if isinstance(item, NonExistent):
+        abort(404, item_name)
     return render_template('content.html',
                            item_name=item.name,
                            data_rendered=Markup(item._render_data()),
@@ -319,7 +327,7 @@
 def get_item(item_name, rev):
     try:
         item = Item.create(item_name, rev_id=rev)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
     return item.do_get()
 
@@ -329,7 +337,7 @@
     try:
         item = Item.create(item_name, rev_id=rev)
         mimetype = request.values.get("mimetype")
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
     return item.do_get(force_attachment=True, mimetype=mimetype)
 
@@ -347,7 +355,7 @@
     contenttype = request.values.get('contenttype')
     try:
         item = Item.create(item_name, rev_id=CURRENT)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
     # We don't care about the name of the converted object
     # It should just be a name which does not exist.
@@ -355,7 +363,7 @@
     item_name_converted = item_name + 'converted'
     try:
         converted_item = Item.create(item_name_converted, contenttype=contenttype)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
     return converted_item._convert(item.internal_representation())
 
@@ -372,7 +380,7 @@
     template_name = request.values.get('template')
     try:
         item = Item.create(item_name, contenttype=contenttype)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
     if not flaskg.user.may.write(item_name):
         abort(403)
@@ -414,8 +422,10 @@
 def revert_item(item_name, rev):
     try:
         item = Item.create(item_name, rev_id=rev)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
+    if isinstance(item, NonExistent):
+        abort(404, item_name)
     if request.method == 'GET':
         form = RevertItemForm.from_defaults()
         TextCha(form).amend_form()
@@ -436,8 +446,10 @@
 def rename_item(item_name):
     try:
         item = Item.create(item_name)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
+    if isinstance(item, NonExistent):
+        abort(404, item_name)
     if request.method == 'GET':
         form = RenameItemForm.from_defaults()
         TextCha(form).amend_form()
@@ -460,8 +472,10 @@
 def delete_item(item_name):
     try:
         item = Item.create(item_name)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
+    if isinstance(item, NonExistent):
+        abort(404, item_name)
     if request.method == 'GET':
         form = DeleteItemForm.from_defaults()
         TextCha(form).amend_form()
@@ -497,7 +511,7 @@
                 item = Item.create(itemname)
                 item.delete(comment)
                 response["status"].append(True)
-            except AccessDeniedError:
+            except AccessDenied:
                 response["status"].append(False)
 
     return jsonify(response)
@@ -522,7 +536,7 @@
                 item = Item.create(itemname)
                 item.destroy(comment=comment, destroy_item=True)
                 response["status"].append(True)
-            except AccessDeniedError:
+            except AccessDenied:
                 response["status"].append(False)
 
     return jsonify(response)
@@ -552,8 +566,10 @@
         destroy_item = False
     try:
         item = Item.create(item_name, rev_id=_rev)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
+    if isinstance(item, NonExistent):
+        abort(404, item_name)
     if request.method == 'GET':
         form = DestroyItemForm.from_defaults()
         TextCha(form).amend_form()
@@ -594,7 +610,7 @@
                        url=url_for('.show_item', item_name=item_name, rev=revid),
                        contenttype=contenttype_to_class(contenttype),
                       )
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
 
 
@@ -603,7 +619,7 @@
 def index(item_name):
     try:
         item = Item.create(item_name) # when item_name='', it gives toplevel index
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
 
     if request.method == 'GET':
@@ -633,14 +649,18 @@
     detailed_index = sorted(detailed_index, key=lambda name: name[0].lower())
 
     item_names = item_name.split(u'/')
+    if item_name:
+        args = dict(item_name=item_name)
+    else:
+        args = dict(item_name=u'', title_name=_(u'Global Index'))
     return render_template(item.index_template,
-                           item_name=item_name,
                            item_names=item_names,
                            index=detailed_index,
                            initials=initials,
                            startswith=startswith,
                            contenttype_groups=ct_groups,
                            form=form,
+                           **args
                           )
 
 
@@ -653,7 +673,7 @@
     """
     my_changes = _mychanges(flaskg.user.itemid)
     return render_template('item_link_list.html',
-                           item_name='+mychanges', # XXX
+                           title_name=_(u'My Changes'),
                            headline=_(u'My Changes'),
                            item_names=my_changes
                           )
@@ -759,10 +779,10 @@
         history.append(dh)
     del history[0]  # kill the dummy
 
-    item_name = request.values.get('item_name', '') # actions menu puts it into qs
+    title_name = _(u'Global History')
     current_timestamp = int(time.time())
     return render_template('global_history.html',
-                           item_name=item_name, # XXX no item
+                           title_name=title_name,
                            history=history,
                            current_timestamp=current_timestamp,
                            bookmark_time=bookmark_time,
@@ -793,10 +813,10 @@
     existing, linked, transcluded = _compute_item_sets()
     referred = linked | transcluded
     wanteds = referred - existing
-    item_name = request.values.get('item_name', '') # actions menu puts it into qs
+    title_name = _(u'Wanted Items')
     return render_template('item_link_list.html',
                            headline=_(u'Wanted Items'),
-                           item_name=item_name,
+                           title_name=title_name,
                            item_names=wanteds)
 
 
@@ -809,9 +829,9 @@
     existing, linked, transcluded = _compute_item_sets()
     referred = linked | transcluded
     orphans = existing - referred
-    item_name = request.values.get('item_name', '') # actions menu puts it into qs
+    title_name = _('Orphaned Items')
     return render_template('item_link_list.html',
-                           item_name=item_name,
+                           title_name=title_name,
                            headline=_(u'Orphaned Items'),
                            item_names=orphans)
 
@@ -929,7 +949,7 @@
 
 @frontend.route('/+register', methods=['GET', 'POST'])
 def register():
-    item_name = 'Register' # XXX
+    title_name = _(u'Register')
     # is openid_submit in the form?
     isOpenID = 'openid_submit' in request.values
 
@@ -990,7 +1010,7 @@
                     return redirect(url_for('.show_root'))
 
     return render_template(template,
-                           item_name=item_name,
+                           title_name=title_name,
                            form=form,
                           )
 
@@ -1023,7 +1043,7 @@
 @frontend.route('/+lostpass', methods=['GET', 'POST'])
 def lostpass():
     # TODO use ?next=next_location check if target is in the wiki and not outside domain
-    item_name = 'LostPass' # XXX
+    title_name = _(u'Lost Password')
 
     if not _using_moin_auth():
         return Response('No MoinAuth in auth list', 403)
@@ -1048,7 +1068,7 @@
             flash(_("If this account exists, you will be notified."), "info")
             return redirect(url_for('.show_root'))
     return render_template('lostpass.html',
-                           item_name=item_name,
+                           title_name=title_name,
                            form=form,
                           )
 
@@ -1085,7 +1105,7 @@
 @frontend.route('/+recoverpass', methods=['GET', 'POST'])
 def recoverpass():
     # TODO use ?next=next_location check if target is in the wiki and not outside domain
-    item_name = 'RecoverPass' # XXX
+    title_name = _(u'Recover Password')
 
     if not _using_moin_auth():
         return Response('No MoinAuth in auth list', 403)
@@ -1103,7 +1123,7 @@
                 flash(_('Your token is invalid!'), "error")
             return redirect(url_for('.show_root'))
     return render_template('recoverpass.html',
-                           item_name=item_name,
+                           title_name=title_name,
                            form=form,
                           )
 
@@ -1153,7 +1173,7 @@
 @frontend.route('/+login', methods=['GET', 'POST'])
 def login():
     # TODO use ?next=next_location check if target is in the wiki and not outside domain
-    item_name = 'Login' # XXX
+    title_name = _(u'Login')
 
     # multistage return
     if flaskg._login_multistage_name == 'openid':
@@ -1174,7 +1194,7 @@
         for msg in flaskg._login_messages:
             flash(msg, "error")
     return render_template('login.html',
-                           item_name=item_name,
+                           title_name=title_name,
                            login_inputs=app.cfg.auth_login_inputs,
                            form=form,
                           )
@@ -1253,7 +1273,7 @@
 @frontend.route('/+usersettings/<part>', methods=['GET', 'POST'])
 def usersettings(part):
     # TODO use ?next=next_location check if target is in the wiki and not outside domain
-    item_name = 'User Settings' # XXX
+    title_name = _('User Settings')
 
     # these forms can't be global because we need app object, which is only available within a request:
     class UserSettingsPersonalForm(Form):
@@ -1295,7 +1315,7 @@
         # 'main' part or some invalid part
         return render_template('usersettings.html',
                                part='main',
-                               item_name=item_name,
+                               title_name=title_name,
                               )
     if request.method == 'GET':
         form = FormClass.from_object(flaskg.user)
@@ -1334,7 +1354,7 @@
                     form = FormClass.from_object(flaskg.user)
                     form['submit'].set_default() # XXX from_object() kills all values
     return render_template('usersettings.html',
-                           item_name=item_name,
+                           title_name=title_name,
                            part=part,
                            form=form,
                           )
@@ -1367,12 +1387,13 @@
 
 @frontend.route('/+diffraw/<path:item_name>')
 def diffraw(item_name):
-    # TODO get_item and get_revision calls may raise an AccessDeniedError.
+    # TODO get_item and get_revision calls may raise an AccessDenied.
     #      If this happens for get_item, don't show the diff at all
     #      If it happens for get_revision, we may just want to skip that rev in the list
+    # TODO verify if it does crash when the item does not exist
     try:
         item = flaskg.storage.get_item(item_name)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
     rev1 = request.values.get('rev1')
     rev2 = request.values.get('rev2')
@@ -1424,7 +1445,7 @@
 
     try:
         item = Item.create(item.name, contenttype=commonmt, rev_id=newrev.revid)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
     rev_ids = [CURRENT]  # XXX TODO we need a reverse sorted list
     return render_template(item.diff_template,
@@ -1444,7 +1465,7 @@
 
     try:
         item = Item.create(item.name, contenttype=commonmt, rev_id=newrev.revid)
-    except AccessDeniedError:
+    except AccessDenied:
         abort(403)
     return item._render_data_diff_raw(oldrev, newrev)
 
@@ -1627,7 +1648,7 @@
         # does not recurse
         try:
             item = flaskg.storage[name]
-        except AccessDeniedError:
+        except AccessDenied:
             return []
         rev = item[CURRENT]
         itemlinks = rev.meta.get(ITEMLINKS, [])
@@ -1648,7 +1669,7 @@
     """
     show a list or tag cloud of all tags in this wiki
     """
-    item_name = request.values.get('item_name', '') # actions menu puts it into qs
+    title_name = _(u'All tags in this wiki')
     revs = flaskg.storage.documents(wikiname=app.cfg.interwikiname)
     tags_counts = {}
     for rev in revs:
@@ -1676,7 +1697,7 @@
         tags = []
     return render_template("global_tags.html",
                            headline=_("All tags in this wiki"),
-                           item_name=item_name,
+                           title_name=title_name,
                            tags=tags)
 
 
@@ -1693,3 +1714,8 @@
                            item_name=tag,
                            item_names=item_names)
 
+@frontend.errorhandler(404)
+def page_not_found(e):
+    return render_template('404.html',
+                           item_name=e.description), 404
+
--- a/MoinMoin/auth/_tests/test_auth.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/auth/_tests/test_auth.py	Sun Oct 30 01:16:40 2011 -0200
@@ -5,11 +5,23 @@
 Test for auth.__init__
 """
 
+from flask import current_app as app
 from flask import g as flaskg
 
+import pytest
+
+from MoinMoin._tests import wikiconfig
 from MoinMoin.auth import GivenAuth, handle_login, get_multistage_continuation_url
 from MoinMoin.user import create_user
-import pytest
+
+class TestConfiguredGivenAuth(object):
+    """ Test: configured GivenAuth """
+    class Config(wikiconfig.Config):
+        auth = [GivenAuth(user_name=u'JoeDoe', autocreate=True), ]
+
+    def test(self):
+        assert flaskg.user.name == u'JoeDoe'
+
 
 class TestGivenAuth(object):
     """ Test: GivenAuth """
--- a/MoinMoin/conftest.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/conftest.py	Sun Oct 30 01:16:40 2011 -0200
@@ -80,10 +80,11 @@
         if inspect.isclass(self.parent.obj.__class__):
             cls = self.parent.obj.__class__
             cfg = getattr(cls, 'Config', wikiconfig.Config)
-            if prev_cfg is not cfg and prev_app is not None:
+            reinit = getattr(cls, 'reinit_storage', False)
+            if (prev_cfg is not cfg or reinit) and prev_app is not None:
                 # other config, previous app exists, so deinit it:
                 deinit_test_app(prev_app, prev_ctx)
-            if prev_cfg is not cfg or prev_app is None:
+            if prev_cfg is not cfg or reinit or prev_app is None:
                 # other config or no app yet, init app:
                 self.app, self.ctx = init_test_app(cfg)
             else:
--- a/MoinMoin/items/__init__.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/items/__init__.py	Sun Oct 30 01:16:40 2011 -0200
@@ -37,6 +37,7 @@
 from MoinMoin.util.tree import moin_page, html, xlink, docbook
 from MoinMoin.util.iri import Iri
 from MoinMoin.util.crypto import cache_key
+from MoinMoin.storage.middleware.protecting import AccessDenied
 
 try:
     import PIL
@@ -66,8 +67,7 @@
 from MoinMoin import wikiutil, config, user
 from MoinMoin.util.send_file import send_file
 from MoinMoin.util.interwiki import url_for_item
-from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, AccessDeniedError, \
-                                   StorageError
+from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, StorageError
 from MoinMoin.config import NAME, NAME_OLD, NAME_EXACT, WIKINAME, MTIME, REVERTED_TO, ACL, \
                             IS_SYSITEM, SYSITEM_VERSION,  USERGROUP, SOMEDICT, \
                             CONTENTTYPE, SIZE, LANGUAGE, ITEMLINKS, ITEMTRANSCLUSIONS, \
@@ -632,6 +632,12 @@
                    for name in names]
         return initials
 
+    delete_template = 'delete.html'
+    destroy_template = 'destroy.html'
+    diff_template = 'diff.html'
+    rename_template = 'rename.html'
+    revert_template = 'revert.html'
+
 class NonExistent(Item):
     def do_get(self, force_attachment=False, mimetype=None):
         abort(404)
@@ -708,7 +714,7 @@
             if form.validate():
                 try:
                     self.modify() # XXX
-                except AccessDeniedError:
+                except AccessDenied:
                     abort(403)
                 else:
                     return redirect(url_for_item(self.name))
@@ -719,12 +725,6 @@
                                form=form,
                               )
 
-    delete_template = 'delete.html'
-    destroy_template = 'destroy.html'
-    diff_template = 'diff.html'
-    rename_template = 'rename.html'
-    revert_template = 'revert.html'
-
     def _render_data_diff(self, oldrev, newrev):
         hash_name = HASH_ALGORITHM
         if oldrev.meta[hash_name] == newrev.meta[hash_name]:
@@ -1169,7 +1169,7 @@
             if form.validate():
                 try:
                     self.modify() # XXX
-                except AccessDeniedError:
+                except AccessDenied:
                     abort(403)
                 else:
                     return redirect(url_for_item(self.name))
@@ -1327,7 +1327,7 @@
             # this POST comes directly from TWikiDraw (not from Browser), thus no validation
             try:
                 self.modify() # XXX
-            except AccessDeniedError:
+            except AccessDenied:
                 abort(403)
             else:
                 # TWikiDraw POSTs more than once, redirecting would break them
@@ -1417,7 +1417,7 @@
             # this POST comes directly from AnyWikiDraw (not from Browser), thus no validation
             try:
                 self.modify() # XXX
-            except AccessDeniedError:
+            except AccessDenied:
                 abort(403)
             else:
                 # AnyWikiDraw POSTs more than once, redirecting would break them
@@ -1503,7 +1503,7 @@
             # this POST comes directly from SvgDraw (not from Browser), thus no validation
             try:
                 self.modify() # XXX
-            except AccessDeniedError:
+            except AccessDenied:
                 abort(403)
             else:
                 # SvgDraw POSTs more than once, redirecting would break them
--- a/MoinMoin/macro/RandomItem.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/macro/RandomItem.py	Sun Oct 30 01:16:40 2011 -0200
@@ -16,8 +16,9 @@
 
 from MoinMoin.util.iri import Iri
 from MoinMoin.util.tree import moin_page, xlink
-from MoinMoin.items import Item, AccessDeniedError
+from MoinMoin.items import Item
 from MoinMoin.macro._base import MacroInlineBase
+from MoinMoin.storage.middleware.protecting import AccessDenied
 
 
 class Macro(MacroInlineBase):
@@ -43,7 +44,7 @@
                 item = Item.create(item_name)
                 random_item_names.append(item_name)
                 found += 1
-            except AccessDeniedError:
+            except AccessDenied:
                 pass
 
         if not random_item_names:
--- a/MoinMoin/script/__init__.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/script/__init__.py	Sun Oct 30 01:16:40 2011 -0200
@@ -51,6 +51,9 @@
     from MoinMoin.script.migration.moin19.import19 import ImportMoin19
     manager.add_command("import19", ImportMoin19())
 
+    from MoinMoin.script.maint.moinshell import MoinShell
+    manager.add_command("moinshell", MoinShell())
+
     return manager.run(default_command=default_command)
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/script/maint/moinshell.py	Sun Oct 30 01:16:40 2011 -0200
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+
+from flask import Flask, _request_ctx_stack
+from flask import current_app as app
+from flask import g as flaskg
+from flaskext.script import Command, Option
+
+from MoinMoin import user
+from MoinMoin.util.clock import Clock
+
+class MoinShell(Command):
+
+    """
+    Runs a Python shell inside Flask application context.
+
+    :param banner: banner appearing at top of shell when started
+    :param make_context: a callable returning a dict of variables 
+                         used in the shell namespace. By default 
+                         returns a dict consisting of just the app.
+    :param use_ipython: use IPython shell if available, ignore if not. 
+                        The IPython shell can be turned off in command 
+                        line by passing the **--no-ipython** flag.
+    """
+
+    banner = ''
+
+    description = 'Runs a Python shell inside Flask application context.'
+    
+    def __init__(self, banner=None, make_context=None, use_ipython=True):
+
+
+        self.banner = banner or self.banner
+        self.use_ipython = use_ipython
+
+        if make_context is None:
+            def make_context():
+                app = _request_ctx_stack.top.app
+                flaskg.unprotected_storage = app.storage
+                flaskg.groups = app.cfg.groups()
+                flaskg.storage = app.storage
+                flaskg.user = user.User()
+                flaskg.clock = Clock()
+                return dict(app=app, flaskg=flaskg)
+
+        self.make_context = make_context
+    
+    def get_options(self):
+
+        return (
+                Option('--no-ipython',
+                       action="store_true",
+                       dest='no_ipython',
+                       default=not(self.use_ipython)),)
+
+    def get_context(self):
+        
+        """
+        Returns a dict of context variables added to the shell namespace.
+        """
+
+        return self.make_context()
+
+    def run(self, no_ipython):
+
+        """
+        Runs the shell. Unless no_ipython is True or use_python is False
+        then runs IPython shell if that is installed.
+        """
+        
+
+        context = self.get_context()
+
+        from IPython import embed
+
+        embed()
+
--- a/MoinMoin/storage/__init__.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/__init__.py	Sun Oct 30 01:16:40 2011 -0200
@@ -41,7 +41,7 @@
 
 def create_mapping(uri, mounts, acls):
     namespace_mapping = [(mounts[nsname],
-                          backend_from_uri(uri % dict(nsname=nsname)))
+                          backend_from_uri(uri % dict(nsname=nsname, kind="%(kind)s")))
                          for nsname in mounts]
     acl_mapping = acls.items()
     # we need the longest mountpoints first, shortest last (-> '' is very last)
--- a/MoinMoin/storage/backends/_tests/test_fileserver.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/backends/_tests/test_fileserver.py	Sun Oct 30 01:16:40 2011 -0200
@@ -13,7 +13,7 @@
 
 import pytest
 
-from MoinMoin.config import MTIME
+from MoinMoin.config import NAME, MTIME, REVID, ITEMID, HASH_ALGORITHM
 from ..fileserver import Backend
 from . import BackendTestBase
 
@@ -35,37 +35,30 @@
                 pass
             with open(fn, 'wb') as f:
                 f.write(data)
+            meta[NAME] = name
             meta = tuple(sorted(meta.items()))
             expected_result.add((meta, data))
         return expected_result
 
+    def test_iter(self):
+        # for the fileserver store, even if the directory is empty,
+        # we will get a revid for the root directory:
+        contents = list(self.be)
+        assert len(contents) == 1
+        root_revid = contents[0]
+        # revids are like relpath.mtime
+        relpath, mtime = root_revid.split('.')
+        assert relpath == ''
+
     def test_files(self):
         # note: as we can only store the data into the file system, meta can
         # only have items that are generated by the fileserver backend:
         items = [#name,  meta,   data
-                 ('foo.png', dict(size=11, contenttype='image/png'), 'png content'),
-                 ('bar.txt', dict(size=12, contenttype='text/plain'), 'text content'),
+                 (u'foo.png', dict(size=11, contenttype=u'image/png'), 'png content'),
+                 (u'bar.txt', dict(size=12, contenttype=u'text/plain'), 'text content'),
                 ]
         expected_result = self._prepare(items)
-        result = set()
-        for i in self.be:
-            meta, data = self.be.retrieve(i)
-            # we don't want to check mtime
-            del meta[MTIME]
-            meta = tuple(sorted(meta.items()))
-            data = data.read()
-            result.add((meta, data))
-        assert result == expected_result
-
-    def test_dir(self):
-        # note: as we can only store the data into the file system, meta can
-        # only have items that are generated by the fileserver backend:
-        items = [#name,  meta,   data
-                 ('dir/foo.png', dict(size=11, contenttype='image/png'), 'png content'),
-                 ('dir/bar.txt', dict(size=12, contenttype='text/plain'), 'text content'),
-                ]
-        expected_result = self._prepare(items)
-        dir_meta = tuple(sorted(dict(size=0, contenttype='text/x.moin.wiki;charset=utf-8').items()))
+        dir_meta = tuple(sorted(dict(name=u'', size=0, contenttype=u'text/x.moin.wiki;charset=utf-8').items()))
         dir_data = """\
 = Directory contents =
  * [[../]]
@@ -76,11 +69,49 @@
         result = set()
         for i in self.be:
             meta, data = self.be.retrieve(i)
-            # we don't want to check mtime
+            # we don't want to check some meta values
             del meta[MTIME]
+            del meta[HASH_ALGORITHM]
+            del meta[ITEMID]
+            del meta[REVID]
             meta = tuple(sorted(meta.items()))
             data = data.read()
             result.add((meta, data))
         assert result == expected_result
 
+    def test_dir(self):
+        # note: as we can only store the data into the file system, meta can
+        # only have items that are generated by the fileserver backend:
+        items = [#name,  meta,   data
+                 (u'dir/foo.png', dict(size=11, contenttype=u'image/png'), 'png content'),
+                 (u'dir/bar.txt', dict(size=12, contenttype=u'text/plain'), 'text content'),
+                ]
+        expected_result = self._prepare(items)
+        dir_meta = tuple(sorted(dict(name=u'', size=0, contenttype=u'text/x.moin.wiki;charset=utf-8').items()))
+        dir_data = """\
+= Directory contents =
+ * [[../]]
+ * [[/dir|dir/]]
+""".replace('\n', '\r\n')
+        expected_result.add((dir_meta, dir_data))
+        dir_meta = tuple(sorted(dict(name=u'dir', size=0, contenttype=u'text/x.moin.wiki;charset=utf-8').items()))
+        dir_data = """\
+= Directory contents =
+ * [[../]]
+ * [[/bar.txt|bar.txt]]
+ * [[/foo.png|foo.png]]
+""".replace('\n', '\r\n')
+        expected_result.add((dir_meta, dir_data))
+        result = set()
+        for i in self.be:
+            meta, data = self.be.retrieve(i)
+            # we don't want to check some meta values
+            del meta[MTIME]
+            del meta[HASH_ALGORITHM]
+            del meta[ITEMID]
+            del meta[REVID]
+            meta = tuple(sorted(meta.items()))
+            data = data.read()
+            result.add((meta, data))
+        assert result == expected_result
 
--- a/MoinMoin/storage/backends/fileserver.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/backends/fileserver.py	Sun Oct 30 01:16:40 2011 -0200
@@ -20,8 +20,9 @@
 import errno
 import stat
 from StringIO import StringIO
+from werkzeug import url_quote, url_unquote
 
-from MoinMoin.config import MTIME, SIZE, CONTENTTYPE
+from MoinMoin.config import NAME, ITEMID, REVID, MTIME, SIZE, CONTENTTYPE, HASH_ALGORITHM
 from . import BackendBase
 
 from MoinMoin.util.mimetype import MimeType
@@ -48,28 +49,39 @@
         pass
 
     def _mkpath(self, key):
+        """
+        key -> rel path, absolute path (strip mtime)
+        """
         # XXX unsafe keys?
-        return os.path.join(self.path, key)
+        try:
+            relpath, mtime = key.rsplit('.', 1)
+        except ValueError:
+            # we only generate revids that look like path.mtime,
+            # so if the split does not work, the revid is invalid
+            # and we raise KeyError like if the rev is not there
+            raise KeyError(key)
+        return relpath, os.path.join(self.path, relpath)
 
     def _mkkey(self, path):
+        """
+        absolute path -> relpath, mtime
+        """
+        st = os.stat(path)
         root = self.path
         assert path.startswith(root)
-        key = path[len(root)+1:]
-        return key
+        return path[len(root)+1:], int(st.st_mtime)
 
-    def __iter__(self):
-        # note: instead of just yielding the relative <path>, yield <path>/<mtime>,
-        # so if the file is updated, the revid will change (and the indexer's
-        # update() method can efficiently update the index).
-        for dirpath, dirnames, filenames in os.walk(self.path):
-            key = self._mkkey(dirpath)
-            if key:
-                yield key
-            for filename in filenames:
-                yield self._mkkey(os.path.join(dirpath, filename))
+    def _encode(self, key):
+        """
+        we need to get rid of slashes in revids because we put them into URLs
+        and it would confuse the URL routing.
+        """
+        return url_quote(key, safe='')
 
-    def _get_meta(self, fn):
-        path = self._mkpath(fn)
+    def _decode(self, qkey):
+        return url_unquote(qkey)
+
+    def _get_meta(self, fn, path):
         try:
             st = os.stat(path)
         except OSError as e:
@@ -77,19 +89,23 @@
                 raise KeyError(fn)
             raise
         meta = {}
+        meta[NAME] = fn
         meta[MTIME] = int(st.st_mtime) # use int, not float
+        meta[REVID] = unicode(self._encode('%s.%d' % (meta[NAME], meta[MTIME])))
+        meta[ITEMID] = meta[REVID]
+        meta[HASH_ALGORITHM] = u'' # XXX crap, but sendfile needs it for etag
         if stat.S_ISDIR(st.st_mode):
             # directory
             # we create a virtual wiki page listing links to subitems:
-            ct = 'text/x.moin.wiki;charset=utf-8'
+            ct = u'text/x.moin.wiki;charset=utf-8'
             size = 0
         elif stat.S_ISREG(st.st_mode):
             # normal file
-            ct = MimeType(filename=fn).content_type()
+            ct = unicode(MimeType(filename=fn).content_type())
             size = int(st.st_size) # use int instead of long
         else:
             # symlink, device file, etc.
-            ct = 'application/octet-stream'
+            ct = u'application/octet-stream'
             size = 0
         meta[CONTENTTYPE] = ct
         meta[SIZE] = size
@@ -118,8 +134,7 @@
             content = unicode(err)
         return content
 
-    def _get_data(self, fn):
-        path = self._mkpath(fn)
+    def _get_data(self, fn, path):
         try:
             st = os.stat(path)
             if stat.S_ISDIR(st.st_mode):
@@ -134,8 +149,21 @@
                 raise KeyError(fn)
             raise
 
-    def retrieve(self, fn):
-        meta = self._get_meta(fn)
-        data = self._get_data(fn)
+    def __iter__(self):
+        # note: instead of just yielding the relative <path>, yield <path>.<mtime>,
+        # so if the file is updated, the revid will change (and the indexer's
+        # update() method can efficiently update the index).
+        for dirpath, dirnames, filenames in os.walk(self.path):
+            key, mtime = self._mkkey(dirpath)
+            if 1: # key:
+                yield self._encode('%s.%d' % (key, mtime))
+            for filename in filenames:
+                yield self._encode('%s.%d' % self._mkkey(os.path.join(dirpath, filename)))
+
+    def retrieve(self, key):
+        key = self._decode(key)
+        fn, path = self._mkpath(key)
+        meta = self._get_meta(fn, path)
+        data = self._get_data(fn, path)
         return meta, data
 
--- a/MoinMoin/storage/error.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/error.py	Sun Oct 30 01:16:40 2011 -0200
@@ -33,21 +33,6 @@
     """
     pass
 
-class AccessDeniedError(AccessError):
-    """
-    Raised if the required rights are not available to perform the action.
-    """
-    def __init__(self, username=None, priv=None, item=None):
-        if None in (username, priv, item):
-            message = _("Permission denied!")
-        else:
-            username = username or L_("You")
-            message = _("%(username)s may not %(priv)s '%(item)s'.",
-                        username=username, priv=_(priv), item=item)
-            # XXX add _('...') for all privs elsewhere for extraction
-
-        AccessError.__init__(self, message)
-
 class LockingError(AccessError):
     """
     Raised if the action could not be commited because the Item is locked
--- a/MoinMoin/storage/middleware/_tests/test_indexing.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/middleware/_tests/test_indexing.py	Sun Oct 30 01:16:40 2011 -0200
@@ -13,33 +13,38 @@
 
 import pytest
 
+from flask import g as flaskg
+
 from MoinMoin.config import NAME, SIZE, ITEMID, REVID, DATAID, HASH_ALGORITHM, CONTENT, COMMENT, \
                             LATEST_REVS, ALL_REVS
 
 from ..indexing import IndexingMiddleware
 
+from MoinMoin.auth import GivenAuth
+from MoinMoin._tests import wikiconfig
 from MoinMoin.storage.backends.stores import MutableBackend
 from MoinMoin.storage.stores.memory import BytesStore as MemoryBytesStore
 from MoinMoin.storage.stores.memory import FileStore as MemoryFileStore
+from MoinMoin.storage import create_simple_mapping
+from MoinMoin.storage.middleware import routing
+
+
+def dumper(indexer, idx_name):
+    print "*** %s ***" % idx_name
+    for kvs in indexer.dump(idx_name=idx_name):
+        for k, v in kvs:
+            print k, repr(v)[:70]
+        print
 
 
 class TestIndexingMiddleware(object):
+    reinit_storage = True # cleanup after each test method
+
     def setup_method(self, method):
-        meta_store = MemoryBytesStore()
-        data_store = MemoryFileStore()
-        self.be = MutableBackend(meta_store, data_store)
-        self.be.create()
-        self.be.open()
-        index_dir = 'ix'
-        self.imw = IndexingMiddleware(index_dir=index_dir, backend=self.be)
-        self.imw.create()
-        self.imw.open()
+        self.imw = flaskg.unprotected_storage
 
     def teardown_method(self, method):
-        self.imw.close()
-        self.imw.destroy()
-        self.be.close()
-        self.be.destroy()
+        pass
 
     def test_nonexisting_item(self):
         item = self.imw[u'foo']
@@ -251,6 +256,9 @@
         expected_all_revids.append(r.revid)
         expected_latest_revids.append(r.revid)
 
+        dumper(self.imw, ALL_REVS)
+        dumper(self.imw, LATEST_REVS)
+
         # now build a fresh index at tmp location:
         self.imw.create(tmp=True)
         self.imw.rebuild(tmp=True)
@@ -278,6 +286,9 @@
         self.imw.move_index()
         self.imw.open()
 
+        dumper(self.imw, ALL_REVS)
+        dumper(self.imw, LATEST_REVS)
+
         # read the index contents we have now:
         all_revids = [doc[REVID] for doc in self.imw._documents(idx_name=ALL_REVS)]
         latest_revids = [doc[REVID] for doc in self.imw._documents()]
@@ -292,9 +303,12 @@
         self.imw.update()
         self.imw.open()
 
+        dumper(self.imw, ALL_REVS)
+        dumper(self.imw, LATEST_REVS)
+
         # read the index contents we have now:
-        all_revids = [rev.revid for rev in self.imw.documents(idx_name=ALL_REVS)]
-        latest_revids = [rev.revid for rev in self.imw.documents()]
+        all_revids = [doc[REVID] for doc in self.imw._documents(idx_name=ALL_REVS)]
+        latest_revids = [doc[REVID] for doc in self.imw._documents()]
 
         # now it should have the previously missing rev and all should be as expected:
         for missing_revid in missing_revids:
@@ -337,29 +351,24 @@
         assert unicode(data) == doc[CONTENT]
 
 class TestProtectedIndexingMiddleware(object):
+    reinit_storage = True # cleanup after each test method
+
+    class Config(wikiconfig.Config):
+        auth = [GivenAuth(user_name=u'joe', autocreate=True), ]
+
     def setup_method(self, method):
-        meta_store = MemoryBytesStore()
-        data_store = MemoryFileStore()
-        self.be = MutableBackend(meta_store, data_store)
-        self.be.create()
-        self.be.open()
-        index_dir = 'ix'
-        self.imw = IndexingMiddleware(index_dir=index_dir, backend=self.be, user_name=u'joe', acl_support=True)
-        self.imw.create()
-        self.imw.open()
+        self.imw = flaskg.storage
 
     def teardown_method(self, method):
-        self.imw.close()
-        self.imw.destroy()
-        self.be.close()
-        self.be.destroy()
+        pass
 
     def test_documents(self):
         item_name = u'public'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=item_name, acl=u'joe:read'), StringIO('public content'))
         revid_public = r.revid
-        revids = [rev.revid for rev in self.imw.documents()]
+        revids = [rev.revid for rev in self.imw.documents()
+                  if rev.meta[NAME] != u'joe'] # the user profile is a revision in the backend
         assert revids == [revid_public]
 
     def test_getitem(self):
--- a/MoinMoin/storage/middleware/_tests/test_routing.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/middleware/_tests/test_routing.py	Sun Oct 30 01:16:40 2011 -0200
@@ -46,33 +46,26 @@
 
     return router
 
-def revid_split(revid):
-    # router revids are <backend_mountpoint>:<backend_revid>, split that:
-    return revid.rsplit(u':', 1)
-
 def test_store_get_del(router):
     root_name = u'foo'
     root_revid = router.store(dict(name=root_name), StringIO(''))
     sub_name = u'sub/bar'
     sub_revid = router.store(dict(name=sub_name), StringIO(''))
 
-    assert revid_split(root_revid)[0] == ''
-    assert revid_split(sub_revid)[0] == 'sub'
-
     # when going via the router backend, we get back fully qualified names:
-    root_meta, _ = router.retrieve(root_revid)
-    sub_meta, _ = router.retrieve(sub_revid)
+    root_meta, _ = router.retrieve(root_name, root_revid)
+    sub_meta, _ = router.retrieve(sub_name, sub_revid)
     assert root_name == root_meta[NAME]
     assert sub_name == sub_meta[NAME]
 
     # when looking into the storage backend, we see relative names (without mountpoint):
-    root_meta, _ = router.mapping[-1][1].retrieve(revid_split(root_revid)[1])
-    sub_meta, _ = router.mapping[0][1].retrieve(revid_split(sub_revid)[1])
+    root_meta, _ = router.mapping[-1][1].retrieve(root_revid)
+    sub_meta, _ = router.mapping[0][1].retrieve(sub_revid)
     assert root_name == root_meta[NAME]
     assert sub_name == 'sub' + '/' + sub_meta[NAME]
     # delete revs:
-    router.remove(root_revid)
-    router.remove(sub_revid)
+    router.remove(root_name, root_revid)
+    router.remove(sub_name, sub_revid)
 
 
 def test_store_readonly_fails(router):
@@ -100,8 +93,9 @@
 
 
 def test_iter(router):
-    existing = set(router)
+    existing_before = set([revid for mountpoint, revid in router])
     root_revid = router.store(dict(name=u'foo'), StringIO(''))
     sub_revid = router.store(dict(name=u'sub/bar'), StringIO(''))
-    assert set(router) == (set([root_revid, sub_revid])|existing)
+    existing_now = set([revid for mountpoint, revid in router])
+    assert existing_now == set([root_revid, sub_revid]) | existing_before
 
--- a/MoinMoin/storage/middleware/_tests/test_serialization.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/middleware/_tests/test_serialization.py	Sun Oct 30 01:16:40 2011 -0200
@@ -11,6 +11,7 @@
 from StringIO import StringIO
 
 from ..indexing import IndexingMiddleware
+from ..routing import Backend as RoutingBackend
 from ..serialization import serialize, deserialize
 
 from MoinMoin.storage.backends.stores import MutableBackend
@@ -49,7 +50,9 @@
 
     meta_store = BytesStore()
     data_store = FileStore()
-    backend = MutableBackend(meta_store, data_store)
+    _backend = MutableBackend(meta_store, data_store)
+    mapping = [('', _backend)]
+    backend = RoutingBackend(mapping)
     backend.create()
     backend.open()
     request.addfinalizer(backend.destroy)
--- a/MoinMoin/storage/middleware/indexing.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/middleware/indexing.py	Sun Oct 30 01:16:40 2011 -0200
@@ -64,7 +64,9 @@
 from whoosh.index import open_dir, create_in, EmptyIndexError
 from whoosh.writing import AsyncWriter
 from whoosh.filedb.multiproc import MultiSegmentWriter
-from whoosh.qparser import QueryParser, MultifieldParser, RegexPlugin
+from whoosh.qparser import QueryParser, MultifieldParser, RegexPlugin, \
+                           PseudoFieldPlugin
+from whoosh.qparser import WordNode
 from whoosh.query import Every, Term
 from whoosh.sorting import FieldFacet
 
@@ -73,7 +75,7 @@
                             CONTENT, ITEMLINKS, ITEMTRANSCLUSIONS, ACL, EMAIL, OPENID, \
                             ITEMID, REVID, CURRENT, PARENTID, \
                             LATEST_REVS, ALL_REVS
-
+from MoinMoin import user
 from MoinMoin.search.analyzers import item_name_analyzer, MimeTokenizer, AclTokenizer
 from MoinMoin.themes import utctimestamp
 from MoinMoin.util.crypto import make_uuid
@@ -379,21 +381,21 @@
             if docnum_remove is not None:
                 # we are removing a revid that is in latest revs index
                 try:
-                    latest_revids = self._find_latest_revids(self.ix[ALL_REVS], Term(ITEMID, itemid))
+                    latest_names_revids = self._find_latest_names_revids(self.ix[ALL_REVS], Term(ITEMID, itemid))
                 except AttributeError:
                     # workaround for bug #200 AttributeError: 'FieldCache' object has no attribute 'code'
-                    latest_revids = []
-                if latest_revids:
+                    latest_names_revids = []
+                if latest_names_revids:
                     # we have a latest revision, just update the document in the index:
-                    assert len(latest_revids) == 1 # this item must have only one latest revision
-                    latest_revid = latest_revids[0]
+                    assert len(latest_names_revids) == 1 # this item must have only one latest revision
+                    latest_name_revid = latest_names_revids[0]
                     # we must fetch from backend because schema for LATEST_REVS is different than for ALL_REVS
                     # (and we can't be sure we have all fields stored, too)
-                    meta, _ = self.backend.retrieve(latest_revid)
+                    meta, _ = self.backend.retrieve(*latest_name_revid)
                     # we only use meta (not data), because we do not want to transform data->content again (this
                     # is potentially expensive) as we already have the transformed content stored in ALL_REVS index:
                     with self.ix[ALL_REVS].searcher() as searcher:
-                        doc = searcher.document(revid=latest_revid)
+                        doc = searcher.document(revid=latest_name_revid[1])
                         content = doc[CONTENT]
                     doc = backend_to_index(meta, content, self.schemas[LATEST_REVS], self.wikiname)
                     writer.update_document(**doc)
@@ -415,9 +417,9 @@
         else:
             writer = MultiSegmentWriter(index, procs, limitmb)
         with writer as writer:
-            for revid in revids:
+            for mountpoint, revid in revids:
                 if mode in ['add', 'update', ]:
-                    meta, data = self.backend.retrieve(revid)
+                    meta, data = self.backend.retrieve(mountpoint, revid)
                     content = convert_to_indexable(meta, data, is_new=False)
                     doc = backend_to_index(meta, content, schema, wikiname)
                 if mode == 'update':
@@ -429,13 +431,13 @@
                 else:
                     raise ValueError("mode must be 'update', 'add' or 'delete', not '{0}'".format(mode))
 
-    def _find_latest_revids(self, index, query=None):
+    def _find_latest_names_revids(self, index, query=None):
         """
         find the latest revids using the all-revs index
 
         :param index: an up-to-date and open ALL_REVS index
         :param query: query to search only specific revisions (optional, default: all items/revisions)
-        :returns: a list of the latest revids
+        :returns: a list of tuples (name, latest revid)
         """
         if query is None:
             query = Every()
@@ -443,8 +445,10 @@
             result = searcher.search(query, groupedby=ITEMID, sortedby=FieldFacet(MTIME, reverse=True))
             by_item = result.groups(ITEMID)
             # values in v list are in same relative order as in results, so latest MTIME is first:
-            latest_revids = [searcher.stored_fields(v[0])[REVID] for v in by_item.values()]
-        return latest_revids
+            latest_names_revids = [(searcher.stored_fields(v[0])[NAME],
+                                    searcher.stored_fields(v[0])[REVID])
+                                   for v in by_item.values()]
+        return latest_names_revids
 
     def rebuild(self, tmp=False, procs=1, limitmb=256):
         """
@@ -461,13 +465,13 @@
             # build an index of all we have (so we know what we have)
             all_revids = self.backend # the backend is an iterator over all revids
             self._modify_index(index, self.schemas[ALL_REVS], self.wikiname, all_revids, 'add', procs, limitmb)
-            latest_revids = self._find_latest_revids(index)
+            latest_names_revids = self._find_latest_names_revids(index)
         finally:
             index.close()
         # now build the index of the latest revisions:
         index = open_dir(index_dir, indexname=LATEST_REVS)
         try:
-            self._modify_index(index, self.schemas[LATEST_REVS], self.wikiname, latest_revids, 'add', procs, limitmb)
+            self._modify_index(index, self.schemas[LATEST_REVS], self.wikiname, latest_names_revids, 'add', procs, limitmb)
         finally:
             index.close()
 
@@ -487,17 +491,24 @@
         index_dir = self.index_dir_tmp if tmp else self.index_dir
         index_all = open_dir(index_dir, indexname=ALL_REVS)
         try:
+            # NOTE: self.backend iterator gives (mountpoint, revid) tuples, which is NOT
+            # the same as (name, revid), thus we do the set operations just on the revids.
             # first update ALL_REVS index:
-            backend_revids = set(self.backend)
+            revids_mountpoints = dict((revid, mountpoint) for mountpoint, revid in self.backend)
+            backend_revids = set(revids_mountpoints)
             with index_all.searcher() as searcher:
-                ix_revids = set([doc[REVID] for doc in searcher.all_stored_fields()])
+                ix_revids_names = dict((doc[REVID], doc[NAME]) for doc in searcher.all_stored_fields())
+            revids_mountpoints.update(ix_revids_names) # this is needed for stuff that was deleted from storage
+            ix_revids = set(ix_revids_names)
             add_revids = backend_revids - ix_revids
             del_revids = ix_revids - backend_revids
             changed = add_revids or del_revids
+            add_revids = [(revids_mountpoints[revid], revid) for revid in add_revids]
+            del_revids = [(revids_mountpoints[revid], revid) for revid in del_revids]
             self._modify_index(index_all, self.schemas[ALL_REVS], self.wikiname, add_revids, 'add')
             self._modify_index(index_all, self.schemas[ALL_REVS], self.wikiname, del_revids, 'delete')
 
-            backend_latest_revids = set(self._find_latest_revids(index_all))
+            backend_latest_names_revids = set(self._find_latest_names_revids(index_all))
         finally:
             index_all.close()
         index_latest = open_dir(index_dir, indexname=LATEST_REVS)
@@ -505,7 +516,9 @@
             # now update LATEST_REVS index:
             with index_latest.searcher() as searcher:
                 ix_revids = set(doc[REVID] for doc in searcher.all_stored_fields())
+            backend_latest_revids = set(revid for name, revid in backend_latest_names_revids)
             upd_revids = backend_latest_revids - ix_revids
+            upd_revids = [(revids_mountpoints[revid], revid) for revid in upd_revids]
             self._modify_index(index_latest, self.schemas[LATEST_REVS], self.wikiname, upd_revids, 'update')
             self._modify_index(index_latest, self.schemas[LATEST_REVS], self.wikiname, del_revids, 'delete')
         finally:
@@ -564,6 +577,16 @@
             raise ValueError("default_fields list must at least contain one field name")
         # TODO before using the RegexPlugin, require a whoosh release that fixes whoosh issues #205 and #206
         #qp.add_plugin(RegexPlugin())
+        def username_pseudo_field(node):
+            username = node.text
+            users = user.search_users(**{NAME_EXACT: username})
+            if users:
+                userid = users[0].meta['userid']
+                node = WordNode(userid)
+                node.set_fieldname("userid")
+                return node
+            return node
+        qp.add_plugin(PseudoFieldPlugin({'username': username_pseudo_field}))
         return qp
 
     def search(self, q, idx_name=LATEST_REVS, **kw):
@@ -754,6 +777,8 @@
         meta[ITEMID] = self.itemid
         if MTIME not in meta:
             meta[MTIME] = int(time.time())
+        #if CONTENTTYPE not in meta:
+        #    meta[CONTENTTYPE] = u'application/octet-stream'
         content = convert_to_indexable(meta, data, is_new=True)
         return meta, data, content
 
@@ -798,7 +823,8 @@
         """
         Destroy revision <revid>.
         """
-        self.backend.remove(revid)
+        rev = Revision(self, revid)
+        self.backend.remove(rev.name, revid)
         self.indexer.remove_revision(revid)
 
     def destroy_all_revisions(self):
@@ -840,7 +866,7 @@
         return self.meta.get(NAME, 'DoesNotExist')
 
     def _load(self):
-        meta, data = self.backend.retrieve(self.revid) # raises KeyError if rev does not exist
+        meta, data = self.backend.retrieve(self._doc[NAME], self.revid) # raises KeyError if rev does not exist
         self.meta = Meta(self, self._doc, meta)
         self._data = data
         return meta, data
--- a/MoinMoin/storage/middleware/routing.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/middleware/routing.py	Sun Oct 30 01:16:40 2011 -0200
@@ -73,11 +73,10 @@
         #       can be given to get_revision and be routed to the right backend.
         for mountpoint, backend in self.mapping:
             for revid in backend:
-                yield u'{0}:{1}'.format(mountpoint, revid)
+                yield (mountpoint, revid)
 
-    def retrieve(self, revid):
-        mountpoint, revid = revid.rsplit(u':', 1)
-        backend = self._get_backend(mountpoint)[0]
+    def retrieve(self, name, revid):
+        backend, _, mountpoint = self._get_backend(name)
         meta, data = backend.retrieve(revid)
         if mountpoint:
             meta[NAME] = u'{0}/{1}'.format(mountpoint, meta[NAME])
@@ -97,16 +96,17 @@
             #XXX else: log info?
 
     def store(self, meta, data):
-        itemname = meta[NAME]
-        backend, itemname, mountpoint = self._get_backend(itemname)
+        mountpoint_itemname = meta[NAME]
+        backend, itemname, mountpoint = self._get_backend(mountpoint_itemname)
         if not isinstance(backend, MutableBackendBase):
             raise TypeError('backend {0!r} mounted at {1!r} is readonly'.format(backend, mountpoint))
         meta[NAME] = itemname
-        return u'{0}:{1}'.format(mountpoint, backend.store(meta, data))
+        revid = backend.store(meta, data)
+        meta[NAME] = mountpoint_itemname # restore the original name
+        return revid
 
-    def remove(self, revid):
-        mountpoint, revid = revid.rsplit(u':', 1)
-        backend = self._get_backend(mountpoint)[0]
+    def remove(self, name, revid):
+        backend, _, mountpoint = self._get_backend(name)
         if not isinstance(backend, MutableBackendBase):
             raise TypeError('backend {0!r} mounted at {1!r} is readonly'.format(backend, mountpoint))
         backend.remove(revid)
--- a/MoinMoin/storage/middleware/serialization.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/middleware/serialization.py	Sun Oct 30 01:16:40 2011 -0200
@@ -45,8 +45,8 @@
             yield block
 
 def serialize_iter(backend):
-    for revid in backend:
-        meta, data = backend.retrieve(revid)
+    for mountpoint, revid in backend:
+        meta, data = backend.retrieve(mountpoint, revid)
         for data in serialize_rev(meta, data):
             yield data
     for data in serialize_rev(None, None):
--- a/MoinMoin/storage/stores/_tests/conftest.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/stores/_tests/conftest.py	Sun Oct 30 01:16:40 2011 -0200
@@ -11,14 +11,14 @@
 import pytest
 from ..wrappers import ByteToStreamWrappingStore
 
-from MoinMoin._tests import test_connection
+from MoinMoin._tests import check_connection
 
 STORES_PACKAGE = 'MoinMoin.storage.stores'
 
 STORES = 'fs kc memory sqlite sqlite:compressed sqla'.split()
 try:
     # check if we can connect to the kt server
-    test_connection(1978)
+    check_connection(1978)
     STORES.append('kt')
 except Exception:
     pass
--- a/MoinMoin/storage/stores/_tests/test_fs.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/stores/_tests/test_fs.py	Sun Oct 30 01:16:40 2011 -0200
@@ -34,3 +34,8 @@
     store.destroy()
     assert not target.check()
 
+
+@pytest.mark.multi(Store=[BytesStore, FileStore])
+def test_from_uri(tmpdir, Store):
+    store = Store.from_uri("%s" % tmpdir)
+    assert store.path == tmpdir
--- a/MoinMoin/storage/stores/_tests/test_kt.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/stores/_tests/test_kt.py	Sun Oct 30 01:16:40 2011 -0200
@@ -13,9 +13,9 @@
 pytest.importorskip('MoinMoin.storage.stores.kt')
 
 
-from MoinMoin._tests import test_connection
+from MoinMoin._tests import check_connection
 try:
-    test_connection(1978)
+    check_connection(1978)
 except Exception as err:
     pytest.skip(str(err))
 
@@ -35,3 +35,13 @@
     store = Store()
     store.destroy()
 
+@pytest.mark.multi(Store=[BytesStore, FileStore])
+def test_from_uri(Store):
+    store = Store.from_uri("localhost")
+    assert store.host == 'localhost'
+    assert store.port == 1978
+
+    store = Store.from_uri("localhost:1970")
+    assert store.host == 'localhost'
+    assert store.port == 1970
+
--- a/MoinMoin/storage/stores/_tests/test_memory.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/stores/_tests/test_memory.py	Sun Oct 30 01:16:40 2011 -0200
@@ -29,3 +29,7 @@
     store.destroy()
     assert store._st is None
 
+@pytest.mark.multi(Store=[BytesStore, FileStore])
+def test_from_uri(Store):
+    store = Store.from_uri("mem://")
+    assert isinstance(store, Store)
--- a/MoinMoin/storage/stores/_tests/test_sqla.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/stores/_tests/test_sqla.py	Sun Oct 30 01:16:40 2011 -0200
@@ -8,8 +8,7 @@
 
 
 import pytest
-
-pytest.importorskip('storage.stores.sqla')
+pytest.importorskip('MoinMoin.storage.stores.sqla')
 from ..sqla import BytesStore, FileStore
 
 @pytest.mark.multi(Store=[BytesStore, FileStore])
@@ -29,3 +28,8 @@
     store.destroy()
     # XXX: check for dropped table
 
+@pytest.mark.multi(Store=[BytesStore, FileStore])
+def test_from_uri(tmpdir, Store):
+    store = Store.from_uri("sqlite://%s/test_base" % tmpdir)
+    assert store.db_uri == "sqlite://%s/test_base" % tmpdir
+    assert store.table_name == "test_base"
--- a/MoinMoin/storage/stores/_tests/test_sqlite.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/stores/_tests/test_sqlite.py	Sun Oct 30 01:16:40 2011 -0200
@@ -46,3 +46,14 @@
     store.destroy()
     # XXX: check for dropped table
 
+@pytest.mark.multi(Store=[BytesStore, FileStore])
+def test_from_uri(tmpdir, Store):
+    store = Store.from_uri("%s:test_table:0" % tmpdir)
+    assert store.db_name == tmpdir
+    assert store.table_name == 'test_table'
+    assert store.compression_level == 0
+
+    store = Store.from_uri("%s:test_table:2" % tmpdir)
+    assert store.db_name == tmpdir
+    assert store.table_name == 'test_table'
+    assert store.compression_level == 2
--- a/MoinMoin/storage/stores/kc.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/stores/kc.py	Sun Oct 30 01:16:40 2011 -0200
@@ -17,7 +17,7 @@
 
 from __future__ import absolute_import, division
 
-import os
+import os, errno
 from StringIO import StringIO
 
 from kyotocabinet import *
@@ -50,6 +50,12 @@
         self.db_opts = db_opts
 
     def create(self):
+        basedir = os.path.dirname(self.path)
+        try:
+            os.makedirs(basedir)
+        except OSError as e:
+            if e.errno != errno.EEXIST:
+                raise
         self.open(mode=self.mode|DB.OCREATE)
         self.close()
 
--- a/MoinMoin/storage/stores/kt.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/stores/kt.py	Sun Oct 30 01:16:40 2011 -0200
@@ -26,7 +26,10 @@
     """
     @classmethod
     def from_uri(cls, uri):
-        return cls(uri)
+        params = uri.split(':')
+        if len(params) == 2:
+            params[1] = int(params[1])
+        return cls(*params)
 
     def __init__(self, host='127.0.0.1', port=1978, timeout=30):
         """
--- a/MoinMoin/storage/stores/sqla.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/stores/sqla.py	Sun Oct 30 01:16:40 2011 -0200
@@ -27,7 +27,18 @@
     """
     @classmethod
     def from_uri(cls, uri):
-        return cls(uri)
+        """
+        Create a new cls instance from the using the uri
+
+        :param cls: Class to create
+        :param uri: The database uri that we pass on to SQLAlchemy.
+        """
+
+        params = [uri]
+        if '/' in uri.rsplit("//")[-1]:
+            table_name = uri.rsplit("/")[-1]
+            params.append(table_name)
+        return cls(*params)
 
     def __init__(self, db_uri=None, table_name='store', verbose=False):
         """
--- a/MoinMoin/storage/stores/sqlite.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/storage/stores/sqlite.py	Sun Oct 30 01:16:40 2011 -0200
@@ -27,7 +27,18 @@
     """
     @classmethod
     def from_uri(cls, uri):
-        return cls(uri)
+        """
+        Create a new cls instance using the parameters provided in the uri
+
+        :param cls: Class to create
+        :param uri: The URI should follow the following template
+                    db_name:table_name:compression_level
+                    where table_name and compression level are optional
+        """
+        params = uri.split(":")
+        if len(params) == 3:
+            params[2] = int(params[2])
+        return cls(*params)
 
     def __init__(self, db_name, table_name='store', compression_level=0):
         """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/404.html	Sun Oct 30 01:16:40 2011 -0200
@@ -0,0 +1,14 @@
+{% import "forms.html" as forms %}
+{% extends theme("layout.html") %}
+{% block content %}
+
+<p>
+{{ _("The item '%(item_name)s' does not exist.", item_name=item_name) }}
+</p>
+
+{% endblock %}
+
+
+
+
+
--- a/MoinMoin/templates/error.html	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/templates/error.html	Sun Oct 30 01:16:40 2011 -0200
@@ -1,6 +1,6 @@
 {% extends theme("layout.html") %}
 {% block content %}
 <h1>{{ title }}</h1>
-{{ description }}
+<p>{{ description }}</p>
 {% endblock %}
 
--- a/MoinMoin/templates/index.html	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/templates/index.html	Sun Oct 30 01:16:40 2011 -0200
@@ -1,4 +1,4 @@
-{% extends theme("show.html") %}
+{% extends theme("layout.html") %}
 {% import "forms.html" as forms %}
 {% block head_scripts %}
 {{ super() }}
--- a/MoinMoin/templates/layout.html	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/templates/layout.html	Sun Oct 30 01:16:40 2011 -0200
@@ -73,10 +73,20 @@
         <span id="moin-pagelocation">
             <span class="moin-pagepath">
                 {% for segment_name, segment_path, exists in theme_supp.location_breadcrumbs(item_name) -%}
-                    <a href="{{ url_for('frontend.show_item', item_name=segment_path) }}" {% if not exists %}class="moin-nonexistent"{% endif %}>
-                        {{ segment_name|shorten_item_name }}
-                    </a>
-                    {% if not loop.last -%}<span class="sep">/</span>{%- endif %}
+                    {% if not loop.last -%}
+                        <a href="{{ url_for('frontend.show_item', item_name=segment_path) }}" {% if not exists %}class="moin-nonexistent"{% endif %}>
+                            {{ segment_name|shorten_item_name }}
+                        </a>
+			<span class="sep">/</span>
+                    {% else %}
+		        {% if title_name %}
+                            {{ title_name }}
+                        {% else %}
+                        <a href="{{ url_for('frontend.show_item', item_name=segment_path) }}" {% if not exists %}class="moin-nonexistent"{% endif %}>
+                            {{ segment_name|shorten_item_name }}
+                        </a>
+		        {%- endif %}
+                    {%- endif %}
                 {%- endfor %}
             </span>
         </span>
--- a/MoinMoin/templates/usersettings.html	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/templates/usersettings.html	Sun Oct 30 01:16:40 2011 -0200
@@ -3,7 +3,7 @@
 
 {% block item %}
 {% if part == 'main' %}
-<h1>{{ _("Settings") }}</h1>
+<h1>{{ _("User Settings") }}</h1>
 <ul>
     <li><a href="{{ url_for('frontend.usersettings', part='personal') }}">{{ _("Personal Settings") }}</a></li>
     <li><a href="{{ url_for('frontend.usersettings', part='password') }}">{{ _("Change password") }}</a></li>
--- a/MoinMoin/themes/modernized/static/css/common.css	Sun Oct 30 00:24:26 2011 -0200
+++ b/MoinMoin/themes/modernized/static/css/common.css	Sun Oct 30 01:16:40 2011 -0200
@@ -107,7 +107,7 @@
 
 /* pre */
 pre { border: 1px solid #AEBDCC; background-color: #F3F5F7; padding: 5px; clear: both;
-            font-family: courier, monospace; margin: .33em 0; white-space: pre; }
+            font-family: monospace; margin: .33em 0; white-space: pre; }
 pre.comment { background-color: #CCC; color: red; padding: 0; margin: 0; border: 0; }
 pre.comment:before { content: url(../img/attention.png); }
 
@@ -333,9 +333,9 @@
 
 .moin-diff-line-number { background-color: #C0C0C0; }
 .moin-diff-added { background-color: #E0FFE0; vertical-align: top; width: 50%; white-space: pre-wrap; word-wrap: break-word;
-            font-family: courier, monospace; }
+            font-family: monospace; }
 .moin-diff-removed { background-color: #FFFFE0; vertical-align: top; width: 50%; white-space: pre-wrap; word-wrap: break-word;
-            font-family: courier, monospace; }
+            font-family: monospace; }
 .moin-diff-added span { background-color: #80FF80; }
 .moin-diff-removed span { background-color: #FFFF80; }
 
@@ -670,7 +670,7 @@
 
 @media print {
 
-html { font-family: Times, serif; font-size: 12pt; width: 100%; }
+html { font-family: serif; font-size: 12pt; width: 100%; }
 body, #moin-page, #moin-page, #moin-content-data { margin: 0; padding: 0; }
 
 a, a:visited,
--- a/docs/admin/configure.rst	Sun Oct 30 00:24:26 2011 -0200
+++ b/docs/admin/configure.rst	Sun Oct 30 01:16:40 2011 -0200
@@ -1031,14 +1031,14 @@
 The `uri` depends on the kind of storage backend and stores you want to use
 (see below). Usually it is a URL-like string that looks like::
 
-    stores:fs:/srv/mywiki/%(nsname)s/%%(kind)s
+    stores:fs:/srv/mywiki/%(nsname)s/%(kind)s
     
 `stores` is the name of the backend, followed by a colon, followed by a store
 specification. `fs` is the name of the store, followed by a specification
 that makes sense for the fs (filesystem) store (== a path with placeholders).
 
 `%(nsname)s` placeholder will be replaced 'content' or 'userprofiles' for
-the respective backend. `%%(kind)s` will be replaced by 'meta' or 'data'
+the respective backend. `%(kind)s` will be replaced by 'meta' or 'data'
 later.
 
 In this case, the mapping created will look like this:
@@ -1100,7 +1100,7 @@
 
     data_dir = '/srv/mywiki/data'
     namespace_mapping, acl_mapping = create_simple_mapping(
-        uri='stores:fs:%s/%%(nsname)s/%%%%(kind)s' % data_dir,
+        uri='stores:fs:{0}/%(nsname)s/%(kind)s'.format(data_dir),
         content_acl=dict(before=u'WikiAdmin:read,write,create,destroy',
                          default=u'All:read,write,create',
                          after=u'', ),
@@ -1142,9 +1142,16 @@
 * very fast
 * single-process only, local only
 
-.. todo:
+`uri` for `create_simple_mapping` looks like e.g.::
 
-   add kc store configuration example
+    stores:kc:/srv/mywiki/data/%(nsname)s_%(kind)s.kch
+
+Please see the kyoto cabinet docs about the part after `kc:`.
+
+If you use kc with the builtin server of moin, you must not use the reloader,
+but disable it by commandline option::
+
+  moin moin -r
 
 
 kt store
--- a/wikiconfig.py	Sun Oct 30 00:24:26 2011 -0200
+++ b/wikiconfig.py	Sun Oct 30 01:16:40 2011 -0200
@@ -29,7 +29,7 @@
     # 'hg:' instead to indicate that you want to use the mercurial backend.
     # Alternatively you can set up the mapping yourself (see HelpOnStorageConfiguration).
     namespace_mapping, acl_mapping = create_simple_mapping(
-                            uri='stores:fs:%s/%%(nsname)s/%%%%(kind)s' % data_dir,
+                            uri='stores:fs:{0}/%(nsname)s/%(kind)s'.format(data_dir),
                             # XXX we use rather relaxed ACLs for the development wiki:
                             content_acl=dict(before=u'',
                                              default=u'All:read,write,create,destroy,admin',