changeset 1022:2256d29e3cd5

file merged
author Fabio Montefuscolo <fabio.montefuscolo@gmail.com>
date Sat, 29 Oct 2011 19:22:16 -0200
parents fdd573c2808c (diff) 2c4087f61d39 (current diff)
children 80043704f37e 1f3d66455a88
files
diffstat 14 files changed, 286 insertions(+), 146 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/apps/admin/views.py	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/apps/admin/views.py	Sat Oct 29 19:22:16 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)
 
@@ -230,7 +230,7 @@
             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	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/apps/feed/_tests/test_feed.py	Sat Oct 29 19:22:16 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	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/apps/feed/views.py	Sat Oct 29 19:22:16 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	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/apps/frontend/views.py	Sat Oct 29 19:22:16 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/items/__init__.py	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/items/__init__.py	Sat Oct 29 19:22:16 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, \
@@ -629,6 +629,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)
@@ -705,7 +711,7 @@
             if form.validate():
                 try:
                     self.modify() # XXX
-                except AccessDeniedError:
+                except AccessDenied:
                     abort(403)
                 else:
                     return redirect(url_for_item(self.name))
@@ -716,12 +722,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]:
@@ -1166,7 +1166,7 @@
             if form.validate():
                 try:
                     self.modify() # XXX
-                except AccessDeniedError:
+                except AccessDenied:
                     abort(403)
                 else:
                     return redirect(url_for_item(self.name))
@@ -1324,7 +1324,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
@@ -1414,7 +1414,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
@@ -1500,7 +1500,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	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/macro/RandomItem.py	Sat Oct 29 19:22:16 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/storage/backends/_tests/test_fileserver.py	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/storage/backends/_tests/test_fileserver.py	Sat Oct 29 19:22:16 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	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/storage/backends/fileserver.py	Sat Oct 29 19:22:16 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	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/storage/error.py	Sat Oct 29 19:22:16 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/indexing.py	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/storage/middleware/indexing.py	Sat Oct 29 19:22:16 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
@@ -575,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):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/404.html	Sat Oct 29 19:22:16 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	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/templates/error.html	Sat Oct 29 19:22:16 2011 -0200
@@ -1,6 +1,6 @@
 {% extends theme("layout.html") %}
 {% block content %}
 <h1>{{ title }}</h1>
-{{ description }}
+<p>{{ description }}</p>
 {% endblock %}
 
--- a/MoinMoin/templates/layout.html	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/templates/layout.html	Sat Oct 29 19:22:16 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	Sat Oct 29 18:45:40 2011 -0200
+++ b/MoinMoin/templates/usersettings.html	Sat Oct 29 19:22:16 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>