changeset 2228:c992a1dda217

Merged thomaswaldmann/moin-2.0 into default
author Ashutosh Singla <ashu1461@gmail.com>
date Mon, 22 Jul 2013 22:51:34 +0530
parents 00debb5be61a (diff) 78ed0ce6b718 (current diff)
children 63bc1dd3d321
files MoinMoin/apps/frontend/views.py MoinMoin/constants/keys.py MoinMoin/items/__init__.py MoinMoin/templates/common.js MoinMoin/templates/ticket.html
diffstat 18 files changed, 702 insertions(+), 374 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/_tests/test_forms.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/_tests/test_forms.py	Mon Jul 22 22:51:34 2013 +0530
@@ -6,9 +6,17 @@
 """
 
 import datetime
+import json
 from calendar import timegm
 
-from MoinMoin.forms import DateTimeUNIX
+from flask import current_app as app
+from flask import g as flaskg
+
+from MoinMoin.forms import DateTimeUNIX, JSON
+from MoinMoin.util.interwiki import CompositeName
+from MoinMoin.items import Item
+from MoinMoin._tests import become_trusted
+from MoinMoin.constants.keys import ITEMID, NAME, CONTENTTYPE, NAMESPACE, FQNAME
 
 
 def test_datetimeunix():
@@ -42,3 +50,27 @@
     d = DateTimeUNIX(None)
     assert d.value is None
     assert d.u == u''
+
+
+def test_validjson():
+    app.cfg.namespace_mapping = [(u'', 'default_backend'), (u'ns1/', 'default_backend'), (u'ns1/ns2/', 'other_backend')]
+    item = Item.create(u'ns1/ns2/existingname')
+    meta = {NAMESPACE: u'ns1/ns2', CONTENTTYPE: u'text/plain;charset=utf-8'}
+    become_trusted()
+    item._save(meta, data='This is a valid Item.')
+
+    valid_itemid = 'a1924e3d0a34497eab18563299d32178'
+    # ('names', 'namespace', 'field', 'value', 'result')
+    tests = [([u'somename', u'@revid'], '', '', 'somename', False),
+             ([u'bar', u'ns1'], '', '', 'bar', False),
+             ([u'foo', u'foo', u'bar'], '', '', 'foo', False),
+             ([u'ns1ns2ns3', u'ns1/subitem'], '', '', 'valid', False),
+             ([u'foobar', u'validname'], '', ITEMID, valid_itemid + '8080', False),
+             ([u'barfoo', u'validname'], '', ITEMID, valid_itemid.replace('a', 'y'), False),
+             ([], '', 'itemid', valid_itemid, True),
+             ([u'existingname'], 'ns1/ns2', '', 'existingname', False),
+             ]
+    for name, namespace, field, value, result in tests:
+        x = JSON(json.dumps({NAME: name}))
+        state = {FQNAME: CompositeName(namespace, field, value), ITEMID: None}
+        assert x.validate(state) == result
--- a/MoinMoin/_tests/test_user.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/_tests/test_user.py	Mon Jul 22 22:51:34 2013 +0530
@@ -185,7 +185,7 @@
         # add quicklink
         theUser.quicklink(u'Test_page_added')
         result_on_addition = theUser.quicklinks
-        expected = [u'MoinTest:Test_page_added']
+        expected = [u'MoinTest/Test_page_added']
         assert result_on_addition == expected
 
         # remove quicklink
@@ -212,7 +212,7 @@
         theUser.add_trail(u'item_added')
         theUser = user.User(name=name, password=password)
         result = theUser.get_trail()
-        expected = [u'MoinTest:item_added']
+        expected = [u'MoinTest/item_added']
         assert result == expected
 
     # Sessions -------------------------------------------------------
--- a/MoinMoin/apps/frontend/views.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/apps/frontend/views.py	Mon Jul 22 22:51:34 2013 +0530
@@ -47,15 +47,16 @@
 from MoinMoin.themes import render_template, contenttype_to_class
 from MoinMoin.apps.frontend import frontend
 from MoinMoin.forms import (OptionalText, RequiredText, URL, YourOpenID, YourEmail, RequiredPassword, Checkbox,
-                            InlineCheckbox, Select, Names, Tags, Natural, Hidden, MultiSelect, Enum)
-from MoinMoin.items import BaseChangeForm, Item, NonExistent, NameNotUniqueError
+                            InlineCheckbox, Select, Names, Tags, Natural, Hidden, MultiSelect, Enum, validate_name,
+                            NameNotValidError)
+from MoinMoin.items import BaseChangeForm, Item, NonExistent, NameNotUniqueError, FieldNotUniqueError
 from MoinMoin.items.content import content_registry
 from MoinMoin import user, util
 from MoinMoin.constants.keys import *
 from MoinMoin.constants.itemtypes import ITEMTYPE_DEFAULT
 from MoinMoin.constants.chartypes import CHARS_UPPER, CHARS_LOWER
 from MoinMoin.util import crypto
-from MoinMoin.util.interwiki import url_for_item
+from MoinMoin.util.interwiki import url_for_item, split_fqname
 from MoinMoin.search import SearchForm
 from MoinMoin.search.analyzers import item_name_analyzer
 from MoinMoin.security.textcha import TextCha, TextChaizedForm
@@ -358,14 +359,24 @@
 @frontend.route('/<itemname:item_name>', defaults=dict(rev=CURRENT), methods=['GET', 'POST'])
 @frontend.route('/+show/+<rev>/<itemname:item_name>', methods=['GET'])
 def show_item(item_name, rev):
-    flaskg.user.add_trail(item_name)
     item_displayed.send(app._get_current_object(),
                         item_name=item_name)
     try:
         item = Item.create(item_name, rev_id=rev)
+        flaskg.user.add_trail(item_name)
         result = item.do_show(rev)
     except AccessDenied:
         abort(403)
+    except FieldNotUniqueError:
+        fqname = split_fqname(item_name)
+        revs = flaskg.storage.documents(**fqname.query)
+        fq_names = []
+        for rev in revs:
+            fq_names.extend(rev.fqnames)
+        return render_template("link_list_no_item_panel.html",
+                               headline=_("Items with %(field)s %(value)s", field=fqname.field, value=fqname.value),
+                               item_name=fqname.fullname,
+                               fq_names=fq_names)
     return result
 
 
@@ -513,8 +524,27 @@
     target = RequiredText.using(label=L_('Target')).with_properties(placeholder=L_("The name of the target item"))
 
 
+class ValidRevert(Validator):
+    """
+    Validator for a valid revert form.
+    """
+    invalid_name_msg = ''
+
+    def validate(self, element, state):
+        """
+        Check whether the names present in the previous meta are not taken by some other item.
+        """
+        try:
+            validate_name(state['meta'], state[FQNAME], state['meta'].get(ITEMID))
+            return True
+        except NameNotValidError as e:
+            self.invalid_name_msg = _(e)
+            return self.note_error(element, state, 'invalid_name_msg')
+
+
 class RevertItemForm(BaseChangeForm):
     name = 'revert_item'
+    validators = [ValidRevert()]
 
 
 class DeleteItemForm(BaseChangeForm):
@@ -545,11 +575,12 @@
     elif request.method == 'POST':
         form = RevertItemForm.from_flat(request.form)
         TextCha(form).amend_form()
-        if form.validate():
+        state = dict(fqname=item.fqname, meta=dict(item.meta))
+        if form.validate(state):
             item.revert(form['comment'])
             return redirect(url_for_item(item_name))
     return render_template(item.revert_template,
-                           item=item, item_name=item_name,
+                           item=item, fqname=item.fqname,
                            rev_id=rev,
                            form=form,
     )
@@ -822,7 +853,7 @@
     q = And([Term(WIKINAME, app.cfg.interwikiname),
              Term(USERID, userid)])
     revs = flaskg.storage.search(q, idx_name=ALL_REVS)
-    return [rev.name for rev in revs]
+    return set([rev.name for rev in revs])
 
 
 @frontend.route('/+backrefs/<itemname:item_name>')
--- a/MoinMoin/constants/keys.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/constants/keys.py	Mon Jul 22 22:51:34 2013 +0530
@@ -114,3 +114,16 @@
 # index names
 LATEST_REVS = 'latest_revs'
 ALL_REVS = 'all_revs'
+
+# key for composite name
+FQNAME = u'fqname'
+# Values that FIELD can take in the composite name: [NAMESPACE/][@FIELD/]NAME
+FIELDS = [
+    NAME_EXACT, ITEMID, REVID, TAGS, USERID, ITEMLINKS, ITEMTRANSCLUSIONS
+]
+# Fields that can be used as a unique identifier.
+UFIELDS = [
+    NAME_EXACT, ITEMID, REVID,
+]
+# Unique fields that are stored as list.
+UFIELDS_TYPELIST = [NAME_EXACT, ]
--- a/MoinMoin/forms.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/forms.py	Mon Jul 22 22:51:34 2013 +0530
@@ -23,11 +23,13 @@
 from whoosh.query import Term, Or, Not, And
 
 from flask import g as flaskg
+from flask import current_app as app
 
 from MoinMoin.constants.forms import *
-from MoinMoin.constants.keys import ITEMID, NAME, LATEST_REVS
+from MoinMoin.constants.keys import ITEMID, NAME, LATEST_REVS, NAMESPACE, FQNAME
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.util.forms import FileStorage
+from MoinMoin.storage.middleware.validation import uuid_validator
 
 
 class Enum(BaseEnum):
@@ -69,40 +71,69 @@
 RequiredMultilineText = MultilineText.validated_by(Present())
 
 
+class NameNotValidError(ValueError):
+    """
+    The name is not valid.
+    """
+
+
+def validate_name(meta, fqname, itemid):
+    """
+    Check whether the names are valid.
+    Will just return, if they are valid, will raise a NameNotValidError if not.
+    """
+    names = meta.get(NAME)
+    current_namespace = meta.get(NAMESPACE, fqname.namespace)
+    if not names:
+        return
+    if len(names) != len(set(names)):
+        raise NameNotValidError(L_("The names in the name list must be unique."))
+    # Item names must not start with '@' or '+', '@something' denotes a field where as '+something' denotes a view.
+    invalid_names = [name for name in names if name.startswith(('@', '+'))]
+    if invalid_names:
+        raise NameNotValidError(L_("Item names (%(invalid_names)s) must not start with '@' or '+'", invalid_names=", ".join(invalid_names)))
+    # Item names must not match with existing namespaces.
+    namespaces = [namespace.rstrip('/') for namespace, _ in app.cfg.namespace_mapping]
+    invalid_names = [name for name in names if name.split('/', 1)[0] in namespaces]
+    if invalid_names:
+        raise NameNotValidError(L_("Item names (%(invalid_names)s) must not match with existing namespaces.", invalid_names=", ".join(invalid_names)))
+    query = And([Or([Term(NAME, name) for name in names]), Term(NAMESPACE, current_namespace)])
+    # There should be not item existing with the same name.
+    if itemid is not None:
+        query = And([query, Not(Term(ITEMID, itemid))])  # search for items except the current item.
+    with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher:
+        results = searcher.search(query)
+        duplicate_names = {name for result in results for name in result[NAME] if name in names}
+        if duplicate_names:
+            raise NameNotValidError(L_("Item(s) named %(duplicate_names)s already exist.", duplicate_names=", ".join(duplicate_names)))
+
+
 class ValidJSON(Validator):
     """Validator for JSON
     """
     invalid_json_msg = L_('Invalid JSON.')
     invalid_name_msg = ""
+    invalid_itemid_msg = L_('Itemid not a proper UUID')
 
-    def validname(self, meta, name, itemid):
-        names = meta.get(NAME)
-        if names is None:
-            self.invalid_name_msg = L_("No name field in the JSON meta.")
-            return False
-        if len(names) != len(set(names)):
-            self.invalid_name_msg = L_("The names in the JSON name list must be unique.")
+    def validitemid(self, itemid):
+        if not itemid:
+            self.invalid_itemid_msg = L_("No ITEMID field")
             return False
-        query = Or([Term(NAME, x) for x in names])
-        if itemid is not None:
-            query = And([query, Not(Term(ITEMID, itemid))])
-        duplicate_names = set()
-        with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher:
-            results = searcher.search(query)
-            for result in results:
-                duplicate_names |= set([x for x in result[NAME] if x in names])
-        if duplicate_names:
-            self.invalid_name_msg = L_("Item(s) named %(duplicate_names)s already exist.", duplicate_names=", ".join(duplicate_names))
-            return False
-        return True
+        return uuid_validator(String(itemid), None)
 
     def validate(self, element, state):
         try:
             meta = json.loads(element.value)
         except:  # catch ANY exception that happens due to unserializing
             return self.note_error(element, state, 'invalid_json_msg')
-        if not self.validname(meta, state[NAME], state[ITEMID]):
+        try:
+            validate_name(meta, state[FQNAME], state[ITEMID])
+        except NameNotValidError as e:
+            self.invalid_name_msg = _(e)
             return self.note_error(element, state, 'invalid_name_msg')
+        if state[FQNAME].field == ITEMID:
+            if not self.validitemid(meta.get(ITEMID, state[FQNAME].value)):
+                return self.note_error(element, state, 'invalid_itemid_msg')
         return True
 
 JSON = OptionalMultilineText.with_properties(lang='en', dir='ltr').validated_by(ValidJSON())
--- a/MoinMoin/items/__init__.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/items/__init__.py	Mon Jul 22 22:51:34 2013 +0530
@@ -41,14 +41,15 @@
 from MoinMoin.i18n import L_
 from MoinMoin.themes import render_template
 from MoinMoin.util.mime import Type
-from MoinMoin.util.interwiki import url_for_item
+from MoinMoin.util.interwiki import url_for_item, split_fqname, get_fqname
 from MoinMoin.util.registry import RegistryBase
 from MoinMoin.util.clock import timed
 from MoinMoin.forms import RequiredText, OptionalText, JSON, Tags
 from MoinMoin.constants.keys import (
     NAME, NAME_OLD, NAME_EXACT, WIKINAME, MTIME, ITEMTYPE,
     CONTENTTYPE, SIZE, ACTION, ADDRESS, HOSTNAME, USERID, COMMENT,
-    HASH_ALGORITHM, ITEMID, REVID, DATAID, CURRENT, PARENTID
+    HASH_ALGORITHM, ITEMID, REVID, DATAID, CURRENT, PARENTID, NAMESPACE,
+    UFIELDS_TYPELIST, UFIELDS
 )
 from MoinMoin.constants.contenttypes import CHARSET, CONTENTTYPE_NONEXISTENT
 from MoinMoin.constants.itemtypes import (
@@ -102,20 +103,28 @@
     """ if we have no stored Revision, we use this dummy """
     def __init__(self, item, itemtype=None, contenttype=None):
         self.item = item
+        fqname = item.fqname
         self.meta = {
             ITEMTYPE: itemtype or ITEMTYPE_NONEXISTENT,
             CONTENTTYPE: contenttype or CONTENTTYPE_NONEXISTENT
         }
         self.data = StringIO('')
         self.revid = None
-        if self.item:
-            self.meta[NAME] = [self.item.name]
+        if item:
+            self.meta[NAMESPACE] = fqname.namespace
+            if fqname.field in UFIELDS_TYPELIST:
+                if fqname.field == NAME_EXACT:
+                    self.meta[NAME] = [fqname.value]
+                else:
+                    self.meta[fqname.field] = [fqname.value]
+            else:
+                self.meta[fqname.field] = fqname.value
 
 
 class DummyItem(object):
     """ if we have no stored Item, we use this dummy """
-    def __init__(self, name):
-        self.name = name
+    def __init__(self, fqname):
+        self.fqname = fqname
 
     def list_revisions(self):
         return []  # same as an empty Item
@@ -124,7 +133,7 @@
         return True
 
 
-def get_storage_revision(name, itemtype=None, contenttype=None, rev_id=CURRENT, item=None):
+def get_storage_revision(fqname, itemtype=None, contenttype=None, rev_id=CURRENT, item=None):
     """
     Get a storage Revision.
 
@@ -144,18 +153,20 @@
     :itemtype and :contenttype are used when creating a DummyRev, where
     metadata is not available from the storage.
     """
+    rev_id = fqname.value if fqname.field == REVID else rev_id
     if 1:  # try:
         if item is None:
-            item = flaskg.storage[name]
+            item = flaskg.storage.get_item(**fqname.query)
         else:
-            name = item.name
+            if item.fqname:
+                fqname = item.fqname
     if not item:  # except NoSuchItemError:
-        logging.debug("No such item: {0!r}".format(name))
-        item = DummyItem(name)
+        logging.debug("No such item: {0!r}".format(fqname))
+        item = DummyItem(fqname)
         rev = DummyRev(item, itemtype, contenttype)
-        logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(name, contenttype))
+        logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(fqname, contenttype))
     else:
-        logging.debug("Got item: {0!r}".format(name))
+        logging.debug("Got item: {0!r}".format(fqname))
         try:
             rev = item.get_revision(rev_id)
         except KeyError:  # NoSuchRevisionError:
@@ -163,10 +174,10 @@
                 rev = item.get_revision(CURRENT)  # fall back to current revision
                 # XXX add some message about invalid revision
             except KeyError:  # NoSuchRevisionError:
-                logging.debug("Item {0!r} has no revisions.".format(name))
+                logging.debug("Item {0!r} has no revisions.".format(fqname))
                 rev = DummyRev(item, itemtype, contenttype)
-                logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(name, contenttype))
-        logging.debug("Got item {0!r}, revision: {1!r}".format(name, rev_id))
+                logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(fqname, contenttype))
+        logging.debug("Got item {0!r}, revision: {1!r}".format(fqname, rev_id))
     return rev
 
 
@@ -243,6 +254,13 @@
     """
 
 
+class FieldNotUniqueError(ValueError):
+    """
+    The Field is not a UFIELD(unique Field).
+    Non unique fields can refer to more than one item.
+    """
+
+
 class Item(object):
     """ Highlevel (not storage) Item, wraps around a storage Revision"""
     # placeholder values for registry entry properties
@@ -275,7 +293,10 @@
         previously created Content instance is assigned to its content
         property.
         """
-        rev = get_storage_revision(name, itemtype, contenttype, rev_id, item)
+        fqname = split_fqname(name)
+        if fqname.field not in UFIELDS:  # Need a unique key to extract stored item.
+            raise FieldNotUniqueError("field {0} is not in UFIELDS".format(fqname.field))
+        rev = get_storage_revision(fqname, itemtype, contenttype, rev_id, item)
         contenttype = rev.meta.get(CONTENTTYPE) or contenttype
         logging.debug("Item {0!r}, got contenttype {1!r} from revision meta".format(name, contenttype))
         #logging.debug("Item %r, rev meta dict: %r" % (name, dict(rev.meta)))
@@ -287,15 +308,14 @@
         itemtype = rev.meta.get(ITEMTYPE) or itemtype or ITEMTYPE_DEFAULT
         logging.debug("Item {0!r}, got itemtype {1!r} from revision meta".format(name, itemtype))
 
-        item = item_registry.get(itemtype, name, rev=rev, content=content)
+        item = item_registry.get(itemtype, fqname, rev=rev, content=content)
         logging.debug("Item class {0!r} handles {1!r}".format(item.__class__, itemtype))
 
         content.item = item
-
         return item
 
-    def __init__(self, name, rev=None, content=None):
-        self.name = name
+    def __init__(self, fqname, rev=None, content=None):
+        self.fqname = fqname
         self.rev = rev
         self.content = content
 
@@ -303,6 +323,34 @@
         return self.rev.meta
     meta = property(fget=get_meta)
 
+    @property
+    def name(self):
+        """
+        returns the first name from the list of names.
+        """
+        try:
+            return self.names[0]
+        except IndexError:
+            return u''
+
+    @property
+    def names(self):
+        """
+        returns a list of 0..n names of the item
+        If we are dealing with a specific name (e.g field being NAME_EXACT),
+        move it to position 0 of the list, so the upper layer can use names[0]
+        if they want that particular name and names for the whole list.
+        TODO make the entire code to be able to use names instead of name
+        """
+        names = self.meta.get(NAME, [])
+        if self.fqname.field == NAME_EXACT:
+            try:
+                names.remove(self.fqname.value)
+            except ValueError:
+                pass
+            names.insert(0, self.fqname.value)
+        return names
+
     # XXX Backward compatibility, remove soon
     @property
     def contenttype(self):
@@ -316,7 +364,7 @@
         kill_keys = [  # shall not get copied from old rev to new rev
             NAME_OLD,
             # are automatically implanted when saving
-            ITEMID, REVID, DATAID,
+            REVID, DATAID,
             HASH_ALGORITHM,
             SIZE,
             COMMENT,
@@ -353,7 +401,7 @@
 
     def _rename(self, name, comment, action, delete=False):
         self._save(self.meta, self.content.data, name=name, action=action, comment=comment, delete=delete)
-        old_prefix = self.subitems_prefix
+        old_prefix = self.subitem_prefixes[0]
         old_prefixlen = len(old_prefix)
         if not delete:
             new_prefix = name + '/'
@@ -458,7 +506,7 @@
     def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=None,
               overwrite=False, delete=False):
         backend = flaskg.storage
-        storage_item = backend[self.name]
+        storage_item = backend.get_item(**self.fqname.query)
         try:
             currentrev = storage_item.get_revision(CURRENT)
             rev_id = currentrev.revid
@@ -469,26 +517,26 @@
             contenttype_current = None
 
         meta = dict(meta)  # we may get a read-only dict-like, copy it
-
         # we store the previous (if different) and current item name into revision metadata
         # this is useful for rename history and backends that use item uids internally
-        if name is None:
-            name = self.name
-        oldname = meta.get(NAME)
-        if oldname:
-            if not isinstance(oldname, list):
-                oldname = [oldname]
-            if delete or name not in oldname:  # this is a delete or rename
-                meta[NAME_OLD] = oldname[:]
-                try:
-                    oldname.remove(self.name)
-                except ValueError:
-                    pass
-                if not delete:
-                    oldname.append(name)
-                meta[NAME] = oldname
-        else:
-            meta[NAME] = [name]
+        if self.fqname.field == NAME_EXACT:
+            if name is None:
+                name = self.fqname.value
+            oldname = meta.get(NAME)
+            if oldname:
+                if not isinstance(oldname, list):
+                    oldname = [oldname]
+                if delete or name not in oldname:  # this is a delete or rename
+                    meta[NAME_OLD] = oldname[:]
+                    try:
+                        oldname.remove(self.name)
+                    except ValueError:
+                        pass
+                    if not delete:
+                        oldname.append(name)
+                    meta[NAME] = oldname
+            else:
+                meta[NAME] = [name]
 
         if comment is not None:
             meta[COMMENT] = unicode(comment)
@@ -510,7 +558,6 @@
 
         if isinstance(data, str):
             data = StringIO(data)
-
         newrev = storage_item.store_revision(meta, data, overwrite=overwrite,
                                              action=unicode(action),
                                              contenttype_current=contenttype_current,
@@ -521,8 +568,20 @@
         return newrev.revid, newrev.meta[SIZE]
 
     @property
-    def subitems_prefix(self):
-        return self.name + u'/' if self.name else u''
+    def subitem_prefixes(self):
+        """
+        Return the possible prefixes for subitems.
+        """
+        names = self.names[0:1] if self.fqname.field == NAME_EXACT else self.names
+        return [name + u'/' for name in names]
+
+    def get_prefix_match(self, name, prefixes):
+        """
+        returns the prefix match found.
+        """
+        for prefix in prefixes:
+            if name.startswith(prefix):
+                return prefix
 
     def get_subitem_revs(self):
         """
@@ -530,11 +589,11 @@
 
         Subitems are in the form of storage Revisions.
         """
-        query = Term(WIKINAME, app.cfg.interwikiname)
+        query = And([Term(WIKINAME, app.cfg.interwikiname), Term(NAMESPACE, self.fqname.namespace)])
         # trick: an item of empty name can be considered as "virtual root item"
         # that has all wiki items as sub items
-        if self.name:
-            query = And([query, Prefix(NAME_EXACT, self.subitems_prefix)])
+        if self.names:
+            query = And([query, Or([Prefix(NAME_EXACT, prefix) for prefix in self.subitem_prefixes])])
         revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
         return revs
 
@@ -554,37 +613,37 @@
         When both a subitem itself and some of its subitems are in the subitems
         list, it appears in both ``files`` and ``dirs``.
         """
-        prefix = self.subitems_prefix
-        prefixlen = len(prefix)
+        prefixes = self.subitem_prefixes
         # IndexEntry instances of "file" subitems
         files = []
         # IndexEntry instances of "directory" subitems
         dirs = []
-        added_dir_relnames = set()
+        added_fullnames = set()
 
         for rev in subitems:
             fullnames = rev.meta[NAME]
             for fullname in fullnames:
-                if fullname.startswith(prefix):
-                    relname = fullname[prefixlen:]
+                prefix = self.get_prefix_match(fullname, prefixes)
+                if not prefix is None:
+                    relname = fullname[len(prefix):]
                     if '/' in relname:
                         # Find the *direct* subitem that is the ancestor of current
                         # (indirect) subitem. e.g. suppose when the index root is
                         # 'foo', and current item (`rev`) is 'foo/bar/lorem/ipsum',
                         # 'foo/bar' will be found.
                         direct_relname = relname.partition('/')[0]
-                        if direct_relname not in added_dir_relnames:
-                            added_dir_relnames.add(direct_relname)
+                        if fullname not in added_fullnames:
+                            added_fullnames.add(fullname)
                             direct_fullname = prefix + direct_relname
-                            direct_rev = get_storage_revision(direct_fullname)
+                            fqname = split_fqname(direct_fullname)
+                            direct_rev = get_storage_revision(fqname)
                             dirs.append(IndexEntry(direct_relname, direct_fullname, direct_rev.meta))
                     else:
                         files.append(IndexEntry(relname, fullname, rev.meta))
-
         return dirs, files
 
     def build_index_query(self, startswith=None, selected_groups=None):
-        prefix = self.subitems_prefix
+        prefix = self.subitem_prefixes[0]
         if startswith:
             query = Prefix(NAME_EXACT, prefix + startswith) | Prefix(NAME_EXACT, prefix + startswith.swapcase())
         else:
@@ -609,8 +668,8 @@
 
     def get_mixed_index(self):
         dirs, files = self.make_flat_index(self.get_subitem_revs())
-        dirs_dict = dict([(e.relname, MixedIndexEntry(*e, hassubitems=True)) for e in dirs])
-        index_dict = dict([(e.relname, MixedIndexEntry(*e, hassubitems=False)) for e in files])
+        dirs_dict = dict([(e.fullname, MixedIndexEntry(*e, hassubitems=True)) for e in dirs])
+        index_dict = dict([(e.fullname, MixedIndexEntry(*e, hassubitems=False)) for e in files])
         index_dict.update(dirs_dict)
         return sorted(index_dict.values())
 
@@ -621,12 +680,13 @@
         return a sorted list of first characters of subitem names,
         optionally all uppercased or lowercased.
         """
-        prefix = self.subitems_prefix
-        prefixlen = len(prefix)
+        prefixes = self.subitem_prefixes
         initials = set()
         for item in subitems:
             for name in item.meta[NAME]:
-                if name.startswith(prefix):
+                prefix = self.get_prefix_match(name, prefixes)
+                prefixlen = len(prefix)
+                if prefix:
                     initial = name[prefixlen]
                     if uppercase:
                         initial = initial.upper()
@@ -669,7 +729,7 @@
         rev_ids = []
         item_templates = self.content.get_templates(self.contenttype)
         return render_template('modify_select_template.html',
-                               item_name=self.name,
+                               item_name=self.fqname.fullname,
                                itemtype=self.itemtype,
                                rev=self.rev,
                                contenttype=self.contenttype,
@@ -685,7 +745,7 @@
         show_navigation = False  # TODO
         first_rev = last_rev = None  # TODO
         return render_template(self.show_template,
-                               item=self, item_name=self.name,
+                               item=self, item_name=self.fqname.fullname,
                                rev=self.rev,
                                contenttype=self.contenttype,
                                first_rev_id=first_rev,
@@ -700,7 +760,7 @@
         if method in ['GET', 'HEAD']:
             if isinstance(self.content, NonExistentContent):
                 return render_template('modify_select_contenttype.html',
-                                       item_name=self.name,
+                                       item_name=self.fqname.fullname,
                                        itemtype=self.itemtype,
                                        group_names=content_registry.group_names,
                                        groups=content_registry.groups,
@@ -725,7 +785,7 @@
                     # break them
                     return "OK"
             form = self.ModifyForm.from_request(request)
-            state = dict(name=self.name, itemid=self.meta.get(ITEMID))
+            state = dict(fqname=self.fqname, itemid=self.meta.get(ITEMID))
             if form.validate(state):
                 meta, data, contenttype_guessed, comment = form._dump(self)
                 contenttype_qs = request.values.get('contenttype')
@@ -734,9 +794,9 @@
                 except AccessDenied:
                     abort(403)
                 else:
-                    return redirect(url_for_item(self.name))
+                    return redirect(url_for_item(**self.fqname.split))
         return render_template(self.modify_template,
-                               item_name=self.name,
+                               item_name=self.fqname.fullname,
                                rows_meta=str(ROWS_META), cols=str(COLS),
                                form=form,
                                search_form=None,
@@ -775,7 +835,7 @@
             content = self._select_itemtype()
         else:
             content = render_template('show_nonexistent.html',
-                                      item_name=self.name,
+                                      item_name=self.fqname.fullname,
                                      )
         return Response(content, 404)
 
@@ -787,7 +847,7 @@
 
     def _select_itemtype(self):
         return render_template('modify_select_itemtype.html',
-                               item_name=self.name,
+                               item_name=self.fqname.fullname,
                                itemtypes=item_registry.shown_entries,
                               )
 
--- a/MoinMoin/storage/middleware/_tests/test_validation.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/storage/middleware/_tests/test_validation.py	Mon Jul 22 22:51:34 2013 +0530
@@ -15,6 +15,8 @@
 
 from MoinMoin.util.crypto import make_uuid
 
+from MoinMoin.util.interwiki import CompositeName
+
 
 class TestValidation(object):
     def test_content(self):
@@ -49,6 +51,7 @@
                  'acl_parent': u"All:read",
                  'contenttype_current': u'text/x.moin.wiki;charset=utf-8',
                  'contenttype_guessed': u'text/plain;charset=utf-8',
+                 keys.FQNAME: CompositeName(u'', u'', u'somename'),
                 }
 
         m = ContentMetaSchema(meta)
@@ -76,6 +79,7 @@
                  keys.ADDRESS: u'127.0.0.1',
                  keys.WIKINAME: u'ThisWiki',
                  keys.NAMESPACE: u'',
+                 keys.FQNAME: CompositeName(u'', u'', u'somename')
                 }
 
         m = UserMetaSchema(meta)
--- a/MoinMoin/storage/middleware/indexing.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/storage/middleware/indexing.py	Mon Jul 22 22:51:34 2013 +0530
@@ -81,7 +81,7 @@
 from MoinMoin.themes import utctimestamp
 from MoinMoin.storage.middleware.validation import ContentMetaSchema, UserMetaSchema, validate_data
 from MoinMoin.storage.error import NoSuchItemError, ItemAlreadyExistsError
-
+from MoinMoin.util.interwiki import split_fqname, CompositeName
 
 WHOOSH_FILESTORAGE = 'FileStorage'
 INDEXES = [LATEST_REVS, ALL_REVS, ]
@@ -118,9 +118,7 @@
     elif not isinstance(names, list):
         raise TypeError("NAME is not a list but %r - fix this!" % names)
     if not names:
-        # we currently never return an empty list, some code
-        # might not be able to deal with it:
-        names = [u'DoesNotExist', ]
+        names = []
     return names
 
 
@@ -168,6 +166,8 @@
                    metadata as a side effect
     :returns: indexable content, text/plain, unicode object
     """
+    fqname = split_fqname(item_name)
+
     class PseudoRev(object):
         def __init__(self, meta, data):
             self.meta = meta
@@ -175,9 +175,9 @@
             self.revid = meta.get(REVID)
 
             class PseudoItem(object):
-                def __init__(self, name):
-                    self.name = name
-            self.item = PseudoItem(item_name)
+                def __init__(self, fqname):
+                    self.fqname = fqname
+            self.item = PseudoItem(fqname)
 
         def read(self, *args, **kw):
             return self.data.read(*args, **kw)
@@ -193,7 +193,10 @@
         return u''
 
     if not item_name:
-        item_name = get_names(meta)[0]
+        try:
+            item_name = get_names(meta)[0]
+        except IndexError:
+            item_name = u'DoesNotExist'
 
     rev = PseudoRev(meta, data)
     try:
@@ -808,7 +811,98 @@
         return Item.existing(self, **query)
 
 
-class Item(object):
+class PropertiesMixin(object):
+    """
+    PropertiesMixin offers methods to find out some additional information from meta.
+    """
+    @property
+    def name(self):
+        if self._name and self._name in self.names:
+            name = self._name
+        else:
+            try:
+                name = self.names[0]
+            except IndexError:
+                # empty name list, no name:
+                name = None
+        assert isinstance(name, unicode) or not name
+        return name
+
+    @property
+    def namespace(self):
+        return self.meta.get(NAMESPACE) or u''
+
+    def _fqname(self, name=None):
+        """
+        return the fully qualified name including the namespace: NS:NAME
+        """
+        if name is not None:
+            return CompositeName(self.namespace, NAME_EXACT, name)
+        else:
+            return CompositeName(self.namespace, ITEMID, self.meta[ITEMID])
+
+    @property
+    def fqname(self):
+        """
+        return the fully qualified name including the namespace: NS:NAME
+        """
+        return self._fqname(self.name)
+
+    @property
+    def fqnames(self):
+        """
+        return the fully qualified names including the namespace: NS:NAME
+        """
+        if self.names:
+            return [self._fqname(name) for name in self.names]
+        else:
+            return [self.fqname]
+
+    @property
+    def parentnames(self):
+        """
+        compute list of parent names (same order as in names, but no dupes)
+
+        :return: parent names (list of unicode)
+        """
+        parent_names = []
+        for name in self.names:
+            parentname_tail = name.rsplit('/', 1)
+            if len(parentname_tail) == 2:
+                parent_name = parentname_tail[0]
+                if parent_name not in parent_names:
+                    parent_names.append(parent_name)
+        return parent_names
+
+    @property
+    def fqparentnames(self):
+        """
+        return the fully qualified parent names including the namespace: NS:NAME
+        """
+        return [self._fqname(name) for name in self.parentnames]
+
+    @property
+    def acl(self):
+        return self.meta.get(ACL)
+
+    @property
+    def ptime(self):
+        dt = self.meta.get(PTIME)
+        if dt is not None:
+            return utctimestamp(dt)
+
+    @property
+    def names(self):
+        return get_names(self.meta)
+
+    @property
+    def mtime(self):
+        dt = self.meta.get(MTIME)
+        if dt is not None:
+            return utctimestamp(dt)
+
+
+class Item(PropertiesMixin):
     def __init__(self, indexer, latest_doc=None, **query):
         """
         :param indexer: indexer middleware instance
@@ -830,11 +924,10 @@
                 # avoid issues in the name(s) property code. if this was a
                 # lookup for some specific item (using a name_exact query), we
                 # put that name into the NAME list, otherwise it'll be empty:
-                if self._name is not None:
-                    names = [self._name, ]
-                else:
-                    names = []
-                latest_doc = {NAME: names}
+                latest_doc = {}
+                for field, value in query.items():
+                    latest_doc[field] = [value] if field in UFIELDS_TYPELIST else value
+                latest_doc[NAME] = latest_doc[NAME_EXACT] if NAME_EXACT in query else []
         self._current = latest_doc
 
     def _get_itemid(self):
@@ -845,38 +938,8 @@
     itemid = property(_get_itemid, _set_itemid)
 
     @property
-    def acl(self):
-        return self._current.get(ACL)
-
-    @property
-    def namespace(self):
-        return self._current.get(NAMESPACE)
-
-    @property
-    def ptime(self):
-        dt = self._current.get(PTIME)
-        if dt is not None:
-            return utctimestamp(dt)
-
-    @property
-    def names(self):
-        return get_names(self._current)
-
-    @property
-    def parentnames(self):
-        """
-        compute list of parent names (same order as in names, but no dupes)
-
-        :return: parent names (list of unicode)
-        """
-        parent_names = []
-        for name in self.names:
-            parentname_tail = name.rsplit('/', 1)
-            if len(parentname_tail) == 2:
-                parent_name = parentname_tail[0]
-                if parent_name not in parent_names:
-                    parent_names.append(parent_name)
-        return parent_names
+    def meta(self):
+        return self._current
 
     @property
     def parentids(self):
@@ -892,59 +955,6 @@
                 parent_ids.add(rev[ITEMID])
         return parent_ids
 
-    @property
-    def mtime(self):
-        dt = self._current.get(MTIME)
-        if dt is not None:
-            return utctimestamp(dt)
-
-    @property
-    def name(self):
-        if self._name and self._name in self.names:
-            name = self._name
-        else:
-            try:
-                name = self.names[0]
-            except IndexError:
-                # empty name list, no name:
-                name = None
-        assert name is None or isinstance(name, unicode)
-        return name
-
-    def _fqname(self, name):
-        """
-        return the fully qualified name including the namespace: NS:NAME
-        """
-        ns = self.namespace
-        name = name or u''
-        if ns:
-            fqn = ns + u':' + name
-        else:
-            fqn = name
-        assert isinstance(fqn, unicode)
-        return fqn
-
-    @property
-    def fqname(self):
-        """
-        return the fully qualified name including the namespace: NS:NAME
-        """
-        return self._fqname(self.name)
-
-    @property
-    def fqnames(self):
-        """
-        return the fully qualified names including the namespace: NS:NAME
-        """
-        return [self._fqname(name) for name in self.names]
-
-    @property
-    def fqparentnames(self):
-        """
-        return the fully qualified parent names including the namespace: NS:NAME
-        """
-        return [self._fqname(name) for name in self.parentnames]
-
     @classmethod
     def create(cls, indexer, **query):
         """
@@ -1010,6 +1020,7 @@
                        contenttype_guessed=None,
                        acl_parent=None,
                        return_rev=False,
+                       fqname=None,
                        ):
         """
         Store a revision into the backend, write metadata and data to it.
@@ -1049,6 +1060,7 @@
                  'contenttype_current': contenttype_current,
                  'contenttype_guessed': contenttype_guessed,
                  'acl_parent': acl_parent,
+                 FQNAME: fqname,
                 }
         ct = meta.get(CONTENTTYPE)
         if ct == CONTENTTYPE_USER:
@@ -1122,7 +1134,7 @@
             self.destroy_revision(rev.revid)
 
 
-class Revision(object):
+class Revision(PropertiesMixin):
     """
     An existing revision (exists in the backend).
     """
@@ -1153,21 +1165,6 @@
         # Note: this does not immediately raise a KeyError for non-existing revs any more
         # If you access data or meta, it will, though.
 
-    @property
-    def names(self):
-        return get_names(self.meta)
-
-    @property
-    def name(self):
-        name = self._name
-        if name is None:
-            try:
-                name = self.names[0]
-            except IndexError:
-                # empty name list, no name:
-                name = None
-        return name
-
     def set_context(self, context):
         for name in self.names:
             if name.startswith(context):
--- a/MoinMoin/storage/middleware/protecting.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/storage/middleware/protecting.py	Mon Jul 22 22:51:34 2013 +0530
@@ -26,6 +26,8 @@
 
 from MoinMoin.security import AccessControlList
 
+from MoinMoin.util.interwiki import split_fqname
+
 # max sizes of some lru caches:
 LOOKUP_CACHE = 100  # ACL lookup for some itemname
 PARSE_CACHE = 100  # ACL string -> ACL object parsing
@@ -79,19 +81,20 @@
         # ACL lookups afterwards will fetch fresh info from the lower layers.
         self.get_acls.cache_clear()
 
-    def _get_configured_acls(self, itemname):
+    def _get_configured_acls(self, fqname):
         """
         for a fully-qualified itemname (namespace:name), get the acl configuration
         for that (part of the) namespace.
 
-        @param itemname: fully qualified itemname
+        @param fqname: fully qualified itemname
         @returns: acl configuration (acl dict from the acl_mapping)
         """
+        itemname = fqname.value if fqname.field == NAME_EXACT else u''
         for prefix, acls in self.acl_mapping:
             if itemname.startswith(prefix):
                 return acls
         else:
-            raise ValueError('No acl_mapping entry found for item {0!r}'.format(itemname))
+            raise ValueError('No acl_mapping entry found for item {0!r}'.format(fqname))
 
     def _get_acls(self, itemid=None, fqname=None):
         """
@@ -109,7 +112,7 @@
         elif fqname is not None:
             # itemid might be None for new, not yet stored items,
             # but we have fqname then
-            q = {NAME_EXACT: fqname}
+            q = fqname.query
         else:
             raise ValueError("need itemid or fqname")
         item = self.get_item(**q)
@@ -183,16 +186,17 @@
         item = self.indexer.existing_item(**query)
         return ProtectedItem(self, item)
 
-    def may(self, itemname, capability, usernames=None):
+    def may(self, fqname, capability, usernames=None):
         if usernames is not None and isinstance(usernames, (str, unicode)):
             # we got a single username (maybe str), make a list of unicode:
             if isinstance(usernames, str):
                 usernames = usernames.decode('utf-8')
             usernames = [usernames, ]
-        if isinstance(itemname, list):
-            # if we get a list of names, just use first one to fetch item
-            itemname = itemname[0]
-        item = self[itemname]
+        # TODO Make sure that fqname must be a CompositeName class instance, not unicode or list.
+        fqname = fqname[0] if isinstance(fqname, list) else fqname
+        if isinstance(fqname, unicode):
+            fqname = split_fqname(fqname)
+        item = self.get_item(**fqname.query)
         allowed = item.allows(capability, user_names=usernames)
         return allowed
 
@@ -227,6 +231,14 @@
         return self.item.name
 
     @property
+    def fqname(self):
+        return self.item.fqname
+
+    @property
+    def fqnames(self):
+        return self.item.fqnames
+
+    @property
     def acl(self):
         return self.item.acl
 
@@ -291,13 +303,13 @@
     def get_revision(self, revid):
         return self[revid]
 
-    def store_revision(self, meta, data, overwrite=False, return_rev=False, **kw):
+    def store_revision(self, meta, data, overwrite=False, return_rev=False, fqname=None, **kw):
         self.require(WRITE)
         if not self:
             self.require(CREATE)
         if overwrite:
             self.require(DESTROY)
-        rev = self.item.store_revision(meta, data, overwrite=overwrite, return_rev=return_rev, **kw)
+        rev = self.item.store_revision(meta, data, overwrite=overwrite, return_rev=return_rev, fqname=fqname, **kw)
         self.protector._clear_acl_cache()
         if return_rev:
             return ProtectedRevision(self.protector, rev, p_item=self)
@@ -348,6 +360,14 @@
         return self.rev.name
 
     @property
+    def fqname(self):
+        return self.rev.fqname
+
+    @property
+    def fqnames(self):
+        return self.rev.fqnames
+
+    @property
     def meta(self):
         self.require(READ, PUBREAD)
         return self.rev.meta
--- a/MoinMoin/storage/middleware/validation.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/storage/middleware/validation.py	Mon Jul 22 22:51:34 2013 +0530
@@ -61,7 +61,8 @@
     an itemid is a uuid that identifies an item
     """
     if not state['trusted'] or element.raw is Unset:
-        itemid = state.get(keys.ITEMID)
+        fqname = state[keys.FQNAME]
+        itemid = fqname.value if fqname and fqname.field == keys.ITEMID else state.get(keys.ITEMID)
         if itemid is None:
             # this is first revision of an item
             itemid = make_uuid()
--- a/MoinMoin/templates/common.js	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/templates/common.js	Mon Jul 22 22:51:34 2013 +0530
@@ -228,6 +228,21 @@
 
 
 
+// OnMouseOver show the fqname of the item else only show the value/id.
+function togglefqname(){
+    "use strict";
+    var fullname, value;
+    $(".moin-fqname").hover(function () {
+        fullname = $(this).attr('data-fqname');
+        value = $(this).html();
+        $(this).html(fullname);
+    },function () {
+        $(this).html(value);
+    });
+}
+$(document).ready(togglefqname);
+
+
 // Executed when user clicks insert-name button defined in modify.html.
 // When a page with subitems is modified, a subitems sidebar is present. User may
 // position caret in textarea and click button to insert name into textarea.
--- a/MoinMoin/templates/layout.html	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/templates/layout.html	Mon Jul 22 22:51:34 2013 +0530
@@ -74,7 +74,7 @@
                 {% for segment_name, segment_path, exists in theme_supp.location_breadcrumbs(item_name) -%}
                     {% 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 }}
+                            {{ segment_name|shorten_fqname }}
                         </a>
                         <span class="sep">/</span>
                     {% else %}
@@ -82,7 +82,7 @@
                             {{ 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 }}
+                            {{ segment_name|shorten_fqname }}
                         </a>
                         {%- endif %}
                     {%- endif %}
@@ -95,17 +95,17 @@
     {% set trail_items = theme_supp.path_breadcrumbs() %}
     {% if trail_items %}
         <div id="moin-pagetrail">
-        {% for wiki_name, item_name, item_href, exists, err in trail_items %}
+        {% for wiki_name, fqname, item_href, exists, err in trail_items %}
             {%- if wiki_name -%}
                 <a href="{{ item_href }}"{{ " " }}
                    title="{{ wiki_name }}"
                    class="{% if err %}moin-badinterwiki{% else %}moin-interwiki{% endif %}">
-                   {{ item_name|shorten_item_name }}
+                   {{ fqname|shorten_fqname }}
                 </a>
             {%- else -%}
-                <a href="{{ url_for('frontend.show_item', item_name=item_name) }}"{{ " " }}
+                <a href="{{ url_for('frontend.show_item', item_name=fqname) }}"{{ " " }}
                    {% if not exists -%}class="moin-nonexistent"{%- endif -%}>
-                   {{ item_name|shorten_item_name }}
+                   {{ fqname|shorten_fqname }}
                 </a>
             {%- endif %}
             {% if not loop.last %}<span class="sep"> &raquo; </span>{% endif %}
--- a/MoinMoin/templates/link_list_no_item_panel.html	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/templates/link_list_no_item_panel.html	Mon Jul 22 22:51:34 2013 +0530
@@ -3,11 +3,11 @@
 {% if headline %}
 <h1>{{ headline }}</h1>
 {% endif %}
-{% if item_names %}
-Total: {{ item_names|count }}
+{% if fq_names %}
+Total: {{ fq_names|count }}
 <ul>
-    {% for item_name in item_names|sort %}
-    <li><a href="{{ url_for('frontend.show_item', item_name=item_name) }}">{{ item_name }}</a></li>
+    {% for fq_name in fq_names|sort(attribute='value') %}
+    <li><a class="moin-fqname" href="{{ url_for('frontend.show_item', item_name=fq_name) }}" data-fqname="{{fq_name}}">{{ fq_name.value }}</a></li>
     {% endfor %}
 </ul>
 {% endif %}
--- a/MoinMoin/templates/revert.html	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/templates/revert.html	Mon Jul 22 22:51:34 2013 +0530
@@ -1,9 +1,9 @@
 {% import "forms.html" as forms %}
 {% extends theme("layout.html") %}
 {% block content %}
-<h1>{{ _("Revert '%(item_name)s' (rev %(rev_id)s)", item_name=item.name, rev_id=rev_id | shorten_id) }}</h1>
+<h1>{{ _("Revert '%(item_name)s' (rev %(rev_id)s, names '%(new_names)s') ", item_name=fqname | shorten_item_name, rev_id=rev_id | shorten_id, new_names=item.meta['name'] | join('|')) }}</h1>
 <div class="moin-form">
-{{ gen.form.open(form, method="post", action=url_for('frontend.revert_item', item_name=item.name, rev=rev_id)) }}
+{{ gen.form.open(form, method="post", action=url_for('frontend.revert_item', item_name=item.fqname, rev=rev_id)) }}
   {{ forms.render_errors(form) }}
   <dl>
     {{ forms.render(form['comment']) }}
--- a/MoinMoin/themes/__init__.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/themes/__init__.py	Mon Jul 22 22:51:34 2013 +0530
@@ -22,9 +22,9 @@
 
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin import wikiutil, user
-from MoinMoin.constants.keys import USERID, ADDRESS, HOSTNAME
+from MoinMoin.constants.keys import USERID, ADDRESS, HOSTNAME, REVID, ITEMID, NAME_EXACT
 from MoinMoin.search import SearchForm
-from MoinMoin.util.interwiki import split_interwiki, getInterwikiHome, is_local_wiki, is_known_wiki, url_for_item
+from MoinMoin.util.interwiki import split_interwiki, getInterwikiHome, is_local_wiki, is_known_wiki, url_for_item, CompositeName, split_fqname
 from MoinMoin.util.crypto import cache_key
 from MoinMoin.util.forms import make_generator
 from MoinMoin.util.clock import timed
@@ -79,18 +79,31 @@
         self.content_dir = 'ltr'  # XXX
         self.meta_items = []  # list of (name, content) for html head <meta>
 
-    def location_breadcrumbs(self, item_name):
+    def location_breadcrumbs(self, fqname):
         """
         Assemble the location using breadcrumbs (was: title)
 
         :rtype: list
-        :returns: location breadcrumbs items in tuple (segment_name, item_name, exists)
+        :returns: location breadcrumbs items in tuple (segment_name, fq_name, exists)
         """
         breadcrumbs = []
         current_item = ''
+        if not isinstance(fqname, CompositeName):
+            fqname = split_fqname(fqname)
+        if fqname.field != NAME_EXACT:
+            return [(fqname, fqname, bool(self.storage.get_item(**fqname.query)))]
+        namespace = fqname.namespace
+        fq_current = CompositeName(u'', NAME_EXACT, namespace)
+        fq_segment = CompositeName(u'', NAME_EXACT, namespace or '~')
+        breadcrumbs.append((CompositeName(fq_segment, fq_current, False)))
+        item_name = fqname.value
+        if not item_name:
+            return breadcrumbs
         for segment in item_name.split('/'):
             current_item += segment
-            breadcrumbs.append((segment, current_item, self.storage.has_item(current_item)))
+            fq_current = CompositeName(namespace, NAME_EXACT, current_item)
+            fq_segment = CompositeName(namespace, NAME_EXACT, segment)
+            breadcrumbs.append((fq_segment, fq_current, bool(self.storage.get_item(**fq_current.query))))
             current_item += '/'
         return breadcrumbs
 
@@ -105,15 +118,17 @@
         breadcrumbs = []
         trail = user.get_trail()
         for interwiki_item_name in trail:
-            wiki_name, namespace, item_name = split_interwiki(interwiki_item_name)
+            wiki_name, namespace, field, item_name = split_interwiki(interwiki_item_name)
+            fqname = CompositeName(namespace, field, item_name)
             err = not is_known_wiki(wiki_name)
-            href = url_for_item(item_name, namespace=namespace, wiki_name=wiki_name)
+            href = url_for_item(wiki_name=wiki_name, **fqname.split)
             if is_local_wiki(wiki_name):
-                exists = self.storage.has_item(item_name)
+                exists = bool(self.storage.get_item(**fqname.query))
                 wiki_name = ''  # means "this wiki" for the theme code
             else:
                 exists = True  # we can't detect existance of remote items
-            breadcrumbs.append((wiki_name, item_name, href, exists, err))
+            if item_name:
+                breadcrumbs.append((wiki_name, fqname, href, exists, err))
         return breadcrumbs
 
     def subitem_index(self, item_name):
@@ -191,10 +206,10 @@
         if target.startswith("wiki:"):
             target = target[5:]
 
-        wiki_name, namespace, item_name = split_interwiki(target)
+        wiki_name, namespace, field, item_name = split_interwiki(target)
         if wiki_name == 'Self':
             wiki_name = ''
-        href = url_for_item(item_name, namespace=namespace, wiki_name=wiki_name)
+        href = url_for_item(item_name, namespace=namespace, wiki_name=wiki_name, field=field)
         if not title:
             title = item_name
         return href, title, wiki_name
@@ -333,6 +348,28 @@
     return result
 
 
+def shorten_fqname(fqname, length=25):
+    """
+    Shorten fqname
+
+    Shorten a given long fqname so that it looks good depending upon whether
+    the field is a UUID or not.
+
+    :param fqname: fqname, namedtuple
+    :param length maximum length for shortened fqnames in case the field
+    is not a UUID.
+    :rtype: unicode
+    :returns: shortened fqname.
+    """
+    name = fqname.value
+    if len(name) > length:
+        if fqname.field in [REVID, ITEMID]:
+            name = shorten_id(name)
+        else:
+            name = shorten_item_name(name, length)
+    return name
+
+
 def shorten_item_name(name, length=25):
     """
     Shorten item names
@@ -405,6 +442,7 @@
 
 
 def setup_jinja_env():
+    app.jinja_env.filters['shorten_fqname'] = shorten_fqname
     app.jinja_env.filters['shorten_item_name'] = shorten_item_name
     app.jinja_env.filters['shorten_id'] = shorten_id
     app.jinja_env.filters['contenttype_to_class'] = contenttype_to_class
--- a/MoinMoin/themes/_tests/test_navi_bar.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/themes/_tests/test_navi_bar.py	Mon Jul 22 22:51:34 2013 +0530
@@ -26,9 +26,9 @@
             #(navilink, (href, text, interwiki)),
             ('ItemName', ('/ItemName', 'ItemName', '')),
             ('[[ItemName|LinkText]]', ('/ItemName', 'LinkText', '')),
-            ('MoinMoin:ItemName', ('http://moinmo.in/ItemName', 'ItemName', 'MoinMoin')),
-            ('[[MoinMoin:ItemName|LinkText]]', ('http://moinmo.in/ItemName', 'LinkText', 'MoinMoin')),
-            ('[[wiki:MoinMoin:ItemName|LinkText]]', ('http://moinmo.in/ItemName', 'LinkText', 'MoinMoin')),
+            ('MoinMoin/ItemName', ('http://moinmo.in/ItemName', 'ItemName', 'MoinMoin')),
+            ('[[MoinMoin/ItemName|LinkText]]', ('http://moinmo.in/ItemName', 'LinkText', 'MoinMoin')),
+            ('[[wiki:MoinMoin/ItemName|LinkText]]', ('http://moinmo.in/ItemName', 'LinkText', 'MoinMoin')),
             ('http://example.org/', ('http://example.org/', 'http://example.org/', '')),
             ('[[http://example.org/|LinkText]]', ('http://example.org/', 'LinkText', '')),
         ]
@@ -41,13 +41,16 @@
         test_segment_name_1, test_item_name_1, test_item_exists_1 = test_result[0]
         test_segment_name_2, test_item_name_2, test_item_exists_2 = test_result[1]
         test_segment_name_3, test_item_name_3, test_item_exists_3 = test_result[2]
+        test_segment_name_4, test_item_name_4, test_item_exists_4 = test_result[3]
 
-        assert test_segment_name_1 == 'some'
-        assert test_item_name_1 == 'some'
-        assert test_segment_name_2 == 'place'
-        assert test_item_name_2 == 'some/place'
-        assert test_segment_name_3 == 'test_item'
-        assert test_item_name_3 == 'some/place/test_item'
+        assert test_segment_name_1.value == '~'
+        assert test_item_name_1.value == ''
+        assert test_segment_name_2.value == 'some'
+        assert test_item_name_2.value == 'some'
+        assert test_segment_name_3.value == 'place'
+        assert test_item_name_3.value == 'some/place'
+        assert test_segment_name_4.value == 'test_item'
+        assert test_item_name_4.value == 'some/place/test_item'
 
     def test_parent_item(self):
         test_result = ThemeSupport.parent_item(self.theme, 'moin/moin-2.0/Item')
--- a/MoinMoin/util/_tests/test_interwiki.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/util/_tests/test_interwiki.py	Mon Jul 22 22:51:34 2013 +0530
@@ -16,7 +16,7 @@
 import pytest
 from flask import current_app as app
 
-from MoinMoin.util.interwiki import split_interwiki, join_wiki, InterWikiMap, url_for_item, _split_namespace
+from MoinMoin.util.interwiki import split_interwiki, join_wiki, InterWikiMap, url_for_item, _split_namespace, split_fqname
 from MoinMoin._tests import wikiconfig
 from MoinMoin.constants.keys import CURRENT
 from MoinMoin.app import before_wiki
@@ -27,100 +27,120 @@
         interwiki_map = {'Self': 'http://localhost:8080/',
                          'MoinMoin': 'http://moinmo.in/',
                          'OtherWiki': 'http://otherwiki.com/',
-                         'OtherWiki:ns1': 'http://otherwiki.com/ns1/',
-                         'OtherWiki:ns1:ns2': 'http://otherwiki.com/ns1/ns2/',
+                         'OtherWiki/ns1': 'http://otherwiki.com/ns1/',
+                         'OtherWiki/ns1/ns2': 'http://otherwiki.com/ns1/ns2/',
         }
 
     def test_url_for_item(self):
         before_wiki()
         revid = 'cdc431e0fc624d6fb8372152dcb66457'
 
-        tests = [(('SomePage', '', '', CURRENT, 'frontend.show_item', False), '/SomePage'),
+        tests = [(('SomePage', '', '', '', CURRENT, 'frontend.show_item', False), '/SomePage'),
                  # Method signature to understand the tuple parameters
                  # (item_name, wiki_name='', namespace='', rev=CURRENT, endpoint='frontend.show_item', _external=False):
-                 (('SomePage', '', '', CURRENT, 'frontend.show_item', True), 'http://localhost:8080/SomePage'),
-                 (('SomePage', '', '', CURRENT, 'frontend.modify_item', False), '/+modify/SomePage'),
+                 (('SomePage', '', '', '', CURRENT, 'frontend.show_item', True), 'http://localhost:8080/SomePage'),
+                 (('SomePage', '', '', '', CURRENT, 'frontend.modify_item', False), '/+modify/SomePage'),
                  # FIXME if you set interwiki_map = dict(Self='http://localhost:8080', MoinMoin='http://moinmo.in/', ),
                  # the above line make it fails, it returns http://localhost/+modify/SomePage
                  # (('SomePage', '', '', CURRENT, 'frontend.modify_item', True), 'http://localhost:8080/+modify/SomePage'),
-                 (('SomePage', '', '', revid, 'frontend.show_item', False), '/+show/+{0}/SomePage'.format(revid)),
-                 (('SomePage', '', '', revid, 'frontend.show_item_meta', False), '/+meta/+{0}/SomePage'.format(revid)),
+                 (('SomeRevID', '', 'revid', '', revid, 'frontend.show_item', False), '/+show/+{0}/%40revid/SomeRevID'.format(revid)),
+                 (('SomePage', '', '', '', revid, 'frontend.show_item_meta', False), '/+meta/+{0}/SomePage'.format(revid)),
                  # Valid namespaces
-                 (('SomePage', '', 'ns1', CURRENT, 'frontend.show_item', False), '/:ns1:SomePage'),
-                 (('SomePage', '', 'ns1:ns2', CURRENT, 'frontend.show_item', True), 'http://localhost:8080/:ns1:ns2:SomePage'),
-                 (('SomePage', '', 'ns1', CURRENT, 'frontend.modify_item', False), '/+modify/:ns1:SomePage'),
-                 (('SomePage', '', 'ns1:ns2', CURRENT, 'frontend.show_item_meta', True), 'http://localhost:8080/+meta/:ns1:ns2:SomePage'),
-                 (('SomePage', '', 'ns1', revid, 'frontend.show_item', False), '/+show/+{0}/:ns1:SomePage'.format(revid)),
-                 (('SomePage', '', 'ns1:ns2', revid, 'frontend.show_item_meta', False), '/+meta/+{0}/:ns1:ns2:SomePage'.format(revid)),
+                 (('SomePage', '', '', 'ns1', CURRENT, 'frontend.show_item', False), '/ns1/SomePage'),
+                 (('SomeTag', '', 'tags', 'ns1', CURRENT, 'frontend.show_item', False), '/ns1/%40tags/SomeTag'),
+                 (('SomePage', '', '', 'ns1/ns2', CURRENT, 'frontend.show_item', True), 'http://localhost:8080/ns1/ns2/SomePage'),
+                 (('SomePage', '', '', 'ns1', CURRENT, 'frontend.modify_item', False), '/+modify/ns1/SomePage'),
+                 (('SomePage', '', '', 'ns1/ns2', CURRENT, 'frontend.show_item_meta', True), 'http://localhost:8080/+meta/ns1/ns2/SomePage'),
+                 (('SomePage', '', '', 'ns1', revid, 'frontend.show_item', False), '/+show/+{0}/ns1/SomePage'.format(revid)),
+                 (('SomePage', '', '', 'ns1/ns2', revid, 'frontend.show_item_meta', False), '/+meta/+{0}/ns1/ns2/SomePage'.format(revid)),
+                 (('SomeRevID', '', 'revid', 'ns1/ns2', CURRENT, 'frontend.show_item_meta', False), '/+meta/ns1/ns2/%40revid/SomeRevID'.format(revid)),
 
-                 (('SomePage', 'MoinMoin', 'ns1', CURRENT, 'frontend.show_item', False), 'http://moinmo.in/:ns1:SomePage'),
-                 (('SomePage', 'MoinMoin', '', CURRENT, 'frontend.show_item', False), 'http://moinmo.in/SomePage'),
+                 (('SomePage', 'MoinMoin', '', 'ns1', CURRENT, 'frontend.show_item', False), 'http://moinmo.in/ns1/SomePage'),
+                 (('SomePage', 'MoinMoin', '', '', CURRENT, 'frontend.show_item', False), 'http://moinmo.in/SomePage'),
                  # FIXME will exist a map for this case? maybe there should be a placeholder for it.
                  # we need that for wiki farms with common search index and search in non-current revisions.
-                 (('SomePage', 'MoinMoin', '', revid, 'frontend.show_item', False), 'http://moinmo.in/+show/+{0}/SomePage'.format(revid)),
-                 (('SomePage', 'non-existent', '', CURRENT, 'frontend.show_item', False), '/non-existent:SomePage'),
-                 (('SomePage', 'non-existent', 'ns1', CURRENT, 'frontend.show_item', False), '/non-existent:ns1:SomePage'),
+                 (('SomePage', 'MoinMoin', '', '', revid, 'frontend.show_item', False), 'http://moinmo.in/+show/+{0}/SomePage'.format(revid)),
+                 (('SomeItemID', 'non-existent', 'itemid', '', CURRENT, 'frontend.show_item', False), '/non-existent/@itemid/SomeItemID'),
+                 (('SomePage', 'non-existent', '', 'ns1', CURRENT, 'frontend.show_item', False), '/non-existent/ns1/SomePage'),
                 ]
 
-        for (item_name, wiki_name, namespace, rev, endpoint, _external), url in tests:
-            assert url_for_item(item_name, wiki_name, namespace, rev, endpoint, _external) == url
+        for (item_name, wiki_name, field, namespace, rev, endpoint, _external), url in tests:
+            assert url_for_item(item_name, wiki_name, field, namespace, rev, endpoint, _external) == url
 
     def test__split_namespace(self):
         map = set()
         map.add(u'ns1')
-        map.add(u'ns1:ns2')
+        map.add(u'ns1/ns2')
         tests = [('', ('', '')),
-                 ('OtherWiki:', ('', 'OtherWiki:')),
-                 ('ns1:', ('ns1', '')),
-                 ('ns3:foo', ('', 'ns3:foo')),
-                 ('ns1:OtherPage', ('ns1', 'OtherPage')),
-                 ('ns1:ns2:OtherPage', ('ns1:ns2', 'OtherPage')),
-                 ('ns1:ns2:ns1:ns2:OtherPage', ('ns1:ns2', 'ns1:ns2:OtherPage')),
+                 ('OtherWiki/', ('', 'OtherWiki/')),
+                 ('ns1/', ('ns1', '')),
+                 ('ns3/foo', ('', 'ns3/foo')),
+                 ('ns1/OtherPage', ('ns1', 'OtherPage')),
+                 ('ns1/ns2/OtherPage', ('ns1/ns2', 'OtherPage')),
+                 ('ns1/ns2/ns1/ns2/OtherPage', ('ns1/ns2', 'ns1/ns2/OtherPage')),
                  ('SomePage', ('', 'SomePage')),
-                 ('OtherWiki:ns1:OtherPage', ('', 'OtherWiki:ns1:OtherPage')),
+                 ('OtherWiki/ns1/OtherPage', ('', 'OtherWiki/ns1/OtherPage')),
                 ]
         for markup, (namespace, pagename) in tests:
             assert _split_namespace(map, markup) == (namespace, pagename)
             namespace, pagename = _split_namespace(map, markup)
 
     def test_split_interwiki(self):
-        app.cfg.namespace_mapping = [(u'', 'default_backend'), (u'ns1:', 'default_backend'), (u'ns1:ns2:', 'other_backend')]
-        tests = [('', ('Self', '', '')),
-                 ('OtherWiki:', ('OtherWiki', '', '')),
-                 (':ns1:', ('Self', 'ns1', '')),
-                 (':ns3:foo', ('Self', '', ':ns3:foo')),
-                 ('SomePage', ('Self', '', 'SomePage')),
-                 ('OtherWiki:OtherPage', ('OtherWiki', '', 'OtherPage')),
-                 ('NonExistentWiki:OtherPage', ('Self', '', 'NonExistentWiki:OtherPage')),
-                 (':ns1:OtherPage', ('Self', 'ns1', 'OtherPage')),
-                 (':ns1:ns2:OtherPage', ('Self', 'ns1:ns2', 'OtherPage')),
-                 ('ns1:OtherPage', ('Self', 'ns1', 'OtherPage')),
-                 ('ns1:ns2:OtherPage', ('Self', 'ns1:ns2', 'OtherPage')),
-                 ('OtherWiki:ns1:OtherPage', ('OtherWiki', 'ns1', 'OtherPage')),
-                 ('OtherWiki:ns1:ns2:OtherPage', ('OtherWiki', 'ns1:ns2', 'OtherPage')),
-                 ('OtherWiki:ns3:ns2:OtherPage/foo', ('OtherWiki', '', 'ns3:ns2:OtherPage/foo')),
+        app.cfg.namespace_mapping = [(u'', 'default_backend'), (u'ns1/', 'default_backend'), (u'ns1/ns2/', 'other_backend')]
+        tests = [('', ('Self', '', 'name_exact', '')),
+                 ('OtherWiki/', ('OtherWiki', '', 'name_exact', '')),
+                 ('/ns1/', ('Self', 'ns1', 'name_exact', '')),
+                 ('/@itemid/', ('Self', '', 'itemid', '')),
+                 ('/ns3/foo', ('Self', '', 'name_exact', 'ns3/foo')),
+                 ('@tags/SomeTag', ('Self', '', 'tags', 'SomeTag')),
+                 ('OtherWiki/OtherPage', ('OtherWiki', '', 'name_exact', 'OtherPage')),
+                 ('NonExistentWiki/OtherPage', ('Self', '', 'name_exact', 'NonExistentWiki/OtherPage')),
+                 ('OtherWiki/ns1/@invalidID/Page', ('OtherWiki', 'ns1', 'name_exact', '@invalidID/Page')),
+                 ('/ns1/OtherPage', ('Self', 'ns1', 'name_exact', 'OtherPage')),
+                 ('/ns1/ns2/OtherPage', ('Self', 'ns1/ns2', 'name_exact', 'OtherPage')),
+                 ('ns1/OtherPage', ('Self', 'ns1', 'name_exact', 'OtherPage')),
+                 ('ns1/ns2/OtherPage', ('Self', 'ns1/ns2', 'name_exact', 'OtherPage')),
+                 ('OtherWiki/ns1/OtherPage', ('OtherWiki', 'ns1', 'name_exact', 'OtherPage')),
+                 ('OtherWiki/ns1/ns2/OtherPage', ('OtherWiki', 'ns1/ns2', 'name_exact', 'OtherPage')),
+                 ('OtherWiki/ns1/ns2/@userid/SomeUserID', ('OtherWiki', 'ns1/ns2', 'userid', 'SomeUserID')),
+                 ('OtherWiki/ns3/ns2/@Notfield/OtherPage/foo', ('OtherWiki', '', 'name_exact', 'ns3/ns2/@Notfield/OtherPage/foo')),
                 ]
-        for markup, (wikiname, namespace, pagename) in tests:
-            assert split_interwiki(markup) == (wikiname, namespace, pagename)
-            wikiname, namespace, pagename = split_interwiki(markup)
+        for markup, (wikiname, namespace, field, pagename) in tests:
+            assert split_interwiki(markup) == (wikiname, namespace, field, pagename)
+            wikiname, namespace, field, pagename = split_interwiki(markup)
             assert isinstance(namespace, unicode)
             assert isinstance(pagename, unicode)
+            assert isinstance(field, unicode)
             assert isinstance(wikiname, unicode)
 
     def testJoinWiki(self):
-        tests = [(('http://example.org/', u'SomePage', ''), 'http://example.org/SomePage'),
-                 (('', u'SomePage', ''), 'SomePage'),
-                 (('http://example.org/?page=$PAGE&action=show', u'SomePage', ''), 'http://example.org/?page=SomePage&action=show'),
-                 (('http://example.org/', u'Aktuelle\xc4nderungen', ''), 'http://example.org/Aktuelle%C3%84nderungen'),
-                 (('http://example.org/$PAGE/show', u'Aktuelle\xc4nderungen', ''), 'http://example.org/Aktuelle%C3%84nderungen/show'),
+        tests = [(('http://example.org/', u'SomePage', '', ''), 'http://example.org/SomePage'),
+                 (('', u'SomePage', '', ''), 'SomePage'),
+                 (('http://example.org/?page=$PAGE&action=show', u'SomePage', '', ''), 'http://example.org/?page=SomePage&action=show'),
+                 (('http://example.org/', u'Aktuelle\xc4nderungen', '', ''), 'http://example.org/Aktuelle%C3%84nderungen'),
+                 (('http://example.org/$PAGE/show', u'Aktuelle\xc4nderungen', '', ''), 'http://example.org/Aktuelle%C3%84nderungen/show'),
 
-                 (('http://example.org/', u'SomePage', u'ns1'), 'http://example.org/:ns1:SomePage'),
-                 (('http://example.org/?page=$PAGE&action=show&namespace=$NAMESPACE', u'SomePage', u'ns1'), 'http://example.org/?page=SomePage&action=show&namespace=ns1'),
-                 (('http://example.org/', u'Aktuelle\xc4nderungen', u'ns1\xc4'), 'http://example.org/:ns1%C3%84:Aktuelle%C3%84nderungen'),
-                 (('http://example.org/$NAMESPACE/$PAGE/show', u'Aktuelle\xc4nderungen', u'ns\xc41'), 'http://example.org/ns%C3%841/Aktuelle%C3%84nderungen/show'),
+                 (('http://example.org/', u'SomeItemID', 'itemid', u'ns1'), 'http://example.org/ns1/@itemid/SomeItemID'),
+                 (('http://example.org/?page=$PAGE&action=show&namespace=$NAMESPACE', u'SomePage', '', u'ns1'), 'http://example.org/?page=SomePage&action=show&namespace=ns1'),
+                 (('http://example.org/', u'Aktuelle\xc4nderungen', '', u'ns1\xc4'), 'http://example.org/ns1%C3%84/Aktuelle%C3%84nderungen'),
+                 (('http://example.org/$NAMESPACE/$PAGE/show', u'Aktuelle\xc4nderungen', '', u'ns\xc41'), 'http://example.org/ns%C3%841/Aktuelle%C3%84nderungen/show'),
+                 (('http://example.org/@$FIELD/$PAGE/show', u'Aktuelle\xc4nderungen', u'itemid', u''), 'http://example.org/@itemid/Aktuelle%C3%84nderungen/show'),
                 ]
-        for (baseurl, pagename, namespace), url in tests:
-            assert join_wiki(baseurl, pagename, namespace) == url
+        for (baseurl, pagename, field, namespace), url in tests:
+            assert join_wiki(baseurl, pagename, field, namespace) == url
+
+    def test_split_fqname(self):
+        app.cfg.namespace_mapping = [(u'', 'default_backend'), (u'ns1/', 'default_backend'), (u'ns1/ns2/', 'other_backend')]
+        tests = [('ns1/ns2/@itemid/SomeItemID', ('ns1/ns2', 'itemid', 'SomeItemID')),
+                 ('ns3/@itemid/SomeItemID', ('', 'name_exact', 'ns3/@itemid/SomeItemID')),
+                 ('Page', ('', 'name_exact', 'Page')),
+                 ('ns1/ns2/@tags/SomeTag', ('ns1/ns2', 'tags', 'SomeTag')),
+                 ('@tags/SomeTag', ('', 'tags', 'SomeTag')),
+                 ('ns1/ns2/@notid', ('ns1/ns2', 'name_exact', '@notid')),
+                 ('ns1/ns2/ns3/Thisisapagename/ns4', ('ns1/ns2', 'name_exact', 'ns3/Thisisapagename/ns4')),
+                ]
+        for url, (namespace, field, pagename) in tests:
+            assert split_fqname(url) == (namespace, field, pagename)
 
 
 class TestInterWikiMapBackend(object):
--- a/MoinMoin/util/interwiki.py	Wed Jul 17 18:17:48 2013 +0300
+++ b/MoinMoin/util/interwiki.py	Mon Jul 22 22:51:34 2013 +0530
@@ -14,8 +14,9 @@
 from flask import url_for
 
 import os.path
+from collections import namedtuple
 
-from MoinMoin.constants.keys import CURRENT
+from MoinMoin.constants.keys import CURRENT, FIELDS, NAME_EXACT, NAMESPACE
 from MoinMoin.constants.contenttypes import CHARSET
 
 from MoinMoin import log
@@ -39,7 +40,19 @@
     return wiki_name in app.cfg.interwiki_map
 
 
-def url_for_item(item_name, wiki_name=u'', namespace=u'', rev=CURRENT, endpoint=u'frontend.show_item', _external=False):
+def get_fqname(item_name, field, namespace):
+    """
+    Compute composite name from item_name, field, namespace
+    composite name == [NAMESPACE/][@FIELD/]NAME
+    """
+    if field and field != NAME_EXACT:
+        item_name = u'@{0}/{1}'.format(field, item_name)
+    if namespace:
+        item_name = u'{0}/{1}'.format(namespace, item_name)
+    return item_name
+
+
+def url_for_item(item_name, wiki_name=u'', field=u'', namespace=u'', rev=CURRENT, endpoint=u'frontend.show_item', _external=False):
     """
     Compute URL for some local or remote/interwiki item.
 
@@ -55,9 +68,10 @@
     URLs are built in the same way as local URLs.
     Computed URLs are always fully specified.
     """
+    if field == NAME_EXACT:
+        field = u''
     if is_local_wiki(wiki_name):
-        if namespace:
-            item_name = u':{0}:{1}'.format(namespace, item_name)
+        item_name = get_fqname(item_name, field, namespace)
         if rev is None or rev == CURRENT:
             url = url_for(endpoint, item_name=item_name, _external=_external)
         else:
@@ -67,10 +81,9 @@
             wiki_base_url = app.cfg.interwiki_map[wiki_name]
         except KeyError, err:
             logging.warning("no interwiki_map entry for {0!r}".format(wiki_name))
-            if namespace:
-                item_name = u'{0}:{1}'.format(namespace, item_name)
+            item_name = get_fqname(item_name, field, namespace)
             if wiki_name:
-                url = u'{0}:{1}'.format(wiki_name, item_name)
+                url = u'{0}/{1}'.format(wiki_name, item_name)
             else:
                 url = item_name
             url = u'/{0}'.format(url)
@@ -78,13 +91,12 @@
             if (rev is None or rev == CURRENT) and endpoint == 'frontend.show_item':
                 # we just want to show latest revision (no special revision given) -
                 # this is the generic interwiki url support, should work for any remote wiki
-                url = join_wiki(wiki_base_url, item_name, namespace)
+                url = join_wiki(wiki_base_url, item_name, field, namespace)
             else:
                 # rev and/or endpoint was given, assume same URL building as for local wiki.
                 # we need this for moin wiki farms, e.g. to link from search results to
                 # some specific item/revision in another farm wiki.
-                if namespace:
-                    item_name = u'{0}:{1}'.format(namespace, item_name)
+                item_name = get_fqname(item_name, field, namespace)
                 local_url = url_for(endpoint, item_name=item_name, rev=rev, _external=False)
                 # we know that everything left of the + belongs to script url, but we
                 # just want e.g. +show/42/FooBar to append it to the other wiki's
@@ -98,22 +110,22 @@
 def _split_namespace(namespaces, url):
     """
     Find the longest namespace in the set.
-    the namespaces are separated by colons (:).
+    the namespaces are separated by  slashes (/).
     Example:
-        namespaces_set(['ns1', 'ns1:ns2'])
-        url: ns1:urlalasd return: ns1, urlalasd
-        url: ns3:urlalasd return: '', ns3:urlalasd
-        url: ns2:urlalasd return: '', ns2:urlalasd
-        url: ns1:ns2:urlalasd return: ns1:ns2, urlalasd
+        namespaces_set(['ns1', 'ns1/ns2'])
+        url: ns1/urlalasd return: ns1, urlalasd
+        url: ns3/urlalasd return: '', ns3/urlalasd
+        url: ns2/urlalasd return: '', ns2/urlalasd
+        url: ns1/ns2/urlalasd return: ns1/ns2, urlalasd
     param namespaces_set: set of namespaces (strings) to search
     param url: string
     returns: (namespace, url)
     """
     namespace = u''
-    tokens_list = url.split(':')
+    tokens_list = url.split('/')
     for token in tokens_list:
         if namespace:
-            token = u'{0}:{1}'.format(namespace, token)
+            token = u'{0}/{1}'.format(namespace, token)
         if token in namespaces:
             namespace = token
         else:
@@ -124,50 +136,102 @@
     return namespace, url
 
 
+class CompositeName(namedtuple('CompositeName', 'namespace, field, value')):
+    """
+    namedtuple to hold the compositename
+    """
+    @property
+    def split(self):
+        """
+        returns a dict of field_names/field_values
+        """
+        return {NAMESPACE: self.namespace, u'field': self.field, u'item_name': self.value}
+
+    @property
+    def fullname(self):
+        return get_fqname(self.value, self.field, self.namespace)
+
+    def __unicode__(self):
+        return self.fullname
+
+    @property
+    def query(self):
+        """
+        returns a dict that can be used as a whoosh query
+        to lookup index documents matching this CompositeName
+        """
+        field = NAME_EXACT if not self.field else self.field
+        return {NAMESPACE: self.namespace, field: self.value}
+
+
+def split_fqname(url):
+    """
+    Split a fully qualified url into namespace, field and pagename
+    url -> [NAMESPACE/][@FIELD/]NAME
+    param: url: the url to split
+    returns: a namedtuple CompositeName(namespace, field, itemname)
+    Example:
+        url: u'ns1/ns2/@itemid/Page' return u'ns1/ns2', u'itemid', u'Page'
+        url: u'@revid/OtherPage' return u'', u'revid', u'OtherPage'
+        url: u'ns1/Page' return u'ns1', u'', u'Page'
+        url: u'ns1/ns2/@notfield' return u'ns1/ns2', u'', u'@notfield'
+    """
+    if not url:
+        return CompositeName(u'', NAME_EXACT, u'')
+    namespaces = {namespace.rstrip('/') for namespace, _ in app.cfg.namespace_mapping}
+    namespace, url = _split_namespace(namespaces, url)
+    field = NAME_EXACT
+    if url.startswith('@'):
+        tokens = url[1:].split('/', 1)
+        if tokens[0] in FIELDS:
+            field = tokens[0]
+            url = tokens[1] if len(tokens) > 1 else u''
+    return CompositeName(namespace, field, url)
+
+
 def split_interwiki(wikiurl):
     """ Split a interwiki name, into wikiname and pagename, e.g:
 
-    'MoinMoin:FrontPage' -> "MoinMoin", "", "FrontPage"
-    'FrontPage' -> "Self", "", "FrontPage"
-    'MoinMoin:Page with blanks' -> "MoinMoin", "", "Page with blanks"
-    'MoinMoin:' -> "MoinMoin", "", ""
-    'MoinMoin:interwikins:AnyPage' -> "MoinMoin", "interwikins", "AnyPage"
-    ':ns:AnyPage' -> "Self", "ns", "AnyPage" if ns namespace exists or "Self", "", ":ns:AnyPage" if not.
-    'ns:AnyPage' -> "Self", "ns", "AnyPage" if ns namespace exists or "Self", "", "ns:AnyPage" if not.
-    ':ns1:ns2:AnyPage' -> "Self", "ns1:ns2", "AnyPage" if ns1:ns2 namespace exists OR
-                         "Self", "ns1", "ns2:AnyPage" if ns1 namespace exists OR
-                         "Self", "", "ns1:ns2:AnyPage" else.
+    'MoinMoin/FrontPage' -> "MoinMoin", "", "", "FrontPage"
+    'FrontPage' -> "Self", "", "", "FrontPage"
+    'MoinMoin/Page with blanks' -> "MoinMoin", "", "", "Page with blanks"
+    'MoinMoin/' -> "MoinMoin", "", "", ""
+    'MoinMoin/@Someid/SomeValue' -> "MoinMoin", "", "Someid", "SomeValue" if Someid field exists or "MoinMoin", "", "", "Someid/SomePage" if not
+    'MoinMoin/interwikins/AnyPage' -> "MoinMoin", "interwikins", "", "AnyPage"
+    'ns/AnyPage' -> "Self", "ns", "", "AnyPage" if ns namespace exists or "Self", "", "", "ns:AnyPage" if not.
+    'ns1/ns2/AnyPage' -> "Self", "ns1/ns2", "", "AnyPage" if ns1/ns2 namespace exists OR
+                         "Self", "ns1", "", "ns2/AnyPage" if ns1 namespace exists OR
+                         "Self", "", "", "ns1/ns2/AnyPage" else.
+    'MoinMoin/ns/@Somefield/AnyPage' -> "MoinMoin", "ns", "", "@Somefield/AnyPage" if ns namespace exists and field Somefield does not OR
+                                     "MoinMoin", "ns", "Somefield", "AnyPage" if ns namespace and field Somefield exist OR
+                                     "MoinMoin", "", "", "ns/@Somefield/AnyPage" else.
     :param wikiurl: the url to split
     :rtype: tuple
-    :returns: (wikiname, namespace, pagename)
+    :returns: (wikiname, namespace, field, pagename)
     """
     if not isinstance(wikiurl, unicode):
         wikiurl = wikiurl.decode('utf-8')
-    namespace_mapping = set()
-    for namespace, _ in app.cfg.namespace_mapping:
-        namespace_mapping.add(namespace.rstrip(':'))
     # Base case: no colon in wikiurl
-    if not ':' in wikiurl:
-        return u'Self', u'', wikiurl
-    if not wikiurl.startswith(':'):
-        wikiname, item_name = _split_namespace(set(app.cfg.interwiki_map.keys()), wikiurl)
-        namespace = u''
+    if not '/' in wikiurl:
+        return u'Self', u'', NAME_EXACT, wikiurl
+    wikiname = field = namespace = u''
+    if not wikiurl.startswith('/'):
+        interwiki_mapping = set()
+        for interwiki_name in app.cfg.interwiki_map:
+            interwiki_mapping.add(interwiki_name.split('/')[0])
+        wikiname, item_name = _split_namespace(interwiki_mapping, wikiurl)
+        if wikiname:
+            wikiurl = wikiurl[len(wikiname)+1:]
+        namespace, field, item_name = split_fqname(wikiurl)
         if not wikiname:
-            namespace, item_name = _split_namespace(set(namespace_mapping), wikiurl)
             wikiname = u'Self'
-        else:
-            if ':' in wikiname:
-                namespace = wikiname.split(':', 1)[1]
-                wikiname = wikiname.split(':', 1)[0]
-        return wikiname, namespace, item_name
+        return wikiname, namespace, field, item_name
     else:
-        namespace, item_name = _split_namespace(set(namespace_mapping), wikiurl.split(':', 1)[1])
-        if not namespace:
-            item_name = u':{0}'.format(item_name)
-        return u'Self', namespace, item_name
+        namespace, field, item_name = split_fqname(wikiurl.split('/', 1)[1])
+        return u'Self', namespace, field, item_name
 
 
-def join_wiki(wikiurl, wikitail, namespace):
+def join_wiki(wikiurl, wikitail, field, namespace):
     """
     Add a (url_quoted) page name to an interwiki url.
 
@@ -181,15 +245,14 @@
     :returns: generated URL of the page in the other wiki
     """
     wikitail = url_quote(wikitail, charset=CHARSET, safe='/')
+    field = url_quote(field, charset=CHARSET, safe='/')
     namespace = url_quote(namespace, charset=CHARSET, safe='/')
-    if not('$PAGE' in wikiurl or '$NAMESPACE' in wikiurl):
-        if namespace:
-            namespace = u':{0}:'.format(namespace)
-        elif not wikiurl:
-            return wikitail
-        return wikiurl + namespace + wikitail
+    if not('$PAGE' in wikiurl or '$NAMESPACE' in wikiurl or '$FIELD' in wikiurl):
+        return wikiurl + get_fqname(wikitail, field, namespace)
     if '$PAGE' in wikiurl:
         wikiurl = wikiurl.replace('$PAGE', wikitail)
+    if '$FIELD' in wikiurl:
+        wikiurl = wikiurl.replace('$FIELD', field)
     if '$NAMESPACE' in wikiurl:
         wikiurl = wikiurl.replace('$NAMESPACE', namespace)
     return wikiurl
@@ -203,7 +266,7 @@
     :rtype: unicode
     :returns: wiki_name:item_name
     """
-    return u"{0}:{1}".format(app.cfg.interwikiname, item_name)
+    return u"{0}/{1}".format(app.cfg.interwikiname, item_name)
 
 
 def getInterwikiHome(username):