Commits

Thomas Waldmann  committed b942154 Merge

merged bootstrap and main repo

  • Participants
  • Parent commits 61884a4, 3322182

Comments (0)

Files changed (107)

 ^dlc/
 ^moin.egg-info/
 ^MoinMoin/_tests/wiki/data/cache/
-^wiki/data/cache/
-^wiki/data/default/data/
-^wiki/data/default/meta/
-^wiki/data/content/
-^wiki/data/userprofiles/
-^wiki/index/
+^wiki/
 ^instance/
 ^wikiconfig_.+\.py
 ^MoinMoin/translations/.*/LC_MESSAGES/messages.mo$
 ^upload.py
 ^build/
 \..*sw[op]$
+^activate.bat$
+^deactivate.bat$
+^moin.bat$
+^m.bat$
+^activate$
+^moin$
+^m$
+^m-.*\.txt$
 include README.txt LICENSE.txt
-include quickinstall quickinstall.bat
+include quickinstall.py
 include wikiconfig.py
 include Makefile
 
 global-exclude *.rej
 
 prune docs/_build
-prune env
-prune env-pypy
-prune env-py26
-prune env-py27
 
 prune wiki/data/content
 prune wiki/data/userprofiles

File Makefile

-#
-# Makefile for MoinMoin
-#
-
-# location for the wikiconfig.py we use for testing:
-export PYTHONPATH=$(PWD)
-
-all:
-	python setup.py build
-
-test:
-	py.test --pep8 -rs
-
-dist: clean-devwiki
-	-rm MANIFEST
-	python setup.py sdist
-
-docs:
-	make -C docs html
-
-# this needs the sphinx-autopackage script in the toplevel dir:
-apidoc:
-	sphinx-apidoc -f -o docs/devel/api MoinMoin
-
-interwiki:
-	wget -U MoinMoin/Makefile -O contrib/interwiki/intermap.txt "http://master19.moinmo.in/InterWikiMap?action=raw"
-	chmod 664 contrib/interwiki/intermap.txt
-
-pylint:
-	@pylint --disable-msg=W0142,W0511,W0612,W0613,C0103,C0111,C0302,C0321,C0322 --disable-msg-cat=R MoinMoin
-
-clean: clean-devwiki clean-pyc clean-orig clean-rej
-	-rm -rf build
-
-clean-devwiki:
-	-rm -rf wiki/data/content
-	-rm -rf wiki/data/userprofiles
-	-rm -rf wiki/index
-
-clean-pyc:
-	find . -name "*.pyc" -exec rm -rf "{}" \; 
-
-clean-orig:
-	find . -name "*.orig" -exec rm -rf "{}" \; 
-
-clean-rej:
-	find . -name "*.rej" -exec rm -rf "{}" \; 
-
-.PHONY: all dist docs interwiki check-tabs pylint \
-	clean clean-devwiki clean-pyc clean-orig clean-rej
-

File MoinMoin/__init__.py

 project = "MoinMoin"
 
 import sys
-if sys.hexversion < 0x2070000:
-    sys.exit("%s requires Python 2.7.x.\n" % project)
+import platform
+
+
+if sys.hexversion < 0x2070000 or sys.hexversion > 0x2999999:
+    sys.exit("Error: %s requires Python 2.7.x., current version is %s\n" % (project, platform.python_version()))
 
 
 from MoinMoin.util.version import Version

File MoinMoin/_tests/test_forms.py

 """
 
 import datetime
+import json
 from calendar import timegm
 
-from MoinMoin.forms import DateTimeUNIX
+from flask import current_app as app
+from flask import g as flaskg
+
+from MoinMoin.forms import DateTimeUNIX, JSON, Names
+from MoinMoin.util.interwiki import CompositeName
+from MoinMoin.items import Item
+from MoinMoin._tests import become_trusted
+from MoinMoin.constants.keys import ITEMID, NAME, CONTENTTYPE, NAMESPACE, FQNAME
 
 
 def test_datetimeunix():
     d = DateTimeUNIX(None)
     assert d.value is None
     assert d.u == u''
+
+
+def test_validjson():
+    app.cfg.namespace_mapping = [(u'', 'default_backend'), (u'ns1/', 'default_backend'), (u'ns1/ns2/', 'other_backend')]
+    item = Item.create(u'ns1/ns2/existingname')
+    meta = {NAMESPACE: u'ns1/ns2', CONTENTTYPE: u'text/plain;charset=utf-8'}
+    become_trusted()
+    item._save(meta, data='This is a valid Item.')
+
+    valid_itemid = 'a1924e3d0a34497eab18563299d32178'
+    # ('names', 'namespace', 'field', 'value', 'result')
+    tests = [([u'somename', u'@revid'], '', '', 'somename', False),
+             ([u'bar', u'ns1'], '', '', 'bar', False),
+             ([u'foo', u'foo', u'bar'], '', '', 'foo', False),
+             ([u'ns1ns2ns3', u'ns1/subitem'], '', '', 'valid', False),
+             ([u'foobar', u'validname'], '', ITEMID, valid_itemid + '8080', False),
+             ([u'barfoo', u'validname'], '', ITEMID, valid_itemid.replace('a', 'y'), False),
+             ([], '', 'itemid', valid_itemid, True),
+             ([u'existingname'], 'ns1/ns2', '', 'existingname', False),
+             ]
+    for name, namespace, field, value, result in tests:
+        meta = {NAME: name, NAMESPACE: namespace}
+        x = JSON(json.dumps(meta))
+        y = Names(name)
+        state = dict(fqname=CompositeName(namespace, field, value), itemid=None, meta=meta)
+        value = x.validate(state) and y.validate(state)
+        assert value == result

File MoinMoin/_tests/test_user.py

 from flask import g as flaskg
 
 from MoinMoin import user
+from MoinMoin.items import Item
+from MoinMoin.constants.keys import (ITEMID, NAME, NAMEPREFIX, NAMERE, NAMESPACE, TAGS)
 
 
 class TestSimple(object):
 
     # Subscriptions ---------------------------------------------------
 
-    def testSubscriptionSubscribedPage(self):
-        """ user: tests is_subscribed_to  """
-        pagename = u'HelpMiscellaneous'
-        name = u'__Jürgen Herman__'
+    def test_subscriptions(self):
+        pagename = u"Foo:foo 123"
+        tagname = u"xxx"
+        regexp = r"\d+"
+        item = Item.create(pagename)
+        item._save({NAMESPACE: u"", TAGS: [tagname]})
+        item = Item.create(pagename)
+        meta = item.meta
+
+        name = u'bar'
         password = name
-        self.createUser(name, password)
-        # Login - this should replace the old password in the user file
-        theUser = user.User(name=name, password=password)
-        theUser.subscribe(pagename)
-        assert theUser.is_subscribed_to([pagename])  # list(!) of pages to check
+        email = "bar@example.org"
+        user.create_user(name, password, email)
+        the_user = user.User(name=name, password=password)
+        assert not the_user.is_subscribed_to(item)
+        the_user.subscribe(NAME, u"SomeOtherPageName", u"")
+        result = the_user.unsubscribe(NAME, u"OneMorePageName", u"")
+        assert result is False
 
-    def testSubscriptionSubPage(self):
-        """ user: tests is_subscribed_to on a subpage """
-        pagename = u'HelpMiscellaneous'
-        testPagename = u'HelpMiscellaneous/FrequentlyAskedQuestions'
-        name = u'__Jürgen Herman__'
-        password = name
-        self.createUser(name, password)
-        # Login - this should replace the old password in the user file
-        theUser = user.User(name=name, password=password)
-        theUser.subscribe(pagename)
-        assert not theUser.is_subscribed_to([testPagename])  # list(!) of pages to check
+        subscriptions = [(ITEMID, meta[ITEMID], None),
+                         (NAME, pagename, meta[NAMESPACE]),
+                         (TAGS, tagname, meta[NAMESPACE]),
+                         (NAMEPREFIX, pagename[:4], meta[NAMESPACE]),
+                         (NAMERE, regexp, meta[NAMESPACE])]
+        for subscription in subscriptions:
+            keyword, value, namespace = subscription
+            the_user.subscribe(keyword, value, namespace)
+            assert the_user.is_subscribed_to(item)
+            the_user.unsubscribe(keyword, value, namespace, item)
+            assert not the_user.is_subscribed_to(item)
 
     # Bookmarks -------------------------------------------------------
 
         # add quicklink
         theUser.quicklink(u'Test_page_added')
         result_on_addition = theUser.quicklinks
-        expected = [u'MoinTest:Test_page_added']
+        expected = [u'MoinTest/Test_page_added']
         assert result_on_addition == expected
 
         # remove quicklink
         theUser.add_trail(u'item_added')
         theUser = user.User(name=name, password=password)
         result = theUser.get_trail()
-        expected = [u'MoinTest:item_added']
+        expected = [u'MoinTest/item_added']
         assert result == expected
 
     # Sessions -------------------------------------------------------

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

     <li><a href="{{ url_for('admin.userbrowser') }}">{{ _("Users") }}</a></li>
     <li><a href="{{ url_for('admin.wikiconfig') }}">{{ _("Show Wiki Configuration") }}</a></li>
     <li><a href="{{ url_for('admin.wikiconfighelp') }}">{{ _("Wiki Configuration Help") }}</a></li>
+    <li><a href="{{ url_for('admin.trash', namespace='all') }}">{{ _("Trash") }}</a></li>
 </ul>
 {% endblock %}

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

+{% extends theme("layout.html") %}
+{% import "utils.html" as utils %}
+{% block content %}
+{% if headline %}
+<h1>{{ headline }}</h1>
+{% endif %}
+Total: {{ results|count }}
+    {% if results %}
+        <table class="zebra">
+            <thead>
+                <tr>
+                    <th>{{ _("Old Name") }}</th>
+                    <th>{{ _("Rev.") }}</th>
+                    <th>{{ _("Timestamp") }}</th>
+                    <th>{{ _("Editor") }}</th>
+                    <th>{{ _("Comment") }}</th>
+                    <th colspan="3">{{ _("Actions") }}</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for result in results| sort(attribute='mtime', reverse=True)%}
+                <tr>
+                    <td class="moin-wordbreak">{{ result.oldname|join(' | ') }}</td>
+                    <td>{{ result.revid | shorten_id }}</td>
+                    <td>{{ result.mtime|datetimeformat }}</td>
+                    <td class="moin-wordbreak">{{ utils.show_editor_info(result.editor)  }}</td>
+                    <td class="moin-wordbreak">{{ result.comment }}</td>
+                    <td><a href="{{ url_for('frontend.show_item', item_name=result.fqname) }}">{{ _('show') }}</a></td>
+                    {% if user.may.write(result.fqname) -%}
+                    <td><a href="{{ url_for('frontend.history', item_name=result.fqname) }}">{{ _('History') }}</a></td>
+                    <td><a href="{{ url_for('frontend.destroy_item', item_name=result.fqname) }}">{{ _('Destroy') }}</a></td>
+                    {%- endif %}
+                </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+    {% endif %}
+{% endblock %}

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

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

File MoinMoin/apps/admin/views.py

 
 This shows the user interface for wiki admins.
 """
-
+from collections import namedtuple
 from flask import request, url_for, flash, redirect
 from flask import current_app as app
 from flask import g as flaskg
-
+from whoosh.query import Term, And
 from MoinMoin.i18n import _, L_, N_
-from MoinMoin.themes import render_template
+from MoinMoin.themes import render_template, get_editor_info
 from MoinMoin.apps.admin import admin
 from MoinMoin import user
-from MoinMoin.constants.keys import NAME, ITEMID, SIZE, EMAIL, DISABLED
+from MoinMoin.constants.keys import NAME, ITEMID, SIZE, EMAIL, DISABLED, NAME_EXACT, WIKINAME, TRASH, NAMESPACE, NAME_OLD, REVID, MTIME, COMMENT
+from MoinMoin.constants.namespaces import NAMESPACE_USERPROFILES, NAMESPACE_DEFAULT, NAMESPACE_ALL
 from MoinMoin.constants.rights import SUPERUSER
 from MoinMoin.security import require_permission
+from MoinMoin.util.interwiki import CompositeName
 
 
 @admin.route('/superuser')
     revs = user.search_users()  # all users
     user_accounts = [dict(uid=rev.meta[ITEMID],
                           name=rev.meta[NAME],
+                          fqname=CompositeName(NAMESPACE_USERPROFILES, NAME_EXACT, rev.name),
                           email=rev.meta[EMAIL],
                           disabled=rev.meta[DISABLED],
                           groups=[groupname for groupname in groups if rev.meta[NAME] in groups[groupname]],
                            title_name=_(u"Item Sizes"),
                            headings=headings,
                            rows=rows)
+
+
+@admin.route('/trash', defaults=dict(namespace=NAMESPACE_DEFAULT), methods=['GET'])
+@admin.route('/<namespace>/trash')
+def trash(namespace):
+    """
+    Returns the trashed items.
+    """
+    trash = _trashed(namespace)
+    return render_template('admin/trash.html',
+                           headline=_(u'Trashed Items'),
+                           title_name=_(u'Trashed Items'),
+                           results=trash)
+
+
+def _trashed(namespace):
+    q = And([Term(WIKINAME, app.cfg.interwikiname), Term(TRASH, True)])
+    if not namespace == NAMESPACE_ALL:
+        q = And([q, Term(NAMESPACE, namespace), ])
+    trashedEntry = namedtuple('trashedEntry', 'fqname oldname revid mtime comment editor')
+    results = []
+    for rev in flaskg.storage.search(q, limit=None):
+        meta = rev.meta
+        results.append(trashedEntry(rev.fqname, meta[NAME_OLD], meta[REVID], meta[MTIME], meta[COMMENT], get_editor_info(meta)))
+    return results

File MoinMoin/apps/frontend/_tests/test_frontend.py

         self._test_view('frontend.quicklink_item', status='302 FOUND', viewopts=dict(item_name='DoesntExist'), data=['<!DOCTYPE HTML'])
 
     def test_subscribe_item(self):
-        self._test_view('frontend.subscribe_item', status='302 FOUND', viewopts=dict(item_name='DoesntExist'), data=['<!DOCTYPE HTML'])
+        self._test_view('frontend.subscribe_item', status='404 NOT FOUND', viewopts=dict(item_name='DoesntExist'))
 
     def test_register(self):
         self._test_view('frontend.register')

File MoinMoin/apps/frontend/views.py

 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.themes import render_template, contenttype_to_class
 from MoinMoin.apps.frontend import frontend
-from MoinMoin.forms import (OptionalText, RequiredText, URL, YourOpenID, YourEmail, RequiredPassword, Checkbox,
-                            InlineCheckbox, Select, Names, Tags, Natural, Hidden, MultiSelect, Enum)
-from MoinMoin.items import BaseChangeForm, Item, NonExistent, NameNotUniqueError
+from MoinMoin.forms import (OptionalText, RequiredText, URL, YourOpenID, YourEmail,
+                            RequiredPassword, Checkbox, InlineCheckbox, Select, Names,
+                            Tags, Natural, Hidden, MultiSelect, Enum, Subscriptions,
+                            validate_name, NameNotValidError)
+from MoinMoin.items import BaseChangeForm, Item, NonExistent, NameNotUniqueError, FieldNotUniqueError
 from MoinMoin.items.content import content_registry
 from MoinMoin import user, util
 from MoinMoin.constants.keys import *
+from MoinMoin.constants.namespaces import *
 from MoinMoin.constants.itemtypes import ITEMTYPE_DEFAULT
 from MoinMoin.constants.chartypes import CHARS_UPPER, CHARS_LOWER
 from MoinMoin.util import crypto
-from MoinMoin.util.interwiki import url_for_item
+from MoinMoin.util.interwiki import url_for_item, split_fqname, CompositeName
 from MoinMoin.search import SearchForm
 from MoinMoin.search.analyzers import item_name_analyzer
 from MoinMoin.security.textcha import TextCha, TextChaizedForm
 
 @frontend.route('/')
 def show_root():
-    item_name = app.cfg.item_root
+    item_name = app.cfg.root_mapping.get(NAMESPACE_DEFAULT, app.cfg.default_root)
     return redirect(url_for_item(item_name))
 
 
     return app.send_static_file('logos/favicon.ico')
 
 
+@frontend.route('/all')
+def global_views():
+    """
+    Provides a link to all the global views.
+    """
+    return render_template('all.html',
+                           title_name=_(u"Global Views."),
+                           fqname=CompositeName(u'all', NAME_EXACT, u'')
+                          )
+
+
 class LookupForm(Form):
     name = OptionalText.using(label='name')
     name_exact = OptionalText.using(label='name_exact')
                                    query=query,
                                    medium_search_form=search_form,
                                    item_name=item_name,
+                                   history=history,
             )
             flaskg.clock.stop('search render')
     else:
 @frontend.route('/<itemname:item_name>', defaults=dict(rev=CURRENT), methods=['GET', 'POST'])
 @frontend.route('/+show/+<rev>/<itemname:item_name>', methods=['GET'])
 def show_item(item_name, rev):
-    flaskg.user.add_trail(item_name)
     item_displayed.send(app._get_current_object(),
                         item_name=item_name)
+    fqname = split_fqname(item_name)
+    if not fqname.value and fqname.field == NAME_EXACT:
+        fqname = fqname.get_root_fqname()
+        return redirect(url_for_item(fqname))
     try:
         item = Item.create(item_name, rev_id=rev)
+        flaskg.user.add_trail(item_name)
         result = item.do_show(rev)
     except AccessDenied:
         abort(403)
+    except FieldNotUniqueError:
+        revs = flaskg.storage.documents(**fqname.query)
+        fq_names = []
+        for rev in revs:
+            fq_names.extend(rev.fqnames)
+        return render_template("link_list_no_item_panel.html",
+                               headline=_("Items with %(field)s %(value)s", field=fqname.field, value=fqname.value),
+                               fqname=fqname,
+                               fq_names=fq_names,
+                               )
     return result
 
 
 def highlight_item(item):
     return render_template('highlight.html',
                            item=item, item_name=item.name,
+                           fqname=item.fqname,
                            data_text=Markup(item.content._render_data_highlight()),
     )
 
             last_rev = rev_ids[-1]
     return render_template('meta.html',
                            item=item, item_name=item.name,
+                           fqname=item.fqname,
                            rev=item.rev,
                            contenttype=item.contenttype,
                            first_rev_id=first_rev,
 @frontend.route('/+content/<itemname:item_name>', defaults=dict(rev=CURRENT))
 def content_item(item_name, rev):
     """ same as show_item, but we only show the content """
-    item_displayed.send(app._get_current_object(),
-                        item_name=item_name)
+    item_displayed.send(app, item_name=item_name)
     try:
         item = Item.create(item_name, rev_id=rev)
     except AccessDenied:
     target = RequiredText.using(label=L_('Target')).with_properties(placeholder=L_("The name of the target item"))
 
 
+class ValidRevert(Validator):
+    """
+    Validator for a valid revert form.
+    """
+    invalid_name_msg = ''
+
+    def validate(self, element, state):
+        """
+        Check whether the names present in the previous meta are not taken by some other item.
+        """
+        try:
+            validate_name(state['meta'], state['meta'].get(ITEMID))
+            return True
+        except NameNotValidError as e:
+            self.invalid_name_msg = _(e)
+            return self.note_error(element, state, 'invalid_name_msg')
+
+
 class RevertItemForm(BaseChangeForm):
     name = 'revert_item'
+    validators = [ValidRevert()]
 
 
 class DeleteItemForm(BaseChangeForm):
     elif request.method == 'POST':
         form = RevertItemForm.from_flat(request.form)
         TextCha(form).amend_form()
-        if form.validate():
+        state = dict(fqname=item.fqname, meta=dict(item.meta))
+        if form.validate(state):
             item.revert(form['comment'])
             return redirect(url_for_item(item_name))
     return render_template(item.revert_template,
-                           item=item, item_name=item_name,
+                           item=item, fqname=item.fqname,
                            rev_id=rev,
                            form=form,
     )
         item = Item.create(item_name)
     except AccessDenied:
         abort(403)
-    if not flaskg.user.may.write(item_name):
+    if not flaskg.user.may.write(item.fqname):
         abort(403)
     if isinstance(item, NonExistent):
         abort(404, item_name)
             target = form['target'].value
             comment = form['comment'].value
             try:
+                fqname = CompositeName(item.fqname.namespace, item.fqname.field, target)
                 item.rename(target, comment)
-                return redirect(url_for_item(target))
+                return redirect(url_for_item(fqname))
             except NameNotUniqueError as e:
                 flash(str(e), "error")
     return render_template(item.rename_template,
                            item=item, item_name=item_name,
+                           fqname=item.fqname,
                            form=form,
     )
 
         item = Item.create(item_name)
     except AccessDenied:
         abort(403)
-    if not flaskg.user.may.write(item_name):
+    if not flaskg.user.may.write(item.fqname):
         abort(403)
     if isinstance(item, NonExistent):
         abort(404, item_name)
             return redirect(url_for_item(item_name))
     return render_template(item.delete_template,
                            item=item, item_name=item_name,
+                           fqname=split_fqname(item_name),
                            form=form,
     )
 
         item = Item.create(item_name, rev_id=_rev)
     except AccessDenied:
         abort(403)
-    if not flaskg.user.may.destroy(item_name):
+    fqname = item.fqname
+    if not flaskg.user.may.destroy(fqname):
         abort(403)
     if isinstance(item, NonExistent):
-        abort(404, item_name)
+        abort(404, fqname.fullname)
     if request.method in ['GET', 'HEAD']:
         form = DestroyItemForm.from_defaults()
         TextCha(form).amend_form()
                 item.destroy(comment=comment, destroy_item=destroy_item)
             except AccessDenied:
                 abort(403)
-            return redirect(url_for_item(item_name))
+            return redirect(url_for_item(fqname.fullname))
     return render_template(item.destroy_template,
                            item=item, item_name=item_name,
+                           fqname=fqname,
                            rev_id=rev,
                            form=form,
     )
         item = Item.create(item_name)
         revid, size = item.modify({}, data, contenttype_guessed=contenttype)
         item_modified.send(app._get_current_object(),
-                           item_name=item_name)
+                           item_name=item_name, action=ACTION_SAVE)
         return jsonify(name=subitem_name,
                        size=size,
                        url=url_for('.show_item', item_name=item_name, rev=revid),
 
     dirs, files = item.get_index(startswith, selected_groups)
     # index = sorted(index, key=lambda e: e.relname.lower())
-
+    fqname = item.fqname
+    if fqname.value == NAMESPACE_ALL:
+        fqname = CompositeName(NAMESPACE_ALL, NAME_EXACT, u'')
     item_names = item_name.split(u'/')
     return render_template(item.index_template,
                            item_names=item_names,
                            item_name=item_name,
+                           fqname=fqname,
                            files=files,
                            dirs=dirs,
                            initials=initials,
                            startswith=startswith,
                            form=form,
+                           title_name=_(u'Global Index'),
     )
 
 
     return render_template('link_list_no_item_panel.html',
                            title_name=_(u'My Changes'),
                            headline=_(u'My Changes'),
-                           item_names=my_changes
+                           fq_names=my_changes
     )
 
 
 def _mychanges(userid):
     """
-    Returns a list with all names of items which user userid has contributed to.
+    Returns a list with all fqnames of items which user userid has contributed to.
 
     :param userid: user itemid
     :type userid: unicode
     """
     q = And([Term(WIKINAME, app.cfg.interwikiname),
              Term(USERID, userid)])
-    revs = flaskg.storage.search(q, idx_name=ALL_REVS)
-    return [rev.name for rev in revs]
+    revs = flaskg.storage.search(q, idx_name=ALL_REVS, limit=None)
+    fq_names = {fq_name for rev in revs for fq_name in rev.fqnames}
+    return fq_names
 
 
 @frontend.route('/+refs/<itemname:item_name>')
     backrefs = _backrefs(item_name)
     return render_template('refs.html',
                            item_name=item_name,
-                           refs=refs,
+                           fqname=split_fqname(item_name),
+                           refs=split_fqname_list(refs),
                            backrefs=backrefs
     )
 
     refs = _forwardrefs(item_name)
     return render_template('link_list_item_panel.html',
                            item_name=item_name,
+                           fqname=split_fqname(item_name),
                            headline=_(u"Items that are referred by '%(item_name)s'", item_name=item_name),
-                           item_names=refs
+                           fq_names=split_fqname_list(refs),
     )
 
 
     :type item_name: unicode
     :returns: the list of all items which are referenced from this item
     """
-    q = {WIKINAME: app.cfg.interwikiname,
-         NAME_EXACT: item_name,
-        }
+    fqname = split_fqname(item_name)
+    q = fqname.query
+    q[WIKINAME] = app.cfg.interwikiname
     rev = flaskg.storage.document(**q)
     if rev is None:
         refs = []
     else:
         refs = rev.meta.get(ITEMLINKS, []) + rev.meta.get(ITEMTRANSCLUSIONS, [])
-    return refs
+    return set(refs)
 
 
 @frontend.route('/+backrefs/<itemname:item_name>')
     :type item_name: unicode
     :returns: a page with all the items which link or transclude item_name
     """
+    try:
+        item = Item.create(item_name)
+    except AccessDenied:
+        abort(403)
     refs_here = _backrefs(item_name)
     return render_template('link_list_item_panel.html',
+                           item=item,
                            item_name=item_name,
+                           fqname=split_fqname(item_name),
                            headline=_(u"Items which refer to '%(item_name)s'", item_name=item_name),
-                           item_names=refs_here
+                           fq_names=refs_here,
     )
 
 
 def _backrefs(item_name):
     """
-    Returns a list with all names of items which ref item_name
+    Returns a list with all names of items which ref fq_name
 
     :param item_name: the name of the item transcluded or linked
     :type item_name: unicode
-    :returns: the list of all items which ref item_name
+    :returns: the list of all items which ref fq_name
     """
     q = And([Term(WIKINAME, app.cfg.interwikiname),
              Or([Term(ITEMTRANSCLUSIONS, item_name), Term(ITEMLINKS, item_name)])])
     revs = flaskg.storage.search(q)
-    return [rev.name for rev in revs]
+    return set([fqname for rev in revs for fqname in rev.fqnames])
 
 
 @frontend.route('/+history/<itemname:item_name>')
 def history(item_name):
+    fqname = split_fqname(item_name)
     offset = request.values.get('offset', 0)
     offset = max(int(offset), 0)
     bookmark_time = int(request.values.get('bookmark', 0))
         results_per_page = flaskg.user.results_per_page
     else:
         results_per_page = app.cfg.results_per_page
-    terms = [Term(WIKINAME, app.cfg.interwikiname), Term(NAME_EXACT, item_name), ]
+    terms = [Term(WIKINAME, app.cfg.interwikiname), ]
+    terms.extend(Term(term, value) for term, value in fqname.query.iteritems())
     if bookmark_time:
         terms.append(DateRange(MTIME, start=datetime.utcfromtimestamp(bookmark_time), end=None))
     query = And(terms)
     # it would be better to use search_page (and an appropriate limit, if needed)
     revs = flaskg.storage.search(query, idx_name=ALL_REVS, sortedby=[MTIME], reverse=True, limit=None)
     # get rid of the content value to save potentially big amounts of memory:
-    history = [dict((k, v) for k, v in rev.meta.iteritems() if k != CONTENT) for rev in revs]
+    history = []
+    for rev in revs:
+        entry = dict(rev.meta)
+        entry[FQNAME] = rev.fqname
+        history.append(entry)
     history_page = util.getPageContent(history, offset, results_per_page)
     return render_template('history.html',
+                           fqname=fqname,
                            item_name=item_name,  # XXX no item here
                            history_page=history_page,
                            bookmark_time=bookmark_time,
     )
 
 
-@frontend.route('/+history')
-def global_history():
+@frontend.route('/<namespace>/+history')
+@frontend.route('/+history', defaults=dict(namespace=NAMESPACE_DEFAULT), methods=['GET'])
+def global_history(namespace):
     all_revs = bool(request.values.get('all'))
     idx_name = ALL_REVS if all_revs else LATEST_REVS
-    query = Term(WIKINAME, app.cfg.interwikiname)
+    terms = [Term(WIKINAME, app.cfg.interwikiname)]
+    fqname = CompositeName(NAMESPACE_ALL, NAME_EXACT, u'')
+    if namespace != NAMESPACE_ALL:
+        terms.append(Term(NAMESPACE, namespace))
+        fqname = split_fqname(namespace)
     bookmark_time = flaskg.user.bookmark
     if bookmark_time is not None:
-        query = And([query, DateRange(MTIME, start=datetime.utcfromtimestamp(bookmark_time), end=None)])
+        terms.append(DateRange(MTIME, start=datetime.utcfromtimestamp(bookmark_time), end=None))
+    query = And(terms)
     revs = flaskg.storage.search(query, idx_name=idx_name, sortedby=[MTIME], reverse=True, limit=1000)
     # Group by date
     history = []
                            history=history,
                            current_timestamp=current_timestamp,
                            bookmark_time=bookmark_time,
+                           fqname=fqname,
     )
 
 
 def _compute_item_sets():
     """
-    compute sets of existing, linked, transcluded and no-revision item names
+    compute sets of existing, linked, transcluded and no-revision item fqnames
     """
     linked = set()
     transcluded = set()
     existing = set()
     revs = flaskg.storage.documents(wikiname=app.cfg.interwikiname)
     for rev in revs:
-        existing.add(rev.name)
+        existing |= set(rev.fqnames)
         linked.update(rev.meta.get(ITEMLINKS, []))
         transcluded.update(rev.meta.get(ITEMTRANSCLUSIONS, []))
-    return existing, linked, transcluded
+    return existing, set(split_fqname_list(linked)), set(split_fqname_list(transcluded))
+
+
+def split_fqname_list(names):
+    """
+    Converts a list of names to a list of fqnames.
+    """
+    return [split_fqname(name) for name in names]
 
 
 @frontend.route('/+wanteds')
     return render_template('link_list_no_item_panel.html',
                            headline=_(u'Wanted Items'),
                            title_name=title_name,
-                           item_names=wanteds)
+                           fq_names=wanteds)
 
 
 @frontend.route('/+orphans')
     return render_template('link_list_no_item_panel.html',
                            title_name=title_name,
                            headline=_(u'Orphaned Items'),
-                           item_names=orphans)
+                           fq_names=orphans)
 
 
 @frontend.route('/+quicklink/<itemname:item_name>')
     u = flaskg.user
     cfg = app.cfg
     msg = None
+    try:
+        item = Item.create(item_name)
+    except AccessDenied:
+        abort(403)
+    if isinstance(item, NonExistent):
+        abort(404)
     if not u.valid:
         msg = _("You must login to use this action: %(action)s.", action="subscribe/unsubscribe"), "error"
     elif not u.may.read(item_name):
         msg = _("You are not allowed to subscribe to an item you may not read."), "error"
-    elif u.is_subscribed_to([item_name]):
+    elif u.is_subscribed_to(item):
         # Try to unsubscribe
-        if not u.unsubscribe(item_name):
-            msg = _("Can't remove regular expression subscription!") + u' ' + \
-                _("Edit the subscription regular expressions in your settings."), "error"
+        if not u.unsubscribe(ITEMID, item.meta[ITEMID]):
+            msg = _(
+                "Can't remove the subscription! You are subscribed to this page in some other way.") + u' ' + _(
+                "Please edit the subscription in your settings."), "error"
     else:
         # Try to subscribe
-        if not u.subscribe(item_name):
+        if not u.subscribe(ITEMID, item.meta[ITEMID]):
             msg = _('You could not get subscribed to this item.'), "error"
     if msg:
         flash(*msg)
     submit_label = L_('Save')
 
 
+class UserSettingsSubscriptionsForm(Form):
+    name = 'usersettings_subscriptions'
+    subscriptions = Subscriptions
+    submit_label = L_('Save')
+
+
 @frontend.route('/+usersettings', methods=['GET', 'POST'])
 def usersettings():
     # TODO use ?next=next_location check if target is in the wiki and not outside domain
         ui=UserSettingsUIForm,
         navigation=UserSettingsNavigationForm,
         options=UserSettingsOptionsForm,
+        subscriptions=UserSettingsSubscriptionsForm,
     )
     forms = dict()
 
     rev_ids = [CURRENT]  # XXX TODO we need a reverse sorted list
     return render_template(item.diff_template,
                            item=item, item_name=item.name,
+                           fqname=item.fqname,
                            diff_html=Markup(item.content._render_data_diff(oldrev, newrev)),
                            rev=item.rev,
                            first_rev_id=rev_ids[0],
     """
     list similar item names
     """
-    start, end, matches = findMatches(item_name)
+    try:
+        item = Item.create(item_name)
+    except AccessDenied:
+        abort(403)
+    fq_name = split_fqname(item_name)
+    start, end, matches = findMatches(fq_name)
     keys = sorted(matches.keys())
     # TODO later we could add titles for the misc ranks:
     # 8 item_name
     # 3 "{0}...{1}".format(start, end)
     # 1 "{0}...".format(start)
     # 2 "...{1}".format(end)
-    item_names = []
+    fq_names = []
     for wanted_rank in [8, 4, 3, 1, 2, ]:
-        for name in keys:
-            rank = matches[name]
+        for fqname in keys:
+            rank = matches[fqname]
             if rank == wanted_rank:
-                item_names.append(name)
+                fq_names.append(fqname)
     return render_template("link_list_item_panel.html",
                            headline=_("Items with similar names to '%(item_name)s'", item_name=item_name),
+                           item=item,
                            item_name=item_name,  # XXX no item
-                           item_names=item_names)
+                           fqname=split_fqname(item_name),
+                           fq_names=fq_names)
 
 
-def findMatches(item_name, s_re=None, e_re=None):
+def findMatches(fq_name, s_re=None, e_re=None):
     """ Find similar item names.
 
-    :param item_name: name to match
+    :param fq_name: fqname to match
     :param s_re: start re for wiki matching
     :param e_re: end re for wiki matching
     :rtype: tuple
     :returns: start word, end word, matches dict
     """
-    item_names = [rev.name for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)
-                  if rev.name is not None]
-    if item_name in item_names:
-        item_names.remove(item_name)
+
+    fq_names = [fqname for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname) for fqname in rev.fqnames
+                if rev.fqname is not None]
+    if fq_name in fq_names:
+        fq_names.remove(fq_name)
     # Get matches using wiki way, start and end of word
-    start, end, matches = wikiMatches(item_name, item_names, start_re=s_re, end_re=e_re)
+    start, end, matches = wikiMatches(fq_name, fq_names, start_re=s_re, end_re=e_re)
     # Get the best 10 close matches
     close_matches = {}
     found = 0
-    for name in closeMatches(item_name, item_names):
-        if name not in matches:
-            # Skip names already in matches
-            close_matches[name] = 8
+    for fqname in closeMatches(fq_name, fq_names):
+        if fqname not in matches:
+            # Skip fqname already in matches
+            close_matches[fqname] = 8
             found += 1
             # Stop after 10 matches
             if found == 10:
     return start, end, matches
 
 
-def wikiMatches(item_name, item_names, start_re=None, end_re=None):
+def wikiMatches(fq_name, fq_names, start_re=None, end_re=None):
     """
-    Get item names that starts or ends with same word as this item name.
+    Get fqnames that starts or ends with same word as this fq_name.
 
     Matches are ranked like this:
-        4 - item is subitem of item_name
+        4 - item is subitem of fq_name
         3 - match both start and end
         2 - match end
         1 - match start
 
-    :param item_name: item name to match
-    :param item_names: list of item names
+    :param fq_name: fqname to match
+    :param fq_names: list of fqnames
     :param start_re: start word re (compile regex)
     :param end_re: end word re (compile regex)
     :rtype: tuple
 
     # If we don't get results with wiki words matching, fall back to
     # simple first word and last word, using spaces.
+    item_name = fq_name.value
     words = item_name.split()
     match = start_re.match(item_name)
     if match:
     subitem = item_name + '/'
 
     # Find any matching item names and rank by type of match
-    for name in item_names:
+    for fqname in fq_names:
+        name = fqname.value
         if name.startswith(subitem):
-            matches[name] = 4
+            matches[fqname] = 4
         else:
             if name.startswith(start):
-                matches[name] = 1
+                matches[fqname] = 1
             if name.endswith(end):
-                matches[name] = matches.get(name, 0) + 2
+                matches[fqname] = matches.get(name, 0) + 2
 
     return start, end, matches
 
 
-def closeMatches(item_name, item_names):
+def closeMatches(fq_name, fq_names):
     """ Get close matches.
 
-    Return all matching item names with rank above cutoff value.
+    Return all matching fqnames with rank above cutoff value.
 
-    :param item_name: item name to match
-    :param item_names: list of item names
+    :param fq_name: fqname to match
+    :param fq_names: list of fqnames
     :rtype: list
     :returns: list of matching item names, sorted by rank
     """
-    if not item_names:
+    if not fq_names:
         return []
     # Match using case insensitive matching
-    # Make mapping from lower item names to item names.
+    # Make mapping from lower item names to fqnames.
     lower = {}
-    for name in item_names:
+    for fqname in fq_names:
+        name = fqname.value
         key = name.lower()
         if key in lower:
-            lower[key].append(name)
+            lower[key].append(fqname)
         else:
-            lower[key] = [name]
-
+            lower[key] = [fqname]
     # Get all close matches
+    item_name = fq_name.value
     all_matches = difflib.get_close_matches(item_name.lower(), lower.keys(),
                                             n=len(lower), cutoff=0.6)
 
     sitemap view shows item link structure, relative to current item
     """
     # first check if this item exists
-    if not flaskg.storage[item_name]:
+    fq_name = split_fqname(item_name)
+    if not flaskg.storage.get_item(**fq_name.query):
         abort(404, item_name)
-    sitemap = NestedItemListBuilder().recurse_build([item_name])
+    sitemap = NestedItemListBuilder().recurse_build([fq_name])
     del sitemap[0]  # don't show current item name as sole toplevel list item
     return render_template('sitemap.html',
                            item_name=item_name,  # XXX no item
                            sitemap=sitemap,
+                           fqname=fq_name,
     )
 
 
         self.numnodes = 0
         self.maxnodes = 35  # approx. max count of nodes, not strict
 
-    def recurse_build(self, names):
+    def recurse_build(self, fq_names):
         result = []
         if self.numnodes < self.maxnodes:
-            for name in names:
-                self.children.add(name)
-                result.append(name)
+            for fq_name in fq_names:
+                self.children.add(fq_name)
+                result.append(fq_name)
                 self.numnodes += 1
-                childs = self.childs(name)
+                childs = self.childs(fq_name)
                 if childs:
                     childs = self.recurse_build(childs)
                     result.append(childs)
         return result
 
-    def childs(self, name):
+    def childs(self, fq_name):
         # does not recurse
         try:
-            item = flaskg.storage[name]
+            item = flaskg.storage.get_item(**fq_name.query)
             rev = item[CURRENT]
         except (AccessDenied, KeyError):
             return []
-        itemlinks = rev.meta.get(ITEMLINKS, [])
+        itemlinks = set(split_fqname_list(rev.meta.get(ITEMLINKS, [])))
         return [child for child in itemlinks if self.is_ok(child)]
 
     def is_ok(self, child):
         if child not in self.children:
             if not flaskg.user.may.read(child):
                 return False
-            if flaskg.storage.has_item(child):
+            if flaskg.storage.get_item(**child.query):
                 self.children.add(child)
                 return True
         return False
 
 
-@frontend.route('/+tags')
-def global_tags():
+@frontend.route('/+tags', defaults=dict(namespace=NAMESPACE_DEFAULT), methods=['GET'])
+@frontend.route('/<namespace>/+tags')
+def global_tags(namespace):
     """
     show a list or tag cloud of all tags in this wiki
     """
     title_name = _(u'All tags in this wiki')
-    revs = flaskg.storage.documents(wikiname=app.cfg.interwikiname)
+    query = {WIKINAME: app.cfg.interwikiname}
+    fqname = CompositeName(NAMESPACE_ALL, NAME_EXACT, u'')
+    if namespace != NAMESPACE_ALL:
+        query[NAMESPACE] = namespace
+        fqname = split_fqname(namespace)
+    revs = flaskg.storage.documents(**query)
     tags_counts = {}
     for rev in revs:
         tags = rev.meta.get(TAGS, [])
     return render_template("global_tags.html",
                            headline=_("All tags in this wiki"),
                            title_name=title_name,
+                           fqname=fqname,
                            tags=tags)
 
 
-@frontend.route('/+tags/<itemname:tag>')
-def tagged_items(tag):
+@frontend.route('/+tags/<itemname:tag>', defaults=dict(namespace=NAMESPACE_DEFAULT), methods=['GET'])
+@frontend.route('/<namespace>/+tags/<itemname:tag>')
+def tagged_items(tag, namespace):
     """
-    show all items' names that have tag <tag>
+    show all items' names that have tag <tag> and belong to namespace <namespace>
     """
-    query = And([Term(WIKINAME, app.cfg.interwikiname), Term(TAGS, tag), ])
-    revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
-    item_names = [rev.name for rev in revs]
+    terms = And([Term(WIKINAME, app.cfg.interwikiname), Term(TAGS, tag), ])
+    if namespace != NAMESPACE_ALL:
+        terms = And([terms, Term(NAMESPACE, namespace), ])
+    query = And(terms)
+    revs = flaskg.storage.search(query, limit=None)
+    fq_names = [fq_name for rev in revs for fq_name in rev.fqnames]
     return render_template("link_list_no_item_panel.html",
                            headline=_("Items tagged with %(tag)s", tag=tag),
                            item_name=tag,
-                           item_names=item_names)
+                           fq_names=fq_names)
 
 
 @frontend.route('/+template/<path:filename>')

File MoinMoin/apps/misc/templates/misc/sitemap.xml

 <?xml version="1.0" encoding="UTF-8"?>
 <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
-{% for item_name, lastmod, changefreq, priority in sitemap -%}
+{% for fq_name, lastmod, changefreq, priority in sitemap -%}
 <url>
-<loc>{{ url_for('frontend.show_item', item_name=item_name, _external=True)|e }}</loc>
+<loc>{{ url_for('frontend.show_item', item_name=fq_name, _external=True)|e }}</loc>
 <lastmod>{{ lastmod }}</lastmod>
 <changefreq>{{ changefreq }}</changefreq>
 <priority>{{ priority }}</priority>

File MoinMoin/apps/misc/templates/misc/urls_names.txt

-{% for item_name in item_names -%}
-{{ url_for('frontend.show_item', item_name=item_name, _external=True) }} {{ item_name }}
+{% for fq_name in fq_names|sort(attribute='value') -%}
+{{ url_for('frontend.show_item', item_name=fq_name, _external=True) }} {{ fq_name.value }}
 {% endfor %}

File MoinMoin/apps/misc/views.py

 from flask import current_app as app
 from flask import g as flaskg
 
+from whoosh.query import Term, Or, And
+
 from MoinMoin.apps.misc import misc
 
-from MoinMoin.constants.keys import MTIME
+from MoinMoin.constants.keys import MTIME, NAME_EXACT, NAMESPACE
 from MoinMoin.themes import render_template
 
 
 
     sitemap = []
     for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname):
-        name = rev.name
+        fqnames = rev.fqnames
         mtime = rev.meta[MTIME]
         # these are the content items:
         changefreq = "daily"
         priority = "0.5"
-        sitemap.append((name, format_timestamp(mtime), changefreq, priority))
-    # add an entry for root url
-    root_item = app.cfg.item_root
-    revs = list(flaskg.storage.documents(wikiname=app.cfg.interwikiname, name=root_item))
-    if revs:
-        mtime = revs[0].meta[MTIME]
-        sitemap.append((u'', format_timestamp(mtime), "hourly", "1.0"))
+        sitemap += [((fqname, format_timestamp(mtime), changefreq, priority)) for fqname in fqnames]
+    # add entries for root urls
+    root_mapping = [(namespace, app.cfg.root_mapping.get(namespace, app.cfg.default_root)) for namespace, _ in app.cfg.namespace_mapping]
+    query = Or([And([Term(NAME_EXACT, root), Term(NAMESPACE, namespace)]) for namespace, root in root_mapping])
+    for rev in flaskg.storage.search(q=query):
+        mtime = rev.meta[MTIME]
+        sitemap.append((rev.meta[NAMESPACE], format_timestamp(mtime), "hourly", "1.0"))
     sitemap.sort()
     content = render_template('misc/sitemap.xml', sitemap=sitemap)
     return Response(content, mimetype='text/xml')
     See: http://usemod.com/cgi-bin/mb.pl?SisterSitesImplementationGuide
     """
     # XXX we currently also get deleted items, fix this
-    item_names = sorted([rev.name for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)])
-    content = render_template('misc/urls_names.txt', item_names=item_names)
+    fq_names = []
+    for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname):
+        fq_names += [fqname for fqname in rev.fqnames]
+    content = render_template('misc/urls_names.txt', fq_names=fq_names)
     return Response(content, mimetype='text/plain')

File MoinMoin/config/default.py

 from MoinMoin import error
 from MoinMoin.constants.rights import ACL_RIGHTS_CONTENTS, ACL_RIGHTS_FUNCTIONS
 from MoinMoin.constants.keys import *
+from MoinMoin.constants.namespaces import NAMESPACE_DEFAULT
 from MoinMoin import datastruct
 from MoinMoin.auth import MoinAuth
 from MoinMoin.util import plugins
         decode_names = (
             'sitename', 'interwikiname', 'user_homewiki',
             'interwiki_preferred',
-            'item_root', 'item_license', 'mail_from',
+            'item_license', 'mail_from',
             'item_dict_regex', 'item_group_regex',
             'acl_functions', 'supplementation_item_names', 'html_pagetitle',
             'theme_default', 'timezone_default', 'locale_default',
     )),
     # ==========================================================================
     'items': ('Special Item Names', None, (
-        ('item_root', u'Home', "Name of the root item (aka 'front page'). [Unicode]"),
+        ('default_root', u'Home', "Default root, use this value in case no match is found in root_mapping. [Unicode]"),
+        ('root_mapping', {}, "mapping of namespaces to item_roots."),
 
         # the following regexes should match the complete name when used in free text
         # the group 'all' shall match all, while the group 'key' shall match the key only
             DISABLED: False,
             BOOKMARKS: {},
             QUICKLINKS: [],
-            SUBSCRIBED_ITEMS: [],
+            SUBSCRIPTIONS: [],
             EMAIL_SUBSCRIBED_EVENTS: [
                 # XXX PageChangedEvent.__name__
                 # XXX PageRenamedEvent.__name__

File MoinMoin/constants/keys.py

 # needs more precise name / use case:
 SOMEDICT = u"somedict"
 
+# TODO review plural constants
 CONTENTTYPE = u"contenttype"
 ITEMTYPE = u"itemtype"
 SIZE = u"size"
 EXTRA = u"extra"
 COMMENT = u"comment"
 SUMMARY = u"summary"
+TRASH = u"trash"
 
 # we need a specific hash algorithm to store hashes of revision data into meta
 # data. meta[HASH_ALGORITHM] = hash(rev_data, HASH_ALGORITHM)
 LOCALE = u"locale"
 TIMEZONE = u"timezone"
 ENC_PASSWORD = u"enc_password"
-SUBSCRIBED_ITEMS = u"subscribed_items"
+SUBSCRIPTIONS = u"subscriptions"
+SUBSCRIPTION_IDS = u"subscription_ids"
+SUBSCRIPTION_PATTERNS = u"subscription_patterns"
 BOOKMARKS = u"bookmarks"
 QUICKLINKS = u"quicklinks"
 SESSION_KEY = u"session_key"
 EMAIL_SUBSCRIBED_EVENTS = u"email_subscribed_events"
 DISABLED = u"disabled"
 EMAIL_UNVALIDATED = u"email_unvalidated"
+NAMERE = u"namere"
+NAMEPREFIX = u"nameprefix"
 
 # in which backend is some revision stored?
 BACKENDNAME = u"backendname"
     # User objects proxy these attributes of the UserProfile objects:
     NAME, DISABLED, ITEMID, DISPLAY_NAME, ENC_PASSWORD, EMAIL, OPENID,
     MAILTO_AUTHOR, SHOW_COMMENTS, RESULTS_PER_PAGE, EDIT_ON_DOUBLECLICK, SCROLL_PAGE_AFTER_EDIT,
-    EDIT_ROWS, THEME_NAME, LOCALE, TIMEZONE, SUBSCRIBED_ITEMS, QUICKLINKS,
-    CSS_URL,
+    EDIT_ROWS, THEME_NAME, LOCALE, TIMEZONE, SUBSCRIPTIONS, QUICKLINKS, CSS_URL,
 ]
 
 # keys for blog homepages
 # index names
 LATEST_REVS = 'latest_revs'
 ALL_REVS = 'all_revs'
+
+# values for ACTION key
+ACTION_SAVE = u"SAVE"
+ACTION_REVERT = u"REVERT"
+ACTION_TRASH = u"TRASH"
+ACTION_COPY = u"COPY"
+ACTION_RENAME = u"RENAME"
+
+# defaul LOCALE key value
+DEFAULT_LOCALE = u"en"
+
+# key for composite name
+FQNAME = u'fqname'
+# Values that FIELD can take in the composite name: [NAMESPACE/][@FIELD/]NAME
+FIELDS = [
+    NAME_EXACT, ITEMID, REVID, TAGS, USERID, ITEMLINKS, ITEMTRANSCLUSIONS
+]
+# Fields that can be used as a unique identifier.
+UFIELDS = [
+    NAME_EXACT, ITEMID, REVID,
+]
+# Unique fields that are stored as list.
+UFIELDS_TYPELIST = [NAME_EXACT, ]

File MoinMoin/constants/namespaces.py

 
 NAMESPACE_DEFAULT = u''
 NAMESPACE_USERPROFILES = u'userprofiles'
+NAMESPACE_ALL = u'all'  # An identifier namespace which acts like a union of all the namespaces.
+NAMESPACES_IDENTIFIER = [NAMESPACE_ALL, ]  # List containing all the identifier namespaces.

File MoinMoin/converter/nonexistent_in.py

         return cls()
 
     def __call__(self, rev, contenttype=None, arguments=None):
-        item_name = rev.item.name
+        item_name = rev.item.fqname.value
         attrib = {
             xlink.href: Iri(scheme='wiki', authority='', path='/' + item_name, query='do=modify'),
         }

File MoinMoin/forms.py

 from whoosh.query import Term, Or, Not, And
 
 from flask import g as flaskg
+from flask import current_app as app
 
 from MoinMoin.constants.forms import *
-from MoinMoin.constants.keys import ITEMID, NAME, LATEST_REVS
+from MoinMoin.constants.keys import ITEMID, NAME, LATEST_REVS, NAMESPACE, FQNAME
+from MoinMoin.constants.namespaces import NAMESPACES_IDENTIFIER
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.util.forms import FileStorage
+from MoinMoin.storage.middleware.validation import uuid_validator
+
+COLS = 60
+ROWS = 10
 
 
 class Enum(BaseEnum):
 RequiredMultilineText = MultilineText.validated_by(Present())
 
 
+class NameNotValidError(ValueError):
+    """
+    The name is not valid.
+    """
+
+
+def validate_name(meta, itemid):
+    """
+    Check whether the names are valid.
+    Will just return, if they are valid, will raise a NameNotValidError if not.
+    """
+    names = meta.get(NAME)
+    current_namespace = meta.get(NAMESPACE)
+    if current_namespace is None:
+        raise NameNotValidError(L_("No namespace field in the meta."))
+    namespaces = [namespace.rstrip('/') for namespace, _ in app.cfg.namespace_mapping]
+
+    if len(names) != len(set(names)):
+        raise NameNotValidError(L_("The names in the name list must be unique."))
+    # Item names must not start with '@' or '+', '@something' denotes a field where as '+something' denotes a view.
+    invalid_names = [name for name in names if name.startswith(('@', '+'))]
+    if invalid_names:
+        raise NameNotValidError(L_("Item names (%(invalid_names)s) must not start with '@' or '+'", invalid_names=", ".join(invalid_names)))
+
+    namespaces = namespaces + NAMESPACES_IDENTIFIER  # Also dont allow item names to match with identifier namespaces.
+    # Item names must not match with existing namespaces.
+    invalid_names = [name for name in names if name.split('/', 1)[0] in namespaces]
+    if invalid_names:
+        raise NameNotValidError(L_("Item names (%(invalid_names)s) must not match with existing namespaces.", invalid_names=", ".join(invalid_names)))
+    query = And([Or([Term(NAME, name) for name in names]), Term(NAMESPACE, current_namespace)])
+    # There should be not item existing with the same name.
+    if itemid is not None:
+        query = And([query, Not(Term(ITEMID, itemid))])  # search for items except the current item.
+    with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher:
+        results = searcher.search(query)
+        duplicate_names = {name for result in results for name in result[NAME] if name in names}
+        if duplicate_names:
+            raise NameNotValidError(L_("Item(s) named %(duplicate_names)s already exist.", duplicate_names=", ".join(duplicate_names)))
+
+
+class ValidName(Validator):
+    """Validator for Name
+    """
+    invalid_name_msg = ""
+
+    def validate(self, element, state):
+        # Make sure that the other meta is valid before validating the name.
+        # TODO Change/Make sure that the below statement holds good.
+        try:
+            if not element.parent.parent['extra_meta_text'].valid:
+                return False
+        except AttributeError:
+            pass
+        try:
+            validate_name(state['meta'], state[ITEMID])
+        except NameNotValidError as e:
+            self.invalid_name_msg = _(e)
+            return self.note_error(element, state, 'invalid_name_msg')
+        return True
+
+
 class ValidJSON(Validator):
     """Validator for JSON
     """
     invalid_json_msg = L_('Invalid JSON.')
-    invalid_name_msg = ""
+    invalid_itemid_msg = L_('Itemid not a proper UUID')
+    invalid_namespace_msg = ''
 
-    def validname(self, meta, name, itemid):
-        names = meta.get(NAME)
-        if names is None:
-            self.invalid_name_msg = L_("No name field in the JSON meta.")
+    def validitemid(self, itemid):
+        if not itemid:
+            self.invalid_itemid_msg = L_("No ITEMID field")
             return False
-        if len(names) != len(set(names)):
-            self.invalid_name_msg = L_("The names in the JSON name list must be unique.")
+        return uuid_validator(String(itemid), None)
+
+    def validnamespace(self, current_namespace):
+        if current_namespace is None:
+            self.invalid_namespace_msg = L_("No namespace field in the meta.")
             return False
-        query = Or([Term(NAME, x) for x in names])
-        if itemid is not None:
-            query = And([query, Not(Term(ITEMID, itemid))])
-        duplicate_names = set()
-        with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher:
-            results = searcher.search(query)
-            for result in results:
-                duplicate_names |= set([x for x in result[NAME] if x in names])
-        if duplicate_names:
-            self.invalid_name_msg = L_("Item(s) named %(duplicate_names)s already exist.", duplicate_names=", ".join(duplicate_names))
+        namespaces = [namespace.rstrip('/') for namespace, _ in app.cfg.namespace_mapping]
+        if current_namespace not in namespaces:  # current_namespace must be an existing namespace.
+            self.invalid_namespace_msg = L_("%(_namespace)s is not a valid namespace.", _namespace=current_namespace)
             return False
         return True
 
             meta = json.loads(element.value)
         except:  # catch ANY exception that happens due to unserializing
             return self.note_error(element, state, 'invalid_json_msg')
-        if not self.validname(meta, state[NAME], state[ITEMID]):
-            return self.note_error(element, state, 'invalid_name_msg')
+        if not self.validnamespace(meta.get(NAMESPACE)):
+            return self.note_error(element, state, 'invalid_namespace_msg')
+        if state[FQNAME].field == ITEMID:
+            if not self.validitemid(meta.get(ITEMID, state[FQNAME].value)):
+                return self.note_error(element, state, 'invalid_itemid_msg')
         return True
 
 JSON = OptionalMultilineText.with_properties(lang='en', dir='ltr').validated_by(ValidJSON())
     def u(self):
         return self.separator.join(child.u for child in self)
 
+
+class SubscriptionsJoinedString(JoinedString):
+    """ A JoinedString that offers the list of children as value property and also
+    appends the name of the item to the end of ITEMID subscriptions.
+    """
+    @property
+    def value(self):
+        subscriptions = []
+        for child in self:
+            if child.value.startswith(ITEMID):
+                value = re.sub(r"\(.*\)", "", child.value)
+            else:
+                value = child.value
+            subscriptions.append(value)
+        return subscriptions
+
+    @property
+    def u(self):
+        subscriptions = []
+        for child in self:
+            if child.u.startswith(ITEMID):
+                value = re.sub(r"\(.*\)", "", child.u)
+                item = flaskg.storage.document(**{ITEMID: value.split(":")[1]})
+                try:
+                    name_ = item.meta['name'][0]
+                except IndexError:
+                    name_ = "This item doesn't exist"
+                value = "{0} ({1})".format(value, name_)
+            else:
+                value = child.u
+            subscriptions.append(value)
+        return self.separator.join(subscriptions)
+
+
 Tags = MyJoinedString.of(String).with_properties(widget=WIDGET_TEXT).using(
     label=L_('Tags'), optional=True, separator=', ', separator_regex=re.compile(r'\s*,\s*'))
 
 Names = MyJoinedString.of(String).with_properties(widget=WIDGET_TEXT).using(
-    label=L_('Names'), optional=True, separator=', ', separator_regex=re.compile(r'\s*,\s*'))
+    label=L_('Names'), optional=True, separator=', ', separator_regex=re.compile(r'\s*,\s*')).validated_by(ValidName())
+
+Subscriptions = SubscriptionsJoinedString.of(String).with_properties(
+    widget=WIDGET_MULTILINE_TEXT, rows=ROWS, cols=COLS).using(
+    label=L_('Subscriptions'), optional=True, separator='\n',
+    separator_regex=re.compile(r'[\r\n]+'))
 
 Search = Text.using(default=u'', optional=True).with_properties(widget=WIDGET_SEARCH, placeholder=L_("Search Query"))
 

File MoinMoin/i18n/__init__.py

 
 
 from babel import Locale
+from contextlib import contextmanager
 
-from flask import current_app, request
+from flask import current_app, request, _request_ctx_stack
 from flask import g as flaskg
 from flask.ext.babel import Babel, gettext, ngettext, lazy_gettext
 
     u = getattr(flaskg, 'user', None)
     if u and u.timezone is not None:
         return u.timezone
+
+
+# Original source is a patch to Flask Babel
+# https://github.com/lalinsky/flask-babel/commit/09ee1702c7129598bb202aa40a0e2e19f5414c24
+@contextmanager
+def force_locale(locale):
+    """Temporarily overrides the currently selected locale. Sometimes
+    it is useful to switch the current locale to different one, do
+    some tasks and then revert back to the original one. For example,
+    if the user uses German on the web site, but you want to send
+    them an email in English, you can use this function as a context
+    manager::
+
+        with force_locale('en_US'):
+            send_email(gettext('Hello!'), ...)
+    """
+    ctx = _request_ctx_stack.top
+    if ctx is None:
+        yield
+        return
+    babel = ctx.app.extensions['babel']
+    orig_locale_selector_func = babel.locale_selector_func
+    orig_attrs = {}
+    for key in ('babel_translations', 'babel_locale'):
+        orig_attrs[key] = getattr(ctx, key, None)
+    try:
+        babel.locale_selector_func = lambda: locale
+        for key in orig_attrs:
+            setattr(ctx, key, None)
+        yield
+    finally:
+        babel.locale_selector_func = orig_locale_selector_func
+        for key, value in orig_attrs.iteritems():
+            setattr(ctx, key, value)

File MoinMoin/i18n/_tests/test_i18n.py

 Test for i18n
 """
 
-from MoinMoin.i18n import get_locale, get_timezone
+import pytest
 
+from flask import Flask
+from flask.ext import babel
+
+from MoinMoin.i18n import get_locale, get_timezone, force_locale
 from MoinMoin.i18n import _, L_, N_
 
 
     assert result1 == 'text1'
     result2 = N_('text1', 'text2', 2)
     assert result2 == 'text2'
+
+
+def test_force_locale():
+    pytest.skip("This test needs to be run with --assert=reinterp or --assert=plain flag")
+    app = Flask(__name__)
+    b = babel.Babel(app)
+
+    @b.localeselector
+    def select_locale():
+        return 'de_DE'
+
+    with app.test_request_context():
+        assert str(babel.get_locale()) == 'de_DE'
+        with force_locale('en_US'):
+            assert str(babel.get_locale()) == 'en_US'
+        assert str(babel.get_locale()) == 'de_DE'

File MoinMoin/items/__init__.py

 from MoinMoin.i18n import L_
 from MoinMoin.themes import render_template
 from MoinMoin.util.mime import Type
-from MoinMoin.util.interwiki import url_for_item
+from MoinMoin.util.interwiki import url_for_item, split_fqname, get_fqname, CompositeName
 from MoinMoin.util.registry import RegistryBase
 from MoinMoin.util.clock import timed
-from MoinMoin.forms import RequiredText, OptionalText, JSON, Tags
+from MoinMoin.forms import RequiredText, OptionalText, JSON, Tags, Names
 from MoinMoin.constants.keys import (
     NAME, NAME_OLD, NAME_EXACT, WIKINAME, MTIME, ITEMTYPE,
     CONTENTTYPE, SIZE, ACTION, ADDRESS, HOSTNAME, USERID, COMMENT,
-    HASH_ALGORITHM, ITEMID, REVID, DATAID, CURRENT, PARENTID
+    HASH_ALGORITHM, ITEMID, REVID, DATAID, CURRENT, PARENTID, NAMESPACE,
+    UFIELDS_TYPELIST, UFIELDS, TRASH,
+    ACTION_SAVE, ACTION_REVERT, ACTION_TRASH, ACTION_RENAME
 )
+from MoinMoin.constants.namespaces import NAMESPACE_ALL
 from MoinMoin.constants.contenttypes import CHARSET, CONTENTTYPE_NONEXISTENT
 from MoinMoin.constants.itemtypes import (
     ITEMTYPE_NONEXISTENT, ITEMTYPE_USERPROFILE, ITEMTYPE_DEFAULT,
 )
+from MoinMoin.util.notifications import DESTROY_REV, DESTROY_ALL
 
 from .content import content_registry, Content, NonExistentContent, Draw
 
     """ if we have no stored Revision, we use this dummy """
     def __init__(self, item, itemtype=None, contenttype=None):
         self.item = item
+        fqname = item.fqname
         self.meta = {
             ITEMTYPE: itemtype or ITEMTYPE_NONEXISTENT,
             CONTENTTYPE: contenttype or CONTENTTYPE_NONEXISTENT
         }
         self.data = StringIO('')
         self.revid = None
-        if self.item:
-            self.meta[NAME] = [self.item.name]
+        if item:
+            self.meta[NAMESPACE] = fqname.namespace
+            if fqname.field in UFIELDS_TYPELIST:
+                if fqname.field == NAME_EXACT:
+                    self.meta[NAME] = [fqname.value]
+                else:
+                    self.meta[fqname.field] = [fqname.value]
+            else:
+                self.meta[fqname.field] = fqname.value
 
 
 class DummyItem(object):
     """ if we have no stored Item, we use this dummy """
-    def __init__(self, name):
-        self.name = name
+    def __init__(self, fqname):
+        self.fqname = fqname
 
     def list_revisions(self):
         return []  # same as an empty Item
         return True
 
 
-def get_storage_revision(name, itemtype=None, contenttype=None, rev_id=CURRENT, item=None):
+def get_storage_revision(fqname, itemtype=None, contenttype=None, rev_id=CURRENT, item=None):
     """
     Get a storage Revision.
 
     :itemtype and :contenttype are used when creating a DummyRev, where
     metadata is not available from the storage.
     """
+    rev_id = fqname.value if fqname.field == REVID else rev_id
     if 1:  # try:
         if item is None:
-            item = flaskg.storage[name]
+            item = flaskg.storage.get_item(**fqname.query)
         else:
-            name = item.name
+            if item.fqname:
+                fqname = item.fqname
     if not item:  # except NoSuchItemError:
-        logging.debug("No such item: {0!r}".format(name))
-        item = DummyItem(name)
+        logging.debug("No such item: {0!r}".format(fqname))
+        item = DummyItem(fqname)
         rev = DummyRev(item, itemtype, contenttype)
-        logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(name, contenttype))
+        logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(fqname, contenttype))
     else:
-        logging.debug("Got item: {0!r}".format(name))
+        logging.debug("Got item: {0!r}".format(fqname))
         try:
             rev = item.get_revision(rev_id)
         except KeyError:  # NoSuchRevisionError:
                 rev = item.get_revision(CURRENT)  # fall back to current revision
                 # XXX add some message about invalid revision
             except KeyError:  # NoSuchRevisionError:
-                logging.debug("Item {0!r} has no revisions.".format(name))
+                logging.debug("Item {0!r} has no revisions.".format(fqname))
                 rev = DummyRev(item, itemtype, contenttype)
-                logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(name, contenttype))
-        logging.debug("Got item {0!r}, revision: {1!r}".format(name, rev_id))
+                logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(fqname, contenttype))
+        logging.debug("Got item {0!r}, revision: {1!r}".format(fqname, rev_id))
     return rev
 
 
     # value, while an emtpy acl and no acl have different semantics
     #acl = OptionalText.using(label=L_('ACL')).with_properties(placeholder=L_("Access Control List"))
     summary = OptionalText.using(label=L_("Summary")).with_properties(placeholder=L_("One-line summary of the item"))
+    name = Names
     tags = Tags
 
 
     """
 
 
+class FieldNotUniqueError(ValueError):
+    """
+    The Field is not a UFIELD(unique Field).
+    Non unique fields can refer to more than one item.
+    """
+
+
 class Item(object):
     """ Highlevel (not storage) Item, wraps around a storage Revision"""
     # placeholder values for registry entry properties
         previously created Content instance is assigned to its content
         property.
         """
-        rev = get_storage_revision(name, itemtype, contenttype, rev_id, item)
+        fqname = split_fqname(name)
+        if fqname.field not in UFIELDS:  # Need a unique key to extract stored item.
+            raise FieldNotUniqueError("field {0} is not in UFIELDS".format(fqname.field))
+
+        rev = get_storage_revision(fqname, itemtype, contenttype, rev_id, item)
         contenttype = rev.meta.get(CONTENTTYPE) or contenttype
         logging.debug("Item {0!r}, got contenttype {1!r} from revision meta".format(name, contenttype))
         #logging.debug("Item %r, rev meta dict: %r" % (name, dict(rev.meta)))
         itemtype = rev.meta.get(ITEMTYPE) or itemtype or ITEMTYPE_DEFAULT
         logging.debug("Item {0!r}, got itemtype {1!r} from revision meta".format(name, itemtype))
 
-        item = item_registry.get(itemtype, name, rev=rev, content=content)
+        item = item_registry.get(itemtype, fqname, rev=rev, content=content)
         logging.debug("Item class {0!r} handles {1!r}".format(item.__class__, itemtype))
 
         content.item = item
-
         return item
 
-    def __init__(self, name, rev=None, content=None):
-        self.name = name
+    def __init__(self, fqname, rev=None, content=None):
+        self.fqname = fqname
         self.rev = rev
         self.content = content
 
         return self.rev.meta
     meta = property(fget=get_meta)
 
+    @property
+    def name(self):
+        """
+        returns the first name from the list of names.
+        """
+        try:
+            return self.names[0]
+        except IndexError:
+            return u''
+
+    @property
+    def names(self):
+        """
+        returns a list of 0..n names of the item
+        If we are dealing with a specific name (e.g field being NAME_EXACT),
+        move it to position 0 of the list, so the upper layer can use names[0]
+        if they want that particular name and names for the whole list.
+        TODO make the entire code to be able to use names instead of name
+        """
+        names = self.meta.get(NAME, [])
+        if self.fqname.field == NAME_EXACT:
+            try:
+                names.remove(self.fqname.value)
+            except ValueError:
+                pass
+            names.insert(0, self.fqname.value)