Commits

Thomas Waldmann  committed 1bec42a Merge

merged namespaces branch into default branch - expect some breakage

  • Participants
  • Parent commits 7fe3283, 70b9962

Comments (0)

Files changed (58)

 ^instance/
 ^wikiconfig_.+\.py
 ^MoinMoin/translations/.*/LC_MESSAGES/messages.mo$
+^MoinMoin/_tests/wiki/index/
 ^docs/_build/
 .coverage
 ^.project

File MoinMoin/_tests/__init__.py

         Thus, for testing purposes (e.g. if you need delete rights), it is
         easier to use become_trusted().
     """
-    flaskg.user.profile[NAME] = username
-    flaskg.user.may.name = username
+    flaskg.user.profile[NAME] = [username, ]
+    flaskg.user.may.names = [username, ]  # see security.DefaultSecurityPolicy class
     flaskg.user.valid = 1
 
 
 
     meta = meta.copy()
     if NAME not in meta:
-        meta[NAME] = name
+        meta[NAME] = [name, ]
     if CONTENTTYPE not in meta:
         meta[CONTENTTYPE] = u'application/octet-stream'
     rev = item.store_revision(meta, StringIO(data), return_rev=True)

File MoinMoin/_tests/test_test_environ.py

         itemname = u"this item shouldn't exist yet"
         assert not storage.has_item(itemname)
         item = storage[itemname]
-        new_rev = item.store_revision({NAME: itemname, CONTENTTYPE: u'text/plain'}, StringIO(''))
+        new_rev = item.store_revision({NAME: [itemname, ], CONTENTTYPE: u'text/plain'}, StringIO(''))
         assert storage.has_item(itemname)
 
 

File MoinMoin/_tests/test_user.py

         email = u"foo@example.org"
         # nonexisting user
         u = user.User(name=name, password=password)
-        assert u.name == name
+        assert u.name == [name, ]
         assert not u.valid
         assert not u.exists()
         # create a user
         assert ret is None, "create_user returned: {0}".format(ret)
         # existing user
         u = user.User(name=name, password=password)
-        assert u.name == name
+        assert u.name == [name, ]
         assert u.email == email
         assert u.valid
         assert u.exists()

File MoinMoin/app.py

     # A ns_mapping consists of several lines, where each line is made up like this:
     # mountpoint, unprotected backend
     # Just initialize with unprotected backends.
-    app.router = routing.Backend(app.cfg.namespace_mapping)
+    app.router = routing.Backend(app.cfg.namespace_mapping, app.cfg.backend_mapping)
     if app.cfg.create_storage:
         app.router.create()
     app.router.open()
 
     # if we still have no user obj, create a dummy:
     if not userobj:
-        userobj = user.User(auth_method='invalid')
+        userobj = user.User(name=u'anonymous', auth_method='invalid')
     # if we have a valid user we store it in the session
     if userobj.valid:
         session['user.itemid'] = userobj.itemid

File MoinMoin/apps/admin/templates/admin/userbrowser.html

     </tr>
     {% for u in user_accounts %}
     <tr>
-        <td><a href="{{ url_for('frontend.show_item', item_name=u.name) }}">{{ u.name }}</a>{{ u.disabled and " (%s)" % _("disabled") or ""}}</td>
+        <td><a href="{{ url_for('frontend.show_item', item_name=u.name[0]) }}">{{ u.name|join(',') }}</a>{{ u.disabled and " (%s)" % _("disabled") or ""}}</td>
         <td>{{ u.groups|join(',') }}</td>
         <td>
             {% if u.email %}

File MoinMoin/apps/admin/views.py

     headings = [_('Size'),
                 _('Item name'),
                ]
-    rows = [(rev.meta[SIZE], rev.meta[NAME])
+    rows = [(rev.meta[SIZE], rev.name)
             for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)]
     rows = sorted(rows, reverse=True)
     return render_template('user/itemsize.html',

File MoinMoin/apps/feed/views.py

             query = And([query, Term(NAME_EXACT, item_name), ])
         history = flaskg.storage.search(query, idx_name=ALL_REVS, sortedby=[MTIME], reverse=True, limit=100)
         for rev in history:
-            name = rev.meta[NAME]
+            name = rev.name
             item = rev.item
             this_revid = rev.meta[REVID]
             previous_revid = rev.meta.get(PARENTID)

File MoinMoin/apps/frontend/views.py

 from flask.ext.babel import format_date
 from flask.ext.themes import get_themes_list
 
-from flatland import Form, Enum
+from flatland import Form, Enum, List
 from flatland.validation import Validator
 
 from jinja2 import Markup
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.themes import render_template, get_editor_info, contenttype_to_class
 from MoinMoin.apps.frontend import frontend
-from MoinMoin.forms import OptionalText, RequiredText, URL, YourOpenID, YourEmail, RequiredPassword, Checkbox, InlineCheckbox, Select, Tags, Natural, Submit, Hidden, MultiSelect
+from MoinMoin.forms import OptionalText, RequiredText, URL, YourOpenID, YourEmail, RequiredPassword, Checkbox, InlineCheckbox, Select, Names, Tags, Natural, Submit, Hidden, MultiSelect
 from MoinMoin.items import BaseChangeForm, Item, NonExistent
 from MoinMoin.items.content import content_registry
 from MoinMoin import config, user, util
         rev = item[rev]
     except KeyError:
         abort(404, item_name)
-    content = convert_to_indexable(rev.meta, rev.data)
+    content = convert_to_indexable(rev.meta, rev.data, item_name)
     return Response(content, 200, mimetype='text/plain')
 
 
     selected_groups = form['contenttype'].value
     startswith = request.values.get("startswith")
 
-    initials = item.name_initial(item.get_subitem_revs())
-    initials = [initial.upper() for initial in initials]
-    initials = list(set(initials))
-    initials = sorted(initials)
+    initials = item.name_initial(item.get_subitem_revs(), uppercase=True)
 
     dirs, files = item.get_index(startswith, selected_groups)
     # index = sorted(index, key=lambda e: e.relname.lower())
     q = And([Term(WIKINAME, app.cfg.interwikiname),
              Term(USERID, userid)])
     revs = flaskg.storage.search(q, idx_name=ALL_REVS)
-    return [rev.meta[NAME] for rev in revs]
+    return [rev.name for rev in revs]
 
 
 @frontend.route('/+backrefs/<itemname:item_name>')
     q = And([Term(WIKINAME, app.cfg.interwikiname),
              Or([Term(ITEMTRANSCLUSIONS, item_name), Term(ITEMLINKS, item_name)])])
     revs = flaskg.storage.search(q)
-    return [rev.meta[NAME] for rev in revs]
+    return [rev.name for rev in revs]
 
 
 @frontend.route('/+history/<itemname:item_name>')
     existing = set()
     revs = flaskg.storage.documents(wikiname=app.cfg.interwikiname)
     for rev in revs:
-        existing.add(rev.meta[NAME])
+        existing.add(rev.name)
         linked.update(rev.meta.get(ITEMLINKS, []))
         transcluded.update(rev.meta.get(ITEMTRANSCLUSIONS, []))
     return existing, linked, transcluded
     # these forms can't be global because we need app object, which is only available within a request:
     class UserSettingsPersonalForm(Form):
         name = 'usersettings_personal' # "name" is duplicate
-        name = RequiredText.using(label=L_('Name')).with_properties(placeholder=L_("The login name you want to use"))
-        aliasname = OptionalText.using(label=L_('Alias-Name')).with_properties(placeholder=L_("Your alias name (informational)"))
+        name = Names.using(label=L_('Names')).with_properties(placeholder=L_("The login names you want to use"))
+        display_name = OptionalText.using(label=L_('Display-Name')).with_properties(placeholder=L_("Your display name (informational)"))
         openid = YourOpenID.using(optional=True)
         #timezones_keys = sorted(Locale('en').time_zones.keys())
         timezones_keys = [unicode(tz) for tz in pytz.common_timezones]
                             # duplicate openid
                             response['flash'].append((_("This openid is already in use."), "error"))
                             success = False
-                        if form['name'].value != flaskg.user.name and user.search_users(name_exact=form['name'].value):
-                            # duplicate name
-                            response['flash'].append((_("This username is already in use."), "error"))
-                            success = False
+                        if set(form['name'].value) != set(flaskg.user.name):
+                            new_names = set(form['name'].value) - set(flaskg.user.name)
+                            for name in new_names:
+                                if user.search_users(name_exact=name):
+                                    # duplicate name
+                                    response['flash'].append((_("The username %(name)r is already in use.", name=name), "error"))
+                                    success = False
                     if part == 'notification':
                         if (form['email'].value != flaskg.user.email and
                             user.search_users(email=form['email'].value) and app.cfg.user_email_unique):
     :rtype: tuple
     :returns: start word, end word, matches dict
     """
-    item_names = [rev.meta[NAME] for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)]
+    item_names = [rev.name for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)
+                  if rev.name is not None]
     if item_name in item_names:
         item_names.remove(item_name)
     # Get matches using wiki way, start and end of word
     tags_counts = {}
     for rev in revs:
         tags = rev.meta.get(TAGS, [])
-        logging.debug("name {0!r} rev {1} tags {2!r}".format(rev.meta[NAME], rev.meta[REVID], tags))
+        logging.debug("name {0!r} rev {1} tags {2!r}".format(rev.name, rev.meta[REVID], tags))
         for tag in tags:
             tags_counts[tag] = tags_counts.setdefault(tag, 0) + 1
     tags_counts = sorted(tags_counts.items())
     """
     query = And([Term(WIKINAME, app.cfg.interwikiname), Term(TAGS, tag), ])
     revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
-    item_names = [rev.meta[NAME] for rev in revs]
+    item_names = [rev.name for rev in revs]
     return render_template("link_list_no_item_panel.html",
                            headline=_("Items tagged with %(tag)s", tag=tag),
                            item_name=tag,

File MoinMoin/apps/misc/views.py

 
     sitemap = []
     for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname):
-        name = rev.meta[NAME]
+        name = rev.name
         mtime = rev.meta[MTIME]
         if False: # was: wikiutil.isSystemItem(name)   XXX add back later, when we have that in the index
             if not SITEMAP_HAS_SYSTEM_ITEMS:
     See: http://usemod.com/cgi-bin/mb.pl?SisterSitesImplementationGuide
     """
     # XXX we currently also get deleted items, fix this
-    item_names = sorted([rev.meta[NAME] for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)])
+    item_names = sorted([rev.name for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)])
     content = render_template('misc/urls_names.txt', item_names=item_names)
     return Response(content, mimetype='text/plain')

File MoinMoin/auth/_tests/test_auth.py

         auth = [GivenAuth(user_name=u'JoeDoe', autocreate=True), ]
 
     def test(self):
-        assert flaskg.user.name == u'JoeDoe'
+        assert flaskg.user.name == [u'JoeDoe', ]
 
 
 class TestGivenAuth(object):
         create_user(u'Test_User', u'test_pass', u'test@moinmoin.org')
         test_user, bool_value = givenauth_obj.request(flaskg.user)
         assert test_user.valid
-        assert test_user.name == u'Test_User'
+        assert test_user.name == [u'Test_User', ]
 
 def test_handle_login():
     # no messages in the beginning
     test_user1 = handle_login(flaskg.user, login_username='test_user', login_password='test_password', stage='moin')
     test_login_message = [u'Invalid username or password.']
     assert flaskg._login_messages == test_login_message
-    assert test_user1.name == u'anonymous'
+    assert test_user1.name0 == u'anonymous'
     assert not test_user1.valid
     # pop the message
     flaskg._login_messages.pop()
     test_user, bool_value = givenauth_obj.request(flaskg.user)
     test_user2 = handle_login(test_user, login_username='Test_User', login_password='test_pass', stage='moin')
     assert not flaskg._login_messages
-    assert test_user2.name == u'Test_User'
+    assert test_user2.name == [u'Test_User', ]
     assert test_user2.valid
 
 def test_get_multistage_continuation_url():

File MoinMoin/auth/_tests/test_http.py

         httpauthmoin_obj = HTTPAuthMoin()
         test_user, bool_val = httpauthmoin_obj.request(flaskg.user)
         assert test_user.valid
-        assert test_user.name == u'ValidUser'
+        assert test_user.name == [u'ValidUser', ]
         assert bool_val
 
         # when auth_method is not 'http'
         flaskg.user.auth_method = 'invalid'
         test_user, bool_val = httpauthmoin_obj.request(flaskg.user)
         assert not test_user.valid
-        assert test_user.name == u'anonymous'
+        assert test_user.name0 == u'anonymous'

File MoinMoin/auth/_tests/test_log.py

         result = authlog_obj.login(flaskg.user)
         assert result.continue_flag
         test_user_obj = result.user_obj
-        assert test_user_obj.name == u'anonymous'
+        assert test_user_obj.name0 == u'anonymous'
 
     def test_request(self):
         authlog_obj = AuthLog()
         result = authlog_obj.request(flaskg.user)
         test_user, bool_value = result
-        assert test_user.name == u'anonymous'
+        assert test_user.name0 == u'anonymous'
         assert not test_user.valid
         assert bool_value
 
         authlog_obj = AuthLog()
         result = authlog_obj.logout(flaskg.user)
         test_user, bool_value = result
-        assert test_user.name == u'anonymous'
+        assert test_user.name0 == u'anonymous'
         assert not test_user.valid
         assert bool_value

File MoinMoin/auth/ldap_login.py

     "Can't contact LDAP server" - more recent debian installations have tls
     support in libldap2 (see dependency on gnutls) and also in python-ldap.
 
-    TODO: allow more configuration (alias name, ...) by using callables as parameters
+    TODO: allow more configuration (display name, ...) by using callables as parameters
 """
 
 from MoinMoin import log
         # some attribute names we use to extract information from LDAP:
         givenname_attribute=None, # ('givenName') ldap attribute we get the first name from
         surname_attribute=None, # ('sn') ldap attribute we get the family name from
-        aliasname_attribute=None, # ('displayName') ldap attribute we get the aliasname from
+        displayname_attribute=None, # ('displayName') ldap attribute we get the display_name from
         email_attribute=None, # ('mail') ldap attribute we get the email address from
         email_callback=None, # called to make up email address
         name_callback=None, # called to use a Wiki name different from the login name
 
         self.givenname_attribute = givenname_attribute
         self.surname_attribute = surname_attribute
-        self.aliasname_attribute = aliasname_attribute
+        self.displayname_attribute = displayname_attribute
         self.email_attribute = email_attribute
         self.email_callback = email_callback
         self.name_callback = name_callback
                 logging.debug("Searching {0!r}".format(filterstr))
                 attrs = [getattr(self, attr) for attr in [
                                          'email_attribute',
-                                         'aliasname_attribute',
+                                         'displayname_attribute',
                                          'surname_attribute',
                                          'givenname_attribute',
                                          ] if getattr(self, attr) is not None]
                 else:
                     email = self.email_callback(ldap_dict)
 
-                aliasname = ''
+                display_name = ''
                 try:
-                    aliasname = ldap_dict[self.aliasname_attribute][0]
+                    display_name = ldap_dict[self.displayname_attribute][0]
                 except (KeyError, IndexError):
                     pass
-                if not aliasname:
+                if not display_name:
                     sn = ldap_dict.get(self.surname_attribute, [''])[0]
                     gn = ldap_dict.get(self.givenname_attribute, [''])[0]
                     if sn and gn:
-                        aliasname = "{0}, {1}".format(sn, gn)
+                        display_name = "{0}, {1}".format(sn, gn)
                     elif sn:
-                        aliasname = sn
-                aliasname = aliasname.decode(coding)
+                        display_name = sn
+                display_name = display_name.decode(coding)
 
                 if self.name_callback:
                     username = self.name_callback(ldap_dict)
                     u = user.User(auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'mailto_author', ),
                                   trusted=self.trusted)
                 u.name = username
-                u.aliasname = aliasname
-                logging.debug("creating user object with name {0!r} email {1!r} alias {2!r}".format(username, email, aliasname))
+                u.display_name = display_name
+                logging.debug("creating user object with name {0!r} email {1!r} display name {2!r}".format(username, email, display_name))
 
             except ldap.INVALID_CREDENTIALS as err:
                 logging.debug("invalid credentials (wrong password?) for dn {0!r} (username: {1!r})".format(dn, username))

File MoinMoin/config/default.py

             raise error.ConfigurationError("No storage configuration specified! You need to define a namespace_mapping. "
                                            "For further reference, please see HelpOnStorageConfiguration.")
 
+        if self.backend_mapping is None:
+            raise error.ConfigurationError("No storage configuration specified! You need to define a backend_mapping. " +
+                                           "For further reference, please see HelpOnStorageConfiguration.")
+
         if self.acl_mapping is None:
             raise error.ConfigurationError("No acl configuration specified! You need to define a acl_mapping. "
                                            "For further reference, please see HelpOnStorageConfiguration.")
     ('interwiki_map', {},
      "Dictionary of wiki_name -> wiki_url"),
     ('namespace_mapping', None,
-    "This needs to point to a list of tuples, each tuple containing: Namespace identifier, backend. " +
-    "E.g.: [('/', FSBackend('wiki/data')), ]. Please see HelpOnStorageConfiguration for further reference."),
+    "A list of tuples, each tuple containing: Namespace identifier, backend name. " +
+    "E.g.: [('', 'default')), ]. Please see HelpOnStorageConfiguration for further reference."),
+    ('backend_mapping', None,
+    "A dictionary that maps backend names to backends. " +
+    "E.g.: {'default': Backend(), }. Please see HelpOnStorageConfiguration for further reference."),
     ('acl_mapping', None,
     "This needs to point to a list of tuples, each tuple containing: name prefix, acl protection to be applied to matching items. " +
     "E.g.: [('', dict(default='All:read,write,create')), ]. Please see HelpOnStorageConfiguration for further reference."),
   'user': ('User Preferences', None, (
     ('user_defaults',
       dict(
-        name=u'anonymous',
-        aliasname=None,
+        name=[],
+        display_name=None,
         email=None,
         openid=None,
         css_url=None,

File MoinMoin/conftest.py

 
 
 def init_test_app(given_config):
-    namespace_mapping, acl_mapping = create_simple_mapping("stores:memory:", given_config.content_acl)
+    namespace_mapping, backend_mapping, acl_mapping = \
+        create_simple_mapping("stores:memory:", given_config.content_acl)
     more_config = dict(
         namespace_mapping=namespace_mapping,
+        backend_mapping=backend_mapping,
         acl_mapping=acl_mapping,
         create_storage=True, # create a fresh storage at each app start
         destroy_storage=True, # kill all storage contents at app shutdown

File MoinMoin/constants/keys.py

 # metadata keys
 NAME = "name"
 NAME_OLD = "name_old"
+NAMESPACE = "namespace"
 
 # if an item is reverted, we store the revision number we used for reverting there:
 REVERTED_TO = "reverted_to"
 # stuff from user profiles / for whoosh index
 EMAIL = "email"
 OPENID = "openid"
-ALIASNAME = "aliasname"
+DISPLAY_NAME = "display_name"
 THEME_NAME = "theme_name"
 LOCALE = "locale"
 TIMEZONE = "timezone"
 RESULTS_PER_PAGE = "results_per_page"
 DISABLED = "disabled"
 
+# in which backend is some revision stored?
+BACKENDNAME = "backendname"
+
 USEROBJ_ATTRS = [
     # User objects proxy these attributes of the UserProfile objects:
-    NAME, DISABLED, ITEMID, ALIASNAME, ENC_PASSWORD, EMAIL, OPENID,
+    NAME, DISABLED, ITEMID, DISPLAY_NAME, ENC_PASSWORD, EMAIL, OPENID,
     MAILTO_AUTHOR, SHOW_COMMENTS, RESULTS_PER_PAGE, EDIT_ON_DOUBLECLICK, SCROLL_PAGE_AFTER_EDIT,
     EDIT_ROWS, THEME_NAME, LOCALE, TIMEZONE, SUBSCRIBED_ITEMS, QUICKLINKS,
     CSS_URL,

File MoinMoin/converter/macro.py

 
         type = Type(type)
         if not (type.type == 'x-moin' and type.subtype == 'macro'):
-            logging.debug("not a macro, skipping: %r" % type)
+            logging.debug("not a macro, skipping: %r" % (type, ))
             return
 
         name = type.parameters['name']

File MoinMoin/datastruct/backends/_tests/test_wiki_groups.py

         assert u'ExampleUser' in flaskg.groups[u'SomeGroup']
         pytest.raises(GroupDoesNotExistError, lambda: flaskg.groups[u'AnotherGroup'])
 
-        item = update_item(u'SomeGroup', {NAME: u'AnotherGroup', USERGROUP: ["ExampleUser"]}, DATA)
+        item = update_item(u'SomeGroup', {NAME: [u'AnotherGroup', ], USERGROUP: ["ExampleUser"]}, DATA)
         assert u'ExampleUser' in flaskg.groups[u'AnotherGroup']
         pytest.raises(GroupDoesNotExistError, lambda: flaskg.groups[u'SomeGroup'])
 

File MoinMoin/forms.py

 
 Tags = MyJoinedString.of(String).with_properties(widget=WIDGET_TEXT).using(label=L_('Tags'), optional=True, separator=', ', separator_regex=re.compile(r'\s*,\s*'))
 
+Names = MyJoinedString.of(String).with_properties(widget=WIDGET_TEXT).using(label=L_('Names'), optional=True, separator=', ', separator_regex=re.compile(r'\s*,\s*'))
+
 Search = Text.using(default=u'', optional=True).with_properties(widget=WIDGET_SEARCH, placeholder=L_("Search Query"))
 
 _Integer = Integer.validated_by(Converted())

File MoinMoin/items/__init__.py

     Each class in this module corresponds to an itemtype.
 """
 
-import time
-import itertools
 import json
 from StringIO import StringIO
 from collections import namedtuple
 from MoinMoin.security.textcha import TextCha, TextChaizedForm
 from MoinMoin.signalling import item_modified
 from MoinMoin.storage.middleware.protecting import AccessDenied
-from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, StorageError
+from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError
 from MoinMoin.i18n import L_
 from MoinMoin.themes import render_template
 from MoinMoin.util.mime import Type
         self.data = StringIO('')
         self.revid = None
         if self.item:
-            self.meta[NAME] = self.item.name
+            self.meta[NAME] = [self.item.name]
 
 
 class DummyItem(object):
             SYSITEM_VERSION,
             NAME_OLD,
             # are automatically implanted when saving
-            NAME,
             ITEMID, REVID, DATAID,
             HASH_ALGORITHM,
             SIZE,
             meta[PARENTID] = revid
         return meta
 
-    def _rename(self, name, comment, action):
-        self._save(self.meta, self.content.data, name=name, action=action, comment=comment)
-        old_prefixlen = len(self.subitems_prefix)
-        new_prefix = name + '/'
+    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_prefixlen = len(old_prefix)
+        if not delete:
+            new_prefix = name + '/'
         for child in self.get_subitem_revs():
-            child_oldname = child.meta[NAME]
-            child_newname = new_prefix + child_oldname[old_prefixlen:]
-            item = Item.create(child_oldname)
-            item._save(item.meta, item.content.data, name=child_newname, action=action, comment=comment)
+            for child_oldname in child.meta[NAME]:
+                if child_oldname.startswith(old_prefix):
+                    if delete:
+                        child_newname = None
+                    else:  # rename
+                        child_newname = new_prefix + child_oldname[old_prefixlen:]
+                    item = Item.create(child_oldname)
+                    item._save(item.meta, item.content.data, name=child_newname, action=action, comment=comment, delete=delete)
 
     def rename(self, name, comment=u''):
         """
-        rename this item to item <name>
+        rename this item to item <name> (replace current name by another name in the NAME list)
         """
         return self._rename(name, comment, action=u'RENAME')
 
     def delete(self, comment=u''):
         """
-        delete this item
+        delete this item (remove current name from NAME list)
         """
-        trash_prefix = u'Trash/' # XXX move to config
-        now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime())
-        # make trash name unique by including timestamp:
-        trashname = u'{0}{1} ({2} UTC)'.format(trash_prefix, self.name, now)
-        return self._rename(trashname, comment, action=u'TRASH')
+        return self._rename(None, comment, action=u'TRASH', delete=True)
 
     def revert(self, comment=u''):
         return self._save(self.meta, self.content.data, action=u'REVERT', comment=comment)
         """
         raise NotImplementedError
 
-    def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=u'', overwrite=False):
+    def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=u'',
+              overwrite=False, delete=False):
         backend = flaskg.storage
         storage_item = backend[self.name]
         try:
         if name is None:
             name = self.name
         oldname = meta.get(NAME)
-        if oldname and oldname != name:
-            meta[NAME_OLD] = oldname
-        meta[NAME] = 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:
             meta[COMMENT] = unicode(comment)
         added_dir_relnames = set()
 
         for rev in subitems:
-            fullname = rev.meta[NAME]
-            relname = fullname[prefixlen:]
-            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)
-                    direct_fullname = prefix + direct_relname
-                    direct_rev = get_storage_revision(direct_fullname)
-                    dirs.append(IndexEntry(direct_relname, direct_rev.meta))
-            else:
-                files.append(IndexEntry(relname, rev.meta))
+            fullnames = rev.meta[NAME]
+            for fullname in fullnames:
+                if fullname.startswith(prefix):
+                    relname = fullname[prefixlen:]
+                    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)
+                            direct_fullname = prefix + direct_relname
+                            direct_rev = get_storage_revision(direct_fullname)
+                            dirs.append(IndexEntry(direct_relname, direct_rev.meta))
+                    else:
+                        files.append(IndexEntry(relname, rev.meta))
 
         return dirs, files
 
 
     index_template = 'index.html'
 
-    def name_initial(self, subitems):
-        prefixlen = len(self.subitems_prefix)
-        initials = [(item.meta[NAME][prefixlen]) for item in subitems]
-        return initials
+    def name_initial(self, subitems, uppercase=False, lowercase=False):
+        """
+        return a sorted list of first characters of subitem names,
+        optionally all uppercased or lowercased.
+        """
+        prefix = self.subitems_prefix
+        prefixlen = len(prefix)
+        initials = set()
+        for item in subitems:
+            for name in item.meta[NAME]:
+                if name.startswith(prefix):
+                    initial = name[prefixlen]
+                    if uppercase:
+                        initial = initial.upper()
+                    elif lowercase:
+                        initial = initial.lower()
+                    initials.add(initial)
+        return sorted(list(initials))
 
     delete_template = 'delete.html'
     destroy_template = 'destroy.html'
                                itemtypes=item_registry.shown_entries,
                               )
 
+    def rename(self, name, comment=u''):
+        # pointless for non-existing items
+        pass
+
+    def delete(self, comment=u''):
+        # pointless for non-existing items
+        pass
+
+    def revert(self, comment=u''):
+        # pointless for non-existing items
+        pass
+
+    def destroy(self, comment=u'', destroy_item=False):
+        # pointless for non-existing items
+        pass
+
 
 from ..util.pysupport import load_package_modules
 load_package_modules(__name__, __path__)

File MoinMoin/items/_tests/test_Item.py

     return [(MixedIndexEntry(relname, Item.create('/'.join((basename, relname))).meta, hassubitem))
             for relname, hassubitem in spec]
 
+
 class TestItem(object):
 
-    def testNonExistent(self):
+    def _testNonExistent(self):
         item = Item.create(u'DoesNotExist')
         assert isinstance(item, NonExistent)
         meta, data = item.meta, item.content.data
     def test_meta_filter(self):
         name = u'Test_item'
         contenttype = u'text/plain;charset=utf-8'
-        meta = {'test_key': 'test_val', CONTENTTYPE: contenttype, 'name': 'test_name'}
+        meta = {'test_key': 'test_val', CONTENTTYPE: contenttype, NAME: [u'test_name'], 'address': u'1.2.3.4'}
         item = Item.create(name)
         result = Item.meta_filter(item, meta)
         # keys like NAME, ITEMID, REVID, DATAID are filtered
-        expected = {'test_key': 'test_val', CONTENTTYPE: contenttype}
+        expected = {'test_key': 'test_val', CONTENTTYPE: contenttype, NAME: [u'test_name']}
         assert result == expected
 
     def test_meta_dict_to_text(self):
         name = u'Test_item'
         contenttype = u'text/plain;charset=utf-8'
-        meta = {'test_key': 'test_val', CONTENTTYPE: contenttype, 'name': 'test_name'}
+        meta = {'test_key': 'test_val', CONTENTTYPE: contenttype, NAME: [u'test_name']}
         item = Item.create(name)
         result = Item.meta_dict_to_text(item, meta)
-        expected = '{\n  "contenttype": "text/plain;charset=utf-8", \n  "test_key": "test_val"\n}'
+        expected = '{\n  "contenttype": "text/plain;charset=utf-8", \n  "name": [\n    "test_name"\n  ], \n  "test_key": "test_val"\n}'
         assert result == expected
 
     def test_meta_text_to_dict(self):
         name = u'Test_item'
         contenttype = u'text/plain;charset=utf-8'
-        text = '{\n  "contenttype": "text/plain;charset=utf-8", \n  "test_key": "test_val", \n "name": "test_name" \n}'
+        text = '{\n  "contenttype": "text/plain;charset=utf-8", \n  "test_key": "test_val", \n "name": ["test_name"] \n}'
         item = Item.create(name)
         result = Item.meta_text_to_dict(item, text)
-        expected = {'test_key': 'test_val', CONTENTTYPE: contenttype}
+        expected = {'test_key': 'test_val', CONTENTTYPE: contenttype, NAME: [u"test_name"]}
         assert result == expected
 
+    def test_item_can_have_several_names(self):
+        content = u"This is page content"
+
+        update_item(u'Page',
+                    {NAME: [u'Page',
+                            u'Another name',
+                            ],
+                     CONTENTTYPE: u'text/x.moin.wiki'}, content)
+
+        item1 = Item.create(u'Page')
+        assert item1.name == u'Page'
+        assert item1.meta[CONTENTTYPE] == 'text/x.moin.wiki'
+        assert item1.content.data == content
+
+        item2 = Item.create(u'Another name')
+        assert item2.name == u'Another name'
+        assert item2.meta[CONTENTTYPE] == 'text/x.moin.wiki'
+        assert item2.content.data == content
+
+        assert item1.rev.revid == item2.rev.revid
+
     def test_rename(self):
         name = u'Test_Item'
         contenttype = u'text/plain;charset=utf-8'
         # item at new name and its contents after renaming
         item = Item.create(new_name)
         assert item.name == u'Test_new_Item'
-        assert item.meta['name_old'] == u'Test_Item'
+        assert item.meta['name_old'] == [u'Test_Item']
         assert item.meta['comment'] == u'renamed'
         assert item.content.data == u'test_data'
 
+    def test_rename_acts_only_in_active_name_in_case_there_are_several_names(self):
+        content = u"This is page content"
+
+        update_item(u'Page',
+                    {NAME: [u'First',
+                            u'Second',
+                            u'Third',
+                            ],
+                     CONTENTTYPE: u'text/x.moin.wiki'}, content)
+
+        item = Item.create(u'Second')
+        item.rename(u'New name', comment=u'renamed')
+
+        item1 = Item.create(u'First')
+        assert item1.name == u'First'
+        assert item1.meta[CONTENTTYPE] == 'text/x.moin.wiki'
+        assert item1.content.data == content
+
+        item2 = Item.create(u'New name')
+        assert item2.name == u'New name'
+        assert item2.meta[CONTENTTYPE] == 'text/x.moin.wiki'
+        assert item2.content.data == content
+
+        item3 = Item.create(u'Third')
+        assert item3.name == u'Third'
+        assert item3.meta[CONTENTTYPE] == 'text/x.moin.wiki'
+        assert item3.content.data == content
+
+        assert item1.rev.revid == item2.rev.revid == item3.rev.revid
+
+        item4 = Item.create(u'Second')
+        assert item4.meta[CONTENTTYPE] == 'application/x-nonexistent'
+
     def test_rename_recursion(self):
         update_item(u'Page', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Page 1')
         update_item(u'Page/Child', {CONTENTTYPE: u'text/x.moin.wiki'}, u'this is child')
         # item at new name and its contents after renaming
         item = Item.create(u'Renamed_Page')
         assert item.name == u'Renamed_Page'
-        assert item.meta['name_old'] == u'Page'
+        assert item.meta['name_old'] == [u'Page']
         assert item.meta['comment'] == u'renamed'
         assert item.content.data == u'Page 1'
 
         item = Item.create(u'Renamed_Page/Child')
         assert item.name == u'Renamed_Page/Child'
-        assert item.meta['name_old'] == u'Page/Child'
+        assert item.meta['name_old'] == [u'Page/Child']
         assert item.meta['comment'] == u'renamed'
         assert item.content.data == u'this is child'
 
         item = Item.create(u'Renamed_Page/Child/Another')
         assert item.name == u'Renamed_Page/Child/Another'
-        assert item.meta['name_old'] == u'Page/Child/Another'
+        assert item.meta['name_old'] == [u'Page/Child/Another']
         assert item.meta['comment'] == u'renamed'
         assert item.content.data == u'another child'
 
+    def test_rename_recursion_with_multiple_names_and_children(self):
+        update_item(u'Foo',
+                    {CONTENTTYPE: u'text/x.moin.wiki',
+                         NAME: [u'Other', u'Page', u'Foo']},
+                    u'Parent')
+        update_item(u'Page/Child', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Child of Page')
+        update_item(u'Other/Child2', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Child of Other')
+        update_item(u'Another',
+                    {CONTENTTYPE: u'text/x.moin.wiki',
+                     NAME: [u'Another', u'Page/Second']
+                         },
+                    u'Both')
+        update_item(u'Page/Second/Child', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Child of Second')
+        update_item(u'Another/Child', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Child of Another')
+
+        item = Item.create(u'Page')
+
+        item.rename(u'Renamed', comment=u'renamed')
+
+        assert Item.create(u'Page/Child').meta[CONTENTTYPE] == 'application/x-nonexistent'
+        assert Item.create(u'Renamed/Child').content.data == u'Child of Page'
+        assert Item.create(u'Page/Second').meta[CONTENTTYPE] == 'application/x-nonexistent'
+        assert Item.create(u'Renamed/Second').content.data == u'Both'
+        assert Item.create(u'Another').content.data == u'Both'
+        assert Item.create(u'Page/Second/Child').meta[CONTENTTYPE] == 'application/x-nonexistent'
+        assert Item.create(u'Renamed/Second/Child').content.data == u'Child of Second'
+        assert Item.create(u'Other/Child2').content.data == u'Child of Other'
+        assert Item.create(u'Another/Child').content.data == u'Child of Another'
+
     def test_delete(self):
         name = u'Test_Item2'
         contenttype = u'text/plain;charset=utf-8'

File MoinMoin/items/blog.py

 from MoinMoin.themes import render_template
 from MoinMoin.forms import OptionalText, Tags, DateTime
 from MoinMoin.storage.middleware.protecting import AccessDenied
-from MoinMoin.constants.keys import NAME, NAME_EXACT, WIKINAME, ITEMTYPE, MTIME, PTIME, TAGS
+from MoinMoin.constants.keys import NAME_EXACT, WIKINAME, ITEMTYPE, MTIME, PTIME, TAGS
 from MoinMoin.items import Item, Default, register, BaseMetaForm
 
 
         ptime_sort_facet = FunctionFacet(ptime_sort_key)
 
         revs = flaskg.storage.search(query, sortedby=ptime_sort_facet, reverse=True, limit=None)
-        blog_entry_items = [Item.create(rev.meta[NAME], rev_id=rev.revid) for rev in revs]
+        blog_entry_items = [Item.create(rev.name, rev_id=rev.revid) for rev in revs]
         return render_template('blog/main.html',
                                item_name=self.name,
                                blog_item=self,

File MoinMoin/items/content.py

             terms.append(Term(CONTENTTYPE, contenttype))
         query = And(terms)
         revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
-        return [rev.meta[NAME] for rev in revs]
+        return [rev.name for rev in revs]
 
 
 @register

File MoinMoin/script/account/create.py

     option_list = (
         Option('--name', '-n', required=True, dest='name', type=unicode,
                help="Set the wiki user name to NAME."),
-        Option('--alias', '-a', required=False, dest="aliasname", type=unicode,
-               help="Set the wiki user alias name to ALIAS (e.g. the real name if NAME is cryptic)."),
+        Option('--display_name', '-d', required=False, dest="display_name", type=unicode,
+               help="Set the wiki user's display name to DISPLAY_NAME (e.g. in case the NAME is cryptic)."),
         Option('--email', '-e', required=True, dest='email', type=unicode,
                help="Set the user's email address to EMAIL."),
         Option('--openid', '-o', required=False, dest='openid', type=unicode,
                help="Set the user's password to PASSWORD."),
     )
 
-    def run(self, name, aliasname, email, openid, password):
+    def run(self, name, display_name, email, openid, password):
         before_wiki()
         msg = user.create_user(username=name,
                                password=password,

File MoinMoin/script/maint/modify_item.py

 from flask import g as flaskg
 from flask.ext.script import Command, Option
 
-from MoinMoin.config import NAME, CURRENT, REVID, DATAID, SIZE, HASH_ALGORITHM
+from MoinMoin.config import CURRENT, ITEMID, REVID, DATAID, SIZE, HASH_ALGORITHM
 
 
 class GetItem(Command):
             meta = mf.read()
         meta = meta.decode('utf-8')
         meta = json.loads(meta)
-        name = meta[NAME]
         to_kill = [SIZE, HASH_ALGORITHM, # gets re-computed automatically
                    DATAID,
                   ]
         if not overwrite:
             # if we remove the REVID, it will create a new one and store under the new one
             meta.pop(REVID, None)
-        item = app.storage[name]
+        query = {ITEMID: meta[ITEMID]}
+        item = app.storage.get_item(**query)
         with open(data_file, 'rb') as df:
             item.store_revision(meta, df, overwrite=overwrite)

File MoinMoin/script/maint/moinshell.py

File contents unchanged.

File MoinMoin/script/maint/serialization.py

     option_list = [
         Option('--file', '-f', dest='filename', type=unicode, required=False,
                help='Filename of the output file.'),
+        Option('--backends', '-b', dest='backends', type=unicode, required=False,
+               help='Backend names to serialize (comma separated).'),
+        Option('--all-backends', '-a', dest='all_backends', action='store_true', default=False,
+               help='Serialize all configured backends.'),
     ]
 
-    def run(self, filename=None):
-        with open_file(filename, "wb") as f:
-            serialize(app.storage.backend, f)
+    def run(self, filename=None, backends=None, all_backends=False):
+        if filename is None:
+            f = sys.stdout
+        else:
+            f = open(filename, "wb")
+        with f as f:
+            existing_backends = set(app.cfg.backend_mapping)
+            if all_backends:
+                backends = set(app.cfg.backend_mapping)
+            elif backends:
+                backends = set(backends.split(','))
+            if backends:
+                # low level - directly serialize some backend contents -
+                # this does not use the index:
+                if backends.issubset(existing_backends):
+                    for backend_name in backends:
+                        backend = app.cfg.backend_mapping.get(backend_name)
+                        serialize(backend, f)
+                else:
+                    print "Error: Wrong backend name given."
+                    print "Given Backends: %r" % backends
+                    print "Configured Backends: %r" % existing_backends
 
 
 class Deserialize(Command):

File MoinMoin/script/migration/moin19/import19.py

         # rename last_saved to MTIME, int MTIME should be enough:
         metadata[MTIME] = int(float(metadata.get('last_saved', '0')))
 
+        # rename aliasname to display_name:
+        metadata['display_name'] = metadata.get('aliasname')
+
         # rename subscribed_pages to subscribed_items
         metadata['subscribed_items'] = metadata.get('subscribed_pages', [])
 
                                  for interwiki, bookmark in metadata.get('bookmarks', {}).items()]
 
         # stuff we want to get rid of:
-        kill = ['real_language', # crap (use 'language')
+        kill = ['aliasname', # renamed to display_name
+                'real_language', # crap (use 'language')
                 'wikiname_add_spaces', # crap magic (you get it like it is)
                 'recoverpass_key', # user can recover again if needed
                 'editor_default', # not used any more
 
         # finally, remove some empty values (that have empty defaults anyway or
         # make no sense when empty):
-        empty_kill = ['aliasname', 'bookmarks', 'enc_password',
+        empty_kill = ['aliasname', 'display_name', 'bookmarks', 'enc_password',
                       'language', 'css_url', 'email', ] # XXX check subscribed_items, quicklinks
         for key in empty_kill:
             if key in metadata and metadata[key] in [u'', tuple(), {}, [], ]:

File MoinMoin/security/__init__.py

             return super(MySecPol, self).read(itemname)
     """
     def __init__(self, user):
-        self.name = user.name
+        self.names = user.name
 
     def read(self, itemname):
         """read permission is special as we have 2 kinds of read capabilities:
            * READ - gives permission to read, unconditionally
            * PUBREAD - gives permission to read, when published
         """
-        return (flaskg.storage.may(itemname, rights.READ, username=self.name)
+        return (flaskg.storage.may(itemname, rights.READ, usernames=self.names)
                 or
-                flaskg.storage.may(itemname, rights.PUBREAD, username=self.name))
+                flaskg.storage.may(itemname, rights.PUBREAD, usernames=self.names))
 
     def __getattr__(self, attr):
         """ Shortcut to handle all known ACL rights.
         :returns: checking function for that right
         """
         if attr in app.cfg.acl_rights_contents:
-            return lambda itemname: flaskg.storage.may(itemname, attr, username=self.name)
+            return lambda itemname: flaskg.storage.may(itemname, attr, usernames=self.names)
         if attr in app.cfg.acl_rights_functions:
-            may = app.cfg.cache.acl_functions.may
-            return lambda: may(self.name, attr)
+            def multiuser_may():
+                # TODO: if "may" would accept multiple names, we could get rid of this
+                may = app.cfg.cache.acl_functions.may
+                for name in self.names:
+                    if may(name, attr):
+                        return True
+                return False
+            return multiuser_may
         raise AttributeError(attr)
 
 
                             handler = getattr(self, "_special_" + special, None)
                             allowed = handler(name, dowhat, rightsdict)
                             break # order of self.special_users is important
-            elif entry == name:
+            elif entry == name:  # XXX TODO maybe change this to "entry in names" to check users with multiple names in one go
                 allowed = rightsdict.get(dowhat)
             if allowed is not None:
                 return allowed

File MoinMoin/security/_tests/test_security.py

 from MoinMoin.security import AccessControlList, ACLStringIterator
 
 from MoinMoin.user import User
-from MoinMoin.config import ACL
+from MoinMoin.config import NAME, ACL
 from MoinMoin.datastruct import ConfigGroups
 
 from MoinMoin._tests import update_item
             (self.mainitem_name, u'JaneDoe', ['read', 'write']), # by item acl
             (self.mainitem_name, u'JoeDoe', []), # by item acl
             (self.subitem1_name, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),
-            (self.subitem1_name, u'AnyUser', ['read', 'write']), # by default acl
+            (self.subitem1_name, u'AnyUser', ['read']), # by after acl
             (self.subitem1_name, u'JoeDoe', []), # by inherited acl from main item
             (self.subitem1_name, u'JaneDoe', ['read', 'write']), # by inherited acl from main item
             (self.subitem2_name, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),
                 yield _not_have_right, u, right, itemname
 
 
+class TestItemHierachicalAclsMultiItemNames(object):
+    """ security: real-life access control list on items testing
+    """
+    # parent / child item names
+    p1 = [u'p1', ]
+    c1 = [u'p1/c1', ]
+    p2 = [u'p2', ]
+    c2 = [u'p2/c2', ]
+    c12 = [u'p1/c12', u'p2/c12', ]
+    items = [
+        # itemnames, acl, content
+        (p1, u'Editor:', p1),  # deny access (due to hierarchic acl mode also effective for children)
+        (c1, None, c1),  # no own acl -> inherit from parent
+        (p2, None, p2),  # default acl effective (also for children)
+        (c2, None, c2),  # no own acl -> inherit from parent
+        (c12, None, c12),  # no own acl -> inherit from parents
+        ]
+
+    from MoinMoin._tests import wikiconfig
+
+    class Config(wikiconfig.Config):
+        content_acl = dict(hierarchic=True, before=u"WikiAdmin:admin,read,write,create,destroy", default=u"Editor:read,write", after=u"All:read")
+
+    def setup_method(self, method):
+        become_trusted(username=u'WikiAdmin')
+        for item_names, item_acl, item_content in self.items:
+            meta = {NAME: item_names}
+            if item_acl is not None:
+                meta.update({ACL: item_acl})
+            update_item(item_names[0], meta, item_content)
+
+    def testItemACLs(self):
+        """ security: test item acls """
+        tests = [
+            # itemname, username, expected_rights
+            (self.p1, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),  # by before acl
+            (self.p2, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),  # by before acl
+            (self.c1, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),  # by before acl
+            (self.c2, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),  # by before acl
+            (self.c12, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),  # by before acl
+            (self.p1, u'Editor', []),  # by p1 acl
+            (self.c1, u'Editor', []),  # by p1 acl
+            (self.p1, u'SomeOne', ['read']),  # by after acl
+            (self.c1, u'SomeOne', ['read']),  # by after acl
+            (self.p2, u'Editor', ['read', 'write']),  # by default acl
+            (self.c2, u'Editor', ['read', 'write']),  # by default acl
+            (self.p2, u'SomeOne', ['read']),  # by after acl
+            (self.c2, u'SomeOne', ['read']),  # by after acl
+            (self.c12, u'SomeOne', ['read']),  # by after acl
+            # now check the rather special stuff:
+            (self.c12, u'Editor', ['read', 'write']),  # disallowed via p1, but allowed via p2 via default acl
+        ]
+
+        for itemnames, username, may in tests:
+            u = User(auth_username=username)
+            u.valid = True
+            itemname = itemnames[0]
+
+            def _have_right(u, right, itemname):
+                can_access = getattr(u.may, right)(itemname)
+                assert can_access, "{0!r} may {1} {2!r} (hierarchic)".format(u.name, right, itemname)
+
+            # User should have these rights...
+            for right in may:
+                yield _have_right, u, right, itemname
+
+            def _not_have_right(u, right, itemname):
+                can_access = getattr(u.may, right)(itemname)
+                assert not can_access, "{0!r} may not {1} {2!r} (hierarchic)".format(u.name, right, itemname)
+
+            # User should NOT have these rights:
+            mayNot = [right for right in app.cfg.acl_rights_contents
+                      if right not in may]
+            for right in mayNot:
+                yield _not_have_right, u, right, itemname
+
+
+# XXX TODO add tests for a user having multiple usernames (one resulting in more permissions than other)
+
 coverage_modules = ['MoinMoin.security']

File MoinMoin/storage/__init__.py

  |                                 listing, lookup by name, ACL checks, ...
  v
  Routing  Middleware               dispatches to multiple backends based on the
- |                 |               name, cares about absolute and relative names
+ |                 |               namespace
  v                 v
  "stores" Backend  Other Backend   simple stuff: store, get, destroy revisions
  |           |
 """
 
 
-CONTENT, USERPROFILES = 'content', 'userprofiles'
+CONTENT, USERPROFILES = u'content', u'userprofiles'
 
 BACKENDS_PACKAGE = 'MoinMoin.storage.backends'
 
     return module.MutableBackend.from_uri(backend_uri)
 
 
-def create_mapping(uri, mounts, acls):
-    namespace_mapping = [(mounts[nsname],
-                          backend_from_uri(uri % dict(nsname=nsname, kind="%(kind)s")))
-                         for nsname in mounts]
+def create_mapping(uri, namespaces, backends, acls):
+    namespace_mapping = namespaces.items()
     acl_mapping = acls.items()
+    backend_mapping = [(backend_name,
+                        backend_from_uri(uri % dict(backend=backend_name, kind="%(kind)s")))
+                        for backend_name in backends]
     # we need the longest mountpoints first, shortest last (-> '' is very last)
     namespace_mapping = sorted(namespace_mapping, key=lambda x: len(x[0]), reverse=True)
     acl_mapping = sorted(acl_mapping, key=lambda x: len(x[0]), reverse=True)
-    return namespace_mapping, acl_mapping
+    return namespace_mapping, dict(backend_mapping), acl_mapping
 
 def create_simple_mapping(uri='stores:fs:instance',
                           content_acl=None, user_profile_acl=None):
 
     :params uri: '<backend_name>:<backend_uri>' (general form)
                  backend_name must be a backend module name (e.g. stores)
-                 the backend_uri must have a %(nsname)s placeholder, it gets replaced
-                 by the CONTENT, USERPROFILES strings and result is given to
-                 to that backend's constructor
+                 the backend_uri must have a %(backend)s placeholder, it gets replaced
+                 by the name of the backend (a simple, ascii string) and result
+                 is given to to that backend's constructor
 
                  for the 'stores' backend, backend_uri looks like '<store_name>:<store_uri>'
                  store_name must be a store module name (e.g. fs)
                  by 'meta' or 'data' and the result is given to that store's constructor
 
                  e.g.:
-                 'stores:fs:/path/to/store/%(nsname)s/%(kind)s' will create a mapping
+                 'stores:fs:/path/to/store/%(backend)s/%(kind)s' will create a mapping
                  using the 'stores' backend with 'fs' stores and everything will be stored
                  to below /path/to/store/.
     """
         content_acl = dict(before=u'', default=u'All:read,write,create', after=u'', hierarchic=False)
     if not user_profile_acl:
         user_profile_acl = dict(before=u'All:', default=u'', after=u'', hierarchic=False)
-    mounts = {
-        CONTENT: '',
-        USERPROFILES: 'UserProfile',
+    namespaces = {
+        u'': CONTENT,
+        u'userprofiles:': USERPROFILES,
+    }
+    backends = {
+        CONTENT: None,
+        USERPROFILES: None,
     }
     acls = {
-        'UserProfile/': user_profile_acl,
+        'userprofiles:': user_profile_acl,
         '': content_acl,
     }
-    return create_mapping(uri, mounts, acls)
+    return create_mapping(uri, namespaces, backends, acls)

File MoinMoin/storage/backends/stores.py

             dataid = meta[DATAID]
             # we will just asume stuff is correct if you pass it with a data id
             if dataid not in self.data_store:
-                # XXX issue: if we do not store if we already have the dataid in the store,
-                # XXX deserialization does not work as the fpos does not advance to the next record,
-                # XXX because we do not read from the source file. Remove the check?
                 self.data_store[dataid] = data
+            else:
+                # this is reading the data to avoid this issue:
+                # if we do not store if we already have the dataid in the store,
+                # deserialization does not work as the fpos does not advance to the next record,
+                # because we do not read from the source file. Remove the check?
+                while data.read(64*1024):
+                    pass
+
         # if something goes wrong below, the data shall be purged by a garbage collection
         metaid = self._store_meta(meta)
         return metaid

File MoinMoin/storage/middleware/_tests/test_indexing.py

 from flask import g as flaskg
 
 from MoinMoin.config import NAME, SIZE, ITEMID, REVID, DATAID, HASH_ALGORITHM, CONTENT, COMMENT, \
-                            LATEST_REVS, ALL_REVS
+                            LATEST_REVS, ALL_REVS, NAMESPACE
 
 from ..indexing import IndexingMiddleware
 
 from MoinMoin.storage.stores.memory import BytesStore as MemoryBytesStore
 from MoinMoin.storage.stores.memory import FileStore as MemoryFileStore
 from MoinMoin.storage import create_simple_mapping
-from MoinMoin.storage.middleware import routing
 
 
 def dumper(indexer, idx_name):
         item_name = u'foo'
         data = 'bar'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=item_name), StringIO(data),
+        rev = item.store_revision(dict(name=[item_name, ]), StringIO(data),
                                   return_rev=True)
         revid = rev.revid
         # check if we have the revision now:
         item = self.imw[item_name]
         assert item # does exist
         rev = item.get_revision(revid)
-        assert rev.meta[NAME] == item_name
+        assert rev.name == item_name
         assert rev.data.read() == data
         revids = [rev.revid for rev in item.iter_revs()]
         assert revids == [revid]
         data = 'bar'
         newdata = 'baz'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=item_name, comment=u'spam'), StringIO(data),
+        rev = item.store_revision(dict(name=[item_name, ], comment=u'spam'), StringIO(data),
                                   return_rev=True)
         revid = rev.revid
         # clear revision:
-        item.store_revision(dict(name=item_name, revid=revid, comment=u'no spam'), StringIO(newdata), overwrite=True)
+        item.store_revision(dict(name=[item_name, ], revid=revid, comment=u'no spam'), StringIO(newdata), overwrite=True)
         # check if the revision was overwritten:
         item = self.imw[item_name]
         rev = item.get_revision(revid)
-        assert rev.meta[NAME] == item_name
+        assert rev.name == item_name
         assert rev.meta[COMMENT] == u'no spam'
         assert rev.data.read() == newdata
         revids = [rev.revid for rev in item.iter_revs()]
     def test_destroy_revision(self):
         item_name = u'foo'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=item_name, mtime=1),
+        rev = item.store_revision(dict(name=[item_name, ], mtime=1),
                                   StringIO('bar'), trusted=True, return_rev=True)
         revid0 = rev.revid
-        rev = item.store_revision(dict(name=item_name, mtime=2),
+        rev = item.store_revision(dict(name=[item_name, ], mtime=2),
                                   StringIO('baz'), trusted=True, return_rev=True)
         revid1 = rev.revid
-        rev = item.store_revision(dict(name=item_name, mtime=3),
+        rev = item.store_revision(dict(name=[item_name, ], mtime=3),
                                   StringIO('...'), trusted=True, return_rev=True)
         revid2 = rev.revid
         print "revids:", revid0, revid1, revid2
         revids = []
         item_name = u'foo'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=item_name, mtime=1),
+        rev = item.store_revision(dict(name=[item_name, ], mtime=1),
                                   StringIO('bar'), trusted=True, return_rev=True)
         revids.append(rev.revid)
-        rev = item.store_revision(dict(name=item_name, mtime=2),
+        rev = item.store_revision(dict(name=[item_name, ], mtime=2),
                                   StringIO('baz'), trusted=True, return_rev=True)
         revids.append(rev.revid)
         # destroy item:
     def test_all_revisions(self):
         item_name = u'foo'
         item = self.imw[item_name]
-        item.store_revision(dict(name=item_name), StringIO('does not count, different name'))
+        item.store_revision(dict(name=[item_name, ]), StringIO('does not count, different name'))
         item_name = u'bar'
         item = self.imw[item_name]
-        item.store_revision(dict(name=item_name), StringIO('1st'))
-        item.store_revision(dict(name=item_name), StringIO('2nd'))
+        item.store_revision(dict(name=[item_name, ]), StringIO('1st'))
+        item.store_revision(dict(name=[item_name, ]), StringIO('2nd'))
         item = self.imw[item_name]
         revs = [rev.data.read() for rev in item.iter_revs()]
         assert len(revs) == 2
     def test_latest_revision(self):
         item_name = u'foo'
         item = self.imw[item_name]
-        item.store_revision(dict(name=item_name), StringIO('does not count, different name'))
+        item.store_revision(dict(name=[item_name, ]), StringIO('does not count, different name'))
         item_name = u'bar'
         item = self.imw[item_name]
-        item.store_revision(dict(name=item_name), StringIO('1st'))
-        expected_rev = item.store_revision(dict(name=item_name), StringIO('2nd'),
+        item.store_revision(dict(name=[item_name, ]), StringIO('1st'))
+        expected_rev = item.store_revision(dict(name=[item_name, ]), StringIO('2nd'),
                                            return_rev=True)
         revs = list(self.imw.documents(name=item_name))
         assert len(revs) == 1  # there is only 1 latest revision
         item_name = u'foo'
         data = 'bar'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=item_name), StringIO(data), return_rev=True)
+        rev = item.store_revision(dict(name=[item_name, ]), StringIO(data), return_rev=True)
         print repr(rev.meta)
-        assert rev.meta[NAME] == item_name
+        assert rev.name == item_name
         assert rev.meta[SIZE] == len(data)
         assert rev.meta[HASH_ALGORITHM] == hashlib.new(HASH_ALGORITHM, data).hexdigest()
         assert ITEMID in rev.meta
     def test_documents(self):
         item_name = u'foo'
         item = self.imw[item_name]
-        rev1 = item.store_revision(dict(name=item_name), StringIO('x'), return_rev=True)
-        rev2 = item.store_revision(dict(name=item_name), StringIO('xx'), return_rev=True)
-        rev3 = item.store_revision(dict(name=item_name), StringIO('xxx'), return_rev=True)
+        rev1 = item.store_revision(dict(name=[item_name, ]), StringIO('x'), return_rev=True)
+        rev2 = item.store_revision(dict(name=[item_name, ]), StringIO('xx'), return_rev=True)
+        rev3 = item.store_revision(dict(name=[item_name, ]), StringIO('xxx'), return_rev=True)
         rev = self.imw.document(idx_name=ALL_REVS, size=2)
         assert rev
         assert rev.revid == rev2.revid
         expected_latest_revids = []
         item_name = u'foo'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=item_name, mtime=1),
+        r = item.store_revision(dict(name=[item_name, ], mtime=1),
                                 StringIO('does not count, different name'),
                                 trusted=True, return_rev=True)
         expected_latest_revids.append(r.revid)
         item_name = u'bar'
         item = self.imw[item_name]
-        item.store_revision(dict(name=item_name, mtime=1),
+        item.store_revision(dict(name=[item_name, ], mtime=1),
                             StringIO('1st'), trusted=True)
-        r = item.store_revision(dict(name=item_name, mtime=2),
+        r = item.store_revision(dict(name=[item_name, ], mtime=2),
                                 StringIO('2nd'), trusted=True, return_rev=True)
         expected_latest_revids.append(r.revid)
 
         missing_revids = []
         item_name = u'updated'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=item_name, mtime=1),
+        r = item.store_revision(dict(name=[item_name, ], mtime=1),
                                 StringIO('updated 1st'),
                                 trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         # we update this item below, so we don't add it to expected_latest_revids
         item_name = u'destroyed'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=item_name, mtime=1),
+        r = item.store_revision(dict(name=[item_name, ], mtime=1),
                                 StringIO('destroyed 1st'),
                                 trusted=True, return_rev=True)
         destroy_revid = r.revid
         # we destroy this item below, so we don't add it to expected_latest_revids
         item_name = u'stayssame'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=item_name, mtime=1),
+        r = item.store_revision(dict(name=[item_name, ], mtime=1),
                                 StringIO('stayssame 1st'),
                                 trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         # we update this item below, so we don't add it to expected_latest_revids
-        r = item.store_revision(dict(name=item_name, mtime=2),
+        r = item.store_revision(dict(name=[item_name, ], mtime=2),
                                 StringIO('stayssame 2nd'),
                                 trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         # this will not change the fresh index, but the old index we are still using.
         item_name = u'updated'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=item_name, mtime=2),
+        r = item.store_revision(dict(name=[item_name, ], mtime=2),
                                 StringIO('updated 2nd'), trusted=True,
                                 return_rev=True)
         expected_all_revids.append(r.revid)
         missing_revids.append(r.revid)
         item_name = u'added'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=item_name, mtime=1),
+        r = item.store_revision(dict(name=[item_name, ], mtime=1),
                                 StringIO('added 1st'),
                                 trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
     def test_revision_contextmanager(self):
         # check if rev.data is closed after leaving the with-block
         item_name = u'foo'
-        meta = dict(name=item_name)
+        meta = dict(name=[item_name, ])
         data = 'some test content'
         item = self.imw[item_name]
         data_file = StringIO(data)
         # TODO: this is a very simple check that assumes that data is put 1:1
         # into index' CONTENT field.
         item_name = u'foo'
-        meta = dict(name=item_name, contenttype=u'text/plain')
+        meta = dict(name=[item_name, ], contenttype=u'text/plain')
         data = 'some test content\n'
         item = self.imw[item_name]
         data_file = StringIO(data)
         assert expected_revid == doc[REVID]
         assert unicode(data) == doc[CONTENT]
 
+    def test_namespaces(self):
+        item_name_n = u'normal'
+        item = self.imw[item_name_n]
+        rev_n = item.store_revision(dict(name=[item_name_n, ], contenttype=u'text/plain'),
+                                    StringIO(str(item_name_n)), return_rev=True)
+        item_name_u = u'userprofiles:userprofile'
+        item = self.imw[item_name_u]
+        rev_u = item.store_revision(dict(name=[item_name_u, ], contenttype=u'text/plain'),
+                                    StringIO(str(item_name_u)), return_rev=True)
+        item = self.imw[item_name_n]
+        rev_n = item.get_revision(rev_n.revid)
+        assert rev_n.meta[NAMESPACE] == u''
+        assert rev_n.meta[NAME] == [item_name_n, ]
+        item = self.imw[item_name_u]
+        rev_u = item.get_revision(rev_u.revid)
+        assert rev_u.meta[NAMESPACE] == u'userprofiles'
+        assert rev_u.meta[NAME] == [item_name_u.split(':')[1]]
+
+    def test_parentnames(self):
+        item_name = u'child'
+        item = self.imw[item_name]
+        item.store_revision(dict(name=[u'child', u'p1/a', u'p2/b', u'p2/c', u'p3/p4/d', ],
+                                 contenttype=u'text/plain'),
+                            StringIO(''))
+        item = self.imw[item_name]
+        assert item.parentnames == [u'p1', u'p2', u'p3/p4', ]  # one p2 duplicate removed
+
 class TestProtectedIndexingMiddleware(object):
     reinit_storage = True # cleanup after each test method
 
     def test_documents(self):
         item_name = u'public'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=item_name, acl=u'joe:read'),
+        r = item.store_revision(dict(name=[item_name, ], acl=u'joe:read'),
                                 StringIO('public content'), return_rev=True)
         revid_public = r.revid
         revids = [rev.revid for rev in self.imw.documents()
-                  if rev.meta[NAME] != u'joe'] # the user profile is a revision in the backend
+                  if rev.name != u'joe'] # the user profile is a revision in the backend
         assert revids == [revid_public]
 
     def test_getitem(self):
         item_name = u'public'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=item_name, acl=u'joe:read'),
+        r = item.store_revision(dict(name=[item_name, ], acl=u'joe:read'),
                                 StringIO('public content'), return_rev=True)
         revid_public = r.revid
         # now testing:
         item_name = u'foo'
         item = self.imw[item_name]
         for i in xrange(100):
-            item.store_revision(dict(name=item_name, acl=u'joe:create joe:read'), StringIO('some content'))
+            item.store_revision(dict(name=[item_name, ], acl=u'joe:create joe:read'), StringIO('some content'))
 
     def test_perf_create_read(self):
         pytest.skip("usually we do no performance tests")
         item_name = u'foo'
         item = self.imw[item_name]
         for i in xrange(100):
-            item.store_revision(dict(name=item_name, acl=u'joe:create joe:read'), StringIO('rev number {0}'.format(i)))
+            item.store_revision(dict(name=[item_name, ], acl=u'joe:create joe:read'), StringIO('rev number {0}'.format(i)))
         for r in item.iter_revs():
             #print r.meta
             #print r.data.read()

File MoinMoin/storage/middleware/_tests/test_protecting.py

     ('', dict(before=u'', default=u'All:read,write,create', after=u'', hierarchic=False)),
 ]
 
-class User(object):
+class FakeUser(object):
     """
     fake user object, just to give user.name
     """
     def __init__(self, name):
-        self.name = name
+        self.name = [name, ]
+    @property
+    def name0(self):
+        return self.name[0]
+
 
 class TestProtectingMiddleware(TestIndexingMiddleware):
     def setup_method(self, method):
         super(TestProtectingMiddleware, self).setup_method(method)
-        self.imw = ProtectingMiddleware(self.imw, User(u'joe'), acl_mapping=acl_mapping)
+        self.imw = ProtectingMiddleware(self.imw, FakeUser(u'joe'), acl_mapping=acl_mapping)
 
     def teardown_method(self, method):
         self.imw = self.imw.indexer
         revids = []
         for item_name, acl, content in items:
             item = self.imw[item_name]
-            r = item.store_revision(dict(name=item_name, acl=acl),
+            r = item.store_revision(dict(name=[item_name, ], acl=acl, contenttype=u'text/plain'),
                                     StringIO(content), return_rev=True)
             revids.append(r.revid)
         return revids
         revid_unprotected, revid_protected = self.make_items(u'joe:write', u'boss:write')
         # now testing:
         item = self.imw[UNPROTECTED]
-        item.store_revision(dict(name=UNPROTECTED, acl=u'joe:write'), StringIO(UNPROTECTED_CONTENT))
+        item.store_revision(dict(name=[UNPROTECTED, ], acl=u'joe:write', contenttype=u'text/plain'), StringIO(UNPROTECTED_CONTENT))
         item = self.imw[PROTECTED]
         with pytest.raises(AccessDenied):
-            item.store_revision(dict(name=PROTECTED, acl=u'boss:write'), StringIO(UNPROTECTED_CONTENT))
+            item.store_revision(dict(name=[PROTECTED, ], acl=u'boss:write', contenttype=u'text/plain'), StringIO(UNPROTECTED_CONTENT))
 
     def test_write_create(self):
         # now testing:
         item_name = u'newitem'
         item = self.imw[item_name]
-        item.store_revision(dict(name=item_name), StringIO('new content'))
+        item.store_revision(dict(name=[item_name, ], contenttype=u'text/plain'), StringIO('new content'))
 
     def test_overwrite_revision(self):
         revid_unprotected, revid_protected = self.make_items(u'joe:write,destroy', u'boss:write,destroy')
         # now testing:
         item = self.imw[UNPROTECTED]
-        item.store_revision(dict(name=UNPROTECTED, acl=u'joe:write,destroy', revid=revid_unprotected),
+        item.store_revision(dict(name=[UNPROTECTED, ], acl=u'joe:write,destroy', contenttype=u'text/plain', revid=revid_unprotected),
                             StringIO(UNPROTECTED_CONTENT), overwrite=True)
         item = self.imw[PROTECTED]
         with pytest.raises(AccessDenied):
-            item.store_revision(dict(name=PROTECTED, acl=u'boss:write,destroy', revid=revid_protected),
+            item.store_revision(dict(name=[PROTECTED, ], acl=u'boss:write,destroy', contenttype=u'text/plain', revid=revid_protected),
                                 StringIO(UNPROTECTED_CONTENT), overwrite=True)
 
     def test_destroy_revision(self):

File MoinMoin/storage/middleware/_tests/test_routing.py

 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
-MoinMoin - router middleware tests
+MoinMoin - routing middleware tests
 """
 
 
 
 import pytest
 
-from MoinMoin.config import NAME, REVID
+from MoinMoin.config import NAME, NAMESPACE, REVID
 
-from ..routing import Backend as RouterBackend
+from ..routing import Backend as RoutingBackend
 
 from MoinMoin.storage.backends.stores import MutableBackend as StoreBackend, Backend as ROBackend
 from MoinMoin.storage.stores.memory import BytesStore as MemoryBytesStore
 
 
 def pytest_funcarg__router(request):
-    root_be = StoreBackend(MemoryBytesStore(), MemoryFileStore())
-    sub_be = StoreBackend(MemoryBytesStore(), MemoryFileStore())
+    default_be = StoreBackend(MemoryBytesStore(), MemoryFileStore())
+    other_be = StoreBackend(MemoryBytesStore(), MemoryFileStore())
     ro_be = make_ro_backend()
-    router = RouterBackend([('sub', sub_be), ('ro', ro_be), ('', root_be)])
+    namespaces = [(u'other:', 'other'), (u'ro:', 'ro'), (u'', 'default')]
+    backends = {'other': other_be, 'ro': ro_be, 'default': default_be, }
+    router = RoutingBackend(namespaces, backends)
     router.create()
     router.open()
 
     return router
 
 def test_store_get_del(router):
-    root_name = u'foo'
-    root_revid = router.store(dict(name=root_name), StringIO(''))
-    sub_name = u'sub/bar'
-    sub_revid = router.store(dict(name=sub_name), StringIO(''))
+    default_name = u'foo'
+    default_backend_name, default_revid = router.store(dict(name=[default_name, ]), StringIO(''))
+    other_name = u'other:bar'
+    other_backend_name, other_revid = router.store(dict(name=[other_name, ]), StringIO(''))
 
-    # when going via the router backend, we get back fully qualified names:
-    root_meta, _ = router.retrieve(root_name, root_revid)
-    sub_meta, _ = router.retrieve(sub_name, sub_revid)
-    assert root_name == root_meta[NAME]
-    assert sub_name == sub_meta[NAME]
+    # check if store() updates the to-store metadata with correct NAMESPACE and NAME
+    default_meta, _ = router.retrieve(default_backend_name, default_revid)
+    other_meta, _ = router.retrieve(other_backend_name, other_revid)
+    assert u'' == default_meta[NAMESPACE]
+    assert [default_name, ] == default_meta[NAME]
+    assert other_name.split(':')[0] == other_meta[NAMESPACE]
+    assert other_name.split(':')[1] == other_meta[NAME][0]
 
-    # when looking into the storage backend, we see relative names (without mountpoint):
-    root_meta, _ = router.mapping[-1][1].retrieve(root_revid)
-    sub_meta, _ = router.mapping[0][1].retrieve(sub_revid)
-    assert root_name == root_meta[NAME]
-    assert sub_name == 'sub' + '/' + sub_meta[NAME]
     # delete revs:
-    router.remove(root_name, root_revid)
-    router.remove(sub_name, sub_revid)
+    router.remove(default_backend_name, default_revid)
+    router.remove(other_backend_name, other_revid)
 
 
 def test_store_readonly_fails(router):
     with pytest.raises(TypeError):
-        router.store(dict(name=u'ro/testing'), StringIO(''))
+        router.store(dict(name=[u'ro:testing', ]), StringIO(''))
 
 def test_del_readonly_fails(router):
-    ro_id = next(iter(router)) # we have only readonly items
-    print ro_id
+    ro_be_name, ro_id = next(iter(router)) # we have only readonly items
+    print ro_be_name, ro_id
     with pytest.raises(TypeError):
-        router.remove(ro_id)
+        router.remove(ro_be_name, ro_id)
 
 
 def test_destroy_create_dont_touch_ro(router):
     existing = set(router)
-    root_revid = router.store(dict(name=u'foo'), StringIO(''))
-    sub_revid = router.store(dict(name=u'sub/bar'), StringIO(''))
+    default_be_name, default_revid = router.store(dict(name=[u'foo', ]), StringIO(''))
+    other_be_name, other_revid = router.store(dict(name=[u'other:bar', ]), StringIO(''))
 
     router.close()
     router.destroy()
 
 
 def test_iter(router):
-    existing_before = set([revid for mountpoint, revid in router])
-    root_revid = router.store(dict(name=u'foo'), StringIO(''))
-    sub_revid = router.store(dict(name=u'sub/bar'), StringIO(''))
-    existing_now = set([revid for mountpoint, revid in router])
-    assert existing_now == set([root_revid, sub_revid]) | existing_before
+    existing_before = set([revid for be_name, revid in router])
+    default_be_name, default_revid = router.store(dict(name=[u'foo', ]), StringIO(''))
+    other_be_name, other_revid = router.store(dict(name=[u'other:bar', ]), StringIO(''))
+    existing_now = set([revid for be_name, revid in router])
+    assert existing_now == set([default_revid, other_revid]) | existing_before

File MoinMoin/storage/middleware/_tests/test_serialization.py