changeset 4872:ab696b0a23f2

merged moin/1.9
author Dmitrijs Milajevs <dimazest@gmail.com>
date Mon, 20 Jul 2009 14:37:00 +0200
parents f486af162cdf (diff) 08dfaece84ad (current diff)
children eb86881788ce
files
diffstat 48 files changed, 1892 insertions(+), 708 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/Page.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/Page.py	Mon Jul 20 14:37:00 2009 +0200
@@ -1144,12 +1144,9 @@
                         openid_username = self.pi['openid.user']
                         userid = user.getUserId(request, openid_username)
 
-                    if request.cfg.openid_server_restricted_users_group:
-                        request.dicts.addgroup(request,
-                                               request.cfg.openid_server_restricted_users_group)
-
-                    if userid is not None and not request.cfg.openid_server_restricted_users_group or \
-                      request.dicts.has_member(request.cfg.openid_server_restricted_users_group, openid_username):
+                    openid_group_name = request.cfg.openid_server_restricted_users_group
+                    if userid is not None and not openid_group_name or \
+                            (openid_group_name in request.groups and openid_username in request.groups[openid_group_name]):
                         html_head = '<link rel="openid2.provider" href="%s">' % \
                                         wikiutil.escape(request.getQualifiedURL(self.url(request,
                                                                                 querystr={'action': 'serveopenid'})), True)
@@ -1866,9 +1863,7 @@
             # WARNING: SLOW
             pages = self.getPageList(user='')
         else:
-            pages = self.request.pages
-            if not pages:
-                pages = self._listPages()
+            pages = self._listPages()
         count = len(pages)
         self.request.clock.stop('getPageCount')
 
--- a/MoinMoin/PageEditor.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/PageEditor.py	Mon Jul 20 14:37:00 2009 +0200
@@ -782,8 +782,8 @@
             # Users can define their own variables via
             # UserHomepage/MyDict, which override the default variables.
             userDictPage = u.name + "/MyDict"
-            if request.dicts.has_dict(userDictPage):
-                variables.update(request.dicts.dict(userDictPage))
+            if userDictPage in request.dicts:
+                variables.update(request.dicts[userDictPage])
 
         for name in variables:
             text = text.replace('@%s@' % name, variables[name])
--- a/MoinMoin/_tests/test_PageEditor.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/_tests/test_PageEditor.py	Mon Jul 20 14:37:00 2009 +0200
@@ -147,13 +147,7 @@
 
     def deleteCaches(self):
         """ Force the wiki to scan the test page into the dicts """
-        from MoinMoin import caching
-        caching.CacheEntry(self.request, 'wikidicts', 'dicts_groups', scope='wiki').remove()
-        if hasattr(self.request, 'dicts'):
-            del self.request.dicts
-        if hasattr(self.request.cfg, 'DICTS_DATA'):
-            del self.request.cfg.DICTS_DATA
-        self.request.pages = {}
+        # New dicts does not require cache refresh.
 
     def deleteTestPage(self):
         """ Delete temporary page, bypass logs and notifications """
--- a/MoinMoin/_tests/test_wikidicts.py	Mon Jul 20 12:47:36 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,231 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - MoinMoin.wikidicts tests
-
-    @copyright: 2003-2004 by Juergen Hermann <jh@web.de>,
-                2007 by MoinMoin:ThomasWaldmann
-    @license: GNU GPL, see COPYING for details.
-"""
-
-import py
-import re
-import shutil
-
-from MoinMoin import wikidicts
-from MoinMoin import Page
-from MoinMoin.PageEditor import PageEditor
-from MoinMoin.user import User
-from MoinMoin._tests import append_page, become_trusted, create_page, create_random_string_list, nuke_page, nuke_user
-
-class TestGroupPage:
-
-    def testCamelCase(self):
-        """ wikidicts: initFromText: CamelCase links """
-        text = """
- * CamelCase
-"""
-        assert self.getMembers(text) == ['CamelCase']
-
-    def testExtendedName(self):
-        """ wikidicts: initFromText: extended names """
-        text = """
- * extended name
-"""
-        assert self.getMembers(text) == ['extended name']
-
-    def testExtendedLink(self):
-        """ wikidicts: initFromText: extended link """
-        text = """
- * [[extended link]]
-"""
-        assert self.getMembers(text) == ['extended link']
-
-    def testIgnoreSecondLevelList(self):
-        """ wikidicts: initFromText: ignore non first level items """
-        text = """
-  * second level
-   * third level
-    * forth level
-     * and then some...
-"""
-        assert self.getMembers(text) == []
-
-    def testIgnoreOther(self):
-        """ wikidicts: initFromText: ignore anything but first level list itmes """
-        text = """
-= ignore this =
- * take this
-
-Ignore previous line and this text.
-"""
-        assert self.getMembers(text) == ['take this']
-
-    def testStripWhitespace(self):
-        """ wikidicts: initFromText: strip whitespace around items """
-        text = """
- *   take this
-"""
-        assert self.getMembers(text) == ['take this']
-
-    def getMembers(self, text):
-        group = wikidicts.Group(self.request, '')
-        group.initFromText(text)
-        return group.members()
-
-
-class TestDictPage:
-
-    def testGroupMembers(self):
-        """ wikidicts: create dict from keys and values in text """
-        text = '''
-Text ignored
- * list items ignored
-  * Second level list ignored
- First:: first item
- text with spaces:: second item
-
-Empty lines ignored, so is this text
-Next line has key with empty value
- Empty string::\x20
- Last:: last item
-'''
-        d = wikidicts.Dict(self.request, '')
-        d.initFromText(text)
-        assert d['First'] == 'first item'
-        assert d['text with spaces'] == 'second item'
-        assert d['Empty string'] == '' # XXX fails if trailing blank is missing
-        assert d['Last'] == 'last item'
-        assert len(d) == 4
-
-class TestGroupDicts:
-
-    def testSystemPagesGroupInDicts(self):
-        """ wikidict: names in SystemPagesGroup should be in request.dicts
-
-        Get a list of all pages, and check that the dicts list all of them.
-
-        Assume that the SystemPagesGroup is in the data or the underlay dir.
-        """
-        assert Page.Page(self.request, 'SystemPagesGroup').exists(), "SystemPagesGroup is missing, Can't run test"
-        systemPages = wikidicts.Group(self.request, 'SystemPagesGroup')
-        #print repr(systemPages)
-        #print repr(self.request.dicts['SystemPagesGroup'])
-        for member in systemPages.members():
-            assert self.request.dicts.has_member('SystemPagesGroup', member), '%s should be in request.dict' % member
-
-        members, groups = self.request.dicts.expand_group('SystemPagesGroup')
-        assert 'SystemPagesInEnglishGroup' in groups
-        assert 'RecentChanges' in members
-        assert 'HelpContents' in members
-
-    def testRenameGroupPage(self):
-        """
-         tests if the dict cache for groups is refreshed after renaming a Group page
-        """
-        request = self.request
-        become_trusted(request)
-        page = create_page(request, u'SomeGroup', u" * ExampleUser")
-        page.renamePage('AnotherGroup')
-        group = wikidicts.Group(request, '')
-        isgroup = request.cfg.cache.page_group_regexact.search
-        grouppages = request.rootpage.getPageList(user='', filter=isgroup)
-        result = request.dicts.has_member(u'AnotherGroup', u'ExampleUser')
-        nuke_page(request, u'AnotherGroup')
-
-        assert result is True
-
-    def testCopyGroupPage(self):
-        """
-         tests if the dict cache for groups is refreshed after copying a Group page
-        """
-        request = self.request
-        become_trusted(request)
-        page = create_page(request, u'SomeGroup', u" * ExampleUser")
-        page.copyPage(u'OtherGroup')
-        group = wikidicts.Group(request, '')
-        isgroup = request.cfg.cache.page_group_regexact.search
-        grouppages = request.rootpage.getPageList(user='', filter=isgroup)
-        result = request.dicts.has_member(u'OtherGroup', u'ExampleUser')
-        nuke_page(request, u'OtherGroup')
-        nuke_page(request, u'SomeGroup')
-
-        assert result is True
-
-    def testAppendingGroupPage(self):
-        """
-         tests scalability by appending a name to a large list of group members
-        """
-        # long list of users
-        page_content = [u" * %s" % member for member in create_random_string_list(length=15, count=30000)]
-        request = self.request
-        become_trusted(request)
-        test_user = create_random_string_list(length=15, count=1)[0]
-        page = create_page(request, u'UserGroup', "\n".join(page_content))
-        page = append_page(request, u'UserGroup', u' * %s' % test_user)
-        result = request.dicts.has_member('UserGroup', test_user)
-        nuke_page(request, u'UserGroup')
-
-        assert result is True
-
-    def testUserAppendingGroupPage(self):
-        """
-         tests appending a username to a large list of group members and user creation
-        """
-        # long list of users
-        page_content = [u" * %s" % member for member in create_random_string_list()]
-        request = self.request
-        become_trusted(request)
-        test_user = create_random_string_list(length=15, count=1)[0]
-        page = create_page(request, u'UserGroup', "\n".join(page_content))
-        page = append_page(request, u'UserGroup', u' * %s' % test_user)
-
-        # now shortly later we create a user object
-        user = User(request, name=test_user)
-        if not user.exists():
-            User(request, name=test_user, password=test_user).save()
-
-        result = request.dicts.has_member('UserGroup', test_user)
-        nuke_page(request, u'UserGroup')
-        nuke_user(request, test_user)
-
-        assert result is True
-
-    def testMemberRemovedFromGroupPage(self):
-        """
-         tests appending a member to a large list of group members and recreating the page without the member
-        """
-        # long list of users
-        page_content = [u" * %s" % member for member in create_random_string_list()]
-        page_content = "\n".join(page_content)
-        request = self.request
-        become_trusted(request)
-        test_user = create_random_string_list(length=15, count=1)[0]
-        page = create_page(request, u'UserGroup', page_content)
-        page = append_page(request, u'UserGroup', u' * %s' % test_user)
-        # saves the text without test_user
-        page.saveText(page_content, 0)
-        result = request.dicts.has_member('UserGroup', test_user)
-        nuke_page(request, u'UserGroup')
-
-        assert result is False
-
-    def testGroupPageTrivialChange(self):
-        """
-         tests appending a username to a group page by trivial change
-        """
-        request = self.request
-        become_trusted(request)
-        test_user = create_random_string_list(length=15, count=1)[0]
-        member = u" * %s\n" % test_user
-        page = create_page(request, u'UserGroup', member)
-        # next member saved  as trivial change
-        test_user = create_random_string_list(length=15, count=1)[0]
-        member = u" * %s\n" % test_user
-        page.saveText(member, 0, trivial=1)
-        result = request.dicts.has_member('UserGroup', test_user)
-        nuke_page(request, u'UserGroup')
-
-        assert result is True
-
-coverage_modules = ['MoinMoin.wikidicts']
-
--- a/MoinMoin/action/SyncPages.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/action/SyncPages.py	Mon Jul 20 14:37:00 2009 +0200
@@ -17,7 +17,6 @@
 from MoinMoin.packages import unpackLine, packLine
 from MoinMoin.PageEditor import PageEditor, conflict_markers
 from MoinMoin.Page import Page
-from MoinMoin.wikidicts import Dict
 from MoinMoin.wikisync import TagStore, UnsupportedWikiException, SyncPage, NotAllowedException
 from MoinMoin.wikisync import MoinLocalWiki, MoinRemoteWiki, UP, DOWN, BOTH, MIMETYPE_MOIN
 from MoinMoin.support.python_compatibility import set
@@ -107,7 +106,7 @@
             "password": None,
         }
 
-        options.update(Dict(self.request, self.pagename))
+        options.update(request.dicts[self.pagename])
 
         # Convert page and group list strings to lists
         if options["pageList"] is not None:
--- a/MoinMoin/action/serveopenid.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/action/serveopenid.py	Mon Jul 20 14:37:00 2009 +0200
@@ -141,10 +141,9 @@
 
         # again, we never put an openid.server link on this page...
         # why are they here?
-        if cfg.openid_server_restricted_users_group:
-            request.dicts.addgroup(request, cfg.openid_server_restricted_users_group)
-            if not request.dicts.has_member(cfg.openid_server_restricted_users_group, received_name):
-                return False
+        openid_group_name = cfg.openid_server_restricted_users_group
+        if openid_group_name and received_name not in request.groups.get(openid_group_name, []):
+            return False
 
         return True
 
--- a/MoinMoin/config/multiconfig.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/config/multiconfig.py	Mon Jul 20 14:37:00 2009 +0200
@@ -17,6 +17,7 @@
 logging = log.getLogger(__name__)
 
 from MoinMoin import config, error, util, wikiutil, web
+from MoinMoin import datastruct
 from MoinMoin.auth import MoinAuth
 import MoinMoin.auth as authmodule
 import MoinMoin.events as events
@@ -698,6 +699,13 @@
 #
 options_no_group_name = {
   # ==========================================================================
+  'datastruct': ('Datastruct settings', None, (
+    ('dicts', lambda cfg, request: datastruct.WikiDicts(request),
+     "function f(cfg, request) that returns a backend which is used to access dicts definitions."),
+    ('groups', lambda cfg, request: datastruct.WikiGroups(request),
+     "function f(cfg, request) that returns a backend which is used to access groups definitions."),
+  )),
+  # ==========================================================================
   'session': ('Session settings', "Session-related settings, see HelpOnSessions.", (
     ('session_service', DefaultExpression('web.session.FileSessionService()'),
      "The session service."),
--- a/MoinMoin/conftest.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/conftest.py	Mon Jul 20 14:37:00 2009 +0200
@@ -115,4 +115,3 @@
         if coverage is not None:
             coverage_modules.update(getattr(self.obj, 'coverage_modules', []))
         return super(Module, self).run(*args, **kwargs)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/__init__.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,19 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - datastruct (groups and dicts) support.
+
+@copyright: 2009 MoinMoin:DmitrijsMilajevs
+@license: GPL, see COPYING for details
+"""
+
+from MoinMoin.datastruct.backends.wiki_dicts import WikiDicts
+from MoinMoin.datastruct.backends.config_dicts import ConfigDicts
+from MoinMoin.datastruct.backends.composite_dicts import CompositeDicts
+
+from MoinMoin.datastruct.backends.wiki_groups import WikiGroups
+from MoinMoin.datastruct.backends.config_groups import ConfigGroups
+from MoinMoin.datastruct.backends.composite_groups import CompositeGroups
+
+from MoinMoin.datastruct.backends import GroupDoesNotExistError
+from MoinMoin.datastruct.backends import DictDoesNotExistError
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/__init__.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,313 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - base classes for datastructs.
+
+@copyright: 2009 MoinMoin:DmitrijsMilajevs
+@license: GPL, see COPYING for details
+"""
+
+
+class GroupDoesNotExistError(Exception):
+    """
+    Raised when a group name is not found in the backend.
+    """
+
+
+class DictDoesNotExistError(Exception):
+    """
+    Raised when a dict name is not found in the backend.
+    """
+
+
+class BaseGroup(object):
+    """
+    Group is something which stores members. Groups are immutable. A
+    member is some arbitrary entity name (Unicode object).
+    """
+
+    def __init__(self, request, name, backend):
+        """
+        Initialize a group.
+
+        @param request
+        @param name: moin group name
+        @backend: backend object which created this object
+        """
+        self.request = request
+        self.name = name
+        self._backend = backend
+
+    def __contains__(self, member, processed_groups=None):
+        raise NotImplementedError()
+
+    def __iter__(self, yielded_members=None, processed_groups=None):
+        raise NotImplementedError()
+
+
+class BaseGroupsBackend(object):
+    """
+    Backend provides access to the group definitions for the other
+    MoinMoin code.
+    """
+
+    def __init__(self, request):
+        self.request = request
+        self.page_group_regex = request.cfg.cache.page_group_regexact
+
+    def is_group_name(self, member):
+        return self.page_group_regex.match(member)
+
+    def __contains__(self, group_name):
+        """
+        Check if a group called <group_name> is available in this backend.
+        """
+        raise NotImplementedError()
+
+    def __iter__(self):
+        """
+        Iterate over moin group names of the groups defined in this backend.
+
+        @return: moin group names
+        """
+        raise NotImplementedError()
+
+    def __getitem__(self, group_name):
+        """
+        Get a group by its moin group name.
+        """
+        raise NotImplementedError()
+
+    def __repr__(self):
+        return "<%s groups=%s>" % (self.__class__, list(self))
+
+    def _retrieve_members(self, group_name):
+        raise NotImplementedError()
+
+    def groups_with_member(self, member):
+        """
+        List all group names of groups containing <member>.
+
+        @param member: member name [unicode]
+        @return: list of group names [unicode]
+        """
+        for group_name in self:
+            try:
+                if member in self[group_name]:
+                    yield group_name
+            except GroupDoesNotExistError:
+                pass
+
+    def get(self, key, default=None):
+        try:
+            return self[key]
+        except GroupDoesNotExistError:
+            return default
+
+
+class LazyGroup(BaseGroup):
+    """
+    A lazy group does not store members internally, but gets them from
+    a backend when needed.
+
+    Lazy group is made only of members. It can not consist of other groups.
+
+    For instance, this is a possible LazyGroup:
+
+     PossibleGroup
+      * OneMember
+      * OtherMember
+
+    This is a group which cannot be LazyGroup:
+
+     NotPossibleGroup
+      * OneMember
+      * OtherMember
+      * OtherGroup
+    """
+
+    def __init__(self, request, name, backend):
+        super(LazyGroup, self).__init__(request, name, backend)
+
+        if name not in backend:
+            raise GroupDoesNotExistError(name)
+
+    def __contains__(self, member, processed_groups=None):
+        # processed_groups are not used here but other group classes
+        # may expect this parameter.
+        return self._backend._group_has_member(self.name, member)
+
+    def __iter__(self, yielded_members=None, processed_groups=None):
+        # processed_groups are not used here but other group classes
+        # may expect this parameter.
+        if yielded_members is None:
+            yielded_members = set()
+
+        for member in self._backend._iter_group_members(self.name):
+            if member not in yielded_members:
+                yielded_members.add(member)
+                yield member
+
+
+class LazyGroupsBackend(BaseGroupsBackend):
+
+    def _iter_group_members(self, group_name):
+        raise NotImplementedError()
+
+    def _group_has_member(self, group_name, member):
+        raise NotImplementedError()
+
+
+class GreedyGroup(BaseGroup):
+    """
+    GreedyGroup gets all members during initialization and stores them internally.
+
+    Members of a group may be names of other groups.
+    """
+
+    def __init__(self, request, name, backend):
+
+        super(GreedyGroup, self).__init__(request, name, backend)
+        self.members, self.member_groups = self._load_group()
+
+    def _load_group(self):
+        """
+        Retrieve group data from the backend and filter it to members and group_members.
+        """
+        members_retrieved = set(self._backend._retrieve_members(self.name))
+
+        member_groups = set(member for member in members_retrieved if self._backend.is_group_name(member))
+        members = members_retrieved - member_groups
+
+        return members, member_groups
+
+    def __contains__(self, member, processed_groups=None):
+        """
+        First check if <member> is part of this group and then check
+        for every subgroup in this group.
+
+        <processed_groups> is needed to avoid infinite recursion, if
+        groups are defined recursively.
+
+        @param member: member name [unicode]
+        @param processed_groups: groups which were checked for containment before [set]
+        """
+
+        if processed_groups is None:
+            processed_groups = set()
+
+        processed_groups.add(self.name)
+
+        if member in self.members or member in self.member_groups:
+            return True
+        else:
+            groups = self.request.groups
+            for group_name in self.member_groups:
+                if group_name not in processed_groups and group_name in groups and groups[group_name].__contains__(member, processed_groups):
+                    return True
+
+        return False
+
+    def __iter__(self, yielded_members=None, processed_groups=None):
+        """
+        Iterate first over members of this group, then over subgroups of this group.
+
+        <yielded_members> and <processed_groups> are needed to avoid infinite recursion.
+        This can happen if there are two groups like these:
+           OneGroup: Something, OtherGroup
+           OtherGroup: OneGroup, SomethingOther
+
+        @param yielded_members: members which have been already yielded before [set]
+        @param processed_groups: group names which have been iterated before [set]
+        """
+
+        if processed_groups is None:
+            processed_groups = set()
+
+        if yielded_members is None:
+            yielded_members = set()
+
+        processed_groups.add(self.name)
+
+        for member in self.members:
+            if member not in yielded_members:
+                yielded_members.add(member)
+                yield member
+
+        groups = self.request.groups
+        for group_name in self.member_groups:
+            if group_name not in processed_groups:
+                if group_name in groups:
+                    for member in groups[group_name].__iter__(yielded_members, processed_groups):
+                        yield member
+                else:
+                    yield group_name
+
+    def __repr__(self):
+        return "<%s name=%s members=%s member_groups=%s>" % (self.__class__,
+                                                             self.name,
+                                                             self.members,
+                                                             self.member_groups)
+
+
+class BaseDict(object):
+
+    def __init__(self, request, name, backend):
+        """
+        Initialize a dict. Dicts are greedy, it stores all keys and
+        items internally.
+
+        @param request
+        @param name: moin dict name
+        @backend: backend object which created this object
+        """
+        self.request = request
+        self.name = name
+        self._backend = backend
+        self._dict = self._load_dict()
+
+    def __iter__(self):
+        return self._dict.__iter__()
+
+    def __len__(self):
+        return self._dict.__len__()
+
+    def __getitem__(self, key):
+        return self._dict[key]
+
+    def get(self, key, default=None):
+        return self._dict.get(key, default)
+
+    def _load_dict(self):
+        """
+        Retrieve dict data from the backend.
+        """
+        return self._backend._retrieve_items(self.name)
+
+    def __repr__(self):
+        return "<%r name=%r items=%r>" % (self.__class__, self.name, self._dict.items())
+
+
+class BaseDictsBackend(object):
+
+    def __init__(self, request):
+        self.request = request
+        self.page_dict_regex = request.cfg.cache.page_dict_regexact
+
+    def is_dict_name(self, name):
+        return self.page_dict_regex.match(name)
+
+    def __contains__(self, dict_name):
+        """
+        Check if a dict called <dict_name> is available in this backend.
+        """
+        raise NotImplementedError()
+
+    def __getitem__(self, dict_name):
+        """
+        Get a dict by its moin dict name.
+        """
+        raise NotImplementedError()
+
+    def _retrieve_items(self, dict_name):
+        raise NotImplementedError()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/_tests/__init__.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,166 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinMoin.datastruct.backends base test classes.
+
+    @copyright: 2003-2004 by Juergen Hermann <jh@web.de>,
+                2007 by MoinMoin:ThomasWaldmann
+                2008 by MoinMoin:MelitaMihaljevic
+                2009 by MoinMoin:DmitrijsMilajevs
+    @license: GNU GPL, see COPYING for details.
+
+"""
+
+from py.test import raises
+
+from MoinMoin import security
+from MoinMoin.datastruct import GroupDoesNotExistError
+
+
+class GroupsBackendTest(object):
+
+    test_groups = {u'EditorGroup': [u'AdminGroup', u'John', u'JoeDoe', u'Editor1', u'John'],
+                   u'AdminGroup': [u'Admin1', u'Admin2', u'John'],
+                   u'OtherGroup': [u'SomethingOther'],
+                   u'RecursiveGroup': [u'Something', u'OtherRecursiveGroup'],
+                   u'OtherRecursiveGroup': [u'RecursiveGroup', u'Anything', u'NotExistingGroup'],
+                   u'ThirdRecursiveGroup': [u'ThirdRecursiveGroup', u'Banana'],
+                   u'EmptyGroup': [],
+                   u'CheckNotExistingGroup': [u'NotExistingGroup']}
+
+
+    expanded_groups = {u'EditorGroup': [u'Admin1', u'Admin2', u'John',
+                                        u'JoeDoe', u'Editor1'],
+                       u'AdminGroup': [u'Admin1', u'Admin2', u'John'],
+                       u'OtherGroup': [u'SomethingOther'],
+                       u'RecursiveGroup': [u'Anything', u'Something', u'NotExistingGroup'],
+                       u'OtherRecursiveGroup': [u'Anything', u'Something', u'NotExistingGroup'],
+                       u'ThirdRecursiveGroup': [u'Banana'],
+                       u'EmptyGroup': [],
+                       u'CheckNotExistingGroup': [u'NotExistingGroup']}
+
+    def test_contains(self):
+        """
+        Test group_wiki Backend and Group containment methods.
+        """
+        groups = self.request.groups
+
+        for group, members in self.expanded_groups.iteritems():
+            assert group in groups
+            for member in members:
+                assert member in groups[group]
+
+        raises(GroupDoesNotExistError, lambda: groups[u'NotExistingGroup'])
+
+    def test_contains_group(self):
+        groups = self.request.groups
+
+        assert u'AdminGroup' in groups[u'EditorGroup']
+        assert u'EditorGroup' not in groups[u'AdminGroup']
+
+    def test_iter(self):
+        groups = self.request.groups
+
+        for group, members in self.expanded_groups.iteritems():
+            returned_members = list(groups[group])
+            assert len(returned_members) == len(members)
+            for member in members:
+                assert member in returned_members
+
+    def test_get(self):
+        groups = self.request.groups
+
+        assert groups.get(u'AdminGroup')
+        assert u'NotExistingGroup' not in groups
+        assert groups.get(u'NotExistingGroup') is None
+
+    def test_groups_with_member(self):
+        groups = self.request.groups
+
+        john_groups = list(groups.groups_with_member(u'John'))
+        assert 2 == len(john_groups)
+        assert u'EditorGroup' in john_groups
+        assert u'AdminGroup' in john_groups
+        assert u'ThirdGroup' not in john_groups
+
+    def test_backend_acl_allow(self):
+        """
+        Test if the wiki group backend works with acl code.
+        Check user which has rights.
+        """
+        request = self.request
+
+        acl_rights = ["AdminGroup:admin,read,write"]
+        acl = security.AccessControlList(request.cfg, acl_rights)
+
+        for user in self.expanded_groups['AdminGroup']:
+            for permission in ["read", "write", "admin"]:
+                assert acl.may(request, u"Admin1", permission), '%s must have %s permission because he is member of the AdminGroup' % (user, permission)
+
+    def test_backend_acl_deny(self):
+        """
+        Test if the wiki group backend works with acl code.
+        Check user which does not have rights.
+        """
+        request = self.request
+
+        acl_rights = ["AdminGroup:read,write"]
+        acl = security.AccessControlList(request.cfg, acl_rights)
+
+        assert u"SomeUser" not in request.groups['AdminGroup']
+        for permission in ["read", "write"]:
+            assert not acl.may(request, u"SomeUser", permission), 'SomeUser must not have %s permission because he is not listed in the AdminGroup' % permission
+
+        assert u'Admin1' in request.groups['AdminGroup']
+        assert not acl.may(request, u"Admin1", "admin")
+
+    def test_backend_acl_with_all(self):
+        request = self.request
+
+        acl_rights = ["EditorGroup:read,write,delete,admin All:read"]
+        acl = security.AccessControlList(request.cfg, acl_rights)
+
+        for member in self.expanded_groups[u'EditorGroup']:
+            for permission in ["read", "write", "delete", "admin"]:
+                assert acl.may(request, member, permission)
+
+        assert acl.may(request, u"Someone", "read")
+        for permission in ["write", "delete", "admin"]:
+            assert not acl.may(request, u"Someone", permission)
+
+    def test_backend_acl_not_existing_group(self):
+        request = self.request
+        assert u'NotExistingGroup' not in request.groups
+
+        acl_rights = ["NotExistingGroup:read,write,delete,admin All:read"]
+        acl = security.AccessControlList(request.cfg, acl_rights)
+
+        assert not acl.may(request, u"Someone", "write")
+
+
+class DictsBackendTest(object):
+
+    dicts = {u'SomeTestDict': {u'First': u'first item',
+                               u'text with spaces': u'second item',
+                               u'Empty string': u'',
+                               u'Last': u'last item'},
+             u'SomeOtherTestDict': {u'One': '1',
+                                    u'Two': '2'}}
+
+    def test_getitem(self):
+        expected_dicts = self.dicts
+        dicts = self.request.dicts
+
+        for dict_name, expected_dict in expected_dicts.items():
+            test_dict = dicts[dict_name]
+            assert len(test_dict) == len(expected_dict)
+            for key, value in expected_dict.items():
+                assert test_dict[key] == value
+
+    def test_contains(self):
+        dicts = self.request.dicts
+
+        for key in self.dicts:
+            assert key in dicts
+
+        assert u'SomeNotExistingDict' not in dicts
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/_tests/test_composite_dicts.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,37 @@
+# -*- coding: iso-8859-1 -*-
+
+"""
+MoinMoin.datastruct.backends.composite_dicts test
+
+@copyright: 2009 MoinMoin:DmitrijsMilajevs
+            2008 MoinMoin: MelitaMihaljevic
+@license: GPL, see COPYING for details
+"""
+
+from py.test import raises
+
+from MoinMoin.datastruct.backends._tests import DictsBackendTest
+from MoinMoin.datastruct import ConfigDicts, CompositeDicts, DictDoesNotExistError
+from MoinMoin._tests import wikiconfig
+from MoinMoin import security
+
+
+class TestCompositeDict(DictsBackendTest):
+
+    class Config(wikiconfig.Config):
+
+        one_dict = {u'SomeTestDict': {u'First': u'first item',
+                                      u'text with spaces': u'second item',
+                                      u'Empty string': u'',
+                                      u'Last': u'last item'}}
+
+        other_dict = {u'SomeOtherTestDict': {u'One': '1',
+                                             u'Two': '2'}}
+
+        def dicts(self, request):
+            return CompositeDicts(request,
+                                  ConfigDicts(request, self.one_dict),
+                                  ConfigDicts(request, self.other_dict))
+
+
+coverage_modules = ['MoinMoin.datastruct.backends.composite_dicts']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/_tests/test_composite_groups.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,88 @@
+# -*- coding: iso-8859-1 -*-
+
+"""
+MoinMoin.datastruct.backends.composite_groups test
+
+@copyright: 2009 MoinMoin:DmitrijsMilajevs
+@license: GPL, see COPYING for details
+"""
+
+from py.test import raises
+
+from MoinMoin.datastruct.backends._tests import GroupsBackendTest
+from MoinMoin.datastruct import ConfigGroups, CompositeGroups, GroupDoesNotExistError
+from MoinMoin._tests import wikiconfig
+from MoinMoin import security
+
+
+class TestCompositeGroupsBackend(GroupsBackendTest):
+
+    class Config(wikiconfig.Config):
+
+        def groups(self, request):
+            groups = GroupsBackendTest.test_groups
+            return CompositeGroups(request, ConfigGroups(request, groups))
+
+
+class TestCompositeGroup(object):
+
+    class Config(wikiconfig.Config):
+
+        admin_group = frozenset([u'Admin', u'JohnDoe'])
+        editor_group = frozenset([u'MainEditor', u'JohnDoe'])
+        fruit_group = frozenset([u'Apple', u'Banana', u'Cherry'])
+
+        first_backend_groups = {u'AdminGroup': admin_group,
+                                u'EditorGroup': editor_group,
+                                u'FruitGroup': fruit_group}
+
+        user_group = frozenset([u'JohnDoe', u'Bob', u'Joe'])
+        city_group = frozenset([u'Bolzano', u'Riga', u'London'])
+
+        # Suppose, someone hacked second backend and added himself to AdminGroup
+        second_admin_group = frozenset([u'TheHacker'])
+
+        second_backend_groups = {u'UserGroup': user_group,
+                                 u'CityGroup': city_group,
+                                 # Here group name clash occurs.
+                                 # AdminGroup is defined in both
+                                 # first_backend and second_backend.
+                                 u'AdminGroup': second_admin_group}
+
+        def groups(self, request):
+            return CompositeGroups(request,
+                                   ConfigGroups(request, self.first_backend_groups),
+                                   ConfigGroups(request, self.second_backend_groups))
+
+    def setup_method(self, method):
+        self.groups = self.request.groups
+
+    def test_getitem(self):
+        raises(GroupDoesNotExistError, lambda: self.groups[u'NotExistingGroup'])
+
+    def test_clashed_getitem(self):
+        """
+        Check the case when groups of the same name are defined in multiple
+        backends. __getitem__ should return the first match (backends are
+        considered in the order they are given in the backends list).
+        """
+        admin_group = self.groups[u'AdminGroup']
+
+        # TheHacker added himself to the second backend, but that must not be
+        # taken into consideration, because AdminGroup is defined in first
+        # backend and we only use the first match.
+        assert u'TheHacker' not in admin_group
+
+    def test_iter(self):
+        all_group_names = list(self.groups)
+
+        assert 5 == len(all_group_names)
+        # There are no duplicates
+        assert len(set(all_group_names)) == len(all_group_names)
+
+    def test_contains(self):
+        assert u'UserGroup' in self.groups
+        assert u'not existing group' not in self.groups
+
+
+coverage_modules = ['MoinMoin.datastruct.backends.composite_groups']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/_tests/test_config_dicts.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,24 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinMoin.backends.config_dicts tests
+
+    @copyright: 2009 by MoinMoin:DmitrijsMilajevs
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from  MoinMoin.datastruct.backends._tests import DictsBackendTest
+from MoinMoin.datastruct import ConfigDicts
+from MoinMoin._tests import wikiconfig
+
+
+class TestConfigDictsBackend(DictsBackendTest):
+
+    class Config(wikiconfig.Config):
+
+        def dicts(self, request):
+            dicts = DictsBackendTest.dicts
+            return ConfigDicts(request, dicts)
+
+
+coverage_modules = ['MoinMoin.datastruct.backends.config_dicts']
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/_tests/test_config_groups.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,24 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinMoin.backends.config_groups tests
+
+    @copyright: 2009 by MoinMoin:DmitrijsMilajevs
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin.datastruct.backends._tests import GroupsBackendTest
+from MoinMoin.datastruct import ConfigGroups
+from MoinMoin._tests import wikiconfig
+
+
+class TestConfigGroupsBackend(GroupsBackendTest):
+
+    class Config(wikiconfig.Config):
+
+        def groups(self, request):
+            groups = GroupsBackendTest.test_groups
+            return ConfigGroups(request, groups)
+
+
+coverage_modules = ['MoinMoin.datastruct.backends.config_groups']
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/_tests/test_lazy_config_groups.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,59 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinMoin.backends.config_lazy_groups tests
+
+    @copyright: 2009 by MoinMoin:DmitrijsMilajevs
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin.datastruct.backends._tests.test_config_groups import TestConfigGroupsBackend
+from MoinMoin.datastruct.backends._tests import GroupsBackendTest
+from MoinMoin.datastruct.backends.config_lazy_groups import ConfigLazyGroups
+from MoinMoin.datastruct import ConfigGroups, CompositeGroups, GroupDoesNotExistError
+from MoinMoin._tests import wikiconfig
+
+
+class TestLazyConfigGroups(TestConfigGroupsBackend):
+
+    test_groups = {u'EditorGroup': [u'John', u'JoeDoe', u'Editor1'],
+                   u'AdminGroup': [u'Admin1', u'Admin2', u'John'],
+                   u'OtherGroup': [u'SomethingOther'],
+                   u'EmptyGroup': []}
+
+    expanded_groups = test_groups
+
+    class Config(wikiconfig.Config):
+
+        def groups(self, request):
+            groups = TestLazyConfigGroups.test_groups
+            return ConfigLazyGroups(request, groups)
+
+    def test_contains_group(self):
+        """
+        ConfigLazyGroups can not contain other group members.
+
+        This test does not make sense.
+        """
+
+
+class TestCompositeAndLazyConfigGroups(GroupsBackendTest):
+
+    class Config(wikiconfig.Config):
+
+        def groups(self, request):
+            config_groups = {u'EditorGroup': [u'AdminGroup', u'John', u'JoeDoe', u'Editor1', u'John'],
+                             u'RecursiveGroup': [u'Something', u'OtherRecursiveGroup'],
+                             u'OtherRecursiveGroup': [u'RecursiveGroup', u'Anything', u'NotExistingGroup'],
+                             u'ThirdRecursiveGroup': [u'ThirdRecursiveGroup', u'Banana'],
+                             u'CheckNotExistingGroup': [u'NotExistingGroup']}
+
+            lazy_groups = {u'AdminGroup': [u'Admin1', u'Admin2', u'John'],
+                           u'OtherGroup': [u'SomethingOther'],
+                           u'EmptyGroup': []}
+
+            return CompositeGroups(request,
+                                   ConfigGroups(request, config_groups),
+                                   ConfigLazyGroups(request, lazy_groups))
+
+
+coverage_modules = ['MoinMoin.datastruct.backends.config_lazy_groups']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/_tests/test_wiki_dicts.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,53 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinMoin.datastruct.backends.wiki_dicts tests
+
+    @copyright: 2003-2004 by Juergen Hermann <jh@web.de>,
+                2007 by MoinMoin:ThomasWaldmann
+                2009 by MoinMoin:DmitrijsMilajevs
+    @license: GNU GPL, see COPYING for details.
+"""
+
+
+from MoinMoin.datastruct.backends._tests import DictsBackendTest
+from MoinMoin.datastruct.backends import wiki_dicts
+from MoinMoin._tests import become_trusted, create_page, nuke_page
+
+
+class TestWikiDictsBackend(DictsBackendTest):
+
+    # Suppose that default configuration for the dicts is used which
+    # is WikiDicts backend.
+
+    def setup_class(self):
+        request = self.request
+        become_trusted(request)
+
+        text = '''
+Text ignored
+ * list items ignored
+  * Second level list ignored
+ First:: first item
+ text with spaces:: second item
+
+Empty lines ignored, so is this text
+Next line has key with empty value
+ Empty string::\x20
+ Last:: last item
+'''
+        create_page(request, u'SomeTestDict', text)
+
+        text = """
+ One:: 1
+ Two:: 2
+"""
+        create_page(request, u'SomeOtherTestDict', text)
+
+    def teardown_class(self):
+        become_trusted(self.request)
+        nuke_page(self.request, u'SomeTestDict')
+        nuke_page(self.request, u'SomeOtherTestDict')
+
+
+coverage_modules = ['MoinMoin.datastruct.backends.wiki_dicts']
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/_tests/test_wiki_groups.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,185 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - MoinMoin.backends.wiki_group tests
+
+@copyright: 2003-2004 by Juergen Hermann <jh@web.de>,
+            2007 by MoinMoin:ThomasWaldmann
+            2008 by MoinMoin:MelitaMihaljevic
+            2009 by MoinMoin:DmitrijsMilajevs
+@license: GNU GPL, see COPYING for details.
+"""
+
+from py.test import raises
+import re, shutil
+
+from MoinMoin.datastruct.backends._tests import GroupsBackendTest
+from MoinMoin.datastruct import WikiGroups
+from MoinMoin import Page, security
+from MoinMoin.PageEditor import PageEditor
+from MoinMoin.user import User
+from MoinMoin._tests import append_page, become_trusted, create_page, create_random_string_list, nuke_page, nuke_user, wikiconfig
+
+
+class TestWikiGroupBackend(GroupsBackendTest):
+
+    # Suppose that default configuration for the groups is used which
+    # is WikiGroups backend.
+
+    def setup_class(self):
+        become_trusted(self.request)
+
+        for (group, members) in self.test_groups.iteritems():
+            page_text = ' * %s' % '\n * '.join(members)
+            create_page(self.request, group, page_text)
+
+    def teardown_class(self):
+        become_trusted(self.request)
+
+        for group in self.test_groups:
+            nuke_page(self.request, group)
+
+    def test_rename_group_page(self):
+        """
+        Tests if the groups cache is refreshed after renaming a Group page.
+        """
+        request = self.request
+        become_trusted(request)
+
+        page = create_page(request, u'SomeGroup', u" * ExampleUser")
+        page.renamePage('AnotherGroup')
+
+        result = u'ExampleUser' in request.groups[u'AnotherGroup']
+        nuke_page(request, u'AnotherGroup')
+
+        assert result is True
+
+    def test_copy_group_page(self):
+        """
+        Tests if the groups cache is refreshed after copying a Group page.
+        """
+        request = self.request
+        become_trusted(request)
+
+        page = create_page(request, u'SomeGroup', u" * ExampleUser")
+        page.copyPage(u'SomeOtherGroup')
+
+        result = u'ExampleUser' in request.groups[u'SomeOtherGroup']
+
+        nuke_page(request, u'OtherGroup')
+        nuke_page(request, u'SomeGroup')
+
+        assert result is True
+
+    def test_appending_group_page(self):
+        """
+        Test scalability by appending a name to a large list of group members.
+        """
+        request = self.request
+        become_trusted(request)
+
+        # long list of users
+        page_content = [u" * %s" % member for member in create_random_string_list(length=15, count=30000)]
+        test_user = create_random_string_list(length=15, count=1)[0]
+        create_page(request, u'UserGroup', "\n".join(page_content))
+        append_page(request, u'UserGroup', u' * %s' % test_user)
+        result = test_user in request.groups['UserGroup']
+        nuke_page(request, u'UserGroup')
+
+        assert result
+
+    def test_user_addition_to_group_page(self):
+        """
+        Test addition of a username to a large list of group members.
+        """
+        request = self.request
+        become_trusted(request)
+
+        # long list of users
+        page_content = [u" * %s" % member for member in create_random_string_list()]
+        create_page(request, u'UserGroup', "\n".join(page_content))
+
+        new_user = create_random_string_list(length=15, count=1)[0]
+        append_page(request, u'UserGroup', u' * %s' % new_user)
+        user = User(request, name=new_user)
+        if not user.exists():
+            User(request, name=new_user, password=new_user).save()
+
+        result = new_user in request.groups[u'UserGroup']
+        nuke_page(request, u'UserGroup')
+        nuke_user(request, new_user)
+
+        assert result
+
+    def test_member_removed_from_group_page(self):
+        """
+        Tests appending a member to a large list of group members and
+        recreating the page without the member.
+        """
+        request = self.request
+        become_trusted(request)
+
+        # long list of users
+        page_content = [u" * %s" % member for member in create_random_string_list()]
+        page_content = "\n".join(page_content)
+        create_page(request, u'UserGroup', page_content)
+
+        test_user = create_random_string_list(length=15, count=1)[0]
+        page = append_page(request, u'UserGroup', u' * %s' % test_user)
+
+        # saves the text without test_user
+        page.saveText(page_content, 0)
+        result = test_user in request.groups[u'UserGroup']
+        nuke_page(request, u'UserGroup')
+
+        assert not result
+
+    def test_group_page_user_addition_trivial_change(self):
+        """
+        Test addition of a user to a group page by trivial change.
+        """
+        request = self.request
+        become_trusted(request)
+
+        test_user = create_random_string_list(length=15, count=1)[0]
+        member = u" * %s\n" % test_user
+        page = create_page(request, u'UserGroup', member)
+
+        # next member saved  as trivial change
+        test_user = create_random_string_list(length=15, count=1)[0]
+        member = u" * %s\n" % test_user
+        page.saveText(member, 0, trivial=1)
+
+        result = test_user in request.groups[u'UserGroup']
+
+        nuke_page(request, u'UserGroup')
+
+        assert result
+
+    def test_wiki_backend_page_acl_append_page(self):
+        """
+        Test if the wiki group backend works with acl code.
+        First check acl rights of a user that is not a member of group
+        then add user member to a page group and check acl rights
+        """
+        request = self.request
+        become_trusted(request)
+
+        create_page(request, u'NewGroup', u" * ExampleUser")
+
+        acl_rights = ["NewGroup:read,write"]
+        acl = security.AccessControlList(request.cfg, acl_rights)
+
+        has_rights_before = acl.may(request, u"AnotherUser", "read")
+
+        # update page - add AnotherUser to a page group NewGroup
+        append_page(request, u'NewGroup', u" * AnotherUser")
+
+        has_rights_after = acl.may(request, u"AnotherUser", "read")
+
+        nuke_page(request, u'NewGroup')
+
+        assert not has_rights_before, 'AnotherUser has no read rights because in the beginning he is not a member of a group page NewGroup'
+        assert has_rights_after, 'AnotherUser must have read rights because after appendage he is member of NewGroup'
+
+coverage_modules = ['MoinMoin.datastruct.backends.wiki_groups']
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/composite_dicts.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,47 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - dict access via various backends.
+
+@copyright: 2009 DmitrijsMilajevs
+@license: GPL, see COPYING for details
+"""
+
+from MoinMoin.datastruct.backends import BaseDictsBackend, DictDoesNotExistError
+
+
+class CompositeDicts(BaseDictsBackend):
+    """
+    Manage several dicts backends.
+    """
+
+    def __init__(self, request, *backends):
+        """
+        @param backends: list of dict backends which are used to get
+                         access to the dict definitions.
+        """
+        super(CompositeDicts, self).__init__(request)
+        self._backends = backends
+
+    def __getitem__(self, dict_name):
+        """
+        Get a dict by its name. First match counts.
+        """
+        for backend in self._backends:
+            try:
+                return backend[dict_name]
+            except DictDoesNotExistError:
+                pass
+        raise DictDoesNotExistError(dict_name)
+
+    def __contains__(self, dict_name):
+        """
+        Check if a dict called dict_name is available in any of the backends.
+        """
+        for backend in self._backends:
+            if dict_name in backend:
+                return True
+        return False
+
+    def __repr__(self):
+        return "<%s backends=%s>" % (self.__class__, self._backends)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/composite_groups.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,70 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - group access via various backends.
+
+The composite_groups is a backend that does not have direct storage,
+but composes other backends to a new one, so group definitions are
+retrieved from several backends. This allows to mix different
+backends.
+
+@copyright: 2009 DmitrijsMilajevs
+@license: GPL, see COPYING for details
+"""
+
+from MoinMoin.datastruct.backends import BaseGroupsBackend, GroupDoesNotExistError
+
+
+class CompositeGroups(BaseGroupsBackend):
+    """
+    Manage several group backends.
+    """
+
+    def __init__(self, request, *backends):
+        """
+        @param backends: list of group backends which are used to get
+                         access to the group definitions.
+        """
+        super(CompositeGroups, self).__init__(request)
+        self._backends = backends
+
+    def __getitem__(self, group_name):
+        """
+        Get a group by its name. First match counts.
+        """
+        for backend in self._backends:
+            try:
+                return backend[group_name]
+            except GroupDoesNotExistError:
+                pass
+        raise GroupDoesNotExistError(group_name)
+
+    def __iter__(self):
+        """
+        Iterate over group names in all backends (filtering duplicates).
+
+        If a group with same name is defined in several backends, the
+        composite_groups backend yields only backend which is listed
+        earlier in self._backends.
+        """
+        yielded_groups = set()
+
+        for backend in self._backends:
+            for group_name in backend:
+                if group_name not in yielded_groups:
+                    yield group_name
+                    yielded_groups.add(group_name)
+
+    def __contains__(self, group_name):
+        """
+        Check if a group called group_name is available in any of the backends.
+
+        @param group_name: name of the group [unicode]
+        """
+        for backend in self._backends:
+            if group_name in backend:
+                return True
+        return False
+
+    def __repr__(self):
+        return "<%s backends=%s>" % (self.__class__, self._backends)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/config_dicts.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,39 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - config dict backend
+
+The config group backend enables you to define dicts in a configuration file.
+
+@copyright: 2009 MoinMoin:DmitrijsMilajevs
+@license: GPL, see COPYING for details
+"""
+
+from MoinMoin.datastruct.backends import BaseDict, BaseDictsBackend, DictDoesNotExistError
+
+
+class ConfigDict(BaseDict):
+    pass
+
+
+class ConfigDicts(BaseDictsBackend):
+
+    def __init__(self, request, dicts):
+        super(ConfigDicts, self).__init__(request)
+
+        self._dicts = dicts
+
+    def __contains__(self, dict_name):
+        return self.is_dict_name(dict_name) and dict_name in self._dicts
+
+    def __iter__(self):
+        return self._dicts.iterkeys()
+
+    def __getitem__(self, dict_name):
+        return ConfigDict(request=self.request, name=dict_name, backend=self)
+
+    def _retrieve_items(self, dict_name):
+        try:
+            return self._dicts[dict_name]
+        except KeyError:
+            raise DictDoesNotExistError(dict_name)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/config_groups.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,44 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - config groups backend
+
+The config_groups backend enables one to define groups and their
+members in a configuration file.
+
+@copyright: 2009 MoinMoin:DmitrijsMilajevs
+@license: GPL, see COPYING for details
+"""
+
+from MoinMoin.datastruct.backends import GreedyGroup, BaseGroupsBackend, GroupDoesNotExistError
+
+
+class ConfigGroup(GreedyGroup):
+    pass
+
+
+class ConfigGroups(BaseGroupsBackend):
+
+    def __init__(self, request, groups):
+        """
+        @param groups: Dictionary of groups where key is group name,
+        and value is list of members of that group.
+        """
+        super(ConfigGroups, self).__init__(request)
+
+        self._groups = groups
+
+    def __contains__(self, group_name):
+        return group_name in self._groups
+
+    def __iter__(self):
+        return self._groups.iterkeys()
+
+    def __getitem__(self, group_name):
+        return ConfigGroup(request=self.request, name=group_name, backend=self)
+
+    def _retrieve_members(self, group_name):
+        try:
+            return self._groups[group_name]
+        except KeyError:
+            raise GroupDoesNotExistError(group_name)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/config_lazy_groups.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,43 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - config group lazy backend.
+
+    The config group backend allows one to define groups in a
+    configuration file.
+
+    NOTE that this is proof-of-concept implementation. LDAP backend
+    should be based on this concept.
+
+    @copyright: 2009 MoinMoin:DmitrijsMilajevs
+    @license: GPL, see COPYING for details
+"""
+
+from MoinMoin.datastruct.backends import LazyGroup, LazyGroupsBackend
+
+
+class ConfigLazyGroup(LazyGroup):
+    pass
+
+
+class ConfigLazyGroups(LazyGroupsBackend):
+
+    def __init__(self, request, groups):
+        super(ConfigLazyGroups, self).__init__(request)
+
+        self._groups = groups
+
+    def __contains__(self, group_name):
+        return group_name in self._groups
+
+    def __iter__(self):
+        return self._groups.iterkeys()
+
+    def __getitem__(self, group_name):
+        return ConfigLazyGroup(self.request, group_name, self)
+
+    def _iter_group_members(self, group_name):
+        if group_name in self:
+            return self._groups[group_name].__iter__()
+
+    def _group_has_member(self, group_name, member):
+        return group_name in self and member in self._groups[group_name]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/wiki_dicts.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,84 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - WikiDict functions.
+
+    @copyright: 2003-2007 MoinMoin:ThomasWaldmann,
+                2003 by Gustavo Niemeyer
+                2009 MoinMoin:DmitrijsMilajevs
+    @license: GNU GPL, see COPYING for details.
+"""
+
+
+import re
+
+from MoinMoin import caching, wikiutil
+from MoinMoin.Page import Page
+from MoinMoin.datastruct.backends import BaseDict, BaseDictsBackend, DictDoesNotExistError
+
+
+class WikiDict(BaseDict):
+    """
+    Mapping of keys to values in a wiki page.
+
+    A dict definition page should look like:
+
+       any text ignored
+        key1:: value1
+        * ignored, too
+        key2:: value2 containing spaces
+        ...
+        keyn:: ....
+       any text ignored
+    """
+
+    def _load_dict(self):
+        request = self.request
+        dict_name = self.name
+
+        page = Page(request, dict_name)
+        if page.exists():
+            arena = 'pagedicts'
+            key = wikiutil.quoteWikinameFS(dict_name)
+            cache = caching.CacheEntry(request, arena, key, scope='wiki', use_pickle=True)
+            try:
+                cache_mtime = cache.mtime()
+                page_mtime = wikiutil.version2timestamp(page.mtime_usecs())
+                # TODO: fix up-to-date check mtime granularity problems
+                if cache_mtime > page_mtime:
+                    # cache is uptodate
+                    return cache.content()
+                else:
+                    raise caching.CacheError
+            except caching.CacheError:
+                # either cache does not exist, is erroneous or not uptodate: recreate it
+                d = super(WikiDict, self)._load_dict()
+                cache.update(d)
+                return d
+        else:
+            raise DictDoesNotExistError(dict_name)
+
+
+class WikiDicts(BaseDictsBackend):
+
+    # Key:: Value - ignore all but key:: value pairs, strip whitespace, exactly one space after the :: is required
+    _dict_page_parse_regex = re.compile(ur'^ (?P<key>.+?):: (?P<val>.*?) *$', re.MULTILINE | re.UNICODE)
+
+    def __contains__(self, dict_name):
+        return self.is_dict_name(dict_name) and Page(self.request, dict_name).exists()
+
+    def __getitem__(self, dict_name):
+        return WikiDict(request=self.request, name=dict_name, backend=self)
+
+    def _retrieve_items(self, dict_name):
+        # XXX in Moin 2.0 regex should not be used instead use DOM
+        # tree to extract dict values. Also it should be possible to
+        # convert dict values to different markups (wiki-markup,
+        # creole...).
+        #
+        # Note that formatter which extracts dictionary from a
+        # page was developed. See
+        # http://hg.moinmo.in/moin/1.9-groups-dmilajevs/file/982f706482e7/MoinMoin/formatter/dicts.py
+        page = Page(self.request, dict_name)
+        text = page.get_raw_body()
+        return dict([match.groups() for match in self._dict_page_parse_regex.finditer(text)])
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/datastruct/backends/wiki_groups.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,99 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - wiki group backend
+
+The wiki_groups backend allows to define groups on wiki pages. See
+SystemPagesGroup as example of a group page.
+
+Normally, the name of the group page has to end with Group like
+FriendsGroup. This lets MoinMoin recognize it as a group. This default
+pattern could be changed (e.g. for non-english languages etc.), see
+HelpOnConfiguration.
+
+MoinMoin.formatter.groups is used to extract group members from a
+page.
+
+
+@copyright: 2008 MoinMoin:ThomasWaldmann,
+            2009 MoinMoin:DmitrijsMilajevs
+@license: GPL, see COPYING for details
+"""
+
+from MoinMoin import caching, wikiutil
+from MoinMoin.Page import Page
+from MoinMoin.datastruct.backends import GreedyGroup, BaseGroupsBackend, GroupDoesNotExistError
+from MoinMoin.formatter.groups import Formatter
+
+
+class WikiGroup(GreedyGroup):
+
+    def _load_group(self):
+        request = self.request
+        group_name = self.name
+
+        page = Page(request, group_name)
+        if page.exists():
+            arena = 'pagegroups'
+            key = wikiutil.quoteWikinameFS(group_name)
+            cache = caching.CacheEntry(request, arena, key, scope='wiki', use_pickle=True)
+            try:
+                cache_mtime = cache.mtime()
+                page_mtime = wikiutil.version2timestamp(page.mtime_usecs())
+                # TODO: fix up-to-date check mtime granularity problems
+                if cache_mtime > page_mtime:
+                    # cache is uptodate
+                    return cache.content()
+                else:
+                    raise caching.CacheError
+            except caching.CacheError:
+                # either cache does not exist, is erroneous or not uptodate: recreate it
+                members, member_groups = super(WikiGroup, self)._load_group()
+                cache.update((members, member_groups))
+                return members, member_groups
+        else:
+            raise GroupDoesNotExistError(group_name)
+
+
+class WikiGroups(BaseGroupsBackend):
+
+    def __contains__(self, group_name):
+        return self.is_group_name(group_name) and Page(self.request, group_name).exists()
+
+    def __iter__(self):
+        """
+        To find group pages, request.cfg.cache.page_group_regexact pattern is used.
+        """
+        return iter(self.request.rootpage.getPageList(user='', filter=self.page_group_regex.search))
+
+    def __getitem__(self, group_name):
+        return WikiGroup(request=self.request, name=group_name, backend=self)
+
+    def _retrieve_members(self, group_name):
+        """
+        MoinMoin.formatter.groups is used to extract group members from a page.
+        """
+        formatter = Formatter(self.request)
+        page = Page(self.request, group_name, formatter=formatter)
+
+        request_page = getattr(self.request, "page", None)
+        self.request.page = page
+        # send_special is set to True because acl of the page should
+        # not be processed to avoid infinite recursion in the
+        # following case.
+        #
+        # Consider page UserGroup content:
+        #
+        # #acl UserGroup:read,write,admin All:read
+        #
+        #  * ExampleUser
+        #  * TestGroup
+        #
+        page.send_page(content_only=True, send_special=True)
+
+        if request_page:
+            self.request.page = request_page
+        else:
+            del self.request.page
+
+        return formatter.members
+
--- a/MoinMoin/events/wikidictsrescan.py	Mon Jul 20 12:47:36 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - wikidicts notification plugin for event system
-
-    When a Group or Dict page changes, we rescan them and recreate the cache.
-
-    @copyright: 2007 by MoinMoin:ThomasWaldmann
-    @license: GNU GPL, see COPYING for details.
-"""
-
-from MoinMoin import log
-logging = log.getLogger(__name__)
-
-from MoinMoin import events as ev
-from MoinMoin import wikidicts
-
-def handle(event):
-    # "changed" includes creation, deletion, renamed and copied
-    if (isinstance(event, ev.PageChangedEvent) or isinstance(event, ev.PageRenamedEvent) or
-        isinstance(event, ev.PageCopiedEvent) or isinstance(event, ev.TrivialPageChangedEvent)):
-        cfg = event.request.cfg
-        pagename = event.page.page_name
-        if cfg.cache.page_dict_regexact.search(pagename) or \
-           cfg.cache.page_group_regexact.search(pagename):
-            return handle_groupsdicts_changed(event)
-
-
-def handle_groupsdicts_changed(event):
-    """ Handles events related to groups and dicts page changes:
-        Scans all pages matching the dict / group regex and pickles the
-        data to disk.
-    """
-    request = event.request
-    page = event.page
-
-    logging.debug("groupsdicts changed: %r, scan_dicts started", page.page_name)
-    del request.dicts
-    gd = wikidicts.GroupDict(request)
-    gd.scan_dicts()
-    logging.debug("groupsdicts changed: scan_dicts finished")
-
--- a/MoinMoin/formatter/_tests/test_formatter.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/formatter/_tests/test_formatter.py	Mon Jul 20 14:37:00 2009 +0200
@@ -132,5 +132,6 @@
                     'MoinMoin.formatter.dom_xml',
                     'MoinMoin.formatter.text_python',
                     'MoinMoin.formatter.pagelinks',
+                    'MoinMoin.formtter.groups',
                    ]
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/formatter/_tests/test_groups.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,227 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinMoin.formatter.groups Tests
+
+    @copyright: 2009 by MoinMoin:DmitrijsMilajevs
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from  MoinMoin.formatter.groups import Formatter
+from MoinMoin.Page import Page
+from MoinMoin._tests import become_trusted, create_page, nuke_page
+
+class TestGroupFormatterWikiMarkup(object):
+
+    def get_members(self, text):
+        request = self.request
+        formatter = Formatter(self.request)
+
+        become_trusted(request)
+        create_page(request, u'TestPageGroup', text)
+        page = Page(request, 'TestPageGroup', formatter=formatter)
+        page.send_page(content_only=True)
+        nuke_page(request, u'TestPageGroup')
+
+        return formatter.members
+
+    def test_CamelCase(self):
+        text = """
+ * CamelCase
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'CamelCase' in members
+
+    def test_extended_name(self):
+        text = """
+ * extended name
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'extended name' in members
+
+    def test_extended_link(self):
+        text = """
+ * [[extended link]]
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'extended link' in members
+
+    def test_extended_link_with_label(self):
+        text = """
+ * [[extended link| label]]
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'extended link' in members
+
+    def test_extended_link_and_text(self):
+        text = """
+ * [[extended link]] other text
+ * other text [[extended link]]
+ * other text [[extended link]] other text
+
+"""
+        members = self.get_members(text)
+        assert len(members) == 3
+        assert u'extended link other text' in members
+        assert u'other text extended link' in members
+        assert u'other text extended link other text' in members
+
+    def test_ignore_not_first_level_list(self):
+        text = """
+ * first level
+  * second level
+   * [[SomeLink]]
+    * forth level
+     * and then some...
+ * again first level
+"""
+        members = self.get_members(text)
+        assert len(members) == 2
+        assert 'first level' in members
+        assert 'again first level' in members
+
+    def test_intended_list(self):
+        text = """
+    * first level
+     * second level
+      * [[SomeLink|label]]
+       * forth level
+        * and then some...
+    * again first level
+"""
+        members = self.get_members(text)
+        assert len(members) == 2
+        assert 'first level' in members
+        assert 'again first level' in members
+
+    def test_ignore_other(self):
+        text = """
+= ignore this =
+ * take this
+
+Ignore previous line and this text.
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'take this' in members
+
+    def test_strip_whitespace(self):
+        text = """
+ *   take this
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'take this' in members
+
+
+class TestGroupFormatterCreole(object):
+
+    def get_members(self, text):
+        request = self.request
+        formatter = Formatter(self.request)
+
+        become_trusted(request)
+        create_page(request, u'TestPageGroup', "#FORMAT creole \n" + text)
+        page = Page(request, 'TestPageGroup', formatter=formatter)
+        page.send_page(content_only=True)
+        nuke_page(request, u'TestPageGroup')
+
+        return formatter.members
+
+    def test_CamelCase(self):
+        text = """
+ * CamelCase
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'CamelCase' in members
+
+    def test_extended_name(self):
+        text = """
+ * extended name
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'extended name' in members
+
+    def test_extended_link(self):
+        text = """
+ * [[extended link]]
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'extended link' in members
+
+    def test_extended_link_with_label(self):
+        text = """
+ * [[FrontPage|named link]]
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'FrontPage' in members
+
+    def test_extended_link_and_text(self):
+        text = """
+ * [[extended link]] other text
+ * other text [[extended link]]
+ * other text [[extended link]] other text
+
+"""
+        members = self.get_members(text)
+        assert len(members) == 3
+        assert u'extended link other text' in members
+        assert u'other text extended link' in members
+        assert u'other text extended link other text' in members
+
+    def test_ignore_not_first_level_list(self):
+        text = """
+* first level
+** second level
+*** [[SomeLink]]
+**** forth level
+***** and then some...
+* again first level
+"""
+        members = self.get_members(text)
+        assert len(members) == 2
+        assert 'first level' in members
+        assert 'again first level' in members
+
+    def test_intended_list(self):
+        text = """
+    * first level
+    ** second level
+    *** [[SomeLink|label]]
+    **** forth level
+    ***** and then some...
+    * again first level
+"""
+        members = self.get_members(text)
+        assert len(members) == 2
+        assert 'first level' in members
+        assert 'again first level' in members
+
+    def test_ignore_other(self):
+        text = """
+= ignore this =
+ * take this
+
+Ignore previous line and this text.
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'take this' in members
+
+    def test_strip_whitespace(self):
+        text = """
+ *   take this
+"""
+        members = self.get_members(text)
+        assert len(members) == 1
+        assert u'take this' in members
+
+
+coverage_modules = ['MoinMoin.formtter.groups']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/formatter/groups.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,74 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinMoin.formatter.groups
+
+    @copyright: 2009 MoinMoin:DmitrijsMilajevs
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin.formatter import FormatterBase
+from MoinMoin import wikiutil
+
+class Formatter(FormatterBase):
+    """
+    Collect members of a group and format nothing.
+
+    Group members are stored in the members attribute.
+    """
+
+    def __init__(self, request, **kw):
+        FormatterBase.__init__(self, request, **kw)
+
+        self.members = []
+        self._bullet_list_level = 0
+        self._inside_link = False
+        self._new_member = ''
+
+    def bullet_list(self, on, **kw):
+        if on:
+            self._bullet_list_level += 1
+        else:
+            self._bullet_list_level -= 1
+
+        assert self._bullet_list_level >= 0
+
+        return self.null()
+
+    def listitem(self, on, **kw):
+        if self._bullet_list_level == 1:
+            if not on:
+                stripped_new_member = self._new_member.strip()
+                if stripped_new_member:
+                    self.members.append(stripped_new_member)
+            self._new_member = ''
+        return self.null()
+
+    def text(self, text, **kw):
+        if self._bullet_list_level == 1 and not self._inside_link:
+            self._new_member += text
+        return self.null()
+
+    def pagelink(self, on, pagename='', page=None, **kw):
+        if self._bullet_list_level == 1:
+            self._inside_link = on
+            if not on:
+                if not pagename and page:
+                    pagename = page.page_name
+                pagename = wikiutil.normalize_pagename(pagename, self.request.cfg)
+                self._new_member += pagename
+        return self.null()
+
+    def null(self, *args, **kw):
+        return ''
+
+    # All these must be overriden here because they raise
+    # NotImplementedError!@#! or return html?! in the base class.
+    set_highlight_re = rawHTML = url = image = smiley = null
+    strong = emphasis = underline = highlight = sup = sub = strike = null
+    code = preformatted = small = big = code_area = code_line = null
+    code_token = linebreak = paragraph = rule = icon = null
+    number_list = definition_list = definition_term = definition_desc = null
+    heading = table = null
+    table_row = table_cell = attachment_link = attachment_image = attachment_drawing = null
+    transclusion = transclusion_param = null
+
--- a/MoinMoin/i18n/__init__.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/i18n/__init__.py	Mon Jul 20 14:37:00 2009 +0200
@@ -304,8 +304,8 @@
             language = languages[lang]['x-language-in-english']
             dictpagename = "%sDict" % language.replace(' ', '')
             dicts = request.dicts
-            if dicts.has_dict(dictpagename):
-                userdict = dicts.dict(dictpagename)
+            if dictpagename in dicts:
+                userdict = dicts[dictpagename]
                 translated = userdict[original]
             else:
                 raise KeyError
--- a/MoinMoin/macro/__init__.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/macro/__init__.py	Mon Jul 20 14:37:00 2009 +0200
@@ -385,7 +385,7 @@
         key = wikiutil.get_unicode(self.request, key, 'key')
         if page is None or key is None:
             raise ValueError("You need to give: pagename, key")
-        d = self.request.dicts.dict(page)
+        d = self.request.dicts[page]
         result = d.get(key, '')
         return self.formatter.text(result)
 
--- a/MoinMoin/packages.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/packages.py	Mon Jul 20 14:37:00 2009 +0200
@@ -371,13 +371,8 @@
         self._extractToFile(filename, pagefile)
 
         # Clear caches
-        try:
-            del self.request.cfg.DICTS_DATA
-        except AttributeError:
-            pass
-        self.request.pages = {}
-        caching.CacheEntry(self.request, 'wikidicts', 'dicts_groups', scope='wiki').remove()
-        page.clean_acl_cache()
+        # TODO Code from MoinMoin/script/maint/cleancache.py may be used
+        page.clean_acl_cache() # It is not necessary should be removed.
 
     def runScript(self, commands):
         """ Runs the commands.
--- a/MoinMoin/script/account/homepage.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/script/account/homepage.py	Mon Jul 20 14:37:00 2009 +0200
@@ -6,7 +6,7 @@
 @license: GNU GPL, see COPYING for details.
 """
 
-from MoinMoin import user, wikidicts
+from MoinMoin import user
 from MoinMoin.Page import Page
 from MoinMoin.PageEditor import PageEditor
 from MoinMoin.script import MoinScript
@@ -108,8 +108,7 @@
         if self.options.user_homepage:
             members = [self.options.user_homepage, ]
         elif self.options.name_of_group_page:
-            user_group = wikidicts.Group(request, self.options.name_of_group_page)
-            members = user_group.members()
+            members = request.groups.get(self.options.name_of_group_page, [])
         elif self.options.all_users:
             uids = user.getUserList(request)
             members = [user.User(request, uid).name for uid in uids]
--- a/MoinMoin/script/maint/cleancache.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/script/maint/cleancache.py	Mon Jul 20 14:37:00 2009 +0200
@@ -19,13 +19,10 @@
 and /data/cache directories
 
 You will usually do this after changing MoinMoin code, by either upgrading
-version, installing or removing macros or changing the regex expression for dicts.
-This often makes the text_html and dict files invalid, so you have to remove them
-(the wiki will recreate them automatically).
+version, installing or removing macros or changing the regex expression for dicts or groups.
+This often makes the text_html file invalid, so you have to remove it (the wiki will recreate it automatically).
 
-text_html is the name of the cache file used for compiled pages formatted
-by the wiki text to html formatter, A dict file does cache the pages which
-do fit to the page_group_regex variable.
+text_html is the name of the cache file used for compiled pages formatted by the wiki text to html formatter.
 
 Detailed Instructions:
 ======================
@@ -56,8 +53,15 @@
             ('charts', 'pagehits'),
             ('charts', 'useragents'),
             ('user', 'name2id'),
-            ('wikidicts', 'dicts_groups'),
         ]
         for arena, key in arena_key_list:
             caching.CacheEntry(request, arena, key, scope='wiki').remove()
 
+        # clean dict and groups related cache
+        arena_scope_list =  [('pagedicts', 'wiki'),
+                             ('pagegroups', 'wiki'),
+        ]
+        for arena, scope in arena_scope_list:
+            for key caching.get_cache_list(request, arena, scope):
+                caching.CacheEntry(request, arena, key, scope=scope).remove()
+
--- a/MoinMoin/script/maint/makecache.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/script/maint/makecache.py	Mon Jul 20 14:37:00 2009 +0200
@@ -19,11 +19,10 @@
 and /data/cache directories
 
 You will usually do this after changing MoinMoin code and calling "maint cleancache", by either upgrading
-version, installing or removing macros or changing the regex expression for dicts.
+version, installing or removing macros.
 
 text_html is the name of the cache file used for compiled pages formatted
-by the wiki text to html formatter, A dict file does cache the pages which
-do fit to the page_group_regex variable.
+by the wiki text to html formatter.
 
 Detailed Instructions:
 ======================
--- a/MoinMoin/script/migration/wikiutil160a.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/script/migration/wikiutil160a.py	Mon Jul 20 14:37:00 2009 +0200
@@ -648,7 +648,7 @@
     @rtype: bool
     @return: true if page is a system page
     """
-    return (request.dicts.has_member('SystemPagesGroup', pagename) or
+    return (pagename in request.groups.get(u'SystemPagesGroup', []) or
         isTemplatePage(request, pagename))
 
 
--- a/MoinMoin/security/__init__.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/security/__init__.py	Mon Jul 20 14:37:00 2009 +0200
@@ -311,19 +311,20 @@
             acl = request.cfg.cache.acl_rights_default.acl
         else: # we have a #acl on the page (self.acl can be [] if #acl is empty!)
             acl = self.acl
-        is_group_member = request.dicts.has_member
-        group_re = request.cfg.cache.page_group_regexact
+
+        groups = request.groups
+
         allowed = None
         for entry, rightsdict in acl:
             if entry in self.special_users:
                 handler = getattr(self, "_special_"+entry, None)
                 allowed = handler(request, name, dowhat, rightsdict)
-            elif group_re.search(entry):
-                if is_group_member(entry, name):
+            elif entry in groups:
+                if name in groups[entry]:
                     allowed = rightsdict.get(dowhat)
                 else:
                     for special in self.special_users:
-                        if is_group_member(entry, special):
+                        if special in entry:
                             handler = getattr(self, "_special_" + special, None)
                             allowed = handler(request, name, dowhat, rightsdict)
                             break # order of self.special_users is important
@@ -455,4 +456,3 @@
     pi, dummy = wikiutil.get_processing_instructions(text)
     acl_lines = [args for verb, args in pi if verb == 'acl']
     return AccessControlList(request.cfg, acl_lines)
-
--- a/MoinMoin/security/autoadmin.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/security/autoadmin.py	Mon Jul 20 14:37:00 2009 +0200
@@ -69,14 +69,15 @@
     def admin(self, pagename):
         try:
             request = self.request
-            has_member = request.dicts.has_member
+            groups = request.groups
             username = request.user.name
             pagename = request.page.page_name
             mainpage = pagename.split('/')[0]
-            if username == mainpage and has_member('AutoAdminGroup', username):
+            if username == mainpage and username in groups.get(u'AutoAdminGroup', []):
                 return True
-            groupname = "%s/AdminGroup" % mainpage
-            if has_member(groupname, username) and has_member('AutoAdminGroup', groupname):
+            group_name = "%s/AdminGroup" % mainpage
+            if (username in groups.get(group_name, []) and
+                group_name in groups.get(u'AutoAdminGroup', [])):
                 return True
         except AttributeError:
             pass # when we get called from xmlrpc, there is no request.page
--- a/MoinMoin/security/textcha.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/security/textcha.py	Mon Jul 20 14:37:00 2009 +0200
@@ -46,10 +46,11 @@
     def _get_textchas(self):
         """ get textchas from the wiki config for the user's language (or default_language or en) """
         request = self.request
+        groups = request.groups
         cfg = request.cfg
         user = request.user
         disabled_group = cfg.textchas_disabled_group
-        if disabled_group and user.name and request.dicts.has_member(disabled_group, user.name):
+        if disabled_group and user.name and user.name in groups.get(disabled_group, []):
             return None
         textchas = cfg.textchas
         if textchas:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/userform/_tests/test_admin.py	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,48 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinMoin.userform.admin Tests
+
+    @copyright: 2009 MoinMoin:DmitrijsMilajevs
+    @license: GNU GPL, see COPYING for details.
+"""
+
+
+from MoinMoin.userform.admin import do_user_browser
+from MoinMoin.datastruct import ConfigGroups
+from MoinMoin.user import User
+from MoinMoin.Page import Page
+from MoinMoin._tests import nuke_user, become_superuser, wikiconfig
+
+class TestAdmin:
+
+    class Config(wikiconfig.Config):
+
+        def groups(self, request):
+            groups = {'OneGroup': ['TestUser, OtherUser'],
+                      'OtherGroup': ['TestUser']}
+            return ConfigGroups(request, groups)
+
+    def setup_class(self):
+        request = self.request
+        user_name = 'TestUser'
+        self.user_name = user_name
+
+        become_superuser(request)
+
+        User(request, name=user_name, password=user_name).save()
+
+    def teardown_class(self):
+        nuke_user(self.request, self.user_name)
+
+    def setup_method(self, method):
+        self.request.page = Page(self.request, 'SystemAdmin')
+
+    def test_do_user_browser(self):
+        request = self.request
+
+        browser = do_user_browser(request)
+        assert browser
+
+
+coverage_modules = ['MoinMoin.userform.admin']
+
--- a/MoinMoin/userform/admin.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/userform/admin.py	Mon Jul 20 14:37:00 2009 +0200
@@ -3,18 +3,24 @@
     MoinMoin - User account administration
 
     @copyright: 2001-2004 Juergen Hermann <jh@web.de>,
-                2003-2007 MoinMoin:ThomasWaldmann
-                2007-2008 MoinMoin:ReimarBauer
+                2003-2007 MoinMoin:ThomasWaldmann,
+                2007-2008 MoinMoin:ReimarBauer,
+                2009 MoinMoin:DmitrijsMilajevs
     @license: GNU GPL, see COPYING for details.
 """
+
+
 from MoinMoin import user, wikiutil
 from MoinMoin.util.dataset import TupleDataset, Column
 from MoinMoin.Page import Page
 from MoinMoin.widget import html
+from MoinMoin.datastruct.backends.wiki_groups import WikiGroup
+
 
 def do_user_browser(request):
     """ Browser for SystemAdmin macro. """
     _ = request.getText
+    groups = request.groups
 
     data = TupleDataset()
     data.columns = [
@@ -25,16 +31,18 @@
         Column('action', label=_('Action')),
     ]
 
-    isgroup = request.cfg.cache.page_group_regexact.search
-    groupnames = request.rootpage.getPageList(user='', filter=isgroup)
-
     # Iterate over users
     for uid in user.getUserList(request):
         account = user.User(request, uid)
 
-        grouppage_links = ', '.join([Page(request, groupname).link_to(request)
-                                     for groupname in groupnames
-                                     if request.dicts.has_member(groupname, account.name)])
+        account_groups = set(groups.groups_with_member(account.name))
+        wiki_groups = set([group for group in account_groups if isinstance(groups[group], WikiGroup)])
+        other_groups = list(account_groups - wiki_groups)
+
+        # First show groups that are defined in wikipages linking to it
+        # after show groups from other backends.
+        grouppage_links = ', '.join([Page(request, group_name).link_to(request) for group_name in wiki_groups] +
+                                    other_groups)
 
         userhomepage = Page(request, account.name)
         if userhomepage.exists():
--- a/MoinMoin/userprefs/oidserv.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/userprefs/oidserv.py	Mon Jul 20 14:37:00 2009 +0200
@@ -26,10 +26,10 @@
         if not self.request.cfg.openid_server_enabled:
             return False
 
-        grp = self.request.cfg.openid_server_restricted_users_group
-        if grp:
-            self.request.dicts.addgroup(self.request, grp)
-            if not self.request.dicts.has_member(grp, self.request.user.name):
+        groups = self.request.groups
+        openid_group_name = self.request.cfg.openid_server_restricted_users_group
+
+        if openid_group_name and self.request.user.name not in groups.get(openid_group_name, []):
                 return False
 
         return True
--- a/MoinMoin/web/contexts.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/web/contexts.py	Mon Jul 20 14:37:00 2009 +0200
@@ -371,12 +371,16 @@
 
     def dicts(self):
         """ Lazy initialize the dicts on the first access """
-        from MoinMoin import wikidicts
-        dicts = wikidicts.GroupDict(self)
-        dicts.load_dicts()
+        dicts = self.cfg.dicts(self)
         return dicts
     dicts = EnvironProxy(dicts)
 
+    def groups(self):
+        """ Lazy initialize the groups on the first access """
+        groups = self.cfg.groups(self)
+        return groups
+    groups = EnvironProxy(groups)
+
     def reset(self):
         self.current_lang = self.cfg.language_default
         if hasattr(self, '_fmt_hd_counters'):
--- a/MoinMoin/wikidicts.py	Mon Jul 20 12:47:36 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,359 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - Dictionary / Group Functions
-
-    @copyright: 2003-2007 MoinMoin:ThomasWaldmann,
-                2003 by Gustavo Niemeyer
-    @license: GNU GPL, see COPYING for details.
-"""
-import re, time
-
-from MoinMoin import caching, Page
-
-# Version of the internal data structure which is pickled.
-# Please increment if you have changed the structure.
-DICTS_PICKLE_VERSION = 6
-
-
-class DictBase(dict):
-    """ Base class for wiki dicts
-
-    To use this class, subclass it and override regex and initFromText.
-    """
-    def __init__(self, request=None, pagename=None):
-        dict.__init__(self)
-        self.name = None
-        if request is not None and pagename is not None:
-            self.loadFromPage(request, pagename)
-
-    # Regular expression used to parse text - subclass must override this
-    regex = None  # re.compile(u'...', re.MULTILINE | re.UNICODE)
-
-    def loadFromPage(self, request, name):
-        """ load the dict from wiki page <name>'s content """
-        self.name = name
-        text = Page.Page(request, name).get_raw_body()
-        self.initFromText(text)
-
-    def initFromText(self, text):
-        """ parse the wiki page text and init the dict from it """
-        raise NotImplementedError('subclasses should override this')
-
-
-class Dict(DictBase):
-    """ Mapping of keys to values in a wiki page.
-
-       How a Dict definition page should look like:
-
-       any text ignored
-        key1:: value1
-        * ignored, too
-        key2:: value2 containing spaces
-        ...
-        keyn:: ....
-       any text ignored
-    """
-    # Key:: Value - ignore all but key:: value pairs, strip whitespace, exactly one space after the :: is required
-    regex = re.compile(ur'^ (?P<key>.+?):: (?P<val>.*?) *$', re.MULTILINE | re.UNICODE)
-
-    def initFromText(self, text):
-        for match in self.regex.finditer(text):
-            key, val = match.groups()
-            self[key] = val
-
-    def __repr__(self):
-        return "<Dict name=%r items=%r>" % (self.name, self.items())
-
-
-class Group(DictBase):
-    """ Group of users, of pages, of whatever.
-
-    How a Group definition page should look like:
-
-    any text ignored
-     * member1
-      * ignored, too
-     * member2
-     * ....
-     * memberN
-    any text ignored
-
-    If there are any free links using [[free link]] notation, the markup
-    is stripped from the member.
-    """
-    # * Member - ignore all but first level list items, strip whitespace, strip free links markup
-    regex = re.compile(ur'^ \* +(?:\[\[)?(?P<member>.+?)(?:\]\])? *$', re.MULTILINE | re.UNICODE)
-
-    def __init__(self, request=None, pagename=None):
-        self._list = []
-        DictBase.__init__(self, request, pagename)
-
-    def initFromText(self, text):
-        for match in self.regex.finditer(text):
-            member = match.group('member')
-            self.addmember(member)
-
-    def update(self, members):
-        self.addmembers(members.keys())
-
-    def __iter__(self):
-        return iter(self._list)
-
-    def members(self):
-        """ return the group's members """
-        return self._list[:]
-
-    def addmembers(self, members):
-        """ add a list of members to the group """
-        for m in members:
-            self.addmember(m)
-
-    def addmember(self, member):
-        """ add a member to the group """
-        self[member] = 1
-        self._list.append(member)
-
-    def has_member(self, member):
-        """ check if the group has member <member> """
-        return self.has_key(member)
-
-    def __repr__(self):
-        return "<Group name=%r items=%r>" % (self.name, self._list)
-
-
-class DictDict:
-    """ a dictionary of Dict objects
-
-       Config:
-           cfg.page_dict_regex
-               Default: ".*Dict$"  Defs$ Vars$ ???????????????????
-    """
-
-    def __init__(self):
-        self.reset()
-
-    def reset(self):
-        self.dictdict = {}
-        self.namespace_timestamp = 0
-        self.pageupdate_timestamp = 0
-        self.base_timestamp = 0
-        self.picklever = DICTS_PICKLE_VERSION
-
-    def has_key(self, dictname, key):
-        """ check if we have key <key> in dict <dictname> """
-        d = self.dictdict.get(dictname)
-        return d and d.has_key(key)
-
-    def keys(self, dictname):
-        """ get keys of dict <dictname> """
-        try:
-            d = self.dictdict[dictname]
-        except KeyError:
-            return []
-        return d.keys()
-
-    def values(self, dictname):
-        """ get values of dict <dictname> """
-        try:
-            d = self.dictdict[dictname]
-        except KeyError:
-            return []
-        return d.values()
-
-    def dict(self, dictname):
-        """ get dict <dictname> """
-        try:
-            d = self.dictdict[dictname]
-        except KeyError:
-            return {}
-        return d
-
-    def adddict(self, request, dictname):
-        """ add a new dict (will be read from the wiki page) """
-        self.dictdict[dictname] = Dict(request, dictname)
-
-    def has_dict(self, dictname):
-        """ check if we have a dict <dictname> """
-        return self.dictdict.has_key(dictname)
-
-    def keydict(self, key):
-        """ list all dicts that contain key """
-        dictlist = []
-        for d in self.dictdict.values():
-            if d.has_key(key):
-                dictlist.append(d.name)
-        return dictlist
-
-
-class GroupDict(DictDict):
-    """ a dictionary of Group objects
-
-       Config:
-           cfg.page_group_regex
-               Default: ".*Group$"
-    """
-
-    def __init__(self, request):
-        self.cfg = request.cfg
-        self.request = request
-
-    def reset(self):
-        self.dictdict = {}
-        self.groupdict = {} # unexpanded groups
-        self.picklever = DICTS_PICKLE_VERSION
-        self.disk_cache_id = None
-
-    def has_member(self, groupname, member):
-        """ check if we have <member> as a member of group <groupname> """
-        group = self.dictdict.get(groupname)
-        return group and group.has_member(member)
-
-    def members(self, groupname):
-        """ get members of group <groupname> """
-        try:
-            group = self.dictdict[groupname]
-        except KeyError:
-            return []
-        return group.members()
-
-    def addgroup(self, request, groupname):
-        """ add a new group (will be read from the wiki page) """
-        grp = Group(request, groupname)
-        self.groupdict[groupname] = grp
-        self.expand_groups()
-
-    def hasgroup(self, groupname):
-        """ check if we have a dict <dictname> """
-        return self.groupdict.has_key(groupname)
-
-    def __getitem__(self, name):
-        return self.groupdict[name]
-
-    def membergroups(self, member):
-        """ list all groups where member is a member of """
-        grouplist = []
-        for group in self.dictdict.values():
-            if group.has_member(member):
-                grouplist.append(group.name)
-        return grouplist
-
-    def expand_groups(self):
-        """ copy expanded groups to self.dictdict """
-        for name in self.groupdict:
-            members, groups = self.expand_group(name)
-            members.update(groups)
-            grp = Group()
-            grp.update(members)
-            self.dictdict[name] = grp
-
-    def expand_group(self, name):
-        """ Recursively expand group <name>, using the groupdict (which is a not expanded
-            dict of all group names -> group dicts). We return a flat list of group member
-            names and group names.
-
-        Given a groupdict (self) with two groups:
-
-            MainGroup: [A, SubGroup]
-            SubGroup: [B, C]
-
-        MainGroup is expanded to:
-
-            self.expand_group('MainGroup') -> [A, B, C], [MainGroup, SubGroup]
-        """
-        groups = {name: 1}
-        members = {}
-        groupmembers = self[name].keys()
-        for member in groupmembers:
-            # Skip duplicates
-            if member in groups:
-                continue
-            # Add member and its children
-            if self.hasgroup(member):
-                new_members, new_groups = self.expand_group(member)
-                groups.update(new_groups)
-                members.update(new_members)
-            else:
-                members[member] = 1
-        return members, groups
-
-    def load_dicts(self):
-        """ load the dict from the cache """
-        request = self.request
-        rescan = False
-        arena = 'wikidicts'
-        key = 'dicts_groups'
-        cache = caching.CacheEntry(request, arena, key, scope='wiki', use_pickle=True)
-        current_disk_cache_id = cache.uid()
-        try:
-            self.__dict__.update(self.cfg.cache.DICTS_DATA)
-            if (current_disk_cache_id is None or
-                current_disk_cache_id != self.disk_cache_id):
-                self.reset()
-                raise AttributeError # not fresh, force load from disk
-            else:
-                return
-        except AttributeError:
-            try:
-                data = cache.content()
-                self.__dict__.update(data)
-                self.disk_cache_id = current_disk_cache_id
-
-                # invalidate the cache if the pickle version changed
-                if self.picklever != DICTS_PICKLE_VERSION:
-                    raise # force rescan
-            except:
-                self.reset()
-                rescan = True
-
-        if rescan:
-            self.scan_dicts()
-            self.load_dicts() # try again
-            return
-
-        data = {
-            "disk_cache_id": self.disk_cache_id,
-            "dictdict": self.dictdict,
-            "groupdict": self.groupdict,
-            "picklever": self.picklever
-        }
-
-        # remember it (persistent environments)
-        self.cfg.cache.DICTS_DATA = data
-
-    def scan_dicts(self):
-        """ scan all pages matching the dict / group regex and
-            cache the results on disk
-        """
-        request = self.request
-        self.reset()
-
-        # XXX get cache write lock here
-        scan_begin_time = time.time()
-
-        # Get all pages in the wiki - without user filtering using filter
-        # function - this makes the page list about 10 times faster.
-        isdict = self.cfg.cache.page_dict_regexact.search
-        dictpages = request.rootpage.getPageList(user='', filter=isdict)
-        for pagename in dictpages:
-            self.adddict(request, pagename)
-
-        isgroup = self.cfg.cache.page_group_regexact.search
-        grouppages = request.rootpage.getPageList(user='', filter=isgroup)
-        for pagename in grouppages:
-            self.addgroup(request, pagename)
-
-        scan_end_time = time.time()
-        self.expand_groups()
-
-        arena = 'wikidicts'
-        key = 'dicts_groups'
-        cache = caching.CacheEntry(request, arena, key, scope='wiki', use_pickle=True)
-        data = {
-            "scan_begin_time": scan_begin_time,
-            "scan_end_time": scan_end_time,
-            "dictdict": self.dictdict,
-            "groupdict": self.groupdict,
-            "picklever": self.picklever
-        }
-        cache.update(data)
-        # XXX release cache write lock here
--- a/MoinMoin/wikisync.py	Mon Jul 20 12:47:36 2009 +0200
+++ b/MoinMoin/wikisync.py	Mon Jul 20 14:37:00 2009 +0200
@@ -310,10 +310,9 @@
 
     def getGroupItems(self, group_list):
         """ Returns all page names that are listed on the page group_list. """
-        from MoinMoin.wikidicts import Group
         pages = []
         for group_pagename in group_list:
-            pages.extend(Group(self.request, group_pagename).members())
+            pages.extend(request.groups.get(group_pagename, []))
         return [self.createSyncPage(x) for x in pages]
 
     def createSyncPage(self, page_name):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/CHANGES.dmilajevs	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,12 @@
+Version 1.9-groups-dmilajevs:
+
+   New features:
+   * Group and dict backends. Wiki, config and composite backends for both data structures.
+
+   Admin Notes:
+   * New dicts and groups configuration options. See wiki/config/more_samples/dicts_wikiconfig_snippet and wiki/config/more_samples/groups_wikiconfig_snippet for configuration examples.
+
+   Developer notes:
+   * request.groups provides to access groups. request.dicts provides access to dicts.
+   * 'SomeDict' in request.dicts must be used instead of has_dict() and request.dicts['SomeDict'] instead of dict().
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/config/more_samples/dicts_wikiconfig_snippet	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,27 @@
+    # This is a sample configuration snippet that shows moin's dicts configuration
+    # See HelpOnConfiguration for more info.
+
+    # The wiki_dicts backend is used by default.
+    def dicts(self, request):
+        from MoinMoin.datastruct import WikiDicts
+        return WikiDicts(request)
+
+    # Use only dicts defined in the configuration file.
+    def dicts(self, request):
+        from MoinMoin.datastruct import ConfigDicts
+        dicts = {u'OneDict': {u'first_key': u'first item',
+                              u'second_key': u'second item'},
+                 u'NumbersDict': {u'1': 'One',
+                                  u'2': 'Two'}}
+        return ConfigDicts(request, dicts)
+
+    # Use both config_dicts and wiki_dicts backends.
+    def dicts(self, request):
+        from MoinMoin.datastruct import ConfigDicts, WikiDicts, CompositeDicts
+        dicts = {u'OneDict': {u'first_key': u'first item',
+                              u'second_key': u'second item'},
+                 u'NumbersDict': {u'1': 'One',
+                                  u'2': 'Two'}}
+        return CompositeDicts(request,
+                              ConfigDicts(request, dicts),
+                              WikiDicts(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/config/more_samples/groups_wikiconfig_snippet	Mon Jul 20 14:37:00 2009 +0200
@@ -0,0 +1,28 @@
+    # This is a sample configuration snippet that shows moin's groups configuration
+    # See HelpOnConfiguration for more info.
+
+    # The wiki_groups backend is used by default.
+    def groups(self, request):
+        from MoinMoin.datastruct import WikiGroups
+        return WikiGroups(request)
+
+    # Use only groups defined in the configuration file.
+    def groups(self, request):
+        from MoinMoin.datastruct import ConfigGroups
+        # Groups are defined here.
+        groups = {u'EditorGroup': [u'AdminGroup', u'John', u'JoeDoe', u'Editor1'],
+                  u'AdminGroup': [u'Admin1', u'Admin2', u'John']}
+        return ConfigGroups(request, groups)
+
+    # Use both config_groups and wiki_groups backends.
+    def groups(self, request):
+        from MoinMoin.datastruct import ConfigGroups, WikiGroups, CompositeGroups
+        groups = {u'EditorGroup': [u'AdminGroup', u'John', u'JoeDoe', u'Editor1'],
+                  u'AdminGroup': [u'Admin1', u'Admin2', u'John']}
+
+        # Here ConfigGroups and WikiGroups backends are used.
+        # Note that order matters! Since ConfigGroups backend is mentioned first
+        # EditorGroup will be retrieved from it, not from WikiGroups.
+        return CompositeGroups(request,
+                               ConfigGroups(request, groups),
+                               WikiGroups(request))