changeset 2519:b942154b2d74

merged bootstrap and main repo
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sun, 02 Feb 2014 17:14:23 +0100
parents 61884a46ae68 (current diff) 3322182f3fe2 (diff)
children 6938dad629da
files Makefile MoinMoin/config/default.py MoinMoin/items/__init__.py MoinMoin/templates/base.html MoinMoin/templates/common.js MoinMoin/templates/index.html MoinMoin/templates/modify_meta.html MoinMoin/themes/__init__.py MoinMoin/themes/basic/templates/modify_meta.html contrib/pep8/DeleteTrailingSpaces.py docs/devel/development.rst quickinstall quickinstall.bat setup.py wikiconfig.py
diffstat 107 files changed, 4350 insertions(+), 1305 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Tue Jan 21 02:25:35 2014 +0530
+++ b/.hgignore	Sun Feb 02 17:14:23 2014 +0100
@@ -7,12 +7,7 @@
 ^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$
@@ -34,3 +29,11 @@
 ^upload.py
 ^build/
 \..*sw[op]$
+^activate.bat$
+^deactivate.bat$
+^moin.bat$
+^m.bat$
+^activate$
+^moin$
+^m$
+^m-.*\.txt$
--- a/MANIFEST.in	Tue Jan 21 02:25:35 2014 +0530
+++ b/MANIFEST.in	Sun Feb 02 17:14:23 2014 +0100
@@ -1,5 +1,5 @@
 include README.txt LICENSE.txt
-include quickinstall quickinstall.bat
+include quickinstall.py
 include wikiconfig.py
 include Makefile
 
@@ -24,10 +24,6 @@
 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
--- a/Makefile	Tue Jan 21 02:25:35 2014 +0530
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-#
-# 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
-
--- a/MoinMoin/__init__.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/__init__.py	Sun Feb 02 17:14:23 2014 +0100
@@ -12,8 +12,11 @@
 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
--- a/MoinMoin/_tests/test_forms.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/_tests/test_forms.py	Sun Feb 02 17:14:23 2014 +0100
@@ -6,9 +6,17 @@
 """
 
 import datetime
+import json
 from calendar import timegm
 
-from MoinMoin.forms import DateTimeUNIX
+from flask import current_app as app
+from flask import g as flaskg
+
+from MoinMoin.forms import DateTimeUNIX, JSON, 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():
@@ -42,3 +50,30 @@
     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
--- a/MoinMoin/_tests/test_user.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/_tests/test_user.py	Sun Feb 02 17:14:23 2014 +0100
@@ -12,6 +12,8 @@
 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):
@@ -119,28 +121,36 @@
 
     # Subscriptions ---------------------------------------------------
 
-    def testSubscriptionSubscribedPage(self):
-        """ user: tests is_subscribed_to  """
-        pagename = u'HelpMiscellaneous'
-        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 theUser.is_subscribed_to([pagename])  # list(!) of pages to check
+    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
 
-    def testSubscriptionSubPage(self):
-        """ user: tests is_subscribed_to on a subpage """
-        pagename = u'HelpMiscellaneous'
-        testPagename = u'HelpMiscellaneous/FrequentlyAskedQuestions'
-        name = u'__Jürgen Herman__'
+        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 not theUser.is_subscribed_to([testPagename])  # 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
+
+        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 -------------------------------------------------------
 
@@ -185,7 +195,7 @@
         # add quicklink
         theUser.quicklink(u'Test_page_added')
         result_on_addition = theUser.quicklinks
-        expected = [u'MoinTest:Test_page_added']
+        expected = [u'MoinTest/Test_page_added']
         assert result_on_addition == expected
 
         # remove quicklink
@@ -212,7 +222,7 @@
         theUser.add_trail(u'item_added')
         theUser = user.User(name=name, password=password)
         result = theUser.get_trail()
-        expected = [u'MoinTest:item_added']
+        expected = [u'MoinTest/item_added']
         assert result == expected
 
     # Sessions -------------------------------------------------------
--- a/MoinMoin/apps/admin/templates/admin/index.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/apps/admin/templates/admin/index.html	Sun Feb 02 17:14:23 2014 +0100
@@ -5,5 +5,6 @@
     <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 %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/admin/templates/admin/trash.html	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,38 @@
+{% 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 %}
--- a/MoinMoin/apps/admin/templates/admin/userbrowser.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/apps/admin/templates/admin/userbrowser.html	Sun Feb 02 17:14:23 2014 +0100
@@ -10,7 +10,7 @@
     </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 %}
--- a/MoinMoin/apps/admin/views.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/apps/admin/views.py	Sun Feb 02 17:14:23 2014 +0100
@@ -11,18 +11,20 @@
 
 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')
@@ -46,6 +48,7 @@
     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]],
@@ -221,3 +224,28 @@
                            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
--- a/MoinMoin/apps/frontend/_tests/test_frontend.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/apps/frontend/_tests/test_frontend.py	Sun Feb 02 17:14:23 2014 +0100
@@ -172,7 +172,7 @@
         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')
--- a/MoinMoin/apps/frontend/views.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/apps/frontend/views.py	Sun Feb 02 17:14:23 2014 +0100
@@ -46,16 +46,19 @@
 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
@@ -75,7 +78,7 @@
 
 @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))
 
 
@@ -129,6 +132,17 @@
     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')
@@ -309,6 +323,7 @@
                                    query=query,
                                    medium_search_form=search_form,
                                    item_name=item_name,
+                                   history=history,
             )
             flaskg.clock.stop('search render')
     else:
@@ -360,14 +375,28 @@
 @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
 
 
@@ -407,6 +436,7 @@
 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()),
     )
 
@@ -424,6 +454,7 @@
             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,
@@ -438,8 +469,7 @@
 @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:
@@ -515,8 +545,27 @@
     target = RequiredText.using(label=L_('Target')).with_properties(placeholder=L_("The name of the target item"))
 
 
+class ValidRevert(Validator):
+    """
+    Validator for a valid revert form.
+    """
+    invalid_name_msg = ''
+
+    def validate(self, element, state):
+        """
+        Check whether the names present in the previous meta are not taken by some other item.
+        """
+        try:
+            validate_name(state['meta'], state['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):
@@ -547,11 +596,12 @@
     elif request.method == 'POST':
         form = RevertItemForm.from_flat(request.form)
         TextCha(form).amend_form()
-        if form.validate():
+        state = dict(fqname=item.fqname, meta=dict(item.meta))
+        if form.validate(state):
             item.revert(form['comment'])
             return redirect(url_for_item(item_name))
     return render_template(item.revert_template,
-                           item=item, item_name=item_name,
+                           item=item, fqname=item.fqname,
                            rev_id=rev,
                            form=form,
     )
@@ -563,7 +613,7 @@
         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)
@@ -578,12 +628,14 @@
             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,
     )
 
@@ -594,7 +646,7 @@
         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)
@@ -613,6 +665,7 @@
             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,
     )
 
@@ -695,10 +748,11 @@
         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()
@@ -711,9 +765,10 @@
                 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,
     )
@@ -737,7 +792,7 @@
         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),
@@ -785,16 +840,20 @@
 
     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'),
     )
 
 
@@ -809,13 +868,13 @@
     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
@@ -823,8 +882,9 @@
     """
     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>')
@@ -840,7 +900,8 @@
     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
     )
 
@@ -857,8 +918,9 @@
     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),
     )
 
 
@@ -870,15 +932,15 @@
     :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>')
@@ -890,30 +952,37 @@
     :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))
@@ -921,7 +990,8 @@
         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)
@@ -929,23 +999,34 @@
     # 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 = []
@@ -971,22 +1052,30 @@
                            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')
@@ -1003,7 +1092,7 @@
     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')
@@ -1019,7 +1108,7 @@
     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>')
@@ -1046,18 +1135,25 @@
     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)
@@ -1480,6 +1576,12 @@
     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
@@ -1519,6 +1621,7 @@
         ui=UserSettingsUIForm,
         navigation=UserSettingsNavigationForm,
         options=UserSettingsOptionsForm,
+        subscriptions=UserSettingsSubscriptionsForm,
     )
     forms = dict()
 
@@ -1739,6 +1842,7 @@
     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],
@@ -1765,7 +1869,12 @@
     """
     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
@@ -1773,40 +1882,43 @@
     # 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:
@@ -1816,18 +1928,18 @@
     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
@@ -1840,6 +1952,7 @@
 
     # 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:
@@ -1857,41 +1970,43 @@
     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)
 
@@ -1909,13 +2024,15 @@
     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,
     )
 
 
@@ -1925,46 +2042,52 @@
         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, [])
@@ -1993,21 +2116,26 @@
     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>')
--- a/MoinMoin/apps/misc/templates/misc/sitemap.xml	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/apps/misc/templates/misc/sitemap.xml	Sun Feb 02 17:14:23 2014 +0100
@@ -1,8 +1,8 @@
 <?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>
--- a/MoinMoin/apps/misc/templates/misc/urls_names.txt	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/apps/misc/templates/misc/urls_names.txt	Sun Feb 02 17:14:23 2014 +0100
@@ -1,3 +1,3 @@
-{% 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 %}
--- a/MoinMoin/apps/misc/views.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/apps/misc/views.py	Sun Feb 02 17:14:23 2014 +0100
@@ -13,9 +13,11 @@
 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
 
 
@@ -30,18 +32,18 @@
 
     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')
@@ -57,6 +59,8 @@
     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')
--- a/MoinMoin/config/default.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/config/default.py	Sun Feb 02 17:14:23 2014 +0100
@@ -23,6 +23,7 @@
 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
@@ -219,7 +220,7 @@
         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',
@@ -462,7 +463,8 @@
     )),
     # ==========================================================================
     '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
@@ -492,7 +494,7 @@
             DISABLED: False,
             BOOKMARKS: {},
             QUICKLINKS: [],
-            SUBSCRIBED_ITEMS: [],
+            SUBSCRIPTIONS: [],
             EMAIL_SUBSCRIBED_EVENTS: [
                 # XXX PageChangedEvent.__name__
                 # XXX PageRenamedEvent.__name__
--- a/MoinMoin/constants/keys.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/constants/keys.py	Sun Feb 02 17:14:23 2014 +0100
@@ -22,6 +22,7 @@
 # needs more precise name / use case:
 SOMEDICT = u"somedict"
 
+# TODO review plural constants
 CONTENTTYPE = u"contenttype"
 ITEMTYPE = u"itemtype"
 SIZE = u"size"
@@ -39,6 +40,7 @@
 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)
@@ -66,7 +68,9 @@
 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"
@@ -83,6 +87,8 @@
 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"
@@ -91,8 +97,7 @@
     # 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
@@ -114,3 +119,26 @@
 # 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, ]
--- a/MoinMoin/constants/namespaces.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/constants/namespaces.py	Sun Feb 02 17:14:23 2014 +0100
@@ -7,3 +7,5 @@
 
 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.
--- a/MoinMoin/converter/nonexistent_in.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/converter/nonexistent_in.py	Sun Feb 02 17:14:23 2014 +0100
@@ -25,7 +25,7 @@
         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'),
         }
--- a/MoinMoin/forms.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/forms.py	Sun Feb 02 17:14:23 2014 +0100
@@ -23,11 +23,17 @@
 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):
@@ -69,30 +75,87 @@
 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.")
-            return False
-        if len(names) != len(set(names)):
-            self.invalid_name_msg = L_("The names in the JSON name list must be unique.")
+    def validitemid(self, itemid):
+        if not itemid:
+            self.invalid_itemid_msg = L_("No ITEMID field")
             return False
-        query = Or([Term(NAME, x) for x in names])
-        if itemid is not None:
-            query = And([query, Not(Term(ITEMID, itemid))])
-        duplicate_names = set()
-        with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher:
-            results = searcher.search(query)
-            for result in results:
-                duplicate_names |= set([x for x in result[NAME] if x in names])
-        if duplicate_names:
-            self.invalid_name_msg = L_("Item(s) named %(duplicate_names)s already exist.", duplicate_names=", ".join(duplicate_names))
+        return 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
+        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
 
@@ -101,8 +164,11 @@
             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())
@@ -152,11 +218,50 @@
     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"))
 
--- a/MoinMoin/i18n/__init__.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/i18n/__init__.py	Sun Feb 02 17:14:23 2014 +0100
@@ -15,8 +15,9 @@
 
 
 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
 
@@ -66,3 +67,37 @@
     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)
--- a/MoinMoin/i18n/_tests/test_i18n.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/i18n/_tests/test_i18n.py	Sun Feb 02 17:14:23 2014 +0100
@@ -5,8 +5,12 @@
 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_
 
 
@@ -32,3 +36,19 @@
     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'
--- a/MoinMoin/items/__init__.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/items/__init__.py	Sun Feb 02 17:14:23 2014 +0100
@@ -42,19 +42,23 @@
 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
 
@@ -103,20 +107,28 @@
     """ if we have no stored Revision, we use this dummy """
     def __init__(self, item, itemtype=None, contenttype=None):
         self.item = item
+        fqname = item.fqname
         self.meta = {
             ITEMTYPE: itemtype or ITEMTYPE_NONEXISTENT,
             CONTENTTYPE: contenttype or CONTENTTYPE_NONEXISTENT
         }
         self.data = StringIO('')
         self.revid = None
-        if self.item:
-            self.meta[NAME] = [self.item.name]
+        if item:
+            self.meta[NAMESPACE] = fqname.namespace
+            if fqname.field in UFIELDS_TYPELIST:
+                if fqname.field == NAME_EXACT:
+                    self.meta[NAME] = [fqname.value]
+                else:
+                    self.meta[fqname.field] = [fqname.value]
+            else:
+                self.meta[fqname.field] = fqname.value
 
 
 class DummyItem(object):
     """ if we have no stored Item, we use this dummy """
-    def __init__(self, name):
-        self.name = name
+    def __init__(self, fqname):
+        self.fqname = fqname
 
     def list_revisions(self):
         return []  # same as an empty Item
@@ -125,7 +137,7 @@
         return True
 
 
-def get_storage_revision(name, itemtype=None, contenttype=None, rev_id=CURRENT, item=None):
+def get_storage_revision(fqname, itemtype=None, contenttype=None, rev_id=CURRENT, item=None):
     """
     Get a storage Revision.
 
@@ -145,18 +157,20 @@
     :itemtype and :contenttype are used when creating a DummyRev, where
     metadata is not available from the storage.
     """
+    rev_id = fqname.value if fqname.field == REVID else rev_id
     if 1:  # try:
         if item is None:
-            item = flaskg.storage[name]
+            item = flaskg.storage.get_item(**fqname.query)
         else:
-            name = item.name
+            if item.fqname:
+                fqname = item.fqname
     if not item:  # except NoSuchItemError:
-        logging.debug("No such item: {0!r}".format(name))
-        item = DummyItem(name)
+        logging.debug("No such item: {0!r}".format(fqname))
+        item = DummyItem(fqname)
         rev = DummyRev(item, itemtype, contenttype)
-        logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(name, contenttype))
+        logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(fqname, contenttype))
     else:
-        logging.debug("Got item: {0!r}".format(name))
+        logging.debug("Got item: {0!r}".format(fqname))
         try:
             rev = item.get_revision(rev_id)
         except KeyError:  # NoSuchRevisionError:
@@ -164,10 +178,10 @@
                 rev = item.get_revision(CURRENT)  # fall back to current revision
                 # XXX add some message about invalid revision
             except KeyError:  # NoSuchRevisionError:
-                logging.debug("Item {0!r} has no revisions.".format(name))
+                logging.debug("Item {0!r} has no revisions.".format(fqname))
                 rev = DummyRev(item, itemtype, contenttype)
-                logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(name, contenttype))
-        logging.debug("Got item {0!r}, revision: {1!r}".format(name, rev_id))
+                logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(fqname, contenttype))
+        logging.debug("Got item {0!r}, revision: {1!r}".format(fqname, rev_id))
     return rev
 
 
@@ -197,6 +211,7 @@
     # 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
 
 
@@ -258,6 +273,13 @@
     """
 
 
+class FieldNotUniqueError(ValueError):
+    """
+    The Field is not a UFIELD(unique Field).
+    Non unique fields can refer to more than one item.
+    """
+
+
 class Item(object):
     """ Highlevel (not storage) Item, wraps around a storage Revision"""
     # placeholder values for registry entry properties
@@ -290,7 +312,11 @@
         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)))
@@ -302,15 +328,14 @@
         itemtype = rev.meta.get(ITEMTYPE) or itemtype or ITEMTYPE_DEFAULT
         logging.debug("Item {0!r}, got itemtype {1!r} from revision meta".format(name, itemtype))
 
-        item = item_registry.get(itemtype, name, rev=rev, content=content)
+        item = item_registry.get(itemtype, fqname, rev=rev, content=content)
         logging.debug("Item class {0!r} handles {1!r}".format(item.__class__, itemtype))
 
         content.item = item
-
         return item
 
-    def __init__(self, name, rev=None, content=None):
-        self.name = name
+    def __init__(self, fqname, rev=None, content=None):
+        self.fqname = fqname
         self.rev = rev
         self.content = content
 
@@ -318,6 +343,34 @@
         return self.rev.meta
     meta = property(fget=get_meta)
 
+    @property
+    def name(self):
+        """
+        returns the first name from the list of names.
+        """
+        try:
+            return self.names[0]
+        except IndexError:
+            return u''
+
+    @property
+    def names(self):
+        """
+        returns a list of 0..n names of the item
+        If we are dealing with a specific name (e.g field being NAME_EXACT),
+        move it to position 0 of the list, so the upper layer can use names[0]
+        if they want that particular name and names for the whole list.
+        TODO make the entire code to be able to use names instead of name
+        """
+        names = self.meta.get(NAME, [])
+        if self.fqname.field == NAME_EXACT:
+            try:
+                names.remove(self.fqname.value)
+            except ValueError:
+                pass
+            names.insert(0, self.fqname.value)
+        return names
+
     # XXX Backward compatibility, remove soon
     @property
     def contenttype(self):
@@ -329,9 +382,10 @@
     def meta_filter(self, meta):
         """ kill metadata entries that we set automatically when saving """
         kill_keys = [  # shall not get copied from old rev to new rev
-            NAME_OLD,
+            # As we have a special field for NAME we don't want NAME to appear in JSON meta.
+            NAME, NAME_OLD,
             # are automatically implanted when saving
-            ITEMID, REVID, DATAID,
+            REVID, DATAID,
             HASH_ALGORITHM,
             SIZE,
             COMMENT,
@@ -368,7 +422,7 @@
 
     def _rename(self, name, comment, action, delete=False):
         self._save(self.meta, self.content.data, name=name, action=action, comment=comment, delete=delete)
-        old_prefix = self.subitems_prefix
+        old_prefix = self.subitem_prefixes[0]
         old_prefixlen = len(old_prefix)
         if not delete:
             new_prefix = name + '/'
@@ -379,7 +433,8 @@
                         child_newname = None
                     else:  # rename
                         child_newname = new_prefix + child_oldname[old_prefixlen:]
-                    item = Item.create(child_oldname)
+                    old_fqname = CompositeName(self.fqname.namespace, self.fqname.field, child_oldname)
+                    item = Item.create(old_fqname.fullname)
                     item._save(item.meta, item.content.data,
                                name=child_newname, action=action, comment=comment, delete=delete)
 
@@ -387,20 +442,24 @@
         """
         rename this item to item <name> (replace current name by another name in the NAME list)
         """
-        if flaskg.storage[name]:
-            raise NameNotUniqueError(L_("An item named %s already exists." % name))
-        return self._rename(name, comment, action=u'RENAME')
+        fqname = CompositeName(self.fqname.namespace, self.fqname.field, name)
+        if flaskg.storage.get_item(**fqname.query):
+            raise NameNotUniqueError(L_("An item named %s already exists in the namespace %s." % (name, fqname.namespace)))
+        return self._rename(name, comment, action=ACTION_RENAME)
 
     def delete(self, comment=u''):
         """
         delete this item (remove current name from NAME list)
         """
-        return self._rename(None, comment, action=u'TRASH', delete=True)
+        return self._rename(None, comment, action=ACTION_TRASH, delete=True)
 
     def revert(self, comment=u''):
-        return self._save(self.meta, self.content.data, action=u'REVERT', comment=comment)
+        return self._save(self.meta, self.content.data, action=ACTION_REVERT, comment=comment)
 
     def destroy(self, comment=u'', destroy_item=False):
+        action = DESTROY_ALL if destroy_item else DESTROY_REV
+        item_modified.send(app, item_name=self.name, action=action, meta=self.meta,
+                           content=self.rev.data, comment=comment)
         # called from destroy UI/POST
         if destroy_item:
             # destroy complete item with all revisions, metadata, etc.
@@ -473,10 +532,10 @@
         """
         raise NotImplementedError
 
-    def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=None,
+    def _save(self, meta, data=None, name=None, action=ACTION_SAVE, contenttype_guessed=None, comment=None,
               overwrite=False, delete=False):
         backend = flaskg.storage
-        storage_item = backend[self.name]
+        storage_item = backend.get_item(**self.fqname.query)
         try:
             currentrev = storage_item.get_revision(CURRENT)
             rev_id = currentrev.revid
@@ -498,27 +557,39 @@
 
         # we store the previous (if different) and current item name into revision metadata
         # this is useful for rename history and backends that use item uids internally
-        if name is None:
-            name = self.name
-        oldname = meta.get(NAME)
-        if oldname:
-            if not isinstance(oldname, list):
-                oldname = [oldname]
-            if delete or name not in oldname:  # this is a delete or rename
-                meta[NAME_OLD] = oldname[:]
-                try:
-                    oldname.remove(self.name)
-                except ValueError:
-                    pass
-                if not delete:
-                    oldname.append(name)
-                meta[NAME] = oldname
-        else:
-            meta[NAME] = [name]
+        if self.fqname.field == NAME_EXACT:
+            if name is None:
+                name = self.fqname.value
+            oldname = meta.get(NAME)
+            if oldname:
+                if not isinstance(oldname, list):
+                    oldname = [oldname]
+                if delete or name not in oldname:  # this is a delete or rename
+                    try:
+                        oldname.remove(self.name)
+                    except ValueError:
+                        pass
+                    if not delete:
+                        oldname.append(name)
+                    meta[NAME] = oldname
+            elif not meta.get(ITEMID):
+                meta[NAME] = [name]
+
+        if not meta.get(NAMESPACE):
+            meta[NAMESPACE] = self.fqname.namespace
 
         if comment is not None:
             meta[COMMENT] = unicode(comment)
 
+        if currentrev:
+            current_names = currentrev.meta.get(NAME, [])
+            new_names = meta.get(NAME, [])
+            deleted_names = set(current_names) - set(new_names)
+            if deleted_names:  # some names have been deleted.
+                meta[NAME_OLD] = current_names
+                if not new_names:  # if no names left, then set the trash flag.
+                    meta[TRASH] = True
+
         if not overwrite and REVID in meta:
             # we usually want to create a new revision, thus we must remove the existing REVID
             del meta[REVID]
@@ -536,19 +607,34 @@
 
         if isinstance(data, str):
             data = StringIO(data)
-
         newrev = storage_item.store_revision(meta, data, overwrite=overwrite,
                                              action=unicode(action),
                                              contenttype_current=contenttype_current,
                                              contenttype_guessed=contenttype_guessed,
                                              return_rev=True,
                                              )
-        item_modified.send(app._get_current_object(), item_name=name)
+        # XXX TODO name might be None here (we have a failing unit test)
+        # maybe this needs to be changed so a fqname is used instead of
+        # a simple name
+        assert name is not None  # fail early
+        item_modified.send(app, item_name=name, action=action)
         return newrev.revid, newrev.meta[SIZE]
 
     @property
-    def subitems_prefix(self):
-        return self.name + u'/' if self.name else u''
+    def subitem_prefixes(self):
+        """
+        Return the possible prefixes for subitems.
+        """
+        names = self.names[0:1] if self.fqname.field == NAME_EXACT else self.names
+        return [name + u'/' if name else u'' for name in names]
+
+    def get_prefix_match(self, name, prefixes):
+        """
+        returns the prefix match found.
+        """
+        for prefix in prefixes:
+            if name.startswith(prefix):
+                return prefix
 
     def get_subitem_revs(self):
         """
@@ -556,15 +642,15 @@
 
         Subitems are in the form of storage Revisions.
         """
-        query = Term(WIKINAME, app.cfg.interwikiname)
+        query = And([Term(WIKINAME, app.cfg.interwikiname), Term(NAMESPACE, self.fqname.namespace)])
         # trick: an item of empty name can be considered as "virtual root item"
         # that has all wiki items as sub items
-        if self.name:
-            query = And([query, Prefix(NAME_EXACT, self.subitems_prefix)])
+        if self.names:
+            query = And([query, Or([Prefix(NAME_EXACT, prefix) for prefix in self.subitem_prefixes])])
         revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
         return revs
 
-    def make_flat_index(self, subitems):
+    def make_flat_index(self, subitems, isglobalindex=False):
         """
         Create two IndexEntry lists - ``dirs`` and ``files`` - from a list of
         subitems.
@@ -579,38 +665,42 @@
 
         When both a subitem itself and some of its subitems are in the subitems
         list, it appears in both ``files`` and ``dirs``.
+
+        :param isglobalindex: True if the query is for global indexes.
         """
-        prefix = self.subitems_prefix
-        prefixlen = len(prefix)
+        prefixes = [u''] if isglobalindex else self.subitem_prefixes
         # IndexEntry instances of "file" subitems
         files = []
         # IndexEntry instances of "directory" subitems
         dirs = []
         added_dir_relnames = set()
-
         for rev in subitems:
             fullnames = rev.meta[NAME]
             for fullname in fullnames:
-                if fullname.startswith(prefix):
-                    relname = fullname[prefixlen:]
+                prefix = self.get_prefix_match(fullname, prefixes)
+                fullname_fqname = CompositeName(rev.meta[NAMESPACE], NAME_EXACT, fullname)
+                if not prefix is None:
+                    relname = fullname[len(prefix):]
                     if '/' in relname:
                         # Find the *direct* subitem that is the ancestor of current
                         # (indirect) subitem. e.g. suppose when the index root is
                         # 'foo', and current item (`rev`) is 'foo/bar/lorem/ipsum',
                         # 'foo/bar' will be found.
                         direct_relname = relname.partition('/')[0]
-                        if direct_relname not in added_dir_relnames:
-                            added_dir_relnames.add(direct_relname)
+                        direct_relname_fqname = CompositeName(rev.meta[NAMESPACE], NAME_EXACT, direct_relname)
+                        if direct_relname_fqname not in added_dir_relnames:
+                            added_dir_relnames.add(direct_relname_fqname)
                             direct_fullname = prefix + direct_relname
-                            direct_rev = get_storage_revision(direct_fullname)
-                            dirs.append(IndexEntry(direct_relname, direct_fullname, direct_rev.meta))
+                            direct_fullname_fqname = CompositeName(rev.meta[NAMESPACE], NAME_EXACT, direct_fullname)
+                            fqname = CompositeName(rev.meta[NAMESPACE], NAME_EXACT, direct_fullname)
+                            direct_rev = get_storage_revision(fqname)
+                            dirs.append(IndexEntry(direct_relname, direct_fullname_fqname, direct_rev.meta))
                     else:
-                        files.append(IndexEntry(relname, fullname, rev.meta))
-
+                        files.append(IndexEntry(relname, fullname_fqname, rev.meta))
         return dirs, files
 
-    def build_index_query(self, startswith=None, selected_groups=None):
-        prefix = self.subitems_prefix
+    def build_index_query(self, startswith=None, selected_groups=None, isglobalindex=False):
+        prefix = u'' if isglobalindex else self.subitem_prefixes[0]
         if startswith:
             query = Prefix(NAME_EXACT, prefix + startswith) | Prefix(NAME_EXACT, prefix + startswith.swapcase())
         else:
@@ -629,14 +719,18 @@
         return query
 
     def get_index(self, startswith=None, selected_groups=None):
-        query = Term(WIKINAME, app.cfg.interwikiname) & self.build_index_query(startswith, selected_groups)
+        fqname = self.fqname
+        isglobalindex = not fqname.value or fqname.value == NAMESPACE_ALL
+        query = Term(WIKINAME, app.cfg.interwikiname) & self.build_index_query(startswith, selected_groups, isglobalindex)
+        if not fqname.value.startswith(NAMESPACE_ALL + '/') and fqname.value != NAMESPACE_ALL:
+            query = Term(NAMESPACE, fqname.namespace) & query
         revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
-        return self.make_flat_index(revs)
+        return self.make_flat_index(revs, isglobalindex)
 
     def get_mixed_index(self):
         dirs, files = self.make_flat_index(self.get_subitem_revs())
-        dirs_dict = dict([(e.relname, MixedIndexEntry(*e, hassubitems=True)) for e in dirs])
-        index_dict = dict([(e.relname, MixedIndexEntry(*e, hassubitems=False)) for e in files])
+        dirs_dict = dict([(e.fullname, MixedIndexEntry(*e, hassubitems=True)) for e in dirs])
+        index_dict = dict([(e.fullname, MixedIndexEntry(*e, hassubitems=False)) for e in files])
         index_dict.update(dirs_dict)
         return sorted(index_dict.values())
 
@@ -647,12 +741,13 @@
         return a sorted list of first characters of subitem names,
         optionally all uppercased or lowercased.
         """
-        prefix = self.subitems_prefix
-        prefixlen = len(prefix)
+        prefixes = self.subitem_prefixes
         initials = set()
         for item in subitems:
             for name in item.meta[NAME]:
-                if name.startswith(prefix):
+                prefix = self.get_prefix_match(name, prefixes)
+                prefixlen = len(prefix)
+                if prefix:
                     initial = name[prefixlen]
                     if uppercase:
                         initial = initial.upper()
@@ -695,7 +790,9 @@
         rev_ids = []
         item_templates = self.content.get_templates(self.contenttype)
         return render_template('modify_select_template.html',
+                               item=self,
                                item_name=self.name,
+                               fqname=self.fqname,
                                itemtype=self.itemtype,
                                rev=self.rev,
                                contenttype=self.contenttype,
@@ -712,6 +809,7 @@
         first_rev = last_rev = None  # TODO
         return render_template(self.show_template,
                                item=self, item_name=self.name,
+                               fqname=self.fqname,
                                rev=self.rev,
                                contenttype=self.contenttype,
                                first_rev_id=first_rev,
@@ -726,6 +824,7 @@
         if method in ['GET', 'HEAD']:
             if isinstance(self.content, NonExistentContent):
                 return render_template('modify_select_contenttype.html',
+                                       fqname=self.fqname,
                                        item_name=self.name,
                                        itemtype=self.itemtype,
                                        group_names=content_registry.group_names,
@@ -751,18 +850,20 @@
                     # break them
                     return "OK"
             form = self.ModifyForm.from_request(request)
-            state = dict(name=self.name, itemid=self.meta.get(ITEMID))
+            meta, data, contenttype_guessed, comment = form._dump(self)
+            state = dict(fqname=self.fqname, itemid=meta.get(ITEMID), meta=meta)
             if form.validate(state):
-                meta, data, contenttype_guessed, comment = form._dump(self)
                 contenttype_qs = request.values.get('contenttype')
                 try:
                     self.modify(meta, data, comment, contenttype_guessed, **{CONTENTTYPE: contenttype_qs})
                 except AccessDenied:
                     abort(403)
                 else:
-                    return redirect(url_for_item(self.name))
+                    return redirect(url_for_item(**self.fqname.split))
         return render_template(self.modify_template,
+                               fqname=self.fqname,
                                item_name=self.name,
+                               item=self,
                                rows_meta=str(ROWS_META), cols=str(COLS),
                                form=form,
                                search_form=None,
@@ -802,6 +903,7 @@
         else:
             content = render_template('show_nonexistent.html',
                                       item_name=self.name,
+                                      fqname=self.fqname,
                                      )
         return Response(content, 404)
 
@@ -813,7 +915,9 @@
 
     def _select_itemtype(self):
         return render_template('modify_select_itemtype.html',
+                               item=self,
                                item_name=self.name,
+                               fqname=self.fqname,
                                itemtypes=item_registry.shown_entries,
                               )
 
--- a/MoinMoin/items/_tests/test_Content.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/items/_tests/test_Content.py	Sun Feb 02 17:14:23 2014 +0100
@@ -8,6 +8,7 @@
 """
 
 import pytest
+from io import BytesIO
 
 from flask import Markup
 
@@ -232,14 +233,15 @@
         item = Item.create(item_name)
         contenttype = u'text/plain;charset=utf-8'
         meta = {CONTENTTYPE: contenttype}
-        item._save(meta)
+        data1 = "old_data"
+        item._save(meta, data1)
         item1 = Item.create(item_name)
-        data = 'test_data'
+        data2 = 'new_data'
         comment = u'next revision'
-        item1._save(meta, data, comment=comment)
+        item1._save(meta, data2, comment=comment)
         item2 = Item.create(item_name)
         result = Text._render_data_diff_text(item1.content, item1.rev, item2.rev)
-        expected = u'- \n+ test_data'
+        expected = u'- old_data\n+ new_data'
         assert result == expected
         assert item2.content.data == ''
 
@@ -258,5 +260,39 @@
         assert u'<pre class="highlight">test_data\n' in result
         assert item2.content.data == ''
 
+    def test__get_data_diff_text(self):
+        item_name = u'Text_Item'
+        item = Item.create(item_name)
+        contenttypes = dict(texttypes=[u'text/plain;charset=utf-8',
+                                       u'text/x-markdown;charset=utf-8', ],
+                            othertypes=[u'image/png', u'audio/wave',
+                                        u'video/ogg',
+                                        u'application/x-svgdraw',
+                                        u'application/octet-stream', ])
+        for key in contenttypes:
+            for contenttype in contenttypes[key]:
+                meta = {CONTENTTYPE: contenttype}
+                item._save(meta)
+                item_ = Item.create(item_name)
+                oldfile = BytesIO("x")
+                newfile = BytesIO("xx")
+                difflines = item_.content._get_data_diff_text(oldfile, newfile)
+                if key == 'texttypes':
+                    assert difflines == ['- x', '+ xx']
+                else:
+                    assert difflines == []
+
+    def test__get_data_diff_html(self):
+        item_name = u"Test_Item"
+        item = Item.create(item_name)
+        contenttype = u'text/plain;charset=utf-8'
+        meta = {CONTENTTYPE: contenttype}
+        item._save(meta)
+        item_ = Item.create(item_name)
+        oldfile = BytesIO("")
+        newfile = BytesIO("x")
+        difflines = item_.content._get_data_diff_html(oldfile, newfile)
+        assert difflines == [(1, Markup(u''), 1, Markup(u'<span>x</span>'))]
+
 
 coverage_modules = ['MoinMoin.items.content']
--- a/MoinMoin/items/_tests/test_Item.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/items/_tests/test_Item.py	Sun Feb 02 17:14:23 2014 +0100
@@ -12,7 +12,11 @@
 
 from MoinMoin._tests import become_trusted, update_item
 from MoinMoin.items import Item, NonExistent, IndexEntry, MixedIndexEntry
-from MoinMoin.constants.keys import ITEMTYPE, CONTENTTYPE, NAME, NAME_OLD, COMMENT, ACTION, ADDRESS
+from MoinMoin.util.interwiki import CompositeName
+from MoinMoin.constants.keys import (ITEMTYPE, CONTENTTYPE, NAME, NAME_OLD, COMMENT,
+                                     ADDRESS, TRASH, ITEMID, NAME_EXACT,
+                                     ACTION, ACTION_REVERT)
+from MoinMoin.constants.namespaces import NAMESPACE_DEFAULT
 from MoinMoin.constants.contenttypes import CONTENTTYPE_NONEXISTENT
 from MoinMoin.constants.itemtypes import ITEMTYPE_NONEXISTENT
 
@@ -21,7 +25,7 @@
     """
     Build a list of IndexEntry by hand, useful as a test helper.
     """
-    return [(IndexEntry(relname, '/'.join((basename, relname)), Item.create('/'.join((basename, relname))).meta))
+    return [(IndexEntry(relname, CompositeName(NAMESPACE_DEFAULT, NAME_EXACT, '/'.join((basename, relname))), Item.create('/'.join((basename, relname))).meta))
             for relname in relnames]
 
 
@@ -31,7 +35,7 @@
 
     :spec is a list of (relname, hassubitem) tuples.
     """
-    return [(MixedIndexEntry(relname, '/'.join((basename, relname)), Item.create('/'.join((basename, relname))).meta, hassubitem))
+    return [(MixedIndexEntry(relname, CompositeName(NAMESPACE_DEFAULT, NAME_EXACT, '/'.join((basename, relname))), Item.create('/'.join((basename, relname))).meta, hassubitem))
             for relname, hassubitem in spec]
 
 
@@ -132,7 +136,7 @@
         item = Item.create(name)
         result = Item.meta_filter(item, meta)
         # keys like NAME, ITEMID, REVID, DATAID are filtered
-        expected = {'test_key': 'test_val', CONTENTTYPE: contenttype, NAME: [u'test_name']}
+        expected = {'test_key': 'test_val', CONTENTTYPE: contenttype}
         assert result == expected
 
     def test_meta_dict_to_text(self):
@@ -141,7 +145,7 @@
         meta = {'test_key': 'test_val', CONTENTTYPE: contenttype, NAME: [u'test_name']}
         item = Item.create(name)
         result = Item.meta_dict_to_text(item, meta)
-        expected = '{\n  "contenttype": "text/plain;charset=utf-8", \n  "name": [\n    "test_name"\n  ], \n  "test_key": "test_val"\n}'
+        expected = '{\n  "contenttype": "text/plain;charset=utf-8", \n  "test_key": "test_val"\n}'
         assert result == expected
 
     def test_meta_text_to_dict(self):
@@ -150,7 +154,7 @@
         text = '{\n  "contenttype": "text/plain;charset=utf-8", \n  "test_key": "test_val", \n "name": ["test_name"] \n}'
         item = Item.create(name)
         result = Item.meta_text_to_dict(item, text)
-        expected = {'test_key': 'test_val', CONTENTTYPE: contenttype, NAME: [u"test_name"]}
+        expected = {'test_key': 'test_val', CONTENTTYPE: contenttype}
         assert result == expected
 
     def test_item_can_have_several_names(self):
@@ -329,7 +333,7 @@
         item = Item.create(name)
         item.revert(u'revert')
         item = Item.create(name)
-        assert item.meta[ACTION] == u'REVERT'
+        assert item.meta[ACTION] == ACTION_REVERT
 
     def test_modify(self):
         name = u'Test_Item'
@@ -372,5 +376,37 @@
         assert item.meta['new_test_key'] == update_meta['new_test_key']
         assert 'none_test_key' not in item.meta
 
+    def test_trash(self):
+        fqname = u'trash_item_test'
+        contenttype = u'text/plain;charset=utf-8'
+        data = 'test_data'
+        meta = {CONTENTTYPE: contenttype}
+        item = Item.create(fqname)
+        # save rev 0
+        item._save(meta, data)
+        item = Item.create(fqname)
+        assert not item.meta.get(TRASH)
+
+        meta = dict(item.meta)
+        meta[NAME] = []
+        # save new rev with no names.
+        item._save(meta, data)
+        new_fqname = u'@itemid/' + item.meta[ITEMID]
+        item = Item.create(new_fqname)
+        assert item.meta[TRASH]
+
+        new_meta = {NAME: [u'foobar', 'buz'], CONTENTTYPE: contenttype}
+        item._save(new_meta, data)
+        item = Item.create(u'foobar')
+
+        item.delete(u'Deleting foobar.')
+        item = Item.create(u'buz')
+        assert not item.meta.get(TRASH)
+
+        # Also delete the only name left.
+        item.delete(u'Moving item to trash.')
+        item = Item.create(new_fqname)
+        assert item.meta[TRASH]
+
 
 coverage_modules = ['MoinMoin.items']
--- a/MoinMoin/items/blog.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/items/blog.py	Sun Feb 02 17:14:23 2014 +0100
@@ -22,6 +22,7 @@
 from MoinMoin.storage.middleware.protecting import AccessDenied
 from MoinMoin.constants.keys import NAME_EXACT, WIKINAME, ITEMTYPE, MTIME, PTIME, TAGS
 from MoinMoin.items import Item, Default, register, BaseMetaForm
+from MoinMoin.util.interwiki import split_fqname
 
 
 ITEMTYPE_BLOG = u'blog'
@@ -86,6 +87,7 @@
         blog_entry_items = [Item.create(rev.name, rev_id=rev.revid) for rev in revs]
         return render_template('blog/main.html',
                                item_name=self.name,
+                               fqname=split_fqname(self.name),
                                blog_item=self,
                                blog_entry_items=blog_entry_items,
                                tag=tag,
@@ -122,6 +124,7 @@
             abort(403)
         return render_template('blog/entry.html',
                                item_name=self.name,
+                               fqname=blog_item.fqname,
                                blog_item=blog_item,
                                blog_entry_item=self,
                               )
--- a/MoinMoin/items/content.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/items/content.py	Sun Feb 02 17:14:23 2014 +0100
@@ -60,6 +60,8 @@
 from MoinMoin.util.mime import Type, type_moin_document
 from MoinMoin.util.tree import moin_page, html, xlink, docbook
 from MoinMoin.util.iri import Iri
+from MoinMoin.util.diff_text import diff as text_diff
+from MoinMoin.util.diff_html import diff as html_diff
 from MoinMoin.util.crypto import cache_key
 from MoinMoin.util.clock import timed
 from MoinMoin.forms import File
@@ -67,7 +69,8 @@
     GROUP_MARKUP_TEXT, GROUP_OTHER_TEXT, GROUP_IMAGE, GROUP_AUDIO, GROUP_VIDEO,
     GROUP_DRAWING, GROUP_OTHER, CONTENTTYPE_NONEXISTENT, CHARSET
 )
-from MoinMoin.constants.keys import NAME_EXACT, WIKINAME, CONTENTTYPE, SIZE, TAGS, HASH_ALGORITHM
+from MoinMoin.constants.keys import (NAME_EXACT, WIKINAME, CONTENTTYPE, SIZE, TAGS,
+                                     HASH_ALGORITHM, ACTION_SAVE)
 
 
 COLS = 80
@@ -270,6 +273,15 @@
         # override this in child classes
         return ''
 
+    def _get_data_diff_text(self, oldfile, newfile):
+        """ Get the text diff of 2 versions of file contents
+
+        :param oldfile: file that contains old content data (bytes)
+        :param newfile: file that contains new content data (bytes)
+        :return: list of diff lines in a unified format without trailing linefeeds
+        """
+        return []
+
     def get_templates(self, contenttype=None):
         """ create a list of templates (for some specific contenttype) """
         terms = [Term(WIKINAME, app.cfg.interwikiname), Term(TAGS, u'template')]
@@ -472,7 +484,7 @@
             # everything we expected has been added to the tar file, save the container as revision
             meta = {CONTENTTYPE: self.contenttype}
             data = open(temp_fname, 'rb')
-            self.item._save(meta, data, name=self.name, action=u'SAVE', comment='')
+            self.item._save(meta, data, name=self.name, action=ACTION_SAVE, comment='')
             data.close()
             os.remove(temp_fname)
 
@@ -839,12 +851,15 @@
         """ convert data from storage format to memory format """
         return data.decode(CHARSET).replace(u'\r\n', u'\n')
 
-    def _get_data_diff_html(self, oldrev, newrev, template):
-        from MoinMoin.util.diff_html import diff
-        old_text = self.data_storage_to_internal(oldrev.data.read())
-        new_text = self.data_storage_to_internal(newrev.data.read())
-        storage_item = flaskg.storage[self.name]
-        diffs = [(d[0], Markup(d[1]), d[2], Markup(d[3])) for d in diff(old_text, new_text)]
+    def _render_data_diff_html(self, oldrev, newrev, template):
+        """ Render HTML formatted meta and content diff of 2 revisions
+
+        :param oldrev: old revision object
+        :param newrev: new revision object
+        :param template: name of the template to be rendered
+        :return: HTML data with meta and content diff
+        """
+        diffs = self._get_data_diff_html(oldrev.data, newrev.data)
         return render_template(template,
                                item_name=self.name,
                                oldrev=oldrev,
@@ -852,18 +867,44 @@
                                diffs=diffs,
                                )
 
+    def _get_data_diff_html(self, oldfile, newfile):
+        """ Get the HTML diff of 2 versions of file contents
+
+        :param oldfile: file that contains old content data (bytes)
+        :param newfile: file that contains new content data (bytes)
+        :return: list of tuples of the format (left lineno, deleted Markup content,
+                 right lineno, added Markup content)
+        """
+        old_text = self.data_storage_to_internal(oldfile.read())
+        new_text = self.data_storage_to_internal(newfile.read())
+        return [(d[0], Markup(d[1]), d[2], Markup(d[3])) for d in html_diff(old_text, new_text)]
+
+    def _get_data_diff_text(self, oldfile, newfile):
+        """ Get the text diff of 2 versions of file contents
+
+        :param oldfile: file that contains old content data (bytes)
+        :param newfile: file that contains new content data (bytes)
+        :return: list of diff lines in a unified format without trailing linefeeds
+        """
+        old_text = self.data_storage_to_internal(oldfile.read())
+        new_text = self.data_storage_to_internal(newfile.read())
+        return text_diff(old_text.splitlines(), new_text.splitlines())
+
     def _render_data_diff_atom(self, oldrev, newrev):
         """ renders diff in HTML for atom feed """
-        return self._get_data_diff_html(oldrev, newrev, 'diff_text_atom.html')
+        return self._render_data_diff_html(oldrev, newrev, 'diff_text_atom.html')
 
     def _render_data_diff(self, oldrev, newrev):
-        return self._get_data_diff_html(oldrev, newrev, 'diff_text.html')
+        return self._render_data_diff_html(oldrev, newrev, 'diff_text.html')
 
     def _render_data_diff_text(self, oldrev, newrev):
-        from MoinMoin.util import diff_text
-        oldlines = self.data_storage_to_internal(oldrev.data.read()).split('\n')
-        newlines = self.data_storage_to_internal(newrev.data.read()).split('\n')
-        difflines = diff_text.diff(oldlines, newlines)
+        """ Render text diff of 2 revisions' contents
+
+        :param oldrev: old revision object
+        :param newrev: new revision object
+        :return: text data of a content diff
+        """
+        difflines = self._get_data_diff_text(oldrev.data, newrev.data)
         return '\n'.join(difflines)
 
     _render_data_diff_raw = _render_data_diff
--- a/MoinMoin/items/ticket.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/items/ticket.py	Sun Feb 02 17:14:23 2014 +0100
@@ -22,7 +22,8 @@
 from MoinMoin.forms import (Form, OptionalText, OptionalMultilineText, SmallNatural, Tags,
                             Reference, BackReference, SelectSubmit)
 from MoinMoin.storage.middleware.protecting import AccessDenied
-from MoinMoin.constants.keys import ITEMTYPE, CONTENTTYPE, ITEMID, CURRENT, SUPERSEDED_BY, DEPENDS_ON, SUBSCRIBED_ITEMS
+from MoinMoin.constants.keys import (ITEMTYPE, CONTENTTYPE, ITEMID, CURRENT,
+                                     SUPERSEDED_BY, SUBSCRIPTIONS, DEPENDS_ON)
 from MoinMoin.constants.contenttypes import CONTENTTYPE_USER
 from MoinMoin.items import Item, Contentful, register, BaseModifyForm
 from MoinMoin.items.content import NonExistentContent
@@ -59,7 +60,7 @@
         id_ = item.meta[ITEMID]
         self['supersedes'].set(Term(SUPERSEDED_BY, id_))
         self['required_by'].set(Term(DEPENDS_ON, id_))
-        self['subscribers'].set(Term(SUBSCRIBED_ITEMS, id_))
+        self['subscribers'].set(Term(SUBSCRIPTIONS, id_))
 
 
 class TicketForm(BaseModifyForm):
--- a/MoinMoin/log.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/log.py	Sun Feb 02 17:14:23 2014 +0100
@@ -1,7 +1,6 @@
 # Copyright: 2008 MoinMoin:ThomasWaldmann
 # Copyright: 2007 MoinMoin:JohannesBerg
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
-
 """
     MoinMoin - init "logging" system
 
@@ -70,14 +69,14 @@
 keys=root
 
 [handlers]
-keys=stderr
+keys=stderr,email
 
 [formatters]
 keys=default
 
 [logger_root]
 level=%(loglevel)s
-handlers=stderr
+handlers=stderr,email
 
 [handler_stderr]
 class=StreamHandler
@@ -85,6 +84,12 @@
 formatter=default
 args=(sys.stderr, )
 
+[handler_email]
+class=MoinMoin.log.EmailHandler
+level=ERROR
+formatter=default
+args=()
+
 [formatter_default]
 format=%(asctime)s %(levelname)s %(name)s:%(lineno)d %(message)s
 datefmt=
@@ -170,3 +175,46 @@
         if isinstance(levelnumber, int):  # that list has also the reverse mapping...
             setattr(logger, levelname, levelnumber)
     return logger
+
+
+class EmailHandler(logging.Handler):
+    """ A custom handler class which sends email for each logging event using
+    wiki mail configuration
+    """
+    def __init__(self, toaddrs=[], subject=u''):
+        """ Initialize the handler
+
+        @param toaddrs: address or a list of email addresses whom to send email
+        @param subject: unicode email's subject
+        """
+        logging.Handler.__init__(self)
+        if isinstance(toaddrs, basestring):
+            toaddrs = [toaddrs]
+        self.toaddrs = toaddrs
+        self.subject = subject
+        self.in_email_handler = False
+
+    def emit(self, record):
+        """ Emit a record.
+
+        Send the record to the specified addresses
+        """
+        # the app config is accessible after logging is initialized, so set the
+        # arguments and make the decision to send mail or not here
+        from flask import current_app as app
+        if not app.cfg.email_tracebacks:
+            return
+
+        if self.in_email_handler:
+            return
+        self.in_email_handler = True
+        try:
+            toaddrs = self.toaddrs if self.toaddrs else app.cfg.admin_emails
+            log_level = logging.getLevelName(self.level)
+            subject = self.subject if self.subject else u'[{0}][{1}] Log message'.format(
+                app.cfg.sitename, log_level)
+            msg = self.format(record)
+            from MoinMoin.mail.sendmail import sendmail
+            sendmail(subject, msg, to=toaddrs)
+        finally:
+            self.in_email_handler = False
--- a/MoinMoin/mail/sendmail.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/mail/sendmail.py	Sun Feb 02 17:14:23 2014 +0100
@@ -59,7 +59,7 @@
         return str(address)
 
 
-def sendmail(subject, text, to=None, cc=None, bcc=None, mail_from=None):
+def sendmail(subject, text, to=None, cc=None, bcc=None, mail_from=None, html=None):
     """ Create and send a text/plain message
 
     Return a tuple of success or error indicator and message.
@@ -76,6 +76,8 @@
     :type bcc: list
     :param mail_from: override default mail_from
     :type mail_from: unicode
+    :param html: html email body text
+    :type html: unicode
 
     :rtype: tuple
     :returns: (is_ok, Description of error or OK message)
@@ -83,6 +85,8 @@
     import smtplib
     import socket
     from email.message import Message
+    from email.mime.multipart import MIMEMultipart
+    from email.mime.text import MIMEText
     from email.charset import Charset, QP
     from email.utils import formatdate, make_msgid
 
@@ -107,18 +111,28 @@
     # Create a message using CHARSET and quoted printable
     # encoding, which should be supported better by mail clients.
     # TODO: check if its really works better for major mail clients
-    msg = Message()
+    text_msg = Message()
     charset = Charset(CHARSET)
     charset.header_encoding = QP
     charset.body_encoding = QP
-    msg.set_charset(charset)
+    text_msg.set_charset(charset)
 
     # work around a bug in python 2.4.3 and above:
-    msg.set_payload('=')
-    if msg.as_string().endswith('='):
+    text_msg.set_payload('=')
+    if text_msg.as_string().endswith('='):
         text = charset.body_encode(text)
 
-    msg.set_payload(text)
+    text_msg.set_payload(text)
+
+    if html:
+        msg = MIMEMultipart('alternative')
+        msg.attach(text_msg)
+        html = html.encode(CHARSET)
+        html_msg = MIMEText(html, 'html')
+        html_msg.set_charset(charset)
+        msg.attach(html_msg)
+    else:
+        msg = text_msg
 
     address = encodeAddress(mail_from, charset)
     msg['From'] = address
--- a/MoinMoin/script/account/disable.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/script/account/disable.py	Sun Feb 02 17:14:23 2014 +0100
@@ -45,7 +45,7 @@
             u.name = u"{0}-{1}".format(u.name, u.id)
             if u.email:
                 u.email = u"{0}-{1}".format(u.email, u.id)
-            u.subscribed_items = []  # avoid using email
+            u.subscriptions = []
             u.save()
             print "- disabled."
         else:
--- a/MoinMoin/script/account/resetpw.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/script/account/resetpw.py	Sun Feb 02 17:14:23 2014 +0100
@@ -13,7 +13,9 @@
 from flask import current_app as app
 from flask.ext.script import Command, Option
 
-from MoinMoin.constants.keys import ITEMID, NAME, NAME_EXACT, EMAIL
+from MoinMoin.constants.keys import (
+    ITEMID, NAME, NAME_EXACT, EMAIL, EMAIL_UNVALIDATED,
+)
 from MoinMoin import user
 from MoinMoin.app import before_wiki
 
@@ -41,9 +43,9 @@
             return
         u.set_password(password)
         u.save()
-        if not u.email:
-            raise UserHasNoEMail('User profile does not have an E-Mail address (name: %r id: %r)!' % (u.name, u.id))
         if notify and not u.disabled:
+            if not u.email:
+                raise UserHasNoEMail('Notification was requested, but User profile does not have a validated E-Mail address (name: %r id: %r)!' % (u.name, u.itemid))
             mailok, msg = u.mail_password_recovery(subject=subject, text=text)
             if not mailok:
                 raise MailFailed(msg)
@@ -103,7 +105,13 @@
         total = len(uids_metas)
         for nr, (uid, meta) in enumerate(uids_metas, start=1):
             name = meta[NAME]
-            email = meta[EMAIL]
+            email = meta.get(EMAIL)
+            if email is None:
+                email = meta.get(EMAIL_UNVALIDATED)
+                if email is None:
+                    raise ValueError("neither EMAIL nor EMAIL_UNVALIDATED key is present in user profile metadata of uid %r name %r" % (uid, name))
+                else:
+                    email += '[email_unvalidated]'
             try:
                 set_password(uid, password, notify=notify, skip_invalid=skip_invalid,
                              subject=subject, text=text)
--- a/MoinMoin/script/migration/moin19/import19.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/script/migration/moin19/import19.py	Sun Feb 02 17:14:23 2014 +0100
@@ -257,7 +257,7 @@
                     editlog_data = {  # make something up
                         NAME: [item.name],
                         MTIME: int(os.path.getmtime(path)),
-                        ACTION: u'SAVE',
+                        ACTION: ACTION_SAVE,
                     }
             meta, data = split_body(content)
         meta.update(editlog_data)
@@ -333,7 +333,7 @@
         except KeyError:
             meta = {  # make something up
                 MTIME: int(os.path.getmtime(attpath)),
-                ACTION: u'SAVE',
+                ACTION: ACTION_SAVE,
             }
         meta[NAME] = [u'{0}/{1}'.format(item_name, attach_name)]
         if acl is not None:
@@ -377,12 +377,12 @@
                 if extra:
                     result[NAME_OLD] = extra
                 del result[EXTRA]
-                result[ACTION] = u'RENAME'
+                result[ACTION] = ACTION_RENAME
             elif action == 'SAVE/REVERT':
                 if extra:
                     result[REVERTED_TO] = int(extra)
                 del result[EXTRA]
-                result[ACTION] = u'REVERT'
+                result[ACTION] = ACTION_REVERT
         userid = result[USERID]
         #TODO
         #if userid:
@@ -401,7 +401,7 @@
         meta = dict([(k, v) for k, v in meta.items() if v])  # remove keys with empty values
         if meta.get(ACTION) == u'SAVENEW':
             # replace SAVENEW with just SAVE
-            meta[ACTION] = u'SAVE'
+            meta[ACTION] = ACTION_SAVE
         return meta
 
     def find_attach(self, attachname):
@@ -416,7 +416,7 @@
             raise KeyError
         del meta['__rev']
         del meta[EXTRA]  # we have full name in NAME
-        meta[ACTION] = u'SAVE'
+        meta[ACTION] = ACTION_SAVE
         meta = dict([(k, v) for k, v in meta.items() if v])  # remove keys with empty values
         return meta
 
@@ -479,7 +479,7 @@
         meta[ITEMID] = make_uuid()
         meta[REVID] = make_uuid()
         meta[SIZE] = 0
-        meta[ACTION] = u'SAVE'
+        meta[ACTION] = ACTION_SAVE
         self.meta = meta
         self.data = StringIO('')
 
@@ -532,8 +532,8 @@
         # rename aliasname to display_name:
         metadata[DISPLAY_NAME] = metadata.get('aliasname')
 
-        # rename subscribed_pages to subscribed_items
-        metadata[SUBSCRIBED_ITEMS] = metadata.get('subscribed_pages', [])
+        # transfer subscribed_pages to subscription_patterns
+        metadata[SUBSCRIPTIONS] = migrate_subscriptions(metadata.get('subscribed_pages', []))
 
         # convert bookmarks from usecs (and str) to secs (int)
         metadata[BOOKMARKS] = [(interwiki, int(long(bookmark) / 1000000))
@@ -633,3 +633,19 @@
     else:
         raise ValueError("unsupported content object: {0!r}".format(content))
     return size, HASH_ALGORITHM, unicode(hash.hexdigest())
+
+
+def migrate_subscriptions(subscribed_items):
+    """ Transfer subscribed_items meta to subscriptions meta
+
+    :param subscribed_items: a list of moin19-format subscribed_items
+    :return: subscriptions
+    """
+    subscriptions = []
+    for subscribed_item in subscribed_items:
+        # TODO: try to determine if pagename is not a regexp and create
+        # a subscription_id with a NAME keyword
+        # TODO: support interwiki wikiname
+        wikiname, pagename = subscribed_item.split(":", 1)
+        subscriptions.append("{0}::{2}".format(NAMERE, pagename))
+    return subscriptions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/script/win/__init__.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,6 @@
+# Copyright: 2013 MoinMoin:RogerHaase
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - Multi-platform alternatives for unix utilities
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/script/win/dos2unix.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+# Copyright: 2013 by MoinMoin:RogerHaase
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+Alternative for unix dos2unix utility that may be run on either windows or unix. Does not implement
+typical unix dos2unix command line syntax.
+
+If passed parameter is a directory, all files in that directory are converted to unix line endings.
+Sub-directories are not processed.  If passed parameter is a filename, only that filename is converted.
+
+Usage: python <path_to>dos2unix.py <target_directory_or_filename>
+"""
+
+import os
+import sys
+
+
+def convert_file(filename):
+    """Replace DOS line endings with unix line endings."""
+    with open(filename, "rb") as f:
+        data = f.read()
+    if '\0' in data:
+        # is binary file
+        return
+    newdata = data.replace("\r\n", "\n")
+    if newdata != data:
+        with open(filename, "wb") as f:
+            f.write(newdata)
+
+
+if __name__ == "__main__":
+    if len(sys.argv) == 2:
+        target = sys.argv[1]
+        if os.path.isdir(target):
+            for (dirpath, dirnames, filenames) in os.walk(target):
+                break
+            for filename in filenames:
+                convert_file(os.path.join(target, filename))
+        elif os.path.isfile(target):
+            convert_file(target)
+        else:
+            print "Error: %s does not exist." % target
+    else:
+        print "Error: incorrect parameters passed."
+        print "usage: python <path_to>dos2unix.py <target_directory>"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/script/win/wget.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,20 @@
+#!/usr/bin/python
+# Copyright: 2013 by MoinMoin:RogerHaase
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+Alternative for unix wget utility that may be run on either windows or unix. Does not implement
+typical unix wget command line syntax.
+
+Usage:  python <path_to>wget.py <url> <output_file>
+"""
+
+import sys
+import urllib
+
+
+if len(sys.argv) == 3:
+    urllib.urlretrieve(sys.argv[1], sys.argv[2])
+else:
+    print "Error: incorrect parameters passed."
+    print "Usage:  python <path_to>wget.py <url> <output_file>"
--- a/MoinMoin/signalling/log.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/signalling/log.py	Sun Feb 02 17:14:23 2014 +0100
@@ -7,6 +7,7 @@
 
 
 from .signals import *
+from flask import got_request_exception
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
@@ -19,6 +20,11 @@
 
 
 @item_modified.connect_via(ANY)
-def log_item_modified(app, item_name):
+def log_item_modified(app, item_name, **kwargs):
     wiki_name = app.cfg.interwikiname
     logging.info(u"item {0}:{1} modified".format(wiki_name, item_name))
+
+
+@got_request_exception.connect_via(ANY)
+def log_exception(sender, exception, **extra):
+    logging.exception(exception)
--- a/MoinMoin/storage/__init__.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/storage/__init__.py	Sun Feb 02 17:14:23 2014 +0100
@@ -85,14 +85,14 @@
         userprofiles_acl = dict(before=u'All:', default=u'', after=u'', hierarchic=False)
     namespaces = {
         NAMESPACE_DEFAULT: BACKEND_DEFAULT,
-        NAMESPACE_USERPROFILES + ':': BACKEND_USERPROFILES,
+        NAMESPACE_USERPROFILES + '/': BACKEND_USERPROFILES,
     }
     backends = {
         BACKEND_DEFAULT: None,
         BACKEND_USERPROFILES: None,
     }
     acls = {
-        NAMESPACE_USERPROFILES + ':': userprofiles_acl,
+        NAMESPACE_USERPROFILES + '/': userprofiles_acl,
         NAMESPACE_DEFAULT: default_acl,
     }
     return create_mapping(uri, namespaces, backends, acls)
--- a/MoinMoin/storage/middleware/_tests/test_indexing.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/storage/middleware/_tests/test_indexing.py	Sun Feb 02 17:14:23 2014 +0100
@@ -16,9 +16,11 @@
 from flask import g as flaskg
 
 from MoinMoin.constants.keys import (NAME, SIZE, ITEMID, REVID, DATAID, HASH_ALGORITHM, CONTENT, COMMENT,
-                                     LATEST_REVS, ALL_REVS, NAMESPACE)
+                                     LATEST_REVS, ALL_REVS, NAMESPACE, NAMERE, NAMEPREFIX)
 from MoinMoin.constants.namespaces import NAMESPACE_USERPROFILES
 
+from MoinMoin.util.interwiki import split_fqname
+
 from MoinMoin.auth import GivenAuth
 from MoinMoin._tests import wikiconfig
 
@@ -367,14 +369,30 @@
         assert expected_revid == doc[REVID]
         assert unicode(data) == doc[CONTENT]
 
+    def test_indexing_subscriptions(self):
+        item_name = u"foo"
+        meta = dict(name=[item_name, ], subscriptions=[u"{0}::foo".format(NAME),
+                                                       u"{0}::.*".format(NAMERE)])
+        item = self.imw[item_name]
+        item.store_revision(meta, StringIO(str(item_name)))
+        doc1 = self.imw.document(subscription_ids=u"{0}::foo".format(NAME))
+        doc2 = self.imw.document(subscription_patterns=u"{0}::.*".format(NAMERE))
+        assert doc1 is not None
+        assert doc2 is not None
+        doc3 = self.imw.document(subscription_ids=u"{0}::.*".format(NAMERE))
+        doc4 = self.imw.document(subscription_patterns=u"{0}::foo".format(NAMEPREFIX))
+        assert doc3 is None
+        assert doc4 is None
+
     def test_namespaces(self):
         item_name_n = u'normal'
         item = self.imw[item_name_n]
         rev_n = item.store_revision(dict(name=[item_name_n, ], contenttype=u'text/plain;charset=utf-8'),
                                     StringIO(str(item_name_n)), return_rev=True)
-        item_name_u = u'%s:userprofile' % NAMESPACE_USERPROFILES
-        item = self.imw[item_name_u]
-        rev_u = item.store_revision(dict(name=[item_name_u, ], contenttype=u'text/plain;charset=utf-8'),
+        item_name_u = u'%s/userprofile' % NAMESPACE_USERPROFILES
+        fqname_u = split_fqname(item_name_u)
+        item = self.imw.get_item(**fqname_u.query)
+        rev_u = item.store_revision(dict(name=[fqname_u.value], namespace=fqname_u.namespace, contenttype=u'text/plain;charset=utf-8'),
                                     StringIO(str(item_name_u)), return_rev=True)
         item = self.imw[item_name_n]
         rev_n = item.get_revision(rev_n.revid)
@@ -383,7 +401,7 @@
         item = self.imw[item_name_u]
         rev_u = item.get_revision(rev_u.revid)
         assert rev_u.meta[NAMESPACE] == NAMESPACE_USERPROFILES
-        assert rev_u.meta[NAME] == [item_name_u.split(':')[1]]
+        assert rev_u.meta[NAME] == [item_name_u.split('/')[1]]
 
     def test_parentnames(self):
         item_name = u'child'
--- a/MoinMoin/storage/middleware/_tests/test_validation.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/storage/middleware/_tests/test_validation.py	Sun Feb 02 17:14:23 2014 +0100
@@ -15,6 +15,8 @@
 
 from MoinMoin.util.crypto import make_uuid
 
+from MoinMoin.util.interwiki import CompositeName
+
 
 class TestValidation(object):
     def test_content(self):
@@ -37,7 +39,7 @@
 
         state = {'trusted': False,  # True for loading a serialized representation or other trusted sources
                  keys.NAME: u'somename',  # name we decoded from URL path
-                 keys.ACTION: u'SAVE',
+                 keys.ACTION: keys.ACTION_SAVE,
                  keys.HOSTNAME: u'localhost',
                  keys.ADDRESS: u'127.0.0.1',
                  keys.USERID: make_uuid(),
@@ -49,6 +51,7 @@
                  'acl_parent': u"All:read",
                  'contenttype_current': u'text/x.moin.wiki;charset=utf-8',
                  'contenttype_guessed': u'text/plain;charset=utf-8',
+                 keys.FQNAME: CompositeName(u'', u'', u'somename'),
                 }
 
         m = ContentMetaSchema(meta)
@@ -67,15 +70,29 @@
             keys.NAME: [u"user name", ],
             keys.NAMESPACE: u"userprofiles",
             keys.EMAIL: u"foo@example.org",
+            keys.SUBSCRIPTIONS: [u"{0}:{1}".format(keys.ITEMID, make_uuid()),
+                                 u"{0}::foo".format(keys.NAME),
+                                 u"{0}::bar".format(keys.TAGS),
+                                 u"{0}::".format(keys.NAMERE),
+                                 u"{0}:userprofiles:a".format(keys.NAMEPREFIX),
+                                 ]
+        }
+
+        invalid_meta = {
+            keys.SUBSCRIPTIONS: [u"", u"unknown_tag:123",
+                                 u"{0}:123".format(keys.ITEMID),
+                                 u"{0}:foo".format(keys.NAME),
+                                 ]
         }
 
         state = {'trusted': False,  # True for loading a serialized representation or other trusted sources
                  keys.NAME: u'somename',  # name we decoded from URL path
-                 keys.ACTION: u'SAVE',
+                 keys.ACTION: keys.ACTION_SAVE,
                  keys.HOSTNAME: u'localhost',
                  keys.ADDRESS: u'127.0.0.1',
                  keys.WIKINAME: u'ThisWiki',
                  keys.NAMESPACE: u'',
+                 keys.FQNAME: CompositeName(u'', u'', u'somename')
                 }
 
         m = UserMetaSchema(meta)
@@ -86,3 +103,11 @@
                 print e.valid, e
             print m.valid, m
         assert valid
+
+        m = UserMetaSchema(invalid_meta)
+        valid = m.validate(state)
+        assert not valid
+        for e in m.children:
+            if e.name in (keys.SUBSCRIPTIONS,):
+                for value in e:
+                    assert not value.valid
--- a/MoinMoin/storage/middleware/indexing.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/storage/middleware/indexing.py	Sun Feb 02 17:14:23 2014 +0100
@@ -81,7 +81,7 @@
 from MoinMoin.themes import utctimestamp
 from MoinMoin.storage.middleware.validation import ContentMetaSchema, UserMetaSchema, validate_data
 from MoinMoin.storage.error import NoSuchItemError, ItemAlreadyExistsError
-
+from MoinMoin.util.interwiki import split_fqname, CompositeName
 
 WHOOSH_FILESTORAGE = 'FileStorage'
 INDEXES = [LATEST_REVS, ALL_REVS, ]
@@ -118,9 +118,7 @@
     elif not isinstance(names, list):
         raise TypeError("NAME is not a list but %r - fix this!" % names)
     if not names:
-        # we currently never return an empty list, some code
-        # might not be able to deal with it:
-        names = [u'DoesNotExist', ]
+        names = []
     return names
 
 
@@ -137,6 +135,8 @@
     doc = dict([(key, value)
                 for key, value in meta.items()
                 if key in schema])
+    if SUBSCRIPTION_IDS in schema and SUBSCRIPTIONS in meta:
+        doc[SUBSCRIPTION_IDS], doc[SUBSCRIPTION_PATTERNS] = backend_subscriptions_to_index(meta[SUBSCRIPTIONS])
     for key in [MTIME, PTIME]:
         if key in doc:
             # we have UNIX UTC timestamp (int), whoosh wants datetime
@@ -148,6 +148,25 @@
     return doc
 
 
+def backend_subscriptions_to_index(subscriptions):
+    """ Split subscriptions list to subscription_ids and subscription_patterns lists
+    which match the fields of the whoosh schema
+
+    :param subscriptions: user subscriptions meta
+    :return: tuple containing a list of subscription_ids and a list of
+             subscription_patterns
+    """
+    subscription_ids = []
+    subscription_patterns = []
+    for subscription in subscriptions:
+        keyword = subscription.split(':')[0]
+        if keyword in (ITEMID, NAME, TAGS, ):
+            subscription_ids.append(subscription)
+        elif keyword in (NAMERE, NAMEPREFIX, ):
+            subscription_patterns.append(subscription)
+    return subscription_ids, subscription_patterns
+
+
 from MoinMoin.util.mime import Type, type_moin_document
 from MoinMoin.util.tree import moin_page
 from MoinMoin.converter import default_registry
@@ -168,6 +187,8 @@
                    metadata as a side effect
     :returns: indexable content, text/plain, unicode object
     """
+    fqname = split_fqname(item_name)
+
     class PseudoRev(object):
         def __init__(self, meta, data):
             self.meta = meta
@@ -175,9 +196,10 @@
             self.revid = meta.get(REVID)
 
             class PseudoItem(object):
-                def __init__(self, name):
-                    self.name = name
-            self.item = PseudoItem(item_name)
+                def __init__(self, fqname):
+                    self.fqname = fqname
+                    self.name = fqname.value
+            self.item = PseudoItem(fqname)
 
         def read(self, *args, **kw):
             return self.data.read(*args, **kw)
@@ -193,7 +215,10 @@
         return u''
 
     if not item_name:
-        item_name = get_names(meta)[0]
+        try:
+            item_name = get_names(meta)[0]
+        except IndexError:
+            item_name = u'DoesNotExist'
 
     rev = PseudoRev(meta, data)
     try:
@@ -298,6 +323,8 @@
             SUMMARY: TEXT(stored=True),
             # DATAID from metadata
             DATAID: ID(stored=True),
+            # TRASH from metadata
+            TRASH: BOOLEAN(stored=True),
             # data (content), converted to text/plain and tokenized
             CONTENT: TEXT(stored=True),
         }
@@ -322,6 +349,9 @@
             EMAIL: ID(stored=True),
             OPENID: ID(stored=True),
             DISABLED: BOOLEAN(stored=True),
+            LOCALE: ID(stored=True),
+            SUBSCRIPTION_IDS: ID(),
+            SUBSCRIPTION_PATTERNS: ID(),
         }
         latest_revs_fields.update(**userprofile_fields)
 
@@ -808,7 +838,98 @@
         return Item.existing(self, **query)
 
 
-class Item(object):
+class PropertiesMixin(object):
+    """
+    PropertiesMixin offers methods to find out some additional information from meta.
+    """
+    @property
+    def name(self):
+        if self._name and self._name in self.names:
+            name = self._name
+        else:
+            try:
+                name = self.names[0]
+            except IndexError:
+                # empty name list, no name:
+                name = None
+        assert isinstance(name, unicode) or not name
+        return name
+
+    @property
+    def namespace(self):
+        return self.meta.get(NAMESPACE, u'')
+
+    def _fqname(self, name=None):
+        """
+        return the fully qualified name including the namespace: NS:NAME
+        """
+        if name is not None:
+            return CompositeName(self.namespace, NAME_EXACT, name)
+        else:
+            return CompositeName(self.namespace, ITEMID, self.meta[ITEMID])
+
+    @property
+    def fqname(self):
+        """
+        return the fully qualified name including the namespace: NS:NAME
+        """
+        return self._fqname(self.name)
+
+    @property
+    def fqnames(self):
+        """
+        return the fully qualified names including the namespace: NS:NAME
+        """
+        if self.names:
+            return [self._fqname(name) for name in self.names]
+        else:
+            return [self.fqname]
+
+    @property
+    def parentnames(self):
+        """
+        compute list of parent names (same order as in names, but no dupes)
+
+        :return: parent names (list of unicode)
+        """
+        parent_names = []
+        for name in self.names:
+            parentname_tail = name.rsplit('/', 1)
+            if len(parentname_tail) == 2:
+                parent_name = parentname_tail[0]
+                if parent_name not in parent_names:
+                    parent_names.append(parent_name)
+        return parent_names
+
+    @property
+    def fqparentnames(self):
+        """
+        return the fully qualified parent names including the namespace: NS:NAME
+        """
+        return [self._fqname(name) for name in self.parentnames]
+
+    @property
+    def acl(self):
+        return self.meta.get(ACL)
+
+    @property
+    def ptime(self):
+        dt = self.meta.get(PTIME)
+        if dt is not None:
+            return utctimestamp(dt)
+
+    @property
+    def names(self):
+        return get_names(self.meta)
+
+    @property
+    def mtime(self):
+        dt = self.meta.get(MTIME)
+        if dt is not None:
+            return utctimestamp(dt)
+
+
+class Item(PropertiesMixin):
     def __init__(self, indexer, latest_doc=None, **query):
         """
         :param indexer: indexer middleware instance
@@ -830,11 +951,10 @@
                 # avoid issues in the name(s) property code. if this was a
                 # lookup for some specific item (using a name_exact query), we
                 # put that name into the NAME list, otherwise it'll be empty:
-                if self._name is not None:
-                    names = [self._name, ]
-                else:
-                    names = []
-                latest_doc = {NAME: names}
+                latest_doc = {}
+                for field, value in query.items():
+                    latest_doc[field] = [value] if field in UFIELDS_TYPELIST else value
+                latest_doc[NAME] = latest_doc[NAME_EXACT] if NAME_EXACT in query else []
         self._current = latest_doc
 
     def _get_itemid(self):
@@ -845,38 +965,8 @@
     itemid = property(_get_itemid, _set_itemid)
 
     @property
-    def acl(self):
-        return self._current.get(ACL)
-
-    @property
-    def namespace(self):
-        return self._current.get(NAMESPACE)
-
-    @property
-    def ptime(self):
-        dt = self._current.get(PTIME)
-        if dt is not None:
-            return utctimestamp(dt)
-
-    @property
-    def names(self):
-        return get_names(self._current)
-
-    @property
-    def parentnames(self):
-        """
-        compute list of parent names (same order as in names, but no dupes)
-
-        :return: parent names (list of unicode)
-        """
-        parent_names = []
-        for name in self.names:
-            parentname_tail = name.rsplit('/', 1)
-            if len(parentname_tail) == 2:
-                parent_name = parentname_tail[0]
-                if parent_name not in parent_names:
-                    parent_names.append(parent_name)
-        return parent_names
+    def meta(self):
+        return self._current
 
     @property
     def parentids(self):
@@ -892,59 +982,6 @@
                 parent_ids.add(rev[ITEMID])
         return parent_ids
 
-    @property
-    def mtime(self):
-        dt = self._current.get(MTIME)
-        if dt is not None:
-            return utctimestamp(dt)
-
-    @property
-    def name(self):
-        if self._name and self._name in self.names:
-            name = self._name
-        else:
-            try:
-                name = self.names[0]
-            except IndexError:
-                # empty name list, no name:
-                name = None
-        assert name is None or isinstance(name, unicode)
-        return name
-
-    def _fqname(self, name):
-        """
-        return the fully qualified name including the namespace: NS:NAME
-        """
-        ns = self.namespace
-        name = name or u''
-        if ns:
-            fqn = ns + u':' + name
-        else:
-            fqn = name
-        assert isinstance(fqn, unicode)
-        return fqn
-
-    @property
-    def fqname(self):
-        """
-        return the fully qualified name including the namespace: NS:NAME
-        """
-        return self._fqname(self.name)
-
-    @property
-    def fqnames(self):
-        """
-        return the fully qualified names including the namespace: NS:NAME
-        """
-        return [self._fqname(name) for name in self.names]
-
-    @property
-    def fqparentnames(self):
-        """
-        return the fully qualified parent names including the namespace: NS:NAME
-        """
-        return [self._fqname(name) for name in self.parentnames]
-
     @classmethod
     def create(cls, indexer, **query):
         """
@@ -1002,7 +1039,7 @@
     def store_revision(self, meta, data, overwrite=False,
                        trusted=False,  # True for loading a serialized representation or other trusted sources
                        name=None,  # TODO name we decoded from URL path
-                       action=u'SAVE',
+                       action=ACTION_SAVE,
                        remote_addr=None,
                        userid=None,
                        wikiname=None,
@@ -1010,6 +1047,7 @@
                        contenttype_guessed=None,
                        acl_parent=None,
                        return_rev=False,
+                       fqname=None,
                        ):
         """
         Store a revision into the backend, write metadata and data to it.
@@ -1049,6 +1087,7 @@
                  'contenttype_current': contenttype_current,
                  'contenttype_guessed': contenttype_guessed,
                  'acl_parent': acl_parent,
+                 FQNAME: fqname,
                 }
         ct = meta.get(CONTENTTYPE)
         if ct == CONTENTTYPE_USER:
@@ -1122,7 +1161,7 @@
             self.destroy_revision(rev.revid)
 
 
-class Revision(object):
+class Revision(PropertiesMixin):
     """
     An existing revision (exists in the backend).
     """
@@ -1153,21 +1192,6 @@
         # Note: this does not immediately raise a KeyError for non-existing revs any more
         # If you access data or meta, it will, though.
 
-    @property
-    def names(self):
-        return get_names(self.meta)
-
-    @property
-    def name(self):
-        name = self._name
-        if name is None:
-            try:
-                name = self.names[0]
-            except IndexError:
-                # empty name list, no name:
-                name = None
-        return name
-
     def set_context(self, context):
         for name in self.names:
             if name.startswith(context):
--- a/MoinMoin/storage/middleware/protecting.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/storage/middleware/protecting.py	Sun Feb 02 17:14:23 2014 +0100
@@ -26,6 +26,8 @@
 
 from MoinMoin.security import AccessControlList
 
+from MoinMoin.util.interwiki import split_fqname
+
 # max sizes of some lru caches:
 LOOKUP_CACHE = 100  # ACL lookup for some itemname
 PARSE_CACHE = 100  # ACL string -> ACL object parsing
@@ -79,19 +81,20 @@
         # ACL lookups afterwards will fetch fresh info from the lower layers.
         self.get_acls.cache_clear()
 
-    def _get_configured_acls(self, itemname):
+    def _get_configured_acls(self, fqname):
         """
         for a fully-qualified itemname (namespace:name), get the acl configuration
         for that (part of the) namespace.
 
-        @param itemname: fully qualified itemname
+        @param fqname: fully qualified itemname
         @returns: acl configuration (acl dict from the acl_mapping)
         """
+        itemname = fqname.value if fqname.field == NAME_EXACT else u''
         for prefix, acls in self.acl_mapping:
             if itemname.startswith(prefix):
                 return acls
         else:
-            raise ValueError('No acl_mapping entry found for item {0!r}'.format(itemname))
+            raise ValueError('No acl_mapping entry found for item {0!r}'.format(fqname))
 
     def _get_acls(self, itemid=None, fqname=None):
         """
@@ -109,7 +112,7 @@
         elif fqname is not None:
             # itemid might be None for new, not yet stored items,
             # but we have fqname then
-            q = {NAME_EXACT: fqname}
+            q = fqname.query
         else:
             raise ValueError("need itemid or fqname")
         item = self.get_item(**q)
@@ -183,16 +186,17 @@
         item = self.indexer.existing_item(**query)
         return ProtectedItem(self, item)
 
-    def may(self, itemname, capability, usernames=None):
+    def may(self, fqname, capability, usernames=None):
         if usernames is not None and isinstance(usernames, (str, unicode)):
             # we got a single username (maybe str), make a list of unicode:
             if isinstance(usernames, str):
                 usernames = usernames.decode('utf-8')
             usernames = [usernames, ]
-        if isinstance(itemname, list):
-            # if we get a list of names, just use first one to fetch item
-            itemname = itemname[0]
-        item = self[itemname]
+        # TODO Make sure that fqname must be a CompositeName class instance, not unicode or list.
+        fqname = fqname[0] if isinstance(fqname, list) else fqname
+        if isinstance(fqname, unicode):
+            fqname = split_fqname(fqname)
+        item = self.get_item(**fqname.query)
         allowed = item.allows(capability, user_names=usernames)
         return allowed
 
@@ -227,6 +231,14 @@
         return self.item.name
 
     @property
+    def fqname(self):
+        return self.item.fqname
+
+    @property
+    def fqnames(self):
+        return self.item.fqnames
+
+    @property
     def acl(self):
         return self.item.acl
 
@@ -291,13 +303,13 @@
     def get_revision(self, revid):
         return self[revid]
 
-    def store_revision(self, meta, data, overwrite=False, return_rev=False, **kw):
+    def store_revision(self, meta, data, overwrite=False, return_rev=False, fqname=None, **kw):
         self.require(WRITE)
         if not self:
             self.require(CREATE)
         if overwrite:
             self.require(DESTROY)
-        rev = self.item.store_revision(meta, data, overwrite=overwrite, return_rev=return_rev, **kw)
+        rev = self.item.store_revision(meta, data, overwrite=overwrite, return_rev=return_rev, fqname=fqname, **kw)
         self.protector._clear_acl_cache()
         if return_rev:
             return ProtectedRevision(self.protector, rev, p_item=self)
@@ -348,6 +360,14 @@
         return self.rev.name
 
     @property
+    def fqname(self):
+        return self.rev.fqname
+
+    @property
+    def fqnames(self):
+        return self.rev.fqnames
+
+    @property
     def meta(self):
         self.require(READ, PUBREAD)
         return self.rev.meta
--- a/MoinMoin/storage/middleware/validation.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/storage/middleware/validation.py	Sun Feb 02 17:14:23 2014 +0100
@@ -25,6 +25,7 @@
 from __future__ import absolute_import, division
 
 import time
+import re
 
 from flatland import Dict, List, Unset, Boolean, Integer, String
 
@@ -61,7 +62,8 @@
     an itemid is a uuid that identifies an item
     """
     if not state['trusted'] or element.raw is Unset:
-        itemid = state.get(keys.ITEMID)
+        fqname = state[keys.FQNAME]
+        itemid = fqname.value if fqname and fqname.field == keys.ITEMID else state.get(keys.ITEMID)
         if itemid is None:
             # this is first revision of an item
             itemid = make_uuid()
@@ -215,7 +217,8 @@
     v = element.value
     if not isinstance(v, unicode):
         return False
-    if v not in [u'SAVE', u'REVERT', u'TRASH', u'COPY', u'RENAME', ]:
+    if v not in [keys.ACTION_SAVE, keys.ACTION_REVERT, keys.ACTION_TRASH,
+                 keys.ACTION_COPY, keys.ACTION_RENAME]:
         return False
     return True
 
@@ -318,6 +321,55 @@
     except ValueError:
         return False
 
+
+def subscription_validator(element, state):
+    """
+    a subscription
+    """
+    try:
+        keyword, value = element.value.split(":", 1)
+    except ValueError:
+        element.add_error("Subscription must contain colon delimiters.")
+        return False
+
+    if keyword in (keys.ITEMID, ):
+        value_element = String(value)
+        valid = uuid_validator(value_element, state)
+    elif keyword in (keys.NAME, keys.TAGS, keys.NAMERE, keys.NAMEPREFIX, ):
+        try:
+            namespace, value = value.split(":", 1)
+        except ValueError:
+            element.add_error("Subscription must contain 2 colon delimiters.")
+            return False
+        namespace_element = String(namespace)
+        if not namespace_validator(namespace_element, state):
+            element.add_error("Not a valid namespace value.")
+            return False
+    else:
+        element.add_error(
+            "Subscription must start with one of the keywords: "
+            "'{0}', '{1}', '{2}', '{3}' or '{4}'.".format(keys.ITEMID,
+                                                          keys.NAME, keys.TAGS,
+                                                          keys.NAMERE,
+                                                          keys.NAMEPREFIX))
+        return False
+
+    value_element = String(value)
+    if keyword == keys.TAGS:
+        valid = tag_validator(value_element, state)
+    elif keyword in (keys.NAME, keys.NAMEPREFIX, ):
+        valid = name_validator(value_element, state)
+    elif keyword == keys.NAMERE:
+        try:
+            re.compile(value, re.U)
+            valid = True
+        except re.error:
+            valid = False
+    if not valid:
+        element.add_error("Subscription has invalid value.")
+    return valid
+
+
 common_meta = (
     String.named(keys.ITEMID).validated_by(itemid_validator),
     String.named(keys.REVID).validated_by(revid_validator),
@@ -368,7 +420,7 @@
     Boolean.named(keys.SCROLL_PAGE_AFTER_EDIT).using(optional=True),
     Boolean.named(keys.MAILTO_AUTHOR).using(optional=True),
     List.named(keys.QUICKLINKS).of(String.named('quicklinks')).using(optional=True),
-    List.named(keys.SUBSCRIBED_ITEMS).of(String.named('subscribed_item')).using(optional=True),
+    List.named(keys.SUBSCRIPTIONS).of(String.named('subscription').validated_by(subscription_validator)).using(optional=True),
     List.named(keys.EMAIL_SUBSCRIBED_EVENTS).of(String.named('email_subscribed_event')).using(optional=True),
     #TODO: DuckDict.named('bookmarks').using(optional=True),
     *common_meta
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/all.html	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,9 @@
+{% extends theme("layout.html") %}
+{% block content %}
+<h1>{{ _("Global Views.") }}</h1>
+<ul>
+    <li><a href="{{ url_for('frontend.global_history', namespace='all') }}">{{ _("History") }}</a></li>
+    <li><a href="{{ url_for('frontend.global_tags', namespace='all') }}">{{ _("Tags") }}</a></li>
+    <li><a href="{{ url_for('frontend.index', item_name='all') }}">{{ _("Index") }}</a></li>
+</ul>
+{% endblock %}
--- a/MoinMoin/templates/base.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/base.html	Sun Feb 02 17:14:23 2014 +0100
@@ -48,7 +48,7 @@
     {% block icons_stylesheet %}
     <link rel="stylesheet" href="{{ url_for('serve.files', name='font_awesome', filename='css/font-awesome.css') }}" />
     {% endblock %}
-    
+
     <link rel="shortcut icon" href="{{ url_for('static', filename='logos/favicon.ico') }}" />
 
     {% block theme_stylesheets %}
@@ -75,9 +75,6 @@
 </div>
 
 {% block body_scripts %} {# js before </body> reduces IE8 js errors related to svgweb #}
-    <!--[if IE 8]>
-        <script src="{{ url_for('serve.files', name='svgweb', filename='svg.js') }}"></script>
-    <![endif]-->
     <script src="{{ url_for('serve.files', name='jquery', filename='jquery.min.js') }}"></script>
     <script src="{{ url_for('serve.files', name='bootstrap', filename='js/bootstrap.min.js') }}"></script>
     <script src="{{ url_for('frontend.template', filename='common.js') }}"></script>
@@ -87,10 +84,6 @@
         {# TODO: use a local copy later #}
         <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
     <![endif]-->
-    <!--[if lt IE 8]>
-        {# required to save user settings with IE7 and earlier #}
-        <script src="{{ url_for('serve.files', name='json_js', filename='json2.js') }}"></script>
-    <![endif]-->
 {% endblock %}
 
 </body>
--- a/MoinMoin/templates/common.js	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/common.js	Sun Feb 02 17:14:23 2014 +0100
@@ -235,6 +235,21 @@
 
 
 
+// OnMouseOver show the fqname of the item else only show the value/id.
+function togglefqname(){
+    "use strict";
+    var fullname, value;
+    $(".moin-fqname").hover(function () {
+        fullname = $(this).attr('data-fqname');
+        value = $(this).html();
+        $(this).html(fullname);
+    },function () {
+        $(this).html(value);
+    });
+}
+$(document).ready(togglefqname);
+
+
 // Executed when user clicks insert-name button defined in modify.html.
 // When a page with subitems is modified, a subitems sidebar is present. User may
 // position caret in textarea and click button to insert name into textarea.
--- a/MoinMoin/templates/delete.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/delete.html	Sun Feb 02 17:14:23 2014 +0100
@@ -1,12 +1,12 @@
 {% import "forms.html" as forms %}
 {% extends theme("show.html") %}
 
-{% set title = _("Delete '%(item_name)s'", item_name=item.name) %}
+{% set title = _("Delete '%(item_name)s'", item_name=fqname.value) %}
 
 {% block content %}
 <h1>{{ title }}</h1>
 <div class="moin-form">
-{{ gen.form.open(form, method="post", action=url_for('frontend.delete_item', item_name=item.name)) }}
+{{ gen.form.open(form, method="post", action=url_for('frontend.delete_item', item_name=fqname)) }}
   {{ forms.render_errors(form) }}
   <dl>
     {{ forms.render(form['comment']) }}
--- a/MoinMoin/templates/destroy.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/destroy.html	Sun Feb 02 17:14:23 2014 +0100
@@ -1,13 +1,13 @@
 {% import "forms.html" as forms %}
 {% extends theme("show.html") %}
 
-{% set title = _("DESTROY COMPLETE ITEM '%(item_name)s'", item_name=item.name) %}
+{% set title = _("DESTROY COMPLETE ITEM '%(item_name)s'", item_name=fqname.value) %}
 
 {% block content %}
 {% if rev_id == None %}
     <h1>{{ title }}</h1>
     <div class="moin-form">
-    {{ gen.form.open(form, method="post", action=url_for('frontend.destroy_item', item_name=item.name)) }}
+    {{ gen.form.open(form, method="post", action=url_for('frontend.destroy_item', item_name=fqname.fullname)) }}
       {{ forms.render_errors(form) }}
       <dl>
         {{ forms.render(form['comment']) }}
@@ -19,7 +19,7 @@
 {% else %}
     <h1>{{ _("DESTROY REVISION '%(item_name)s' (rev %(rev_id)s)", item_name=item.name, rev_id=rev_id | shorten_id) }}</h1>
     <div class="moin-form">
-    {{ gen.form.open(form, method="post", action=url_for('frontend.destroy_item', item_name=item.name, rev=rev_id)) }}
+    {{ gen.form.open(form, method="post", action=url_for('frontend.destroy_item', item_name=fqname.fullname, rev=rev_id)) }}
       {{ forms.render_errors(form) }}
       <dl>
         {{ forms.render(form['comment']) }}
--- a/MoinMoin/templates/global_history.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/global_history.html	Sun Feb 02 17:14:23 2014 +0100
@@ -28,13 +28,13 @@
                                 <span class="moin-history-{{ meta.action|lower }}"></span>
                             </td>
                             <td class="moin-history-links">
-                                <a href="{{ url_for('frontend.history', item_name=meta.name[0], bookmark=bookmark_time) }}">HIST</a>
+                                <a href="{{ url_for('frontend.history', item_name=rev.fqname, bookmark=bookmark_time) }}">HIST</a>
                                 {% if bookmark_time -%}
-                                    <a href="{{ url_for('frontend.diff', item_name=meta.name[0], bookmark=bookmark_time) }}">DIFF</a>
+                                    <a href="{{ url_for('frontend.diff', item_name=rev.fqname, bookmark=bookmark_time) }}">DIFF</a>
                                 {%- endif %}
                             </td>
                             <td class="moin-history-time">{{ meta.mtime|timeformat }}</td>
-                            <td class="moin-history-item"><a class="{{ meta.contenttype|contenttype_to_class }}" href="{{ url_for('frontend.show_item', item_name=meta.name[0]) }}" title="{{ meta.contenttype }}">{{ meta.name|join(' | ') }}</a></td>
+                            <td class="moin-history-item"><a class="{{ meta.contenttype|contenttype_to_class }}" href="{{ url_for('frontend.show_item', item_name=rev.fqname) }}" title="{{ meta.contenttype }}">{{ meta.name|join(' | ') }}</a></td>
                         </tr>
                     {% endfor %}
                     </table>
--- a/MoinMoin/templates/global_tags.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/global_tags.html	Sun Feb 02 17:14:23 2014 +0100
@@ -6,7 +6,7 @@
 {% if tags %}
 <ul class="moin-tags">
     {% for cls, tag in tags %}
-    <li class="{{ cls }}"><a href="{{ url_for('frontend.tagged_items', tag=tag) }}">{{ tag }}</a></li>
+    <li class="{{ cls }}"><a href="{{ url_for('frontend.tagged_items', tag=tag, namespace=fqname.namespace) }}">{{ tag }}</a></li>
     {% endfor %}
 </ul>
 {% endif %}
--- a/MoinMoin/templates/history.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/history.html	Sun Feb 02 17:14:23 2014 +0100
@@ -1,7 +1,7 @@
 {% extends theme("layout.html") %}
 {% import "utils.html" as utils %}
 
-{% set title = _("History of '%(item_name)s'", item_name=item_name) %}
+{% set title = _("History of '%(fqname)s'", fqname=fqname|shorten_fqname) %}
 
 {% block content %}
     {% set (history, next_offset, previous_offset) = history_page %}
@@ -21,7 +21,12 @@
         <table class="zebra">
             <thead>
                 <tr>
-                    <th>{{ _("Name") }}</th>
+                    {% if fqname.field == 'name_exact' %}
+                        <th>{{ _("Item ID") }}</th> 
+                    {% else %}
+                        <th>{{ _("Name") }}</th>
+                    {% endif %}
+
                     <th>{{ _("Rev.") }}</th>
                     <th>{{ _("Timestamp") }}</th>
                     <th class="moin-integer">{{ _("Size") }}</th>
@@ -35,8 +40,19 @@
             <tbody>
                 {% for doc in history %}
                 <tr>
-                    <td class="moin-wordbreak">{{ doc.name|join(' | ') }}</td>
-                    <td>{{ doc.revid | shorten_id }}</td>
+                    {% if fqname.field == 'name_exact' %}
+                        <td class="moin-wordbreak">
+                            <a href="{{ url_for_item(doc.itemid, field='itemid', namespace=fqname.namespace, endpoint='frontend.history') }}">{{ doc.itemid|shorten_id }}</a>
+                        </td>
+                    {% else %}
+                        <td class="moin-wordbreak">
+                            {% for name in doc.name %}
+                                {% if not loop.first %}|{% endif %}
+                                <a href="{{ url_for_item(name, namespace=fqname.namespace, endpoint='frontend.history') }}">{{ name }}</a>
+                            {% endfor %}
+                        </td>
+                    {% endif %}
+                    <td>{{ doc.revid|shorten_id }}</td>
                     <td>{{ doc.mtime|datetimeformat }}</td>
                     <td class="moin-integer">{{ doc.size }}</td>
                     <td>
@@ -48,15 +64,15 @@
                     <td class="moin-wordbreak">{{ utils.editor_info(doc) }}</td>
                     <td class="moin-wordbreak">{{ doc.contenttype }}</td>
                     <td class="moin-wordbreak">{{ doc.comment }}</td>
-                    <td><a href="{{ url_for('frontend.show_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('show') }}</a></td>
-                    <td><a href="{{ url_for('frontend.show_item_meta', item_name=doc.name[0], rev=doc.revid) }}">{{ _('meta') }}</a></td>
-                    <td><a href="{{ url_for('frontend.download_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('download') }}</a></td>
-                    <td><a href="{{ url_for('frontend.highlight_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('highlight') }}</a></td>
-                    {% if user.may.write(item_name) -%}
-                    <td><a href="{{ url_for('frontend.revert_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('revert') }}</a></td>
+                    <td><a href="{{ url_for('frontend.show_item', item_name=fqname, rev=doc.revid) }}">{{ _('show') }}</a></td>
+                    <td><a href="{{ url_for('frontend.show_item_meta', item_name=fqname, rev=doc.revid) }}">{{ _('meta') }}</a></td>
+                    <td><a href="{{ url_for('frontend.download_item', item_name=fqname, rev=doc.revid) }}">{{ _('download') }}</a></td>
+                    <td><a href="{{ url_for('frontend.highlight_item', item_name=fqname, rev=doc.revid) }}">{{ _('highlight') }}</a></td>
+                    {% if user.may.write(fqname) -%}
+                        <td><a href="{{ url_for('frontend.revert_item', item_name=fqname, rev=doc.revid) }}">{{ _('revert') }}</a></td>
                     {%- endif %}
-                    {% if user.may.destroy(item_name) -%}
-                    <td><a href="{{ url_for('frontend.destroy_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('destroy') }}</a></td>
+                    {% if user.may.destroy(fqname) -%}
+                        <td><a href="{{ url_for('frontend.destroy_item', item_name=fqname, rev=doc.revid) }}">{{ _('destroy') }}</a></td>
                     {%- endif %}
                 </tr>
                 {% endfor %}
--- a/MoinMoin/templates/index.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/index.html	Sun Feb 02 17:14:23 2014 +0100
@@ -120,7 +120,11 @@
             {% for i in range(0, item_names|count) %}
                 {% set fullname = item_names[:i+1]|join('/') %}
                 {% set relname = item_names[i] %}
-                <a href="{{ url_for('frontend.index', item_name=fullname) }}" title="{{ relname }}">{{ relname }}</a>
+                {% if relname.startswith('@') %}
+                    {{ relname }}
+                {% else %}
+                    <a href="{{ url_for('frontend.index', item_name=fullname) }}" title="{{ relname }}">{{ relname }}</a>
+                {% endif %}
                 <span class="moin-path-separator">{{ ("/") }}</span>
             {% endfor %}
         </div>
--- a/MoinMoin/templates/itemviews.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/itemviews.html	Sun Feb 02 17:14:23 2014 +0100
@@ -1,4 +1,4 @@
-{% set exists = storage.has_item(item_name) %}
+{% set exists = fqname and storage.get_item(**fqname.query) %}
 <ul class="moin-itemviews panel">
     {%- for endpoint, label, title, check_exists in cfg.item_views if not endpoint in cfg.endpoints_excluded %}
         {%- if not check_exists or check_exists and exists %}
@@ -9,27 +9,26 @@
                 'frontend.similar_names', 'frontend.copy_item',
             ] %}
                 <li>
-                    <a href="{{ url_for(endpoint, item_name=item_name) }}" title="{{ title }}" rel="nofollow">{{ label }}</a>
+                    <a href="{{ url_for(endpoint, item_name=fqname) }}" title="{{ title }}" rel="nofollow">{{ label }}</a>
                 </li>
             {%- endif %}
-
-            {%- if endpoint == 'frontend.modify_item' and user.may.write(item_name) %}
+            {%- if endpoint == 'frontend.modify_item' and user.may.write(fqname) %}
                 <li>
-                    <a class="moin-modify-button" href="{{ url_for(endpoint, item_name=item_name) }}" title="{{ title }}" rel="nofollow">{{ label }}</a>
+                    <a class="moin-modify-button" href="{{ url_for(endpoint, item_name=fqname) }}" title="{{ title }}" rel="nofollow">{{ label }}</a>
                 </li>
             {%- endif %}
 
             {%- if endpoint in [
                 'frontend.rename_item', 'frontend.delete_item',
-            ] and user.may.write(item_name) %}
+            ] and user.may.write(fqname) %}
                 <li>
-                    <a href="{{ url_for(endpoint, item_name=item_name) }}" title="{{ title }}" rel="nofollow">{{ label }}</a>
+                    <a href="{{ url_for(endpoint, item_name=fqname) }}" title="{{ title }}" rel="nofollow">{{ label }}</a>
                 </li>
             {%- endif %}
 
-            {%- if endpoint == 'frontend.destroy_item' and user.may.destroy(item_name) %}
+            {%- if endpoint == 'frontend.destroy_item' and user.may.destroy(fqname) %}
                 <li>
-                    <a href="{{ url_for(endpoint, item_name=item_name) }}" title="{{ title }}" rel="nofollow">{{ label }}</a>
+                    <a href="{{ url_for(endpoint, item_name=fqname) }}" title="{{ title }}" rel="nofollow">{{ label }}</a>
                 </li>
             {%- endif %}
 
@@ -44,8 +43,8 @@
 
             {%- if endpoint == 'frontend.quicklink_item' and user.valid %}
                 <li>
-                    <a href="{{ url_for(endpoint, item_name=item_name) }}" title="{{ title }}" rel="nofollow">
-                        {%- if user.is_quicklinked_to([item_name]) %}
+                    <a href="{{ url_for(endpoint, item_name=fqname) }}" title="{{ title }}" rel="nofollow">
+                        {%- if user.is_quicklinked_to([fqname]) %}
                             {{ _('Remove Link') }}
                         {%- else %}
                             {{ _('Add Link') }}
@@ -56,8 +55,8 @@
 
             {%- if endpoint == 'frontend.subscribe_item' and user.valid %}
                 <li>
-                    <a href="{{ url_for(endpoint, item_name=item_name) }}" title="{{ title }}" rel="nofollow">
-                        {%- if user.is_subscribed_to([item_name]) %}
+                    <a href="{{ url_for(endpoint, item_name=fqname) }}" title="{{ title }}" rel="nofollow">
+                        {%- if user.is_subscribed_to(item) %}
                             {{ _('Unsubscribe') }}
                         {%- else %}
                             {{ _('Subscribe') }}
@@ -77,12 +76,11 @@
             <a href="#" onClick="toggleTransclusionOverlays();return false;" title="{{ title }}">{{ label }}</a>
             </li>
         {%- endif %}
-
-            {%- if endpoint == 'special.supplementation' %}
+            {%- if endpoint == 'special.supplementation' and fqname %}
                 {%- for sub_item_name in cfg.supplementation_item_names %}
                     {%- set current_sub = item_name.rsplit('/', 1)[-1] %}
                     {%- if not current_sub in cfg.supplementation_item_names %}
-                        {%- set supp_name = '%s/%s' % (item_name, sub_item_name) %}
+                        {%- set supp_name = '%s/%s/%s' % (fqname.namespace, item_name, sub_item_name) %}
                         {%- if storage.has_item(supp_name) or user.may.write(supp_name) %}
                             <li>
                                 <a href="{{ url_for('frontend.show_item', item_name=supp_name) }}" rel="nofollow">{{ _(sub_item_name) }}</a>
--- a/MoinMoin/templates/layout.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/layout.html	Sun Feb 02 17:14:23 2014 +0100
@@ -24,7 +24,7 @@
 
     {% if logo %}
     <div id="moin-logo">
-        <a href="{{ url_for('frontend.show_item', item_name=cfg.item_root) }}">{{ logo }}</a>
+        <a href="{{ url_for('frontend.show_item', item_name=cfg.root_mapping.get('', cfg.default_root)) }}">{{ logo }}</a>
     </div>
     {% endif %}
 
@@ -62,7 +62,7 @@
     <div id="moin-locationline">
         {% if cfg.show_interwiki -%}
         <span id="moin-interwiki">
-            <a href="{{ url_for('frontend.show_item', item_name=cfg.item_root) }}" rel="nofollow">
+            <a href="{{ url_for('frontend.show_item', item_name=cfg.root_mapping['']) }}" rel="nofollow">
                 {% if cfg.interwikiname %}{{ cfg.interwikiname }}{% else %}Self{% endif %}
             </a>
             <span class="sep">: </span>
@@ -71,22 +71,43 @@
         <span id="moin-pagelocation">
             <span class="moin-pagepath">
             {% block pagepath %}
-                {% for segment_name, segment_path, exists in theme_supp.location_breadcrumbs(item_name) -%}
-                    {% if not loop.last -%}
-                        <a href="{{ url_for('frontend.show_item', item_name=segment_path) }}" {% if not exists %}class="moin-nonexistent"{% endif %}>
-                            {{ segment_name|shorten_item_name }}
-                        </a>
-                        <span class="sep">/</span>
+            {% for segment_name, segment_path, exists in theme_supp.location_breadcrumbs(fqname) -%}
+                {% if loop.first %}
+                    <ul class="moin-bcs-item">
+                        <li>
+                            <a href="{{ url_for('frontend.show_item', item_name=segment_path) }}" {% if not exists %}class="moin-nonexistent"{% endif %}>{{ segment_name|shorten_fqname }}</a>
+                            <ul class="moin-bcs-subitems">
+                                {% for namespace, root in theme_supp.get_namespaces(segment_name) -%}
+                                    <li><a href="{{ url_for('frontend.show_item', item_name=root) }}">{{ namespace }}</a></li>
+                                {%- endfor %}
+                                {% if fqname and fqname.namespace != "all" %}
+                                    <li><a href="{{ url_for('frontend.global_views') }}">{{ _("all") }}</a></li>
+                                {%- endif %}
+                           </ul>
+                        </li>
+                    </ul>
+                    <span class="sep">/</span>
+                {% elif not loop.last %}
+                    <a href="{{ url_for('frontend.show_item', item_name=segment_path) }}" {% if not exists %}class="moin-nonexistent"{% endif %}>{{ segment_name|shorten_fqname }}</a>
+                    <span class="sep">/</span>
+                {%- endif %}
+                {% if loop.last %}
+                    {% if title_name %}
+                        {{ title_name }}
                     {% else %}
-                        {% if title_name %}
-                            {{ title_name }}
-                        {% else %}
-                        <a href="{{ url_for('frontend.show_item', item_name=segment_path) }}" {% if not exists %}class="moin-nonexistent"{% endif %}>
-                            {{ segment_name|shorten_item_name }}
-                        </a>
-                        {%- endif %}
-                    {%- endif %}
-                {%- endfor %}
+                        <ul class="moin-bcs-item">
+                            <li><a href="{{ url_for('frontend.show_item', item_name=segment_path) }}" {% if not exists %}class="moin-nonexistent"{% endif %}>
+                                {{ segment_name|shorten_fqname }}</a>
+                                <ul class="moin-bcs-subitems">
+                                    {% for fq_name in theme_supp.get_fqnames(segment_name) -%}
+                                        <li><a href="{{ url_for('frontend.show_item', item_name=fq_name) }}">{{ fq_name|shorten_fqname(length=25) }}</a></li>
+                                    {%- endfor %}
+                                </ul>
+                            </li>
+                        </ul>
+                     {%- endif %}
+                {%- endif %}
+            {%- endfor %}
             {% endblock %}
             </span>
         </span>
@@ -95,17 +116,17 @@
     {% set trail_items = theme_supp.path_breadcrumbs() %}
     {% if trail_items %}
         <div id="moin-pagetrail">
-        {% for wiki_name, item_name, item_href, exists, err in trail_items %}
+        {% for wiki_name, fqname, item_href, exists, err in trail_items %}
             {%- if wiki_name -%}
                 <a href="{{ item_href }}"{{ " " }}
                    title="{{ wiki_name }}"
                    class="{% if err %}moin-badinterwiki{% else %}moin-interwiki{% endif %}">
-                   {{ item_name|shorten_item_name }}
+                   {{ fqname|shorten_fqname }}
                 </a>
             {%- else -%}
-                <a href="{{ url_for('frontend.show_item', item_name=item_name) }}"{{ " " }}
+                <a href="{{ url_for('frontend.show_item', item_name=fqname) }}"{{ " " }}
                    {% if not exists -%}class="moin-nonexistent"{%- endif -%}>
-                   {{ item_name|shorten_item_name }}
+                   {{ fqname|shorten_fqname }}
                 </a>
             {%- endif %}
             {% if not loop.last %}<span class="sep"> &raquo; </span>{% endif %}
@@ -113,7 +134,7 @@
         </div>
     {% endif %}
 
-    {% set navibar_items = theme_supp.navibar(item_name) %}
+    {% set navibar_items = theme_supp.navibar(fqname) %}
     {% if navibar_items %}
         <ul id="moin-navibar" class="panel">
         {% for cls, url, text, title in navibar_items %}
--- a/MoinMoin/templates/link_list_item_panel.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/link_list_item_panel.html	Sun Feb 02 17:14:23 2014 +0100
@@ -3,11 +3,11 @@
 {% if headline %}
 <h1>{{ headline }}</h1>
 {% endif %}
-{% if item_names %}
-Total: {{ item_names|count }}
+{% if fq_names %}
+Total: {{ fq_names|count }}
 <ul>
-    {% for item_name in item_names|sort %}
-    <li><a href="{{ url_for('frontend.show_item', item_name=item_name) }}">{{ item_name }}</a></li>
+    {% for fq_name in fq_names|sort(attribute='value') %}
+        <li><a class="moin-fqname" href="{{ url_for('frontend.show_item', item_name=fq_name) }}" data-fqname="{{fq_name}}">{{ fq_name.value }}</a></li>
     {% endfor %}
 </ul>
 {% endif %}
--- a/MoinMoin/templates/link_list_no_item_panel.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/link_list_no_item_panel.html	Sun Feb 02 17:14:23 2014 +0100
@@ -3,11 +3,11 @@
 {% if headline %}
 <h1>{{ headline }}</h1>
 {% endif %}
-{% if item_names %}
-Total: {{ item_names|count }}
+{% if fq_names %}
+Total: {{ fq_names|count }}
 <ul>
-    {% for item_name in item_names|sort %}
-    <li><a href="{{ url_for('frontend.show_item', item_name=item_name) }}">{{ item_name }}</a></li>
+    {% for fq_name in fq_names|sort(attribute='value') %}
+    <li><a class="moin-fqname" href="{{ url_for('frontend.show_item', item_name=fq_name) }}" data-fqname="{{fq_name}}">{{ fq_name.value }}</a></li>
     {% endfor %}
 </ul>
 {% endif %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/mail/content_diff.html	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,30 @@
+{% macro content_diff(diff) %}
+<table border="0" cellpadding="0" cellspacing="0" style="margin:0; padding:1em;
+        width:100%; font-family:monospace; background-color:#F9F9F9;">
+{% for line in diff -%}
+{% if line[:2] == "+ " -%}
+    <tr style="color:#00B000; height:1.2em;">
+{% elif line[:2] == "- " -%}
+    <tr style="color:#991111; height:1.2em;">
+{% elif line[:2] == "@@" -%}
+    <tr style="color:#440088; height:1.2em;">
+{% else -%}
+    <tr style="color:#000000; height:1.2em;">
+{%- endif %}
+    {% if line[:2] == "+ " %}
+        <td style="font-family:monospace; width:1.2em; vertical-align:top;">{{ line[0] }}</td>
+        <td style="font-family:monospace;"><ins style="text-decoration:none;">{{ line[2:] }}</ins></td>
+    {% elif line[:2] == "- " -%}
+        <td style="font-family:monospace; width:1.2em; vertical-align:top;">{{ line[0] }}</td>
+        <td style="font-family:monospace;"><del style="text-decoration:none;">{{ line[2:] }}</del></td>
+    {% elif line[:2] == "@@" -%}
+        <td style="font-family:monospace; width:1.2em;"></td>
+        <td style="font-family:monospace;">{{ line }}</td>
+    {% else -%}
+        <td style="font-family:monospace; width:1.2em;"></td>
+        <td style="font-family:monospace;">{{ line[2:] }}</td>
+    {%- endif %}
+    </tr>
+{%- endfor %}
+</table>
+{% endmacro %}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/mail/html_base.html	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,28 @@
+{# HTML mail templates are based on htmlemailboilerplate authored by Sean Powell, The Engage Group, MIT licensed #}
+{# For more information visit: http://htmlemailboilerplate.com/ #}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <title>{% block title %}{% endblock %}</title>
+    <style type="text/css">
+        #outlook a {padding:0;}
+        #backgroundTable {margin:0; padding:0; width:100% !important; line-height:100% !important;}
+        .ExternalClass {width:100%;}
+        .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font,
+        .ExternalClass td, .ExternalClass div {line-height:100%;}
+        p {margin:1em 0;}
+        h1, h2, h3, h4, h5, h6 {color:black !important;}
+        table td {border-collapse:collapse; padding:0;}
+    </style>
+</head>
+<body style="width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;">
+<table border="0" cellpadding="0" cellspacing="0" width="100%" id="backgroundTable">
+    <tr>
+        <td>
+            {% block content %}{% endblock %}
+        </td>
+    </tr>
+</table>
+</body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/mail/meta_diff.html	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,29 @@
+{% macro meta_diff(diff) %}
+<table border="0" cellpadding="0" cellspacing="0" style="margin:0; padding:10px;
+        width:100%; font-family:monospace; background-color:#F9F9F9">
+{% for change in diff -%}
+{% if change[0] == "insert" -%}
+    <tr style="color:#00B000; height:1.2em;">
+    {% set marker = '+' -%}
+{% elif change[0] == "delete" -%}
+    <tr style="color:#991111; height:1.2em;">
+    {% set marker = '-' -%}
+{%- endif %}
+        <td style="font-family:monospace; width:1.2em; vertical-align:top;">{{ marker }}</td>
+        <td style="font-family:monospace;">
+        {% if change[0] == "insert" -%}
+            <ins style="text-decoration:none;">
+                {{ change[1] | join(".") }}{% if change[1] -%}:{%- endif %}
+                {{ change[2] }}
+            </ins>
+        {% elif change[0] == "delete" -%}
+            <del style="text-decoration:none;">
+                {{ change[1] | join(".") }}{% if change[1] -%}:{%- endif %}
+                {{ change[2] }}
+            </del>
+        {% endif %}
+        </td>
+    </tr>
+{%- endfor %}
+</table>
+{% endmacro %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/mail/notification.txt	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,29 @@
+{{ _("Dear Wiki user,") }}
+
+{{ notification_sentence }}
+{{ diff_url }}
+
+{% if comment -%}
+{{ _("Comment:") }}
+{{ comment }}
+{%- endif %}
+
+{% if content_diff_ -%}
+{{ _("Data changes:") }}
+{%- for line in content_diff_ %}
+{{ line }}
+{%- endfor -%}
+{%- endif %}
+
+{% if meta_diff_ -%}
+{{ _("Metadata changes:") }}
+{%- for line in meta_diff_ %}
+{{ line }}
+{%- endfor -%}
+{%- endif %}
+
+{{ _("You are receiving this because you have subscribed to a wiki item on
+'%(wiki_name)s' for change notifications.", wiki_name=wiki_name) }}
+{{ _("Item link: %(item_url)s", item_url=item_url) }}
+{{ _("To unsubscribe use: %(unsubscribe_url)s",
+unsubscribe_url=unsubscribe_url) }}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/mail/notification_main.html	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,46 @@
+{% extends theme("mail/html_base.html") %}
+{% from "mail/content_diff.html" import content_diff %}
+{% from "mail/meta_diff.html" import meta_diff %}
+
+{% block title %}Wiki Notification{% endblock %}
+
+{% block content -%}
+    <div>
+        <p style="text-align:center; margin-bottom:0.6em;">{{ _("Dear Wiki user,") }}</p>
+        <p style="text-align:center; margin-top:0.4em; margin-bottom:0;">
+            {{ notification_sentence }}
+        </p>
+        <p style="text-align:center; margin-top:0.4em; margin-bottom:0;">
+            {{ _("Item link:") }}
+            <a href="{{ item_url }}"
+               style="text-decoration:none; color:#3CA7DD;">{{ item_url }}
+            </a>
+        </p>
+        <p style="text-align:center; margin-top:0.4em;">
+            <a href="{{ diff_url }}"
+                style="text-decoration:none; color:#3CA7DD;">{{ diff_url }}
+            </a>
+        </p>
+    </div>
+    {% if comment -%}
+    <h3 style="margin:0 1.3em 0.5em;">{{ _("Comment") }}</h3>
+    <p>{{ comment }}</p>
+    {%- endif %}
+    {% if content_diff_ -%}
+    <h3 style="margin:1em 1.3em 0.5em;">{{ _("Content") }}</h3>
+    {{ content_diff(content_diff_) }}
+    {%- endif %}
+    {% if meta_diff_ -%}
+    <h3 style="margin:1em 1.3em 0.5em;">{{ _("Metadata") }}</h3>
+    {{ meta_diff(meta_diff_) }}
+    {%- endif %}
+    <div style="font-size:0.7em; margin-top:2em;">
+        <p style="text-align:center; color:#A6A6A6; margin-bottom:0;">
+            {{ _("You are receiving this because you have subscribed to a wiki
+        item on %(wiki_name)s for change notifications.", wiki_name=wiki_name) }}</p>
+        <p style="text-align:center; margin-top:0;"><strong><a
+                href="{{ unsubscribe_url }}"
+            style="text-decoration:none; color:#3CA7DD;">{{ _("Unsubscribe from
+            this item notifications") }}</a></strong></p>
+    </div>
+{%- endblock %}
\ No newline at end of file
--- a/MoinMoin/templates/modify.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/modify.html	Sun Feb 02 17:14:23 2014 +0100
@@ -10,7 +10,7 @@
 {% set extra_head = content_template.extra_head %}
 {% set data_editor = content_template.data_editor %}
 
-{% set title = _("Modifying '%(item_name)s'", item_name=item_name) %}
+{% set title = _("Modifying '%(item_name)s'", item_name=fqname.value) %}
 
 {% block head %}
     {{ super() }}
@@ -20,7 +20,7 @@
 {% endblock %}
 
 {% block subitem_navigation %}
-    {% call(fullname, shortname, contenttype, has_children) utils.render_subitem_navigation(item_name, True) %}
+    {% call(fullname, shortname, contenttype, has_children) utils.render_subitem_navigation(fqname, True) %}
         {% set shortname = shortname|json_dumps %}
         {% set fullname = fullname|json_dumps %}
         <button class="moin-insertname-action" onclick='InsertName({{ fullname }})'
--- a/MoinMoin/templates/modify_meta.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/modify_meta.html	Sun Feb 02 17:14:23 2014 +0100
@@ -9,6 +9,7 @@
             'summary',
             'tags',
             'acl',
+            'name',
             ] %}
             {{ forms.render(form[e]) }}
         {% endfor %}
--- a/MoinMoin/templates/modify_select_contenttype.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/modify_select_contenttype.html	Sun Feb 02 17:14:23 2014 +0100
@@ -13,7 +13,7 @@
     <tr>
         <td> |&nbsp;
         {% for e in groups[group] %}
-            <a href="{{ url_for('frontend.modify_item', item_name=item_name, itemtype=itemtype, contenttype=Type(e.content_type, parameters=e.default_contenttype_params)|string) }}">{{ e.display_name }}</a> &nbsp;|&nbsp;
+            <a href="{{ url_for('frontend.modify_item', item_name=fqname, itemtype=itemtype, contenttype=Type(e.content_type, parameters=e.default_contenttype_params)|string) }}">{{ e.display_name }}</a> &nbsp;|&nbsp;
         {% endfor %}
         </td>
     </tr>
--- a/MoinMoin/templates/modify_select_itemtype.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/modify_select_itemtype.html	Sun Feb 02 17:14:23 2014 +0100
@@ -2,13 +2,13 @@
 {% block content %}
 <h1>{{ _("Item not found, create it now?") }}</h1>
 <p>
-{{ _("Item '%(name)s' does not exist (yet), but you can try creating it now. Please select the type of the item you want to create.", name=item_name) }}
+{{ _("Item '%(fqname)s' does not exist (yet), but you can try creating it now. Please select the type of the item you want to create.", fqname=fqname) }}
 </p>
 <table class="zebra">
     {% for e in itemtypes %}
     <tr>
         <td>
-            <a href="{{ url_for('frontend.modify_item', item_name=item_name, itemtype=e.itemtype) }}">{{ e.display_name }}</a> - {{ e.description }}
+            <a href="{{ url_for('frontend.modify_item', item_name=fqname, itemtype=e.itemtype) }}">{{ e.display_name }}</a> - {{ e.description }}
         </td>
     </tr>
     {% endfor %}
--- a/MoinMoin/templates/modify_select_template.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/modify_select_template.html	Sun Feb 02 17:14:23 2014 +0100
@@ -2,14 +2,13 @@
 {% block content %}
 <h1>{{ _("Create new item?") }}</h1>
 <p>
-{{ _("You can either <a href='%(modifyhref)s'>create the item from scratch</a> or select a template.",
-modifyhref=url_for('frontend.modify_item', item_name=item_name, itemtype=itemtype, contenttype=contenttype, template='') ) }}
+{{ _("You can either <a href='%(modifyhref)s'>create the item from scratch</a> or select a template.", modifyhref=url_for('frontend.modify_item', item_name=fqname, itemtype=itemtype, contenttype=contenttype, template='') ) }}
 </p>
 <h2>{{ _("Available template items") }}</h2>
 <ul>
     {% for template in templates %}
     <li>
-    <a href="{{ url_for('frontend.modify_item', item_name=item_name, itemtype=itemtype, contenttype=contenttype, template=template) }}">{{ template }}</a>
+    <a href="{{ url_for('frontend.modify_item', item_name=fqname, itemtype=itemtype, contenttype=contenttype, template=template) }}">{{ template }}</a>
     </li>
     {% endfor %}
 </ul>
--- a/MoinMoin/templates/refs.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/refs.html	Sun Feb 02 17:14:23 2014 +0100
@@ -2,14 +2,16 @@
 {% block content %}
 <h1>{{ _('Outgoing Item References') }}</h1>
 <ul>
-    {% for item_name in refs|sort %}
-    <li><a href="{{ url_for('frontend.show_item', item_name=item_name) }}">{{ item_name }}</a></li>
+    {% for fq_name in refs|sort(attribute='value') %}
+    <li><a class="moin-fqname" href="{{ url_for('frontend.show_item', item_name=fq_name) }}" data-fqname="{{fq_name}}">{{ fq_name.value }}</a></li>
+
     {% endfor %}
 </ul>
 <h1>{{ _('Incoming Item References') }}</h1>
 <ul>
-    {% for item_name in backrefs|sort %}
-    <li><a href="{{ url_for('frontend.show_item', item_name=item_name) }}">{{ item_name }}</a></li>
+    {% for fq_name in backrefs|sort(attribute='value') %}
+    <li><a class="moin-fqname" href="{{ url_for('frontend.show_item', item_name=fq_name) }}" data-fqname="{{fq_name}}">{{ fq_name.value }}</a></li>
+
     {% endfor %}
 </ul>
 {% endblock %}
--- a/MoinMoin/templates/rename.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/rename.html	Sun Feb 02 17:14:23 2014 +0100
@@ -1,12 +1,11 @@
 {% import "forms.html" as forms %}
 {% extends theme("show.html") %}
 
-{% set title = _("Rename '%(item_name)s'", item_name=item.name) %}
-
+{% set title = _("Rename '%(item_name)s'", item_name=item.fqname.value) %}
 {% block content %}
 <h1>{{ title }}</h1>
 <div class="moin-form">
-{{ gen.form.open(form, method="post", action=url_for('frontend.rename_item', item_name=item.name)) }}
+{{ gen.form.open(form, method="post", action=url_for('frontend.rename_item', item_name=fqname)) }}
   {{ forms.render_errors(form) }}
   <dl>
     {{ forms.render(form['target']) }}
--- a/MoinMoin/templates/revert.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/revert.html	Sun Feb 02 17:14:23 2014 +0100
@@ -1,9 +1,9 @@
 {% import "forms.html" as forms %}
 {% extends theme("layout.html") %}
 {% block content %}
-<h1>{{ _("Revert '%(item_name)s' (rev %(rev_id)s)", item_name=item.name, rev_id=rev_id | shorten_id) }}</h1>
+<h1>{{ _("Revert '%(item_name)s' (rev %(rev_id)s, names '%(new_names)s') ", item_name=fqname | shorten_item_name, rev_id=rev_id | shorten_id, new_names=item.meta['name'] | join('|')) }}</h1>
 <div class="moin-form">
-{{ gen.form.open(form, method="post", action=url_for('frontend.revert_item', item_name=item.name, rev=rev_id)) }}
+{{ gen.form.open(form, method="post", action=url_for('frontend.revert_item', item_name=item.fqname, rev=rev_id)) }}
   {{ forms.render_errors(form) }}
   <dl>
     {{ forms.render(form['comment']) }}
--- a/MoinMoin/templates/search.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/search.html	Sun Feb 02 17:14:23 2014 +0100
@@ -44,7 +44,11 @@
                         {% if result['wikiname'] == cfg.interwikiname %}
                             <tr>
                                 <td class="moin-wordbreak">{{ result.pos + 1 }}
-                                <a href="{{ url_for_item(item_name=result['name'][0], wiki_name='Self', rev=result['revid']) }}"><b>{{ result['name'] | join(' | ')}}</b></a>
+                                    {% if history %}
+                                        <a href="{{ url_for_item(item_name=result['revid'], wiki_name='Self', namespace=result['namespace'], field='revid') }}"><b>{{ result['name'] | join(' | ')}}</b></a>
+                                    {% else %}
+                                        <a href="{{ url_for_item(item_name=result['name'][0], wiki_name='Self', namespace=result['namespace']) }}"><b>{{ result['name'] | join(' | ')}}</b></a>
+                                    {% endif %}
                                 </td>
                             </tr>
                             {% if result['summary'] %}
--- a/MoinMoin/templates/show.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/show.html	Sun Feb 02 17:14:23 2014 +0100
@@ -37,7 +37,7 @@
 {% endmacro %}
 
 {% block subitem_navigation %}
-    {{ utils.render_subitem_navigation(item_name, False) }}
+    {{ utils.render_subitem_navigation(fqname or item_name, False) }}
 {% endblock %}
 
 {% block content %}
--- a/MoinMoin/templates/sitemap.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/sitemap.html	Sun Feb 02 17:14:23 2014 +0100
@@ -11,7 +11,7 @@
         </ul>
     {% else %}
         <li>
-        <a href="{{ url_for('frontend.sitemap', item_name=entry) }}">{{ entry }}</a>
+        <a class="moin-fqname" href="{{ url_for('frontend.show_item', item_name=entry) }}" data-fqname="{{entry}}">{{ entry.value }}</a>
         </li>
     {% endif %}
 {% endfor %}
--- a/MoinMoin/templates/usersettings_ajax.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/usersettings_ajax.html	Sun Feb 02 17:14:23 2014 +0100
@@ -12,4 +12,6 @@
     {{ user_forms.navigation(form) }}
 {% elif part == 'options' %}
     {{ user_forms.options(form) }}
+{% elif part == 'subscriptions' %}
+    {{ user_forms.subscriptions(form) }}
 {% endif %}
--- a/MoinMoin/templates/usersettings_forms.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/usersettings_forms.html	Sun Feb 02 17:14:23 2014 +0100
@@ -83,6 +83,17 @@
 {{ gen.form.close() }}
 {% endmacro %}
 
+{% macro subscriptions(form) %}
+{{ gen.form.open(form, method="post", action=url_for('frontend.usersettings')) }}
+{{ forms.render_errors(form) }}
+<dl>
+    {{ forms.render(form['subscriptions']) }}
+</dl>
+{{ forms.render_hidden('part', 'subscriptions') }}
+{{ forms.render_submit(form) }}
+{{ gen.form.close() }}
+{% endmacro %}
+
 {# javascript functions within common.js are dependent upon the structure, classes and ids defined here #}
 {% macro all_usersettings_forms(form_objs) %}
 <div id="moin-usersettings">
@@ -110,5 +121,9 @@
         <h2 class="moin-settings-head"><a href="#options">{{ _("Options") }}</a></h2>
         {{ options(form_objs.options) }}
     </div>
+    <div id="subscriptions" class="moin-tab-body moin-form">
+        <h2 class="moin-settings-head"><a href="#subscriptions">{{ _("Subscriptions") }}</a></h2>
+        {{ subscriptions(form_objs.subscriptions) }}
+    </div>
 </div>
 {% endmacro %}
--- a/MoinMoin/templates/utils.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/templates/utils.html	Sun Feb 02 17:14:23 2014 +0100
@@ -78,9 +78,9 @@
     </ul>
 {% endmacro %}
 
-{% macro render_subitem_navigation(itemname, newtab, subitems=None) %}
+{% macro render_subitem_navigation(fqname, newtab, subitems=None) %}
     {% if not subitems %}
-        {% set subitems = theme_supp.subitem_index(itemname) %}
+        {% set subitems = theme_supp.subitem_index(fqname) %}
     {% endif %}
     {% if caller %}
         {% set mycaller = caller %}
--- a/MoinMoin/themes/__init__.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/themes/__init__.py	Sun Feb 02 17:14:23 2014 +0100
@@ -22,9 +22,10 @@
 
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin import wikiutil, user
-from MoinMoin.constants.keys import USERID, ADDRESS, HOSTNAME
+from MoinMoin.constants.keys import USERID, ADDRESS, HOSTNAME, REVID, ITEMID, NAME_EXACT
+from MoinMoin.constants.namespaces import NAMESPACE_DEFAULT, NAMESPACE_USERPROFILES, NAMESPACE_ALL
 from MoinMoin.search import SearchForm
-from MoinMoin.util.interwiki import split_interwiki, getInterwikiHome, is_local_wiki, is_known_wiki, url_for_item
+from MoinMoin.util.interwiki import split_interwiki, getInterwikiHome, is_local_wiki, is_known_wiki, url_for_item, CompositeName, split_fqname
 from MoinMoin.util.crypto import cache_key
 from MoinMoin.util.forms import make_generator
 from MoinMoin.util.clock import timed
@@ -217,18 +218,31 @@
                 'special.transclusions': "icon-edit", }
         return icon
 
-    def location_breadcrumbs(self, item_name):
+    def location_breadcrumbs(self, fqname):
         """
         Assemble the location using breadcrumbs (was: title)
 
         :rtype: list
-        :returns: location breadcrumbs items in tuple (segment_name, item_name, exists)
+        :returns: location breadcrumbs items in tuple (segment_name, fq_name, exists)
         """
         breadcrumbs = []
         current_item = ''
+        if not isinstance(fqname, CompositeName):
+            fqname = split_fqname(fqname)
+        if fqname.field != NAME_EXACT:
+            return [(fqname, fqname, bool(self.storage.get_item(**fqname.query)))]
+        namespace = fqname.namespace
+        fq_current = CompositeName(u'', NAME_EXACT, namespace)
+        fq_segment = CompositeName(u'', NAME_EXACT, namespace or '~')
+        breadcrumbs.append((fq_segment, fq_current, False))
+        item_name = fqname.value
+        if not item_name:
+            return breadcrumbs
         for segment in item_name.split('/'):
             current_item += segment
-            breadcrumbs.append((segment, current_item, self.storage.has_item(current_item)))
+            fq_current = CompositeName(namespace, NAME_EXACT, current_item)
+            fq_segment = CompositeName(namespace, NAME_EXACT, segment)
+            breadcrumbs.append((fq_segment, fq_current, bool(self.storage.get_item(**fq_current.query))))
             current_item += '/'
         return breadcrumbs
 
@@ -243,26 +257,30 @@
         breadcrumbs = []
         trail = user.get_trail()
         for interwiki_item_name in trail:
-            wiki_name, namespace, item_name = split_interwiki(interwiki_item_name)
+            wiki_name, namespace, field, item_name = split_interwiki(interwiki_item_name)
+            fqname = CompositeName(namespace, field, item_name)
             err = not is_known_wiki(wiki_name)
-            href = url_for_item(item_name, namespace=namespace, wiki_name=wiki_name)
+            href = url_for_item(wiki_name=wiki_name, **fqname.split)
             if is_local_wiki(wiki_name):
-                exists = self.storage.has_item(item_name)
+                exists = bool(self.storage.get_item(**fqname.query))
                 wiki_name = ''  # means "this wiki" for the theme code
             else:
                 exists = True  # we can't detect existance of remote items
-            breadcrumbs.append((wiki_name, item_name, href, exists, err))
+            if item_name:
+                breadcrumbs.append((wiki_name, fqname, href, exists, err))
         return breadcrumbs
 
-    def subitem_index(self, item_name):
+    def subitem_index(self, fqname):
         """
-        Get a list of subitems for the given item_name
+        Get a list of subitems for the given fqname
 
         :rtype: list
         :returns: list of item tuples (item_name, item_title, item_mime_type, has_children)
         """
         from MoinMoin.items import Item
-        item = Item.create(item_name)
+        if not isinstance(fqname, CompositeName):
+            fqname = split_fqname(fqname)
+        item = Item.create(fqname.fullname)
         return item.get_mixed_index()
 
     def userhome(self):
@@ -283,7 +301,7 @@
         else:
             # We cannot check if wiki pages exists in remote wikis
             exists = True
-        wiki_href = url_for_item(itemname, wiki_name=wikiname)
+        wiki_href = url_for_item(itemname, wiki_name=wikiname, namespace=NAMESPACE_USERPROFILES)
         return wiki_href, display_name, title, exists
 
     def split_navilink(self, text):
@@ -329,29 +347,38 @@
         if target.startswith("wiki:"):
             target = target[5:]
 
-        wiki_name, namespace, item_name = split_interwiki(target)
+        wiki_name, namespace, field, item_name = split_interwiki(target)
         if wiki_name == 'Self':
             wiki_name = ''
-        href = url_for_item(item_name, namespace=namespace, wiki_name=wiki_name)
+        href = url_for_item(item_name, namespace=namespace, wiki_name=wiki_name, field=field)
         if not title:
-            title = item_name
+            title = shorten_fqname(CompositeName(namespace, field, item_name))
         return href, title, wiki_name
 
     @timed()
-    def navibar(self, item_name):
+    def navibar(self, fqname):
         """
         Assemble the navibar
 
         :rtype: list
         :returns: list of tuples (css_class, url, link_text, title)
         """
+        if not isinstance(fqname, CompositeName):
+            fqname = split_fqname(fqname)
+        item_name = fqname.value
         current = item_name
         # Process config navi_bar
         items = []
         for cls, endpoint, args, link_text, title in self.cfg.navi_bar:
             if endpoint == "frontend.show_root":
                 endpoint = "frontend.show_item"
-                args['item_name'] = app.cfg.item_root
+                root_fqname = fqname.get_root_fqname()
+                default_root = app.cfg.root_mapping.get(NAMESPACE_DEFAULT, app.cfg.default_root)
+                args['item_name'] = root_fqname.fullname if fqname.namespace != NAMESPACE_ALL else default_root
+            elif endpoint in ["frontend.global_history", "frontend.global_tags"]:
+                args['namespace'] = fqname.namespace
+            elif endpoint == "frontend.index":
+                args['item_name'] = fqname.namespace
             items.append((cls, url_for(endpoint, **args), link_text, title))
 
         # Add user links to wiki links.
@@ -420,6 +447,31 @@
             url = url or url_for('frontend.login')
         return url
 
+    def get_fqnames(self, fqname):
+        """
+        Return the list of other fqnames associated with the item.
+        """
+        if fqname.field != NAME_EXACT:
+            return []
+        item = self.storage.get_item(**fqname.query)
+        fqnames = item.fqnames
+        fqnames.remove(fqname)
+        return fqnames or []
+
+    def get_namespaces(self, ns):
+        """
+        Return the list of tuples (composite name, namespace) referring to namespaces other
+        than the current namespace.
+        """
+        ns = u'' if ns.value == '~' else ns.value
+        namespace_root_mapping = []
+        for namespace, _ in app.cfg.namespace_mapping:
+            namespace = namespace.rstrip('/')
+            if namespace != ns:
+                fq_namespace = CompositeName(namespace, NAME_EXACT, u'')
+                namespace_root_mapping.append((namespace or '~', fq_namespace.get_root_fqname()))
+        return namespace_root_mapping
+
 
 def get_editor_info(meta, external=False):
     addr = meta.get(ADDRESS)
@@ -471,6 +523,28 @@
     return result
 
 
+def shorten_fqname(fqname, length=25):
+    """
+    Shorten fqname
+
+    Shorten a given long fqname so that it looks good depending upon whether
+    the field is a UUID or not.
+
+    :param fqname: fqname, namedtuple
+    :param length maximum length for shortened fqnames in case the field
+    is not a UUID.
+    :rtype: unicode
+    :returns: shortened fqname.
+    """
+    name = fqname.value
+    if len(name) > length:
+        if fqname.field in [REVID, ITEMID]:
+            name = shorten_id(name)
+        else:
+            name = shorten_item_name(name, length)
+    return name
+
+
 def shorten_item_name(name, length=25):
     """
     Shorten item names
@@ -565,6 +639,7 @@
 
 
 def setup_jinja_env():
+    app.jinja_env.filters['shorten_fqname'] = shorten_fqname
     app.jinja_env.filters['shorten_item_name'] = shorten_item_name
     app.jinja_env.filters['shorten_id'] = shorten_id
     app.jinja_env.filters['contenttype_to_class'] = contenttype_to_class
@@ -586,7 +661,7 @@
         'storage': flaskg.storage,
         'clock': flaskg.clock,
         'cfg': app.cfg,
-        'item_name': u'@NONAMEGIVEN',
+        'item_name': u'@NONAMEGIVEN',  # XXX can we just use u'' ?
         'url_for_item': url_for_item,
         'get_editor_info': lambda meta: get_editor_info(meta),
         'utctimestamp': lambda dt: utctimestamp(dt),
--- a/MoinMoin/themes/_tests/test_navi_bar.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/themes/_tests/test_navi_bar.py	Sun Feb 02 17:14:23 2014 +0100
@@ -26,9 +26,9 @@
             #(navilink, (href, text, interwiki)),
             ('ItemName', ('/ItemName', 'ItemName', '')),
             ('[[ItemName|LinkText]]', ('/ItemName', 'LinkText', '')),
-            ('MoinMoin:ItemName', ('http://moinmo.in/ItemName', 'ItemName', 'MoinMoin')),
-            ('[[MoinMoin:ItemName|LinkText]]', ('http://moinmo.in/ItemName', 'LinkText', 'MoinMoin')),
-            ('[[wiki:MoinMoin:ItemName|LinkText]]', ('http://moinmo.in/ItemName', 'LinkText', 'MoinMoin')),
+            ('MoinMoin/ItemName', ('http://moinmo.in/ItemName', 'ItemName', 'MoinMoin')),
+            ('[[MoinMoin/ItemName|LinkText]]', ('http://moinmo.in/ItemName', 'LinkText', 'MoinMoin')),
+            ('[[wiki:MoinMoin/ItemName|LinkText]]', ('http://moinmo.in/ItemName', 'LinkText', 'MoinMoin')),
             ('http://example.org/', ('http://example.org/', 'http://example.org/', '')),
             ('[[http://example.org/|LinkText]]', ('http://example.org/', 'LinkText', '')),
         ]
@@ -41,13 +41,16 @@
         test_segment_name_1, test_item_name_1, test_item_exists_1 = test_result[0]
         test_segment_name_2, test_item_name_2, test_item_exists_2 = test_result[1]
         test_segment_name_3, test_item_name_3, test_item_exists_3 = test_result[2]
+        test_segment_name_4, test_item_name_4, test_item_exists_4 = test_result[3]
 
-        assert test_segment_name_1 == 'some'
-        assert test_item_name_1 == 'some'
-        assert test_segment_name_2 == 'place'
-        assert test_item_name_2 == 'some/place'
-        assert test_segment_name_3 == 'test_item'
-        assert test_item_name_3 == 'some/place/test_item'
+        assert test_segment_name_1.value == '~'
+        assert test_item_name_1.value == ''
+        assert test_segment_name_2.value == 'some'
+        assert test_item_name_2.value == 'some'
+        assert test_segment_name_3.value == 'place'
+        assert test_item_name_3.value == 'some/place'
+        assert test_segment_name_4.value == 'test_item'
+        assert test_item_name_4.value == 'some/place/test_item'
 
     def test_parent_item(self):
         test_result = ThemeSupport.parent_item(self.theme, 'moin/moin-2.0/Item')
--- a/MoinMoin/themes/basic/templates/modify_meta.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/themes/basic/templates/modify_meta.html	Sun Feb 02 17:14:23 2014 +0100
@@ -6,6 +6,7 @@
         'contenttype',
         'summary',
         'tags',
+        'name',
         ] %}
         <div class="form-group">
             {{ gen.label(form[e]) }}
--- a/MoinMoin/themes/foobar/templates/layout.html	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/themes/foobar/templates/layout.html	Sun Feb 02 17:14:23 2014 +0100
@@ -17,7 +17,7 @@
 
 {% if logo %}
 <div id="moin-logo">
-    <a style="background-image: url({{ '/static/logos/moinmoin.png' }});" href="{{ url_for('frontend.show_item', item_name=cfg.item_root) }}"></a>
+    <a style="background-image: url({{ '/static/logos/moinmoin.png' }});" href="{{ url_for('frontend.show_item', item_name=cfg.root_mapping.get('',cfg.default_root)) }}"></a>
 </div>
 {% endif %}
 
@@ -110,7 +110,7 @@
     <div id="moin-breadcrumbs-location">
         {% if cfg.show_interwiki -%}
         <span id="moin-interwiki">
-            <a href="{{ url_for('frontend.show_item', item_name=cfg.item_root) }}" rel="nofollow">
+            <a href="{{ url_for('frontend.show_item', item_name=cfg.root_mapping['']) }}" rel="nofollow">
                 {% if cfg.interwikiname %}{{ cfg.interwikiname }}{% else %}Self{% endif %}
             </a>
             <span class="sep">: </span>
--- a/MoinMoin/themes/modernized/static/css/common.css	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/themes/modernized/static/css/common.css	Sun Feb 02 17:14:23 2014 +0100
@@ -484,9 +484,7 @@
 .moin-subitem-navigation a:hover{text-decoration:underline}
 .moin-subitem-navigation li ul{border-left:1px dotted #4e7da9;display:none}
 .moin-subitem-navigation .expander{background:url("../img/moin-expand.png") no-repeat center center;cursor:pointer;height:16px;overflow:hidden;vertical-align:middle;width:16px;border:hidden;text-indent:-9000%}
-.link-action,.transclude-action{border:hidden;text-indent:-9000%;padding:0;margin:0 -2px;vertical-align:middle;height:16px;width:16px;overflow:hidden;cursor:pointer;box-shadow:none}
-.moin-subitem-navigation .link-action{background:url("../img/moin-link.png") no-repeat center center}
-.moin-subitem-navigation .transclude-action{background:url("../img/moin-transclusion.png") no-repeat center center}
+.moin-insertname-action{border:hidden;text-indent:-9000%;padding:0;margin:0 -2px;vertical-align:middle;height:16px;width:16px;overflow:hidden;cursor:pointer;box-shadow:none;background:url("../img/moin-link.png") no-repeat center center}
 #moin-footer{clear:both;margin:0 0}
 #moin-footer hr{margin:0;background-color:#4e7da9}
 #moin-pageinfo,#moin-wikilicense,#moin-credits,#moin-version,#moin-timings{margin:10px 20px;text-align:left;font-size:.7em;color:#737373;}
@@ -532,4 +530,11 @@
 table.navigation{float:right;margin:2px}
 .navigation td,.navigation a{font-size:10pt;padding:2px}
 #moin-header,#moin-sidebar,#moin-footer,#moin-timings,#moin-credits,#moin-interwiki,#moin-pagelocation,#moin-pageinfo{display:none}
-}
+}ul.moin-bcs-item ul.moin-bcs-subitems{background-color:#708090}
+ul.moin-bcs-item,ul.moin-bcs-item ul.moin-bcs-subitems{padding:0;margin:0;display:inline-block}
+ul.moin-bcs-item li,ul.moin-bcs-item ul.moin-bcs-subitems li{list-style-type:none;display:block}
+ul.moin-bcs-item li ul.moin-bcs-subitems li a{padding:5px;display:inline-block}
+ul.moin-bcs-item li{position:relative}
+ul.moin-bcs-item li ul.moin-bcs-subitems{display:none;position:absolute}
+ul.moin-bcs-item li:hover ul.moin-bcs-subitems{display:block}
+
--- a/MoinMoin/themes/modernized/static/css/stylus/main.styl	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/themes/modernized/static/css/stylus/main.styl	Sun Feb 02 17:14:23 2014 +0100
@@ -1622,3 +1622,29 @@
     #moin-header, #moin-sidebar, #moin-footer, #moin-timings, #moin-credits, #moin-interwiki,
     #moin-pagelocation, #moin-pageinfo
         display none
+
+ul.moin-bcs-item ul.moin-bcs-subitems
+    background-color panel_color
+
+ul.moin-bcs-item, ul.moin-bcs-item ul.moin-bcs-subitems
+    padding 0
+    margin 0
+    display inline-block
+
+ul.moin-bcs-item li, ul.moin-bcs-item ul.moin-bcs-subitems li
+    list-style-type none
+    display block
+
+ul.moin-bcs-item li ul.moin-bcs-subitems li a
+    padding 5px
+    display inline-block
+
+ul.moin-bcs-item li
+    position relative
+
+ul.moin-bcs-item li ul.moin-bcs-subitems
+    display none
+    position absolute
+
+ul.moin-bcs-item li:hover ul.moin-bcs-subitems
+    display block
--- a/MoinMoin/user.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/user.py	Sun Feb 02 17:14:23 2014 +0100
@@ -30,6 +30,7 @@
 from flask import current_app as app
 from flask import g as flaskg
 from flask import session, request, url_for, render_template
+from jinja2.runtime import Undefined
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
@@ -43,12 +44,24 @@
 from MoinMoin.mail import sendmail
 from MoinMoin.util.interwiki import getInterwikiHome, getInterwikiName, is_local_wiki
 from MoinMoin.util.crypto import generate_token, valid_token, make_uuid
+from MoinMoin.util.subscriptions import get_matched_subscription_patterns
 from MoinMoin.storage.error import NoSuchItemError, ItemAlreadyExistsError, NoSuchRevisionError
 
 
-def create_user(username, password, email, openid=None, validate=True, is_encrypted=False, is_disabled=False):
-    """ create a user """
-    # Create user profile
+def create_user(username, password, email, validate=True, is_encrypted=False, **meta):
+    """
+    Create a new user
+
+    :param username: unique user name
+    :param password: user's password - see also is_encrypted param
+    :param email: unique email address
+    :param validate: if True (default) will validate username, password, email
+                        and the uniqueness of the user created
+    :param is_encrypted: if False (default) defines that the password is in
+                        plaintext, when True - password was already encrypted
+    :param meta: a dictionary of key-value pairs that represent user metadata and
+                    will be stored into user profile metadata
+    """
     theuser = User(auth_method="new-user")
 
     # Don't allow creating users with invalid names
@@ -86,14 +99,15 @@
         theuser.profile[EMAIL] = email
 
     # Openid should be unique
+    openid = meta.get(OPENID)
     if validate and openid and search_users(openid=openid):
         return _('This OpenID already belongs to somebody else.')
 
-    theuser.profile[OPENID] = openid
+    theuser.profile[DISABLED] = meta.get("is_disabled", False)
 
-    theuser.profile[DISABLED] = is_disabled
-
-    # save data
+    # TODO requires validation (preferably using flatland)
+    for key, value in meta.items():
+        theuser.profile[key] = value
     theuser.save()
 
 
@@ -184,6 +198,27 @@
     return (name == normalized) and not wikiutil.isGroupItem(name)
 
 
+def assemble_subscription(keyword, value, namespace=None):
+    """ Create a valid subscription string
+
+    :param keyword: the keyword (itemid, name, tags, nameprefix, namere) by which
+                    the type of the subscription is determined
+    :param value: the subscription value (itemid, name, tag, regexp or nameprefix value)
+    :param namespace: the namespace of the subscription
+    :return: subscription string
+    """
+    if keyword == ITEMID:
+        subscription = "{0}:{1}".format(ITEMID, value)
+    elif keyword in [NAME, TAGS, NAMERE, NAMEPREFIX, ]:
+        if namespace is not None:
+            subscription = "{0}:{1}:{2}".format(keyword, namespace, value)
+        else:
+            raise ValueError("The subscription by {0} keyword requires a namespace".format(keyword))
+    else:
+        raise ValueError("Invalid keyword string: {0}".format(keyword))
+    return subscription
+
+
 class UserProfile(object):
     """ A User Profile"""
 
@@ -533,82 +568,84 @@
 
     # Subscribed Items -------------------------------------------------------
 
-    def is_subscribed_to(self, pagelist):
-        """ Check if user subscription matches any page in pagelist.
-
-        The subscription contains interwiki page names. e.g 'WikiName:Page_Name'
+    def is_subscribed_to(self, item):
+        """ Check if user is subscribed to the following item
 
-        TODO: check if it's fast enough when getting called for many
-              users from page.getSubscribersList()
-
-        :param pagelist: list of pages to check for subscription
+        :param item: Item object
         :rtype: bool
-        :returns: if user is subscribed any page in pagelist
+        :returns: if user is subscribed to the item
         """
-        if not self.valid:
+        from MoinMoin.items import NonExistent
+        if not self.valid or isinstance(item, (NonExistent, Undefined)):
             return False
 
-        # Create a new list with interwiki names.
-        pages = [getInterwikiName(pagename) for pagename in pagelist]
-        # Create text for regular expression search
-        text = '\n'.join(pages)
-
-        for pattern in self.subscribed_items:
-            # Try simple match first
-            if pattern in pages:
-                return True
-            # Try regular expression search, skipping bad patterns
-            try:
-                pattern = re.compile(r'^{0}$'.format(pattern), re.M)
-            except re.error:
-                continue
-            if pattern.search(text):
-                return True
+        meta = item.meta
+        item_namespace = meta[NAMESPACE]
+        subscriptions = set()
+        itemid = meta.get(ITEMID)
+        if itemid is not None:
+            subscriptions.update(["{0}:{1}".format(ITEMID, itemid)])
+        subscriptions.update("{0}:{1}:{2}".format(NAME, item_namespace, name)
+                             for name in meta.get(NAME, []))
+        subscriptions.update("{0}:{1}:{2}".format(TAGS, item_namespace, tag)
+                             for tag in meta.get(TAGS, []))
+        if subscriptions & set(self.subscriptions):
+            return True
 
-        return False
-
-    def subscribe(self, pagename):
-        """ Subscribe to a wiki page.
-
-        Page names are saved as interwiki names.
-
-        :param pagename: name of the page to subscribe
-        :type pagename: unicode
-        :rtype: bool
-        :returns: if page was subscribed
-        """
-        pagename = getInterwikiName(pagename)
-        subscribed_items = self.subscribed_items
-        if pagename not in subscribed_items:
-            subscribed_items.append(pagename)
-            self.save(force=True)
-            # XXX SubscribedToPageEvent
+        if get_matched_subscription_patterns(self.subscriptions, **meta):
             return True
         return False
 
-    def unsubscribe(self, pagename):
-        """ Unsubscribe a wiki page.
-
-        Try to unsubscribe by removing interwiki name from the subscription
-        list.
-
-        Its possible that the user will be subscribed to a page by more
-        than one pattern. It can be both interwiki name and a regex pattern that
-        both match the page. Therefore, we must check if the user is
-        still subscribed to the page after we try to remove names from the list.
+    def subscribe(self, keyword, value, namespace=None):
+        """ Subscribe to a wiki page.
 
-        :param pagename: name of the page to subscribe
-        :type pagename: unicode
+        The user can subscribe in 5 different ways:
+        * by itemid - ITEMID:<itemid value>
+        * by item name - NAME:<namespace>:<name value>
+        * by a tagname - TAGS:<namespace>:<tag value>
+        * by a prefix name - NAMEPREFIX:<namespace>:<name prefix>
+        * by a regular expression - NAMERE:<namespace>:<name regexp>
+
+:       :param keyword: the keyword (itemid, name, tags, nameprefix, namere) by which
+                        the type of the subscription is determined
+        :param value: the subscription value (itemid, name, tag, regexp or nameprefix value)
+        :param namespace: the namespace of the subscription; itemid keyword doesn't
+                            require a namespace
         :rtype: bool
-        :returns: if unsubscribe was successful. If the user has a
-            regular expression that matches, unsubscribe will always fail.
+        :returns: if user was subscribed
         """
-        interWikiName = getInterwikiName(pagename)
-        subscribed_items = self.profile[SUBSCRIBED_ITEMS]
-        if interWikiName and interWikiName in subscribed_items:
-            subscribed_items.remove(interWikiName)
+        subscription = assemble_subscription(keyword, value, namespace)
+        subscriptions = self.subscriptions
+        if subscription not in subscriptions:
+            subscriptions.append(subscription)
             self.save(force=True)
-        return not self.is_subscribed_to([pagename])
+            return True
+        return False
+
+    def unsubscribe(self, keyword, value, namespace=None, item=None):
+        """ Unsubscribe from a wiki page.
+
+        Same as for subscribing, user can also unsubscribe in 5 ways.
+        The unsubscribe action doesn't guarantee that user will not receive any
+        notification for this item, since user can be subscribed by some other
+        patterns that match current item.
+
+        :param keyword: the keyword (itemid, name, tags, nameprefix, namere) by which
+                        the type of the subscription is determined
+        :param value: the subscription value (itemid, name, tag, regexp or nameprefix value)
+        :param namespace: the namespace of the subscription; itemid keyword doesn't
+                            require a namespace
+        :param item: Item object to check if the user is still subscribed
+        :rtype: bool
+        :returns: if user was unsubscribed
+        """
+        subscription = assemble_subscription(keyword, value, namespace)
+        subscriptions = self.subscriptions
+        if subscription in subscriptions:
+            subscriptions.remove(subscription)
+            self.save(force=True)
+            return not self.is_subscribed_to(item) if item else True
+        return False
 
     # Quicklinks -------------------------------------------------------------
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/_tests/test_diff_datastruct.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,148 @@
+# Copyright: 2013 MoinMoin:AnaBalica
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - MoinMoin.util.diff_datastruct Tests
+"""
+
+import pytest
+
+from MoinMoin.util.diff_datastruct import diff, make_text_diff, Undefined, INSERT, DELETE
+
+
+class TestDiffDatastruct(object):
+
+    def _test_make_text_diff(self, tests):
+        for changes, expected in tests:
+            for got in make_text_diff(changes):
+                assert got == expected
+
+    def test_diff_no_change(self):
+        datastruct = [None, True, 42, u"value", [1, 2, 3], dict(one=1, two=2)]
+        for d in datastruct:
+            assert diff(d, d) == []
+
+    def test_diff_none(self):
+        tests = [(None, None, []),
+                 (Undefined, None, [(INSERT, [], None)]),
+                 (None, Undefined, [(DELETE, [], None)])]
+        for d1, d2, expected in tests:
+            assert diff(d1, d2) == expected
+
+    def test_diff_bool(self):
+        tests = [(True, True, []),
+                 (Undefined, True, [(INSERT, [], True)]),
+                 (True, Undefined, [(DELETE, [], True)]),
+                 (True, False, [(DELETE, [], True), (INSERT, [], False)])]
+        for d1, d2, expected in tests:
+            assert diff(d1, d2) == expected
+
+    def test_diff_int(self):
+        tests = [(1, 1, []),
+                 (Undefined, 2, [(INSERT, [], 2)]),
+                 (2, Undefined, [(DELETE, [], 2)]),
+                 (3, 4, [(DELETE, [], 3), (INSERT, [], 4)])]
+        for d1, d2, expected in tests:
+            assert diff(d1, d2) == expected
+
+    def test_diff_float(self):
+        tests = [(1.1, 1.1, []),
+                 (Undefined, 2.2, [(INSERT, [], 2.2)]),
+                 (2.2, Undefined, [(DELETE, [], 2.2)]),
+                 (3.3, 4.4, [(DELETE, [], 3.3), (INSERT, [], 4.4)])]
+        for d1, d2, expected in tests:
+            assert diff(d1, d2) == expected
+
+    def test_diff_unicode(self):
+        tests = [(u"same", u"same", []),
+                 (Undefined, u"new", [(INSERT, [], u"new")]),
+                 (u"old", Undefined, [(DELETE, [], u"old")]),
+                 (u"some value", u"some other value",
+                  [(DELETE, [], u"some value"), (INSERT, [], u"some other value")])]
+        for d1, d2, expected in tests:
+            assert diff(d1, d2) == expected
+
+    def test_diff_list(self):
+        tests = [([1], [1], []),
+                 (Undefined, [2], [(INSERT, [], [2])]),
+                 ([2], Undefined, [(DELETE, [], [2])]),
+                 ([1, 2], [2, 3], [(DELETE, [], [1]), (INSERT, [], [3])]),
+                 ([9, 8], [8, 7, 6, 5], [(DELETE, [], [9]), (INSERT, [], [7, 6, 5])])]
+        for d1, d2, expected in tests:
+            assert diff(d1, d2) == expected
+
+    def test_diff_dict(self):
+        tests = [(dict(same=1), dict(same=1), []),
+                 (Undefined, dict(new=1), [(INSERT, ["new"], 1)]),
+                 (dict(old=1), Undefined, [(DELETE, ["old"], 1)]),
+                 (dict(same=1, old=2), dict(same=1, new1=3, new2=4),
+                  [(INSERT, ["new1"], 3), (INSERT, ["new2"], 4),
+                   (DELETE, ["old"], 2)])]
+        for d1, d2, expected in tests:
+            assert diff(d1, d2) == expected
+
+    def test_diff_nested_dict(self):
+        tests = [(dict(key=dict(same=None)), dict(key=dict(same=None)), []),
+                 (dict(key=dict()), dict(key=dict(added=None)), [(INSERT, ["key", "added"], None)]),
+                 (dict(key=dict(removed=None)), dict(key=dict()), [(DELETE, ["key", "removed"], None)])]
+        for d1, d2, expected in tests:
+            assert diff(d1, d2) == expected
+
+    def test_diff_str_unicode_keys(self):
+        d1 = {"old": u"old", u"same1": u"same1", "same2": u"same2"}
+        d2 = {u"new": u"new", "same1": u"same1", u"same2": u"same2"}
+        assert diff(d1, d2) == [(INSERT, ["new"], u"new"),
+                                (DELETE, ["old"], u"old")]
+
+    def test_diff_errors(self):
+        tests = [(u"foo", True),
+                 ((1, 2, ), (3, 4, )),
+                 (dict(key=(1, 2, )), dict()),
+                 (None, [1, 2, ])]
+        for d1, d2 in tests:
+            with pytest.raises(TypeError):
+                diff(d1, d2)
+
+    def test_make_text_diff_empty(self):
+        for got in make_text_diff([]):
+            assert got == u""
+
+    def test_make_text_diff_none(self):
+        tests = [([(INSERT, [], None)], u"+ None"),
+                 ([(DELETE, [], None)], u"- None")]
+        self._test_make_text_diff(tests)
+
+    def test_make_text_diff_bool(self):
+        tests = [([(INSERT, [], True)], u"+ True"),
+                 ([(DELETE, [], False)], u"- False")]
+        self._test_make_text_diff(tests)
+
+    def test_make_text_diff_int(self):
+        tests = [([(INSERT, [], 123)], u"+ 123"),
+                 ([(DELETE, [], 321)], u"- 321")]
+        self._test_make_text_diff(tests)
+
+    def test_make_text_diff_float(self):
+        tests = [([(INSERT, [], 1.2)], u"+ 1.2"),
+                 ([(DELETE, [], 3.4)], u"- 3.4")]
+        self._test_make_text_diff(tests)
+
+    def test_make_text_diff_unicode(self):
+        tests = [([(INSERT, [], u"new value")], u"+ new value"),
+                 ([(DELETE, [], u"old value")], u"- old value")]
+        self._test_make_text_diff(tests)
+
+    def test_make_text_diff_list(self):
+        tests = [([(INSERT, [], [1, 2, 3, ])], u"+ [1, 2, 3]"),
+                 ([(DELETE, [], [4, 5, ])], u"- [4, 5]")]
+        self._test_make_text_diff(tests)
+
+    def test_make_text_diff_one_key(self):
+        tests = [([(INSERT, ["key"], u"new value")], u"+ key: new value"),
+                 ([(DELETE, ["key"], u"old value")], u"- key: old value")]
+        self._test_make_text_diff(tests)
+
+    def test_make_text_diff_multiple_keys(self):
+        tests = [([(INSERT, ["key1", "key2"], 1)], u"+ key1.key2: 1"),
+                 ([(DELETE, ["key1", "key2", "key3"], 2)], u"- key1.key2.key3: 2")]
+        self._test_make_text_diff(tests)
--- a/MoinMoin/util/_tests/test_interwiki.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/util/_tests/test_interwiki.py	Sun Feb 02 17:14:23 2014 +0100
@@ -16,7 +16,7 @@
 import pytest
 from flask import current_app as app
 
-from MoinMoin.util.interwiki import split_interwiki, join_wiki, InterWikiMap, url_for_item, _split_namespace
+from MoinMoin.util.interwiki import split_interwiki, join_wiki, InterWikiMap, url_for_item, _split_namespace, split_fqname
 from MoinMoin._tests import wikiconfig
 from MoinMoin.constants.keys import CURRENT
 from MoinMoin.app import before_wiki
@@ -27,100 +27,120 @@
         interwiki_map = {'Self': 'http://localhost:8080/',
                          'MoinMoin': 'http://moinmo.in/',
                          'OtherWiki': 'http://otherwiki.com/',
-                         'OtherWiki:ns1': 'http://otherwiki.com/ns1/',
-                         'OtherWiki:ns1:ns2': 'http://otherwiki.com/ns1/ns2/',
+                         'OtherWiki/ns1': 'http://otherwiki.com/ns1/',
+                         'OtherWiki/ns1/ns2': 'http://otherwiki.com/ns1/ns2/',
         }
 
     def test_url_for_item(self):
         before_wiki()
         revid = 'cdc431e0fc624d6fb8372152dcb66457'
 
-        tests = [(('SomePage', '', '', CURRENT, 'frontend.show_item', False), '/SomePage'),
+        tests = [(('SomePage', '', '', '', CURRENT, 'frontend.show_item', False), '/SomePage'),
                  # Method signature to understand the tuple parameters
                  # (item_name, wiki_name='', namespace='', rev=CURRENT, endpoint='frontend.show_item', _external=False):
-                 (('SomePage', '', '', CURRENT, 'frontend.show_item', True), 'http://localhost:8080/SomePage'),
-                 (('SomePage', '', '', CURRENT, 'frontend.modify_item', False), '/+modify/SomePage'),
+                 (('SomePage', '', '', '', CURRENT, 'frontend.show_item', True), 'http://localhost:8080/SomePage'),
+                 (('SomePage', '', '', '', CURRENT, 'frontend.modify_item', False), '/+modify/SomePage'),
                  # FIXME if you set interwiki_map = dict(Self='http://localhost:8080', MoinMoin='http://moinmo.in/', ),
                  # the above line make it fails, it returns http://localhost/+modify/SomePage
                  # (('SomePage', '', '', CURRENT, 'frontend.modify_item', True), 'http://localhost:8080/+modify/SomePage'),
-                 (('SomePage', '', '', revid, 'frontend.show_item', False), '/+show/+{0}/SomePage'.format(revid)),
-                 (('SomePage', '', '', revid, 'frontend.show_item_meta', False), '/+meta/+{0}/SomePage'.format(revid)),
+                 (('SomeRevID', '', 'revid', '', revid, 'frontend.show_item', False), '/+show/+{0}/%40revid/SomeRevID'.format(revid)),
+                 (('SomePage', '', '', '', revid, 'frontend.show_item_meta', False), '/+meta/+{0}/SomePage'.format(revid)),
                  # Valid namespaces
-                 (('SomePage', '', 'ns1', CURRENT, 'frontend.show_item', False), '/:ns1:SomePage'),
-                 (('SomePage', '', 'ns1:ns2', CURRENT, 'frontend.show_item', True), 'http://localhost:8080/:ns1:ns2:SomePage'),
-                 (('SomePage', '', 'ns1', CURRENT, 'frontend.modify_item', False), '/+modify/:ns1:SomePage'),
-                 (('SomePage', '', 'ns1:ns2', CURRENT, 'frontend.show_item_meta', True), 'http://localhost:8080/+meta/:ns1:ns2:SomePage'),
-                 (('SomePage', '', 'ns1', revid, 'frontend.show_item', False), '/+show/+{0}/:ns1:SomePage'.format(revid)),
-                 (('SomePage', '', 'ns1:ns2', revid, 'frontend.show_item_meta', False), '/+meta/+{0}/:ns1:ns2:SomePage'.format(revid)),
+                 (('SomePage', '', '', 'ns1', CURRENT, 'frontend.show_item', False), '/ns1/SomePage'),
+                 (('SomeTag', '', 'tags', 'ns1', CURRENT, 'frontend.show_item', False), '/ns1/%40tags/SomeTag'),
+                 (('SomePage', '', '', 'ns1/ns2', CURRENT, 'frontend.show_item', True), 'http://localhost:8080/ns1/ns2/SomePage'),
+                 (('SomePage', '', '', 'ns1', CURRENT, 'frontend.modify_item', False), '/+modify/ns1/SomePage'),
+                 (('SomePage', '', '', 'ns1/ns2', CURRENT, 'frontend.show_item_meta', True), 'http://localhost:8080/+meta/ns1/ns2/SomePage'),
+                 (('SomePage', '', '', 'ns1', revid, 'frontend.show_item', False), '/+show/+{0}/ns1/SomePage'.format(revid)),
+                 (('SomePage', '', '', 'ns1/ns2', revid, 'frontend.show_item_meta', False), '/+meta/+{0}/ns1/ns2/SomePage'.format(revid)),
+                 (('SomeRevID', '', 'revid', 'ns1/ns2', CURRENT, 'frontend.show_item_meta', False), '/+meta/ns1/ns2/%40revid/SomeRevID'.format(revid)),
 
-                 (('SomePage', 'MoinMoin', 'ns1', CURRENT, 'frontend.show_item', False), 'http://moinmo.in/:ns1:SomePage'),
-                 (('SomePage', 'MoinMoin', '', CURRENT, 'frontend.show_item', False), 'http://moinmo.in/SomePage'),
+                 (('SomePage', 'MoinMoin', '', 'ns1', CURRENT, 'frontend.show_item', False), 'http://moinmo.in/ns1/SomePage'),
+                 (('SomePage', 'MoinMoin', '', '', CURRENT, 'frontend.show_item', False), 'http://moinmo.in/SomePage'),
                  # FIXME will exist a map for this case? maybe there should be a placeholder for it.
                  # we need that for wiki farms with common search index and search in non-current revisions.
-                 (('SomePage', 'MoinMoin', '', revid, 'frontend.show_item', False), 'http://moinmo.in/+show/+{0}/SomePage'.format(revid)),
-                 (('SomePage', 'non-existent', '', CURRENT, 'frontend.show_item', False), '/non-existent:SomePage'),
-                 (('SomePage', 'non-existent', 'ns1', CURRENT, 'frontend.show_item', False), '/non-existent:ns1:SomePage'),
+                 (('SomePage', 'MoinMoin', '', '', revid, 'frontend.show_item', False), 'http://moinmo.in/+show/+{0}/SomePage'.format(revid)),
+                 (('SomeItemID', 'non-existent', 'itemid', '', CURRENT, 'frontend.show_item', False), '/non-existent/@itemid/SomeItemID'),
+                 (('SomePage', 'non-existent', '', 'ns1', CURRENT, 'frontend.show_item', False), '/non-existent/ns1/SomePage'),
                 ]
 
-        for (item_name, wiki_name, namespace, rev, endpoint, _external), url in tests:
-            assert url_for_item(item_name, wiki_name, namespace, rev, endpoint, _external) == url
+        for (item_name, wiki_name, field, namespace, rev, endpoint, _external), url in tests:
+            assert url_for_item(item_name, wiki_name, field, namespace, rev, endpoint, _external) == url
 
     def test__split_namespace(self):
         map = set()
         map.add(u'ns1')
-        map.add(u'ns1:ns2')
+        map.add(u'ns1/ns2')
         tests = [('', ('', '')),
-                 ('OtherWiki:', ('', 'OtherWiki:')),
-                 ('ns1:', ('ns1', '')),
-                 ('ns3:foo', ('', 'ns3:foo')),
-                 ('ns1:OtherPage', ('ns1', 'OtherPage')),
-                 ('ns1:ns2:OtherPage', ('ns1:ns2', 'OtherPage')),
-                 ('ns1:ns2:ns1:ns2:OtherPage', ('ns1:ns2', 'ns1:ns2:OtherPage')),
+                 ('OtherWiki/', ('', 'OtherWiki/')),
+                 ('ns1/', ('ns1', '')),
+                 ('ns3/foo', ('', 'ns3/foo')),
+                 ('ns1/OtherPage', ('ns1', 'OtherPage')),
+                 ('ns1/ns2/OtherPage', ('ns1/ns2', 'OtherPage')),
+                 ('ns1/ns2/ns1/ns2/OtherPage', ('ns1/ns2', 'ns1/ns2/OtherPage')),
                  ('SomePage', ('', 'SomePage')),
-                 ('OtherWiki:ns1:OtherPage', ('', 'OtherWiki:ns1:OtherPage')),
+                 ('OtherWiki/ns1/OtherPage', ('', 'OtherWiki/ns1/OtherPage')),
                 ]
         for markup, (namespace, pagename) in tests:
             assert _split_namespace(map, markup) == (namespace, pagename)
             namespace, pagename = _split_namespace(map, markup)
 
     def test_split_interwiki(self):
-        app.cfg.namespace_mapping = [(u'', 'default_backend'), (u'ns1:', 'default_backend'), (u'ns1:ns2:', 'other_backend')]
-        tests = [('', ('Self', '', '')),
-                 ('OtherWiki:', ('OtherWiki', '', '')),
-                 (':ns1:', ('Self', 'ns1', '')),
-                 (':ns3:foo', ('Self', '', ':ns3:foo')),
-                 ('SomePage', ('Self', '', 'SomePage')),
-                 ('OtherWiki:OtherPage', ('OtherWiki', '', 'OtherPage')),
-                 ('NonExistentWiki:OtherPage', ('Self', '', 'NonExistentWiki:OtherPage')),
-                 (':ns1:OtherPage', ('Self', 'ns1', 'OtherPage')),
-                 (':ns1:ns2:OtherPage', ('Self', 'ns1:ns2', 'OtherPage')),
-                 ('ns1:OtherPage', ('Self', 'ns1', 'OtherPage')),
-                 ('ns1:ns2:OtherPage', ('Self', 'ns1:ns2', 'OtherPage')),
-                 ('OtherWiki:ns1:OtherPage', ('OtherWiki', 'ns1', 'OtherPage')),
-                 ('OtherWiki:ns1:ns2:OtherPage', ('OtherWiki', 'ns1:ns2', 'OtherPage')),
-                 ('OtherWiki:ns3:ns2:OtherPage/foo', ('OtherWiki', '', 'ns3:ns2:OtherPage/foo')),
+        app.cfg.namespace_mapping = [(u'', 'default_backend'), (u'ns1/', 'default_backend'), (u'ns1/ns2/', 'other_backend')]
+        tests = [('', ('Self', '', 'name_exact', '')),
+                 ('OtherWiki/', ('OtherWiki', '', 'name_exact', '')),
+                 ('/ns1/', ('Self', 'ns1', 'name_exact', '')),
+                 ('/@itemid/', ('Self', '', 'itemid', '')),
+                 ('/ns3/foo', ('Self', '', 'name_exact', 'ns3/foo')),
+                 ('@tags/SomeTag', ('Self', '', 'tags', 'SomeTag')),
+                 ('OtherWiki/OtherPage', ('OtherWiki', '', 'name_exact', 'OtherPage')),
+                 ('NonExistentWiki/OtherPage', ('Self', '', 'name_exact', 'NonExistentWiki/OtherPage')),
+                 ('OtherWiki/ns1/@invalidID/Page', ('OtherWiki', 'ns1', 'name_exact', '@invalidID/Page')),
+                 ('/ns1/OtherPage', ('Self', 'ns1', 'name_exact', 'OtherPage')),
+                 ('/ns1/ns2/OtherPage', ('Self', 'ns1/ns2', 'name_exact', 'OtherPage')),
+                 ('ns1/OtherPage', ('Self', 'ns1', 'name_exact', 'OtherPage')),
+                 ('ns1/ns2/OtherPage', ('Self', 'ns1/ns2', 'name_exact', 'OtherPage')),
+                 ('OtherWiki/ns1/OtherPage', ('OtherWiki', 'ns1', 'name_exact', 'OtherPage')),
+                 ('OtherWiki/ns1/ns2/OtherPage', ('OtherWiki', 'ns1/ns2', 'name_exact', 'OtherPage')),
+                 ('OtherWiki/ns1/ns2/@userid/SomeUserID', ('OtherWiki', 'ns1/ns2', 'userid', 'SomeUserID')),
+                 ('OtherWiki/ns3/ns2/@Notfield/OtherPage/foo', ('OtherWiki', '', 'name_exact', 'ns3/ns2/@Notfield/OtherPage/foo')),
                 ]
-        for markup, (wikiname, namespace, pagename) in tests:
-            assert split_interwiki(markup) == (wikiname, namespace, pagename)
-            wikiname, namespace, pagename = split_interwiki(markup)
+        for markup, (wikiname, namespace, field, pagename) in tests:
+            assert split_interwiki(markup) == (wikiname, namespace, field, pagename)
+            wikiname, namespace, field, pagename = split_interwiki(markup)
             assert isinstance(namespace, unicode)
             assert isinstance(pagename, unicode)
+            assert isinstance(field, unicode)
             assert isinstance(wikiname, unicode)
 
     def testJoinWiki(self):
-        tests = [(('http://example.org/', u'SomePage', ''), 'http://example.org/SomePage'),
-                 (('', u'SomePage', ''), 'SomePage'),
-                 (('http://example.org/?page=$PAGE&action=show', u'SomePage', ''), 'http://example.org/?page=SomePage&action=show'),
-                 (('http://example.org/', u'Aktuelle\xc4nderungen', ''), 'http://example.org/Aktuelle%C3%84nderungen'),
-                 (('http://example.org/$PAGE/show', u'Aktuelle\xc4nderungen', ''), 'http://example.org/Aktuelle%C3%84nderungen/show'),
+        tests = [(('http://example.org/', u'SomePage', '', ''), 'http://example.org/SomePage'),
+                 (('', u'SomePage', '', ''), 'SomePage'),
+                 (('http://example.org/?page=$PAGE&action=show', u'SomePage', '', ''), 'http://example.org/?page=SomePage&action=show'),
+                 (('http://example.org/', u'Aktuelle\xc4nderungen', '', ''), 'http://example.org/Aktuelle%C3%84nderungen'),
+                 (('http://example.org/$PAGE/show', u'Aktuelle\xc4nderungen', '', ''), 'http://example.org/Aktuelle%C3%84nderungen/show'),
 
-                 (('http://example.org/', u'SomePage', u'ns1'), 'http://example.org/:ns1:SomePage'),
-                 (('http://example.org/?page=$PAGE&action=show&namespace=$NAMESPACE', u'SomePage', u'ns1'), 'http://example.org/?page=SomePage&action=show&namespace=ns1'),
-                 (('http://example.org/', u'Aktuelle\xc4nderungen', u'ns1\xc4'), 'http://example.org/:ns1%C3%84:Aktuelle%C3%84nderungen'),
-                 (('http://example.org/$NAMESPACE/$PAGE/show', u'Aktuelle\xc4nderungen', u'ns\xc41'), 'http://example.org/ns%C3%841/Aktuelle%C3%84nderungen/show'),
+                 (('http://example.org/', u'SomeItemID', 'itemid', u'ns1'), 'http://example.org/ns1/@itemid/SomeItemID'),
+                 (('http://example.org/?page=$PAGE&action=show&namespace=$NAMESPACE', u'SomePage', '', u'ns1'), 'http://example.org/?page=SomePage&action=show&namespace=ns1'),
+                 (('http://example.org/', u'Aktuelle\xc4nderungen', '', u'ns1\xc4'), 'http://example.org/ns1%C3%84/Aktuelle%C3%84nderungen'),
+                 (('http://example.org/$NAMESPACE/$PAGE/show', u'Aktuelle\xc4nderungen', '', u'ns\xc41'), 'http://example.org/ns%C3%841/Aktuelle%C3%84nderungen/show'),
+                 (('http://example.org/@$FIELD/$PAGE/show', u'Aktuelle\xc4nderungen', u'itemid', u''), 'http://example.org/@itemid/Aktuelle%C3%84nderungen/show'),
                 ]
-        for (baseurl, pagename, namespace), url in tests:
-            assert join_wiki(baseurl, pagename, namespace) == url
+        for (baseurl, pagename, field, namespace), url in tests:
+            assert join_wiki(baseurl, pagename, field, namespace) == url
+
+    def test_split_fqname(self):
+        app.cfg.namespace_mapping = [(u'', 'default_backend'), (u'ns1/', 'default_backend'), (u'ns1/ns2/', 'other_backend')]
+        tests = [('ns1/ns2/@itemid/SomeItemID', ('ns1/ns2', 'itemid', 'SomeItemID')),
+                 ('ns3/@itemid/SomeItemID', ('', 'name_exact', 'ns3/@itemid/SomeItemID')),
+                 ('Page', ('', 'name_exact', 'Page')),
+                 ('ns1/ns2/@tags/SomeTag', ('ns1/ns2', 'tags', 'SomeTag')),
+                 ('@tags/SomeTag', ('', 'tags', 'SomeTag')),
+                 ('ns1/ns2/@notid', ('ns1/ns2', 'name_exact', '@notid')),
+                 ('ns1/ns2/ns3/Thisisapagename/ns4', ('ns1/ns2', 'name_exact', 'ns3/Thisisapagename/ns4')),
+                ]
+        for url, (namespace, field, pagename) in tests:
+            assert split_fqname(url) == (namespace, field, pagename)
 
 
 class TestInterWikiMapBackend(object):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/_tests/test_notifications.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,102 @@
+# Copyright: 2013 MoinMoin:AnaBalica
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - MoinMoin.util.notifications Tests
+"""
+
+from io import StringIO
+
+from flask import g as flaskg
+from flask import current_app as app
+from flask import url_for
+
+from MoinMoin.constants.keys import ACTION_SAVE, ACTION_TRASH
+from MoinMoin.items import Item
+from MoinMoin.util.diff_datastruct import diff as dict_diff
+from MoinMoin.util.notifications import Notification, get_item_last_revisions, DESTROY_REV, DESTROY_ALL
+
+
+class TestNotifications(object):
+    reinit_storage = True
+
+    def setup_method(self, method):
+        self.imw = flaskg.unprotected_storage
+        self.item_name = u"foo"
+
+    def test_get_last_item_revisions(self):
+        assert get_item_last_revisions(app, self.item_name) == []
+        item = self.imw[self.item_name]
+        rev1 = item.store_revision(dict(name=[self.item_name, ]),
+                                   StringIO(u'x'), trusted=True, return_rev=True)
+        assert get_item_last_revisions(app, self.item_name) == [rev1]
+        rev2 = item.store_revision(dict(name=[self.item_name, ]),
+                                   StringIO(u'xx'), trusted=True, return_rev=True)
+        assert get_item_last_revisions(app, self.item_name) == [rev2, rev1]
+        rev3 = item.store_revision(dict(name=[self.item_name, ]),
+                                   StringIO(u'xxx'), trusted=True, return_rev=True)
+        assert get_item_last_revisions(app, self.item_name) == [rev3, rev2]
+
+    def test_get_content_diff(self):
+        item = self.imw[self.item_name]
+        rev1 = item.store_revision(dict(name=[self.item_name, ], contenttype='text/plain'),
+                                   StringIO(u'x'), trusted=True, return_rev=True)
+        notification = Notification(app, self.item_name, [rev1], action=ACTION_SAVE)
+        assert notification.get_content_diff() == ["+ x"]
+        rev1.data.seek(0, 0)
+
+        rev2 = item.store_revision(dict(name=[self.item_name, ], contenttype='text/plain'),
+                                   StringIO(u'xx'), trusted=True, return_rev=True)
+        notification = Notification(app, self.item_name, [rev2, rev1], action=ACTION_SAVE)
+        assert notification.get_content_diff() == ['- x', '+ xx']
+        rev2.data.seek(0, 0)
+
+        notification = Notification(app, self.item_name, [rev2, rev1], action=ACTION_TRASH)
+        assert notification.get_content_diff() == ['- xx']
+        rev2.data.seek(0, 0)
+
+        item = Item.create(self.item_name)
+        notification = Notification(app, self.item_name, [], content=item.rev.data,
+                                    meta=rev2.meta, action=DESTROY_REV)
+        assert notification.get_content_diff() == ['- xx']
+        rev2.data.seek(0, 0)
+
+        item = Item.create(self.item_name)
+        notification = Notification(app, self.item_name, [], content=item.rev.data,
+                                    meta=rev2.meta, action=DESTROY_ALL)
+        assert notification.get_content_diff() == ['- xx']
+
+    def test_get_meta_diff(self):
+        item = self.imw[self.item_name]
+        rev1 = item.store_revision(dict(name=[self.item_name, ]), StringIO(u'x'),
+                                   trusted=True, return_rev=True)
+        notification = Notification(app, self.item_name, [rev1], action=ACTION_SAVE)
+        assert notification.get_meta_diff() == dict_diff(dict(), rev1.meta._meta)
+
+        rev2 = item.store_revision(dict(name=[self.item_name, ]), StringIO(u'xx'),
+                                   trusted=True, return_rev=True)
+        notification = Notification(app, self.item_name, [rev2, rev1], action=ACTION_SAVE)
+        assert notification.get_meta_diff() == dict_diff(rev1.meta._meta, rev2.meta._meta)
+
+        actions = [DESTROY_REV, DESTROY_ALL, ACTION_TRASH, ]
+        for action in actions:
+            notification = Notification(app, self.item_name, [rev2, rev1], meta=rev2.meta, action=action)
+            assert notification.get_meta_diff() == dict_diff(rev2.meta._meta, dict())
+
+    def test_generate_diff_url(self):
+        domain = "http://test.com"
+        notification = Notification(app, self.item_name, [], action=DESTROY_REV)
+        assert notification.generate_diff_url(domain) == u""
+
+        item = self.imw[self.item_name]
+        rev1 = item.store_revision(dict(name=[self.item_name, ]), StringIO(u'x'),
+                                   trusted=True, return_rev=True)
+        notification.revs = [rev1]
+        assert notification.generate_diff_url(domain) == u""
+
+        rev2 = item.store_revision(dict(name=[self.item_name, ]), StringIO(u'xx'),
+                                   trusted=True, return_rev=True)
+        notification.revs = [rev2, rev1]
+        assert notification.generate_diff_url(domain) == u"{0}{1}".format(
+            domain, url_for('frontend.diff', item_name=self.item_name,
+                            rev1=rev1.revid, rev2=rev2.revid))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/_tests/test_subscriptions.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,133 @@
+# Copyright: 2013 MoinMoin:AnaBalica
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - MoinMoin.util.subscriptions Tests
+"""
+
+import pytest
+
+from MoinMoin import user
+from MoinMoin.items import Item
+from MoinMoin.constants.keys import (ACL, ITEMID, CONTENTTYPE, NAME, NAMERE, NAMEPREFIX,
+                                     SUBSCRIPTIONS, TAGS)
+from MoinMoin.constants.namespaces import NAMESPACE_DEFAULT, NAMESPACE_USERPROFILES
+from MoinMoin.util.subscriptions import get_subscribers, get_matched_subscription_patterns
+
+
+class TestSubscriptions(object):
+    reinit_storage = True
+
+    def setup_method(self, method):
+        # create an item
+        self.item_name = u'foo'
+        self.tagname = u'XXX'
+        self.namespace = NAMESPACE_DEFAULT
+        meta = {CONTENTTYPE: u'text/plain;charset=utf-8', TAGS: [self.tagname]}
+        item = Item.create(self.item_name)
+        item._save(meta)
+        self.item = Item.create(self.item_name)
+
+    def test_get_subscribers(self):
+        users = get_subscribers(**self.item.meta)
+        assert users == set()
+
+        name1 = u'baz'
+        password = u'password'
+        email1 = u'baz@example.org'
+        name2 = u"bar"
+        email2 = u"bar@example.org"
+        name3 = u"barbaz"
+        email3 = u"barbaz@example.org"
+        user.create_user(username=name1, password=password, email=email1, validate=False, locale=u'en')
+        user1 = user.User(name=name1, password=password)
+        user.create_user(username=name2, password=password, email=email2, validate=False)
+        user2 = user.User(name=name2, password=password)
+        user.create_user(username=name3, password=password, email=email3, locale=u"en")
+        user3 = user.User(name=name3, password=password, email1=email3)
+        subscribers = get_subscribers(**self.item.meta)
+        assert subscribers == set()
+
+        namere = r'.*'
+        nameprefix = u"fo"
+        subscription_lists = [
+            ["{0}:{1}".format(ITEMID, self.item.meta[ITEMID])],
+            ["{0}:{1}:{2}".format(TAGS, self.namespace, self.tagname)],
+            ["{0}:{1}:{2}".format(NAME, self.namespace, self.item_name)],
+            ["{0}:{1}:{2}".format(NAMERE, self.namespace, namere)],
+            ["{0}:{1}:{2}".format(NAMEPREFIX, self.namespace, nameprefix)],
+        ]
+        users = [user1, user2, user3]
+        expected_names = {user1.name0, user2.name0}
+        for subscriptions in subscription_lists:
+            for user_ in users:
+                user_.profile._meta[SUBSCRIPTIONS] = subscriptions
+                user_.save(force=True)
+            subscribers = get_subscribers(**self.item.meta)
+            subscribers_names = {subscriber.name for subscriber in subscribers}
+            assert subscribers_names == expected_names
+
+        meta = {CONTENTTYPE: u'text/plain;charset=utf-8',
+                ACL: u"{0}: All:read,write".format(user1.name0)}
+        self.item._save(meta, comment=u"")
+        self.item = Item.create(self.item_name)
+        subscribers = get_subscribers(**self.item.meta)
+        assert {subscriber.name for subscriber in subscribers} == {user2.name0}
+
+    def test_get_matched_subscription_patterns(self):
+        meta = self.item.meta
+        patterns = get_matched_subscription_patterns([], **meta)
+        assert patterns == []
+        non_matching_patterns = [
+            "{0}:{1}:{2}".format(NAMERE, NAMESPACE_USERPROFILES, ".*"),
+            "{0}:{1}:{2}".format(NAMERE, self.namespace, "\d+"),
+            "{0}:{1}:{2}".format(NAMEPREFIX, self.namespace, "bar"),
+        ]
+        patterns = get_matched_subscription_patterns(non_matching_patterns, **meta)
+        assert patterns == []
+
+        matching_patterns = [
+            "{0}:{1}:{2}".format(NAMERE, self.namespace, "fo+"),
+            "{0}:{1}:{2}".format(NAMEPREFIX, self.namespace, "fo"),
+        ]
+        patterns = get_matched_subscription_patterns(non_matching_patterns + matching_patterns, **meta)
+        assert patterns == matching_patterns
+
+    def test_perf_get_subscribers(self):
+        pytest.skip("usually we do no performance tests")
+        password = u"password"
+        subscriptions = [
+            "{0}:{1}".format(ITEMID, self.item.meta[ITEMID]),
+            "{0}:{1}:{2}".format(NAME, self.namespace, self.item_name),
+            "{0}:{1}:{2}".format(TAGS, self.namespace, self.tagname),
+            "{0}:{1}:{2}".format(NAMEPREFIX, self.namespace, u"fo"),
+            "{0}:{1}:{2}".format(NAMERE, self.namespace, r"\wo")
+        ]
+        users = set()
+        expected_names = set()
+        for i in xrange(10000):
+            i = unicode(i)
+            user.create_user(username=i, password=password, email="{0}@example.org".format(i),
+                             validate=False, locale=u'en')
+            user_ = user.User(name=i, password=password)
+            users.add(user_)
+            expected_names.add(user_.name0)
+
+        users_sliced = list(users)[:100]
+        expected_names_sliced = {user_.name0 for user_ in users_sliced}
+        tests = [(users_sliced, expected_names_sliced), (users, expected_names)]
+
+        import time
+        for users_, expected_names_ in tests:
+            print "\nTesting {0} subscribers from a total of {1} users".format(
+                len(users_), len(users))
+            for subscription in subscriptions:
+                for user_ in users_:
+                    user_.profile._meta[SUBSCRIPTIONS] = [subscription]
+                    user_.save(force=True)
+                t = time.time()
+                subscribers = get_subscribers(**self.item.meta)
+                elapsed_time = time.time() - t
+                print "{0}: {1} s".format(subscription.split(':', 1)[0], elapsed_time)
+                subscribers_names = {subscriber.name for subscriber in subscribers}
+                assert subscribers_names == expected_names_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/diff_datastruct.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,94 @@
+# Copyright: 2013 MoinMoin:AnaBalica
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+import difflib
+from types import NoneType
+from collections import Hashable
+
+INSERT = u"insert"
+DELETE = u"delete"
+REPLACE = u"replace"
+
+
+class UndefinedType(object):
+    """ Represents a non-existing value """
+
+
+Undefined = UndefinedType()
+
+
+def diff(d1, d2, basekeys=None):
+    """ Get the diff of 2 datastructures (usually 2 meta dicts)
+
+    :param d1: old datastructure
+    :param d2: new datastructure
+    :param basekeys: list of data keys' basenames (default: None, meaning [])
+    :return: a list of tuples of the format (<change type>, <basekeys>, <value>)
+             that can be used to format a diff
+    """
+    if basekeys is None:
+        basekeys = []
+    changes = []
+
+    if isinstance(d1, UndefinedType) and isinstance(d2, (dict, list, )):
+        d1 = type(d2)()
+    elif isinstance(d2, UndefinedType) and isinstance(d1, (dict, list, )):
+        d2 = type(d1)()
+    if isinstance(d1, dict) and isinstance(d2, dict):
+        added = set(d2) - set(d1)
+        removed = set(d1) - set(d2)
+        all_ = set(d1) | set(d2)
+        for key in sorted(all_):
+            keys = basekeys + [key]
+            if key in added:
+                changes.extend(diff(Undefined, d2[key], keys))
+            elif key in removed:
+                changes.extend(diff(d1[key], Undefined, keys))
+            else:
+                changes.extend(diff(d1[key], d2[key], keys))
+    elif isinstance(d1, list) and isinstance(d2, list):
+        hashable = all(isinstance(d1, unicode) or all(
+            isinstance(v, Hashable) for v in d) for d in [d1, d2])
+        if hashable:
+            matches = difflib.SequenceMatcher(None, d1, d2)
+            for tag, d1_start, d1_end, d2_start, d2_end in matches.get_opcodes():
+                if tag == REPLACE:
+                    changes.extend([(DELETE, basekeys, d1[d1_start:d1_end]),
+                                    (INSERT, basekeys, d2[d2_start:d2_end])])
+                elif tag == DELETE:
+                    changes.append((DELETE, basekeys, d1[d1_start:d1_end]))
+                elif tag == INSERT:
+                    changes.append((INSERT, basekeys, d2[d2_start:d2_end]))
+        else:
+            changes.extend(diff(unicode(d1), unicode(d2), basekeys))
+    elif any(isinstance(d, (NoneType, bool, int, long, float, unicode, )) for d in (d1, d2)):
+        if isinstance(d1, UndefinedType):
+            changes.append((INSERT, basekeys, d2))
+        elif isinstance(d2, UndefinedType):
+            changes.append((DELETE, basekeys, d1))
+        elif type(d1) == type(d2):
+            if d1 != d2:
+                changes.extend([(DELETE, basekeys, d1), (INSERT, basekeys, d2)])
+        else:
+            raise TypeError(
+                "Unsupported diff between {0} and {1} data types".format(
+                    type(d1), type(d2)))
+    else:
+        raise TypeError(
+            "Unsupported diff between {0} and {1} data types".format(
+                type(d1), type(d2)))
+    return changes
+
+
+def make_text_diff(changes):
+    """ Transform change tuples into text diffs
+
+    :param changes: a list of tuples of the format (<change type>, <basekeys>, <value>)
+                    that represent a diff
+    :return: a generator of text diffs
+    """
+    marker = {INSERT: u"+", DELETE: u"-"}
+    for change_type, keys, value in changes:
+        yield "{0} {1}{2}{3}".format(marker[change_type],
+                                     ".".join(unicode(key) for key in keys),
+                                     ": " if keys else "", unicode(value))
--- a/MoinMoin/util/interwiki.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/MoinMoin/util/interwiki.py	Sun Feb 02 17:14:23 2014 +0100
@@ -14,8 +14,9 @@
 from flask import url_for
 
 import os.path
+from collections import namedtuple
 
-from MoinMoin.constants.keys import CURRENT
+from MoinMoin.constants.keys import CURRENT, FIELDS, NAME_EXACT, NAMESPACE
 from MoinMoin.constants.contenttypes import CHARSET
 
 from MoinMoin import log
@@ -39,7 +40,19 @@
     return wiki_name in app.cfg.interwiki_map
 
 
-def url_for_item(item_name, wiki_name=u'', namespace=u'', rev=CURRENT, endpoint=u'frontend.show_item', _external=False):
+def get_fqname(item_name, field, namespace):
+    """
+    Compute composite name from item_name, field, namespace
+    composite name == [NAMESPACE/][@FIELD/]NAME
+    """
+    if field and field != NAME_EXACT:
+        item_name = u'@{0}/{1}'.format(field, item_name)
+    if namespace:
+        item_name = u'{0}/{1}'.format(namespace, item_name)
+    return item_name
+
+
+def url_for_item(item_name, wiki_name=u'', field=u'', namespace=u'', rev=CURRENT, endpoint=u'frontend.show_item', _external=False):
     """
     Compute URL for some local or remote/interwiki item.
 
@@ -55,9 +68,10 @@
     URLs are built in the same way as local URLs.
     Computed URLs are always fully specified.
     """
+    if field == NAME_EXACT:
+        field = u''
     if is_local_wiki(wiki_name):
-        if namespace:
-            item_name = u':{0}:{1}'.format(namespace, item_name)
+        item_name = get_fqname(item_name, field, namespace)
         if rev is None or rev == CURRENT:
             url = url_for(endpoint, item_name=item_name, _external=_external)
         else:
@@ -67,10 +81,9 @@
             wiki_base_url = app.cfg.interwiki_map[wiki_name]
         except KeyError, err:
             logging.warning("no interwiki_map entry for {0!r}".format(wiki_name))
-            if namespace:
-                item_name = u'{0}:{1}'.format(namespace, item_name)
+            item_name = get_fqname(item_name, field, namespace)
             if wiki_name:
-                url = u'{0}:{1}'.format(wiki_name, item_name)
+                url = u'{0}/{1}'.format(wiki_name, item_name)
             else:
                 url = item_name
             url = u'/{0}'.format(url)
@@ -78,13 +91,12 @@
             if (rev is None or rev == CURRENT) and endpoint == 'frontend.show_item':
                 # we just want to show latest revision (no special revision given) -
                 # this is the generic interwiki url support, should work for any remote wiki
-                url = join_wiki(wiki_base_url, item_name, namespace)
+                url = join_wiki(wiki_base_url, item_name, field, namespace)
             else:
                 # rev and/or endpoint was given, assume same URL building as for local wiki.
                 # we need this for moin wiki farms, e.g. to link from search results to
                 # some specific item/revision in another farm wiki.
-                if namespace:
-                    item_name = u'{0}:{1}'.format(namespace, item_name)
+                item_name = get_fqname(item_name, field, namespace)
                 local_url = url_for(endpoint, item_name=item_name, rev=rev, _external=False)
                 # we know that everything left of the + belongs to script url, but we
                 # just want e.g. +show/42/FooBar to append it to the other wiki's
@@ -98,22 +110,22 @@
 def _split_namespace(namespaces, url):
     """
     Find the longest namespace in the set.
-    the namespaces are separated by colons (:).
+    the namespaces are separated by  slashes (/).
     Example:
-        namespaces_set(['ns1', 'ns1:ns2'])
-        url: ns1:urlalasd return: ns1, urlalasd
-        url: ns3:urlalasd return: '', ns3:urlalasd
-        url: ns2:urlalasd return: '', ns2:urlalasd
-        url: ns1:ns2:urlalasd return: ns1:ns2, urlalasd
+        namespaces_set(['ns1', 'ns1/ns2'])
+        url: ns1/urlalasd return: ns1, urlalasd
+        url: ns3/urlalasd return: '', ns3/urlalasd
+        url: ns2/urlalasd return: '', ns2/urlalasd
+        url: ns1/ns2/urlalasd return: ns1/ns2, urlalasd
     param namespaces_set: set of namespaces (strings) to search
     param url: string
     returns: (namespace, url)
     """
     namespace = u''
-    tokens_list = url.split(':')
+    tokens_list = url.split('/')
     for token in tokens_list:
         if namespace:
-            token = u'{0}:{1}'.format(namespace, token)
+            token = u'{0}/{1}'.format(namespace, token)
         if token in namespaces:
             namespace = token
         else:
@@ -124,50 +136,109 @@
     return namespace, url
 
 
+class CompositeName(namedtuple('CompositeName', 'namespace, field, value')):
+    """
+    namedtuple to hold the compositename
+    """
+    @property
+    def split(self):
+        """
+        returns a dict of field_names/field_values
+        """
+        return {NAMESPACE: self.namespace, u'field': self.field, u'item_name': self.value}
+
+    @property
+    def fullname(self):
+        return get_fqname(self.value, self.field, self.namespace)
+
+    def __unicode__(self):
+        return self.fullname
+
+    @property
+    def query(self):
+        """
+        returns a dict that can be used as a whoosh query
+        to lookup index documents matching this CompositeName
+        """
+        field = NAME_EXACT if not self.field else self.field
+        return {NAMESPACE: self.namespace, field: self.value}
+
+    def get_root_fqname(self):
+        """
+        Set value to the item_root of that namespace, and return
+        the new CompisteName.
+        """
+        return CompositeName(self.namespace, NAME_EXACT, app.cfg.root_mapping.get(self.namespace, app.cfg.default_root))
+
+
+def split_fqname(url):
+    """
+    Split a fully qualified url into namespace, field and pagename
+    url -> [NAMESPACE/][@FIELD/]NAME
+    param: url: the url to split
+    returns: a namedtuple CompositeName(namespace, field, itemname)
+    Example:
+        url: u'ns1/ns2/@itemid/Page' return u'ns1/ns2', u'itemid', u'Page'
+        url: u'@revid/OtherPage' return u'', u'revid', u'OtherPage'
+        url: u'ns1/Page' return u'ns1', u'', u'Page'
+        url: u'ns1/ns2/@notfield' return u'ns1/ns2', u'', u'@notfield'
+    """
+    if not url:
+        return CompositeName(u'', NAME_EXACT, u'')
+    namespaces = {namespace.rstrip('/') for namespace, _ in app.cfg.namespace_mapping}
+    namespace, url = _split_namespace(namespaces, url)
+    field = NAME_EXACT
+    if url.startswith('@'):
+        tokens = url[1:].split('/', 1)
+        if tokens[0] in FIELDS:
+            field = tokens[0]
+            url = tokens[1] if len(tokens) > 1 else u''
+    return CompositeName(namespace, field, url)
+
+
 def split_interwiki(wikiurl):
     """ Split a interwiki name, into wikiname and pagename, e.g:
 
-    'MoinMoin:FrontPage' -> "MoinMoin", "", "FrontPage"
-    'FrontPage' -> "Self", "", "FrontPage"
-    'MoinMoin:Page with blanks' -> "MoinMoin", "", "Page with blanks"
-    'MoinMoin:' -> "MoinMoin", "", ""
-    'MoinMoin:interwikins:AnyPage' -> "MoinMoin", "interwikins", "AnyPage"
-    ':ns:AnyPage' -> "Self", "ns", "AnyPage" if ns namespace exists or "Self", "", ":ns:AnyPage" if not.
-    'ns:AnyPage' -> "Self", "ns", "AnyPage" if ns namespace exists or "Self", "", "ns:AnyPage" if not.
-    ':ns1:ns2:AnyPage' -> "Self", "ns1:ns2", "AnyPage" if ns1:ns2 namespace exists OR
-                         "Self", "ns1", "ns2:AnyPage" if ns1 namespace exists OR
-                         "Self", "", "ns1:ns2:AnyPage" else.
+    'MoinMoin/FrontPage' -> "MoinMoin", "", "", "FrontPage"
+    'FrontPage' -> "Self", "", "", "FrontPage"
+    'MoinMoin/Page with blanks' -> "MoinMoin", "", "", "Page with blanks"
+    'MoinMoin/' -> "MoinMoin", "", "", ""
+    'MoinMoin/@Someid/SomeValue' -> "MoinMoin", "", "Someid", "SomeValue" if Someid field exists or "MoinMoin", "", "", "Someid/SomePage" if not
+    'MoinMoin/interwikins/AnyPage' -> "MoinMoin", "interwikins", "", "AnyPage"
+    'ns/AnyPage' -> "Self", "ns", "", "AnyPage" if ns namespace exists or "Self", "", "", "ns:AnyPage" if not.
+    'ns1/ns2/AnyPage' -> "Self", "ns1/ns2", "", "AnyPage" if ns1/ns2 namespace exists OR
+                         "Self", "ns1", "", "ns2/AnyPage" if ns1 namespace exists OR
+                         "Self", "", "", "ns1/ns2/AnyPage" else.
+    'MoinMoin/ns/@Somefield/AnyPage' -> "MoinMoin", "ns", "", "@Somefield/AnyPage" if ns namespace exists and field Somefield does not OR
+                                     "MoinMoin", "ns", "Somefield", "AnyPage" if ns namespace and field Somefield exist OR
+                                     "MoinMoin", "", "", "ns/@Somefield/AnyPage" else.
     :param wikiurl: the url to split
     :rtype: tuple
-    :returns: (wikiname, namespace, pagename)
+    :returns: (wikiname, namespace, field, pagename)
     """
     if not isinstance(wikiurl, unicode):
         wikiurl = wikiurl.decode('utf-8')
-    namespace_mapping = set()
-    for namespace, _ in app.cfg.namespace_mapping:
-        namespace_mapping.add(namespace.rstrip(':'))
     # Base case: no colon in wikiurl
-    if not ':' in wikiurl:
-        return u'Self', u'', wikiurl
-    if not wikiurl.startswith(':'):
-        wikiname, item_name = _split_namespace(set(app.cfg.interwiki_map.keys()), wikiurl)
-        namespace = u''
+    if not '/' in wikiurl:
+        return u'Self', u'', NAME_EXACT, wikiurl
+    wikiname = field = namespace = u''
+    if not wikiurl.startswith('/'):
+        interwiki_mapping = set()
+        for interwiki_name in app.cfg.interwiki_map:
+            interwiki_mapping.add(interwiki_name.split('/')[0])
+        wikiname, item_name = _split_namespace(interwiki_mapping, wikiurl)
+        if wikiname:
+            wikiurl = wikiurl[len(wikiname) + 1:]
+        namespace, field, item_name = split_fqname(wikiurl)
         if not wikiname:
-            namespace, item_name = _split_namespace(set(namespace_mapping), wikiurl)
             wikiname = u'Self'
-        else:
-            if ':' in wikiname:
-                namespace = wikiname.split(':', 1)[1]
-                wikiname = wikiname.split(':', 1)[0]
-        return wikiname, namespace, item_name
+        return wikiname, namespace, field, item_name
     else:
-        namespace, item_name = _split_namespace(set(namespace_mapping), wikiurl.split(':', 1)[1])
-        if not namespace:
-            item_name = u':{0}'.format(item_name)
-        return u'Self', namespace, item_name
+        namespace, field, item_name = split_fqname(wikiurl.split('/', 1)[1])
+        return u'Self', namespace, field, item_name
 
 
-def join_wiki(wikiurl, wikitail, namespace):
+def join_wiki(wikiurl, wikitail, field, namespace):
     """
     Add a (url_quoted) page name to an interwiki url.
 
@@ -181,15 +252,14 @@
     :returns: generated URL of the page in the other wiki
     """
     wikitail = url_quote(wikitail, charset=CHARSET, safe='/')
+    field = url_quote(field, charset=CHARSET, safe='/')
     namespace = url_quote(namespace, charset=CHARSET, safe='/')
-    if not('$PAGE' in wikiurl or '$NAMESPACE' in wikiurl):
-        if namespace:
-            namespace = u':{0}:'.format(namespace)
-        elif not wikiurl:
-            return wikitail
-        return wikiurl + namespace + wikitail
+    if not('$PAGE' in wikiurl or '$NAMESPACE' in wikiurl or '$FIELD' in wikiurl):
+        return wikiurl + get_fqname(wikitail, field, namespace)
     if '$PAGE' in wikiurl:
         wikiurl = wikiurl.replace('$PAGE', wikitail)
+    if '$FIELD' in wikiurl:
+        wikiurl = wikiurl.replace('$FIELD', field)
     if '$NAMESPACE' in wikiurl:
         wikiurl = wikiurl.replace('$NAMESPACE', namespace)
     return wikiurl
@@ -203,7 +273,7 @@
     :rtype: unicode
     :returns: wiki_name:item_name
     """
-    return u"{0}:{1}".format(app.cfg.interwikiname, item_name)
+    return u"{0}/{1}".format(app.cfg.interwikiname, item_name)
 
 
 def getInterwikiHome(username):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/notifications.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,222 @@
+# Copyright: 2013 MoinMoin:AnaBalica
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - Notifications
+"""
+
+from io import BytesIO
+
+from blinker import ANY
+from urlparse import urljoin
+from whoosh.query import Term, And
+
+from flask import url_for, g as flaskg
+
+from MoinMoin.constants.keys import (ACTION_COPY, ACTION_RENAME, ACTION_REVERT,
+                                     ACTION_SAVE, ACTION_TRASH, ALL_REVS, CONTENTTYPE,
+                                     MTIME, NAME_EXACT, WIKINAME)
+from MoinMoin.i18n import _, L_, N_
+from MoinMoin.i18n import force_locale
+from MoinMoin.items.content import Content
+from MoinMoin.mail.sendmail import sendmail
+from MoinMoin.themes import render_template
+from MoinMoin.signalling.signals import item_modified
+from MoinMoin.util.subscriptions import get_subscribers
+from MoinMoin.util.diff_datastruct import make_text_diff, diff as dict_diff
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+# additional action values
+ACTION_CREATE = u"CREATE"
+ACTION_MODIFY = u"MODIFY"
+
+# destroy types
+DESTROY_REV = u"DESTROY_REV"
+DESTROY_ALL = u"DESTROY_ALL"
+
+
+def msgs():
+    """ Encapsulates the main notification messages
+
+    :return: a dictionary of notification messages
+    """
+    _ = lambda x: x
+    messages = {
+        ACTION_CREATE: _("The '%(item_name)s' item on '%(wiki_name)s' has been created by %(user_name)s:"),
+        ACTION_MODIFY: _("The '%(item_name)s' item on '%(wiki_name)s' has been modified by %(user_name)s:"),
+        ACTION_RENAME: _("The '%(item_name)s' item on '%(wiki_name)s' has been renamed by %(user_name)s:"),
+        ACTION_COPY: _("The '%(item_name)s' item on '%(wiki_name)s' has been copied by %(user_name)s:"),
+        ACTION_REVERT: _("The '%(item_name)s' item on '%(wiki_name)s' has been reverted by %(user_name)s:"),
+        ACTION_TRASH: _("The '%(item_name)s' item on '%(wiki_name)s' has been deleted by %(user_name)s:"),
+        DESTROY_REV: _("The '%(item_name)s' item on '%(wiki_name)s' has one revision destroyed by %(user_name)s:"),
+        DESTROY_ALL: _("The '%(item_name)s' item on '%(wiki_name)s' has been destroyed by %(user_name)s:"),
+    }
+    return messages
+
+MESSAGES = msgs()
+
+
+class Notification(object):
+    """
+    Represents a mail notification about an item change
+    """
+    txt_template = "mail/notification.txt"
+    html_template = "mail/notification_main.html"
+
+    def __init__(self, app, item_name, revs, **kwargs):
+        self.app = app
+        self.item_name = item_name
+        self.revs = revs
+        self.action = kwargs.get('action', None)
+        self.content = kwargs.get('content', None)
+        self.meta = kwargs.get('meta', None)
+        self.comment = kwargs.get('comment', None)
+        self.wiki_name = self.app.cfg.interwikiname
+
+        if self.action == ACTION_SAVE:
+            self.action = ACTION_CREATE if len(self.revs) == 1 else ACTION_MODIFY
+
+        if self.action == ACTION_TRASH:
+            self.meta = self.revs[0].meta
+
+        kw = dict(item_name=self.item_name, wiki_name=self.wiki_name, user_name=flaskg.user.name0)
+        self.notification_sentence = L_(MESSAGES[self.action], **kw)
+
+    def get_content_diff(self):
+        """ Create a content diff for the last item change
+
+        :return: list of diff lines
+        """
+        if self.action in [DESTROY_REV, DESTROY_ALL, ]:
+            contenttype = self.meta[CONTENTTYPE]
+            oldfile, newfile = self.content, BytesIO("")
+        elif self.action == ACTION_TRASH:
+            contenttype = self.meta[CONTENTTYPE]
+            oldfile, newfile = self.revs[0].data, BytesIO("")
+        else:
+            newfile = self.revs[0].data
+            if len(self.revs) == 1:
+                contenttype = self.revs[0].meta[CONTENTTYPE]
+                oldfile = BytesIO("")
+            else:
+                from MoinMoin.apps.frontend.views import _common_type
+                contenttype = _common_type(self.revs[0].meta[CONTENTTYPE], self.revs[1].meta[CONTENTTYPE])
+                oldfile = self.revs[1].data
+        content = Content.create(contenttype)
+        return content._get_data_diff_text(oldfile, newfile)
+
+    def get_meta_diff(self):
+        """ Create a meta diff for the last item change
+
+        :return: a list of tuples of the format (<change type>, <basekeys>, <value>)
+                 that can be used to format a diff
+        """
+        if self.action in [ACTION_TRASH, DESTROY_REV, DESTROY_ALL, ]:
+            old_meta, new_meta = dict(self.meta), dict()
+        else:
+            new_meta = dict(self.revs[0].meta)
+            if len(self.revs) == 1:
+                old_meta = dict()
+            else:
+                old_meta = dict(self.revs[1].meta)
+        meta_diff = dict_diff(old_meta, new_meta)
+        return meta_diff
+
+    def generate_diff_url(self, domain):
+        """ Generate the URL that leads to diff page of the last 2 revisions
+
+        :param domain: domain name
+        :return: the absolute URL to the diff page
+        """
+        if len(self.revs) < 2:
+            return u""
+        else:
+            revid1 = self.revs[1].revid
+            revid2 = self.revs[0].revid
+        diff_rel_url = url_for('frontend.diff', item_name=self.item_name, rev1=revid1, rev2=revid2)
+        return urljoin(domain, diff_rel_url)
+
+    def render_templates(self, content_diff, meta_diff):
+        """ Render both plain text and HTML templates by providing all the
+        necessary arguments
+
+        :return: tuple consisting of plain text and HTML notification message
+         """
+        meta_diff_txt = list(make_text_diff(meta_diff))
+        domain = self.app.cfg.interwiki_map[self.app.cfg.interwikiname]
+        unsubscribe_url = urljoin(domain, url_for('frontend.subscribe_item',
+                                                  item_name=self.item_name))
+        diff_url = self.generate_diff_url(domain)
+        item_url = urljoin(domain, url_for('frontend.show_item', item_name=self.item_name))
+        if self.comment is not None:
+            comment = self.meta["comment"]
+        else:
+            comment = self.revs[0].meta["comment"]
+        txt_template = render_template(Notification.txt_template,
+                                       wiki_name=self.wiki_name,
+                                       notification_sentence=self.notification_sentence,
+                                       diff_url=diff_url,
+                                       item_url=item_url,
+                                       comment=comment,
+                                       content_diff_=content_diff,
+                                       meta_diff_=meta_diff_txt,
+                                       unsubscribe_url=unsubscribe_url,
+                                       )
+        html_template = render_template(Notification.html_template,
+                                        wiki_name=self.wiki_name,
+                                        notification_sentence=self.notification_sentence,
+                                        diff_url=diff_url,
+                                        item_url=item_url,
+                                        comment=comment,
+                                        content_diff_=content_diff,
+                                        meta_diff_=meta_diff,
+                                        unsubscribe_url=unsubscribe_url,
+        )
+        return txt_template, html_template
+
+
+def get_item_last_revisions(app, item_name):
+    """ Get 2 or less most recent item revisions from the index
+
+    :param app: local proxy app
+    :param item_name: the name of the item
+    :return: a list of revisions
+    """
+    terms = [Term(WIKINAME, app.cfg.interwikiname), Term(NAME_EXACT, item_name), ]
+    query = And(terms)
+    return list(
+        flaskg.storage.search(query, idx_name=ALL_REVS, sortedby=[MTIME],
+                              reverse=True, limit=2))
+
+
+@item_modified.connect_via(ANY)
+def send_notifications(app, item_name, **kwargs):
+    """ Send mail notifications to subscribers on item change
+
+    :param app: local proxy app
+    :param item_name: name of the changed item
+    :param kwargs: key/value pairs that contain extra information about the item
+                   required in order to create a notification
+    """
+    action = kwargs.get('action')
+    revs = get_item_last_revisions(app, item_name) if action not in [
+        DESTROY_REV, DESTROY_ALL, ] else []
+    notification = Notification(app, item_name, revs, **kwargs)
+    content_diff = notification.get_content_diff()
+    meta_diff = notification.get_meta_diff()
+
+    u = flaskg.user
+    meta = kwargs.get('meta') if action in [DESTROY_REV, DESTROY_ALL, ] else revs[0].meta._meta
+    subscribers = {subscriber for subscriber in get_subscribers(**meta) if
+                   subscriber.itemid != u.itemid}
+    subscribers_locale = {subscriber.locale for subscriber in subscribers}
+    for locale in subscribers_locale:
+        with force_locale(locale):
+            txt_msg, html_msg = notification.render_templates(content_diff, meta_diff)
+            subject = L_('[%(moin_name)s] Update of "%(item_name)s" by %(user_name)s',
+                         moin_name=app.cfg.interwikiname, item_name=item_name, user_name=u.name0)
+            subscribers_emails = [subscriber.email for subscriber in subscribers
+                                  if subscriber.locale == locale]
+            sendmail(subject, txt_msg, to=subscribers_emails, html=html_msg)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/subscriptions.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,91 @@
+# Copyright: 2013 MoinMoin:AnaBalica
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - Subscriptions
+"""
+
+import re
+from collections import namedtuple
+from itertools import chain
+
+from flask import g as flaskg
+
+from whoosh.query import Term, Or
+
+from MoinMoin.constants.keys import (DEFAULT_LOCALE, EMAIL, EMAIL_UNVALIDATED, ITEMID,
+                                     LATEST_REVS, LOCALE, NAME, NAMERE, NAMEPREFIX,
+                                     NAMESPACE, SUBSCRIPTION_IDS, SUBSCRIPTION_PATTERNS, TAGS)
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+
+Subscriber = namedtuple('Subscriber', [ITEMID, NAME, EMAIL, LOCALE])
+
+
+def get_subscribers(**meta):
+    """ Get all users that are subscribed to the item
+
+    :param meta: key/value pairs from item metadata - itemid, name, namespace, tags keys
+    :return: a set of Subscriber objects
+    """
+    itemid = meta.get(ITEMID)
+    name = meta.get(NAME)
+    namespace = meta.get(NAMESPACE)
+    tags = meta.get(TAGS)
+    terms = []
+    if itemid is not None:
+        terms.extend([Term(SUBSCRIPTION_IDS, "{0}:{1}".format(ITEMID, itemid))])
+    if namespace is not None:
+        if name is not None:
+            terms.extend(Term(SUBSCRIPTION_IDS, "{0}:{1}:{2}".format(NAME, namespace, name_))
+                         for name_ in name)
+        if tags is not None:
+            terms.extend(Term(SUBSCRIPTION_IDS, "{0}:{1}:{2}".format(TAGS, namespace, tag))
+                         for tag in tags)
+    query = Or(terms)
+    with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher:
+        result_iterators = [searcher.search(query, limit=None), ]
+        subscription_patterns = searcher.lexicon(SUBSCRIPTION_PATTERNS)
+        patterns = get_matched_subscription_patterns(subscription_patterns, **meta)
+        result_iterators.extend(searcher.documents(subscription_patterns=pattern) for pattern in patterns)
+        subscribers = set()
+        for user in chain.from_iterable(result_iterators):
+            email = user.get(EMAIL)
+            if email:
+                from MoinMoin.user import User
+                u = User(uid=user.get(ITEMID))
+                if u.may.read(name):
+                    locale = user.get(LOCALE, DEFAULT_LOCALE)
+                    subscribers.add(Subscriber(user[ITEMID], user[NAME][0], email, locale))
+    return subscribers
+
+
+def get_matched_subscription_patterns(subscription_patterns, **meta):
+    """ Get all the subscriptions with patterns that match at least one of item names
+
+    :param subscription_patterns: a list of subscription patterns (the ones that
+                                    start with NAMERE or NAMEPREFIX)
+    :param meta: key/value pairs from item metadata - name and namespace keys
+    :return: a list of matched subscription patterns
+    """
+    item_names = meta.get(NAME)
+    item_namespace = meta.get(NAMESPACE)
+    matched_subscriptions = []
+    for subscription in subscription_patterns:
+        keyword, value = subscription.split(":", 1)
+        if keyword in (NAMEPREFIX, NAMERE, ) and item_namespace is not None and item_names:
+            namespace, pattern = value.split(":", 1)
+            if item_namespace == namespace:
+                if keyword == NAMEPREFIX:
+                    if any(name.startswith(pattern) for name in item_names):
+                        matched_subscriptions.append(subscription)
+                elif keyword == NAMERE:
+                    try:
+                        pattern = re.compile(pattern, re.U)
+                    except re.error:
+                        logging.error("Subscription pattern '{0}' has failed compilation.".format(pattern))
+                        continue
+                    if any(pattern.search(name) for name in item_names):
+                        matched_subscriptions.append(subscription)
+    return matched_subscriptions
--- a/contrib/pep8/DeleteTrailingSpaces.py	Tue Jan 21 02:25:35 2014 +0530
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-#!/usr/bin/env python
-# Copyright: 2012 by MoinMoin:RogerHaase
-# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
-
-"""
-Detect and correct violations of the moin2 coding standards:
-    - no trailing blanks
-    - exactly one linefeed at file end, see PEP8
-    - DOS line endings on .bat and .cmd files, unix line endings everywhere else
-
-Execute this script from the root directory of the moin2 repository or
-from anywhere within the contrib path.
-"""
-
-import sys
-import os
-import warnings
-warnings.simplefilter("once")
-
-# file types to be processed
-SELECTED_SUFFIXES = set("py bat cmd html css js styl".split())
-
-# stuff considered DOS/WIN
-WIN_SUFFIXES = set("bat cmd".split())
-
-
-def directories_to_ignore(starting_dir):
-    """Return a list of directories that will not be processed."""
-    # list format: [(fully qualified directory name, sub-directory name), ... ]
-    ignore_dirs = []
-    level2_dirs = ".hg contrib dlc docs env moin.egg-info wiki".split()
-    for dir in level2_dirs:
-        ignore_dirs.append((starting_dir, dir))
-    ignore_dirs.append((starting_dir + os.sep + "MoinMoin", "translations"))
-    return ignore_dirs
-
-
-def check_files(filename, suffix):
-    """Delete trailing blanks,
-        force a single linefeed at file end,
-        force line ending to be \r\n for bat files and \n for all others."""
-    suffix = suffix.lower()
-    if suffix in WIN_SUFFIXES:
-        line_end = "\r\n"
-    else:
-        line_end = "\n"
-
-    with open(filename, "rb") as f:
-        lines = f.readlines()
-
-    # now look at file end and get rid of all whitespace-only lines there:
-    while lines:
-        if not lines[-1].strip():
-            del lines[-1]
-            warnings.warn(u"%s was changed to remove empty lines at eof" % filename)
-        else:
-            break
-
-    with open(filename, "wb") as f:
-        for line in lines:
-            pep8_line = line.rstrip() + line_end
-            f.write(pep8_line)
-            # if line was changed, issue warning once for each type of change
-            if suffix in WIN_SUFFIXES and not line.endswith("\r\n"):
-                warnings.warn(u"%s was changed to DOS line endings" % filename)
-            elif suffix not in WIN_SUFFIXES and line.endswith("\r\n"):
-                warnings.warn(u"%s was changed to Unix line endings" % filename)
-            elif pep8_line != line:
-                warnings.warn(u"%s was changed to remove trailing blanks" % filename)
-
-
-def file_picker(starting_dir):
-    """Select target files and pass each to file checker."""
-    ignore_dirs = directories_to_ignore(starting_dir)
-
-    for root, dirs, files in os.walk(starting_dir):
-        # delete directories in ignore list
-        for mama_dir, baby_dir in ignore_dirs:
-            if mama_dir == root and baby_dir in dirs:
-                dirs.remove(baby_dir)
-        # check files with selected suffixes
-        for file in files:
-            suffix = file.split(".")[-1]
-            if suffix in SELECTED_SUFFIXES:
-                filename = os.path.join(root, file)
-                check_files(filename, suffix)
-
-
-if __name__ == "__main__":
-    if len(sys.argv) > 1:
-        starting_dir = os.path.abspath(sys.argv[1])
-    else:
-        starting_dir = os.path.abspath(os.path.dirname(__file__))
-        starting_dir = starting_dir.split(os.sep + 'contrib')[0]
-    warnings.warn(u"%s is starting directory" % starting_dir)
-    file_picker(starting_dir)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/pep8/coding_std.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# Copyright: 2012 by MoinMoin:RogerHaase
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+Detect and correct violations of the moin2 coding standards:
+    - no trailing blanks
+    - exactly one linefeed at file end, see PEP8
+    - DOS line endings on .bat and .cmd files, unix line endings everywhere else
+
+Execute this script from the root directory of the moin2 repository or
+from anywhere within the contrib path.
+"""
+
+import sys
+import os
+
+
+# file types to be processed
+SELECTED_SUFFIXES = set("py bat cmd html css js styl".split())
+
+# stuff considered DOS/WIN
+WIN_SUFFIXES = set("bat cmd".split())
+
+
+class NoDupsLogger(object):
+    """Suppress duplicate messages."""
+    def __init__(self):
+        self.seen = set()
+
+    def log(self, msg):
+        if msg not in self.seen:
+            print msg
+            self.seen.add(msg)
+
+
+def directories_to_ignore(starting_dir):
+    """Return a list of directories that will not be processed."""
+    # list format: [(fully qualified directory name, sub-directory name), ... ]
+    ignore_dirs = []
+    level2_dirs = ".hg contrib dlc docs env moin.egg-info wiki".split()
+    for dir in level2_dirs:
+        ignore_dirs.append((starting_dir, dir))
+    ignore_dirs.append((starting_dir + os.sep + "MoinMoin", "translations"))
+    return ignore_dirs
+
+
+def check_files(filename, suffix):
+    """Delete trailing blanks,
+        force a single linefeed at file end,
+        force line ending to be \r\n for bat files and \n for all others."""
+    suffix = suffix.lower()
+    if suffix in WIN_SUFFIXES:
+        line_end = "\r\n"
+    else:
+        line_end = "\n"
+    logger = NoDupsLogger()
+
+    with open(filename, "rb") as f:
+        lines = f.readlines()
+
+    # now look at file end and get rid of all whitespace-only lines there:
+    while lines:
+        if not lines[-1].strip():
+            del lines[-1]
+            logger.log(u"%s was changed to remove empty lines at eof" % filename)
+        else:
+            break
+
+    with open(filename, "wb") as f:
+        for line in lines:
+            pep8_line = line.rstrip() + line_end
+            f.write(pep8_line)
+            # if line was changed, issue warning once for each type of change
+            if suffix in WIN_SUFFIXES and not line.endswith("\r\n"):
+                logger.log(u"%s was changed to DOS line endings" % filename)
+            elif suffix not in WIN_SUFFIXES and line.endswith("\r\n"):
+                logger.log(u"%s was changed to Unix line endings" % filename)
+            elif pep8_line != line:
+                if len(pep8_line) < len(line):
+                    logger.log(u"%s was changed to remove trailing blanks" % filename)
+                else:
+                    logger.log(u"%s was changed to add end of line character at end of file" % filename)
+
+
+def file_picker(starting_dir):
+    """Select target files and pass each to file checker."""
+    ignore_dirs = directories_to_ignore(starting_dir)
+
+    for root, dirs, files in os.walk(starting_dir):
+        # delete directories in ignore list
+        for mama_dir, baby_dir in ignore_dirs:
+            if mama_dir == root and baby_dir in dirs:
+                dirs.remove(baby_dir)
+        # check files with selected suffixes
+        for file in files:
+            suffix = file.split(".")[-1]
+            if suffix in SELECTED_SUFFIXES:
+                filename = os.path.join(root, file)
+                check_files(filename, suffix)
+
+
+if __name__ == "__main__":
+    if len(sys.argv) > 1:
+        starting_dir = os.path.abspath(sys.argv[1])
+    else:
+        starting_dir = os.path.abspath(os.path.dirname(__file__))
+        starting_dir = starting_dir.split(os.sep + 'contrib')[0]
+    NoDupsLogger().log(u"Starting directory is %s" % starting_dir)
+    file_picker(starting_dir)
--- a/docs/admin/configure.rst	Tue Jan 21 02:25:35 2014 +0530
+++ b/docs/admin/configure.rst	Sun Feb 02 17:14:23 2014 +0100
@@ -1280,6 +1280,77 @@
 * might be useful together with SMBMount pseudo-authenticator
 
 
+namespaces
+----------
+Moin has support for multiple namespaces. You can configure them as per your need.
+A sample configuration looks like e.g::
+
+    import os
+
+    from wikiconfig import *
+
+    from MoinMoin.storage import create_mapping
+    from MoinMoin.constants.namespaces import NAMESPACE_DEFAULT, NAMESPACE_USERPROFILES
+
+    class LocalConfig(Config):
+        wikiconfig_dir = os.path.abspath(os.path.dirname(__file__))
+        instance_dir = os.path.join(wikiconfig_dir, 'wiki')
+        data_dir = os.path.join(instance_dir, 'data')
+
+        index_storage = 'FileStorage', (os.path.join(instance_dir, "index"), ), {}
+
+        uri = 'stores:fs:{0}/%(backend)s/%(kind)s'.format(data_dir)
+        namespaces = {
+            # maps namespace name -> backend name
+            # first, configure the required, standard namespaces:
+            NAMESPACE_DEFAULT: u'default',
+            NAMESPACE_USERPROFILES + '/': u'userprofiles',
+            # then some additional custom namespaces:
+            u'foo/': u'default',
+            u'bar/': u'default',
+            u'baz/': u'default',
+        }
+        backends = {
+            # maps backend name -> storage
+            u'default': None,
+            u'userprofiles': None,
+        }
+        acls = {
+            # maps namespace name -> acl configuration dict for that namespace
+            NAMESPACE_USERPROFILES + '/': dict(before=u'',
+                                               default=u'All:read,write,create,destroy,admin',
+                                               after=u'',
+                                               hierarchic=False, ),
+            NAMESPACE_DEFAULT: dict(before=u'',
+                                    default=u'All:read,write,create,destroy,admin',
+                                    after=u'',
+                                    hierarchic=False, ),
+            u'foo/': dict(before=u'',
+                          default=u'All:read,write,create,destroy,admin',
+                          after=u'',
+                          hierarchic=False, ),
+            u'bar/': dict(before=u'',
+                          default=u'All:read,write,create,destroy,admin',
+                          after=u'',
+                          hierarchic=False, ),
+            u'baz/': dict(before=u'',
+                          default=u'All:read,write,create,destroy,admin',
+                          after=u'',
+                          hierarchic=False, ),
+        }
+        namespace_mapping, backend_mapping, acl_mapping = create_mapping(uri, namespaces, backends, acls
+
+        # define mapping of namespaces to item_roots (home pages within namespaces).
+        root_mapping = {u'foo': u'fooHome'}
+        # default root, use this value in case a particular namespace key is not present in the above mapping.
+        default_root = u'Home'
+
+    MOINCFG = LocalConfig
+    DEBUG = True
+
+
+.. _mail-configuration:
+
 Mail configuration
 ==================
 
@@ -1287,15 +1358,17 @@
 --------------
 Moin can optionally send E-Mail. Possible uses:
 
-* send out item change notifications.
+* send out item change notifications
 * enable users to reset forgotten passwords
+* inform admins about runtime exceptions
 
 You need to configure some settings before sending E-Mail can be supported::
 
     # the "from:" address [Unicode]
     mail_from = u"wiki <wiki@example.org>"
 
-    # a) using an SMTP server, e.g. "mail.provider.com" (None to disable mail)
+    # a) using an SMTP server, e.g. "mail.provider.com" with optional `:port`
+    appendix, which defaults to 25 (set None to disable mail)
     mail_smarthost = "smtp.example.org"
 
     # if you need to use SMTP AUTH at your mail_smarthost:
@@ -1310,6 +1383,19 @@
 
    describe more moin configuration
 
+Admin Traceback E-Mails
+-----------------------
+If you want to enable admins to receive Python tracebacks, you need to configure
+the following::
+
+    # list of admin emails
+    admin_emails = [u"admin <admin@example.org>"]
+
+    # send tracebacks to admins
+    email_tracebacks = True
+
+
+Please also check the logging configuration example in `docs/examples/config/logging/email`.
 
 User E-Mail Address Verification
 --------------------------------
@@ -1375,3 +1461,4 @@
 Please note that the logging configuration has to be a separate file, so don't
 try this in your wiki configuration file!
 
+
--- a/docs/admin/install.rst	Tue Jan 21 02:25:35 2014 +0530
+++ b/docs/admin/install.rst	Sun Feb 02 17:14:23 2014 +0100
@@ -4,167 +4,153 @@
 
 Downloading
 ===========
-For moin2, there is currently no packaged download available, so you have to get
-it from the repository.
-You can use one of two repository URLs and either use Mercurial to keep a 
-constantly updated copy of the code or download an archive of the files in tar.gz format:
-
-Using Mercurial to clone one of the repositories::
+The recommended way to download moin2 is to clone
+the moin2 Mercurial repository or its mirror. Open a terminal
+window or a command prompt, cd to the directory that will hold
+your project root directory and enter either one of the commands
+below::
 
  hg clone http://hg.moinmo.in/moin/2.0 moin-2.0
+
  OR
+
  hg clone http://bitbucket.org/thomaswaldmann/moin-2.0 moin-2.0
 
 Now make sure your work directory is using the default branch::
 
  hg up -C default
 
-Alternatively, visit http://hg.moinmo.in/moin/2.0 with your web browser and download the archive
-(usually for the "default" branch) and unpack it.
+An alternative installation method is to download the bz2 archive
+from http://hg.moinmo.in/moin/2.0 and unpack it. Once unpacked,
+continue to follow the instructions below.
 
 Installing
 ==========
 Before you can run moin, you need to install it:
 
-Developer install
------------------
-Using your standard Python install or a virtualenv directory
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Please make sure you have `virtualenv` installed, it includes `pip`.
-
-If you just want to run moin in-place in your mercurial working directory
-with your normal system installation of Python, run the following command
-from your mercurial moin2 working directory. You should not run this as an
-administrator or root user; use your standard user account::
-
- ./quickinstall  # for Linux or other POSIX OSes
- # or
- quickinstall.bat  # for Windows
-
-This will use virtualenv to create a directory `env/` within the current directory,
-create a virtual environment for MoinMoin and then install moin2 including all dependencies into that directory.
-`pip` will automatically fetch all dependencies from PyPI and install them, so this may take a while.
-It will also compile the translations (`*.po` files) to binary `*.mo` files.
-
-Please review the output of the quickinstall script, and check whether there were fatal errors.
-
-If you have a bad network connection that makes the downloads fail, you can try to do the steps in quickinstall manually.
-
-Further, the quickinstall script will create a `moin` script for your
-platform which you can use for starting the built-in server or invoke moin script commands.
-
-After you activated the virtual environment, the built-in server script, which is named 
-`moin`, will be in the standard PATH, so you can just run the command `moin` on the command line.
-
-**Note:** in this special mode, it won't copy the MoinMoin code to the env/ directory,
-it will run everything from your work dir, so you can modify code and directly try it out.
-You only need to do this installation procedure once.
-
-Using a different Python or a different virtualenv directory
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-For example, if you want to use `PyPy` and want to name the virtualenv directory `env-pypy`,
-use::
-
- # for linux
- DIR=env-pypy
- PYTHON=/opt/pypy/bin/pypy
-
-That way, you can test with different versions of Python in different virtualenv directories within your moin2 workdir.
-
-Activating the virtual env
---------------------------
-
-IMPORTANT: you always need to activate the virtual environment before running
-anything that executes moin code! Otherwise it won't find the moin command,
-nor the moin code nor the libraries it needs. Also, if you want to install
-additional software into the virtual environment, activate it before running pip!::
-
- source env/bin/activate  # for linux (or other posix OSes)
- # or
- env\Scripts\activate.bat  # for windows
-
-As you have activated the virtual env now, the moin command should be in your
-path now, so you can invoke it using "moin".
-
-Letting moin find the wiki configuration
-----------------------------------------
+Using your standard user account, run the following command
+from the project root directory. Replace <python> in the command
+below with the path to a python 2.7 executable. This is usually
+just "python", but may be "python2.7", "/opt/pypy/bin/pypy"
+or even <some-other-path-to-python>::
 
-moin needs to find the wiki configuration. If you want it to run in the most
-simple way without giving parameters to the moin command, it is easiest if
-you are in the same directory as the configuration files, e.g. wikiconfig.py.
-
-If you are working from a repository workdir, this is the top level
-directory and there is already a ready-to-use wikiconfig.py.
-
-In case you want to just give the configuration file location, make sure you
-use an **absolute path**. moin will try to find its configuration in this
-order:
-
-- command line argument `--config /path/to/wikiconfig.py`
-- environment variable `MOINCFG=/path/to/wikiconfig.py`
-- current directory, file `wikiconfig_local.py`
-- current directory, file `wikiconfig.py`
-
-Initializing index and/or storage
----------------------------------
-If you have an existing storage AND a valid index (for this storage’s content and for this moin version),
-you can skip this section.
-
-If you start from scratch, ie no storage and no index created yet,
-you need to create an empty storage and an empty index::
-
- # create storage and index:
- moin index-create -s -i
-
-Loading some items
-------------------
-If you don't want to have a completely empty wiki, you can optionally load
-some example items into the storage like this::
+ <python> quickinstall.py
 
- # load some example items:
- moin load --file contrib/serialized/items.moin
-
-Building the index
-------------------
-If you have some items in your storage, but no index built yet, you need
-to build an index::
-
- moin index-build
-
-
-Installing PIL / pillow
-~~~~~~~~~~~~~~~~~~~~~~~
-For some image processing functions that MoinMoin uses like resizing and rotating,
-you need PIL, which is the Python Imaging Library (sometimes also referred to as
-python-imaging). Instead of PIL, you can also use pillow, which is a compatible
-fork of PIL (with more active maintenance and it also has been ported to Python 3).
+ OR
 
-Windows users who want to install PIL should skip the remainder of this section and read
-Troubleshooting -- PIL Installation Under Windows below.
-
-If you install PIL with pip, then pip will try to find a jpeg support library and associated development
-headers on your system and if you do not have them, there will be no jpeg support in PIL.
-So, if you want jpeg support, make sure you have the jpeg libs/headers::
+ <python> quickinstall.py <path-to-venv> --download-cache <path-to-cache>
 
- # install jpeg library and development headers:
- sudo apt-get install libjpeg62-dev  # Ubuntu / Debian-based
- yum install libjpeg-turbo-devel  # Fedora / Redhat-based
-
-Now activate your virtual environment and install PIL into it::
+The above will download all dependent packages to a cache,
+install the packages in a virtual environment, and compile the translations
+(`*.po` files) to binary `*.mo` files. This process may take several minutes.
 
- pip install pil  # for Linux (or other POSIX OSes)
+The default cache and virtual environment directory names are:
 
-Alternatively, if you prefer to use pillow::
+ * ~/.pip/pip-download-cache # windows: ~\\pip\\pip-download-cache
+ * ../<PROJECT>-venv-<PYTHON>/
 
- pip install pillow  # for Linux (or other POSIX OSes)
+where <PROJECT> is the name of the project root directory, and <PYTHON>
+is the name of your python interpreter. As noted above, the default
+names may be overridden.
 
+Check the output of quickinstall.py to determine whether there were
+fatal errors. The output messages will normally state that stdout
+and stderr messages were written to a file, a few key success/failure
+messages will be extracted and written to the terminal window, and
+finally a message to type "m" to display a menu.
+
+If there are failure messages, see the troubleshooting section below.
+
+Typing 'm" will display a menu similar to::
+
+    usage: "m <target>" where <target> is:
+
+    quickinstall    update virtual environment with required packages
+    docs            create moin html documentation
+    extras          install OpenID, Pillow, pymongo, sqlalchemy, ldap, upload.py
+    interwiki       refresh contrib\interwiki\intermap.txt (hg version control)
+    log <target>    view detailed log generated by <target>, omit to see list
+
+    new-wiki        create empty wiki
+    sample          create wiki and load sample data
+    restore *       create wiki and restore wiki\backup.moin *option, specify file
+    import <dir>    import a moin 1.9 wiki/data instance from <dir>
+
+    run             run built-in wiki server with local OS and logging options
+    backup *        roll 3 prior backups and create new backup *option, specify file
+
+    css             run Stylus to update CSS files
+    tests           run tests, output goes to pytest.txt and pytestpep8.txt
+    coding-std      correct scripts that taint the repository with trailing spaces..
+    api             update moin api docs (files are under hg version control)
+    dist            delete wiki data, then create distribution archive in /dist
+
+    del-all         same as running the 4 del-* commands below
+    del-orig        delete all files matching *.orig
+    del-pyc         delete all files matching *.pyc
+    del-rej         delete all files matching *.rej
+    del-wiki        create a backup, then delete all wiki data
+
+While most of the above menu choices may be executed now, new users should
+do::
+
+ m sample
+
+to create a wiki instance and load it with sample data. Next, run the
+built-in wiki server::
+
+ m run
+
+As the server starts, about 20 log messages will be output to the
+terminal window.  Point your browser to http://127.0.0.1:8080, the
+sample Home page will appear and more log messages will be output
+to the terminal window. Do a quick test by accessing some of the
+demo items and do a modify and save. If all goes well, your installation
+is complete. The built-in wiki server may be stopped by typing ctrl-C
+in the terminal window.
+
+Next Steps
+==========
+
+If you plan on contributing to the moin2 project, there are more
+instructions waiting for you under the Development topic.
+
+If you plan on just using moin2 as a desktop wiki (and maybe
+help by reporting bugs), then some logical menu choices are:
+
+ * `m docs` - to create docs, see User tab, Documentation (local)
+ * `m extras` - to install Pillow for manipulating images
+ * `m del-wiki` - get rid of the sample data
+ * `m new-wiki` or `m import ...` - no data or moin 1.9 data
+ * `m backup` - backup wiki data as needed or as scheduled
+
+Warning: Backing up data at this point may provide a false sense
+of security because no migration tool has been developed to migrate
+data between moin2 versions.  In its current alpha state, there
+may be code changes that impact the structure of the wiki data or
+indexes. Should this occur, you must start over with an empty
+wiki and somehow copy and paste the contents of all the old wiki
+items into the new wiki. While no such changes are planned,
+they have happened in the past and may happen in the future.
+
+If you installed moin2 by cloning the Moin2 Mercurial repository,
+then you will likely want to install updates on a periodic basis.
+To determine if there are updates available, open a terminal
+window or command prompt, cd to your project root, and enter the
+command below::
+
+  hg incoming
+
+If there are any updates, a brief description of each update will
+be displayed. To add the updates to your cloned repository, do::
+
+  hg pull -u
 
 Troubleshooting
----------------
+===============
 
 PyPi down
-~~~~~~~~~
+---------
 Now and then, PyPi might be down or unreachable.
 
 There are mirrors b.pypi.python.org, c.pypi.python.org, d.pypi.python.org
@@ -174,46 +160,45 @@
  [global]
  index-url = http://c.pypi.python.org/simple
 
-In case that doesn't work either, try our mini pypi that should have all
-packages you need for moin::
-
- # put this into ~/.pip/pip.conf
- [global]
- index-url = http://pypi.moinmo.in/simple
-
 Bad Network Connection
-~~~~~~~~~~~~~~~~~~~~~~
-If you have a poor or limited network connection, you may run into trouble with the commands issued by
-the quickinstall script.
-You may see tracebacks from pip, timeout errors, etc. See the output of the quickinstall script.
-
-If this is the case, try it manually::
-
- # enter your virtual environment:
- source env/bin/activate
-
- # confirm the problems by running:
- pip install -e .
+----------------------
+If you have a poor or limited network connection, you may run into
+trouble with the commands issued by the quickinstall.py script.
+You may see tracebacks from pip, timeout errors, etc. within the output
+of the quickinstall script.
 
-Now install each package into your virtual env manually:
-
-* Find the required packages by looking at "install_requires" within `setup.py`.
-* Download each required package from http://pypi.python.org/
-* Install each of them individually::
-
-    pip install package.tar
-
-* Now try again::
+If this is the case, you may try rerunning the "python quickinstall.py"
+script multiple times. With each subsequent run, packages that are
+all ready cached (view the contents of pip-download-cache) will not
+be downloaded again. Hopefully, any temporary download errors will
+cease with multiple tries.
 
-    pip install -e .
-
-Repeat these steps until you don't see fatal errors.
+ActiveState Python
+------------------
+While ActiveState bundles pip and virtualenv in its distribution,
+there are two missing files. The result is the following error
+messages followed by a traceback::
 
-PIL/pillow Installation Under Windows
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-PIL version 1.1.7 does not install correctly via "pip install pil" on Windows.
-Some users have had success using "pip install pillow", a fork of PIL fixing
-a packaging issue. Other users have resorted to installing PIL 1.1.6 in the
-main Python directory using the Windows installers available at
-http://www.pythonware.com/products/pil/
 
+  Cannot find sdist setuptools-*.tar.gz
+  Cannot find sdist pip-*.tar.gz
+
+To install the missing files, do the following and then rerun
+"python quickinstall.py"::
+
+  \Python27\Scripts\pip.exe uninstall virtualenv
+  \Python27\Scripts\easy_install virtualenv
+
+Other Issues
+------------
+
+If you encounter some other issue not described above, try
+researching the unresolved issues at
+https://bitbucket.org/thomaswaldmann/moin-2.0/issues?status=new&status=open.
+If you find a similar issue, please
+add a note saying you also have the problem and add any new
+information that may assist in the problem resolution.
+
+If you cannot find a similar issue please create a new issue.
+Or, if you are not sure what to do, join us on IRC at #moin-dev
+and describe the problem you have encountered.
--- a/docs/admin/requirements.rst	Tue Jan 21 02:25:35 2014 +0530
+++ b/docs/admin/requirements.rst	Sun Feb 02 17:14:23 2014 +0100
@@ -2,15 +2,43 @@
 Requirements
 ============
 
-MoinMoin requires Python 2.7.x.
-We usually test using CPython and this is what we recommend.
+MoinMoin requires Python 2.7.x. A CPython distribution is
+recommended because it will likely be the fastest and most stable.
+Most developers use a CPython distribution for testing.
+Typical linux distributions will either have Python 2.7 installed by
+default or will have a package manager that will install Python 2.7
+as a secondary Python.
+Windows users may download CPython distributions from  http://www.python.org/ or
+http://www.activestate.com/.
 
-You can also try PyPy, it seems to work.
+An alternative implementation of Python, PyPy, is available
+from http://pypy.org/.
+
+The `virtualenv` Python package is also required. The
+installation process for `virtualenv` varies with your OS and
+Python distribution.
+Many linux distributions have a package manager that may do
+the installation. Windows users (and perhaps others) may download
+setuptools from https://pypi.python.org/pypi/setuptools. Once setuptools is installed, do "`easy_install virtualenv`". Current ActiveState
+distributions include virtualenv in the installation bundle.
+If all else fails, try Google.
+
+Mercurial (hg) is required should you wish to contribute
+patches to the moin2 development effort. Even if you do not
+intend to contribute, Mercurial is highly recommended as it
+will make it easy for you to obtain fixes and enhancements
+from the moin2 repositories. Mercurial can be installed
+with most linux package managers or downloaded
+from http://mercurial.selenic.com/. As an alternative,
+most Windows users will prefer to install TortoiseHG
+(includes Mercurial) from
+http://tortoisehg.bitbucket.org/.
+
 
 Servers
 =======
 
-For moin, you can use any server compatible with WSGI:
+For moin2, you can use any server compatible with WSGI:
 
 * the builtin "moin" server is recommended for desktop wikis, testing,
   debugging, development, adhoc-wikis, etc.
@@ -24,24 +52,15 @@
 Dependencies
 ============
 
-For dependency information, please see setup.py.
-
-If you use easy_install or pip or our ``quickinstall`` script, then
-dependencies are usually automatically dealt with.
-
+Dependent packages will be automatically downloaded and installed during the moin2 installation process. For a list of dependencies, see setup.py.
 
 Clients
 =======
 On the client side, you need:
 
-* a decent web browser that supports W3C standards HTML 5 and CSS 2.1 as well
-  as JavaScript:
+* a web browser that supports W3C standards HTML 5, CSS 2.1, and JavaScript:
 
-  - any current version of Firefox, Chrome, Opera, Safari, Internet Explorer
-    (IE9 or IE10) should work.
-  - usage of older Internet Explorer versions is not recommended and not
-    supported because they are known for causing issues.
-    For Windows 7 (or 8) Microsoft provides IE9 or IE10.
-* Java browser plugin (optional, needed if you want to use TWikiDraw or
-  AnyWikiDraw drawing applets).
+  - any current version of Firefox, Chrome, Opera, Safari, Maxthon, Internet Explorer (IE9 or newer).
+  - use of older Internet Explorer versions is not recommended and not supported.
 
+* a Java browser plugin is required only if you want to use the TWikiDraw or AnyWikiDraw drawing applets.
--- a/docs/devel/development.rst	Tue Jan 21 02:25:35 2014 +0530
+++ b/docs/devel/development.rst	Sun Feb 02 17:14:23 2014 +0100
@@ -23,119 +23,174 @@
 
 * http://bitbucket.org/thomaswaldmann/moin-2.0/issues
 
-Code Repositories (using Mercurial DVCS):
+Code Repositories (using Mercurial DVCS http://mercurial.selenic.com/):
 
 * http://hg.moinmo.in/moin/2.0  (main repository)
 * http://bitbucket.org/thomaswaldmann/moin-2.0  (bitbucket mirror for your
   convenience, simplifying forking and contributing)
 
-Code review (get feedback about code changes):
+Code review (always use this to get feedback about code changes):
 
-* http://codereview.appspot.com/
+* http://code.google.com/p/rietveld/wiki/CodeReviewHelp
+* http://codereview.appspot.com/ (list of current codereview activity)
 
-Pastebin (temporary stuff - do not use for code reviews, do not use from issue
-tracker or for any long-term needed stuff):
+Pastebin (temporary storage - do not use for code review or any long-term need):
 
 * http://rn0.ru/
 
-
 Typical development workflow
 ============================
 
-This is the typical workflow for non-trivial changes and developers that likely
-want to contribute more than one change:
-
-* create your own development environment (only once):
-
-  - create a google account (if you don't have one already, it's free), so you
-    can use codereview.appspot.com
-  - create a bitbucket account (if you don't have one already, it's free)
-  - clone ("fork") the main repository on bitbucket, so you have your own bb
-    repo to publish your work
-  - clone your own bb repo to your local development machine
-  - do a development install from your local repo - read the moin2 install docs
-    for detailled instructions.
-  - join #moin-dev IRC channel and stay there whenever possible
-
-* find some stuff to work on:
-
-  - look at the issue tracker to find some stuff you can solve
-  - in case you want to work on some (non-trivial) new issue or idea that is
-    not on the issue tracker yet, first create an issue there with a detailled
-    description of it
-  - discuss with / get feedback from other developers on the #moin-dev IRC
-    channel
-
-* work on the stuff:
+This is the typical workflow for anyone that wants to contribute to the development of Moin2.
 
-  - to avoid double work, add a comment on the issue tracker that you are
-    working on that issue
-  - work in your local repo on your local development machine (make sure you
-    work in the right branch)
-  - concentrate on one issue / one topic, create a clean set of changes (that
-    means not doing more than needed to fix the issue, but also it means fixing
-    the issue completely and everywhere)
-  - write good, clean, easy-to-understand code.
-  - obey PEP-8
-  - do not fix or change unrelated stuff while working, but rather create new
-    issues on the tracker, so it's not forgotten
-  - regularly run the unit tests ("make test"), the amount of failing tests
-    shall not increase due to your changes
-  - if you fix something that had no test, first try to write a (correct, but
-    still failing) test for it, then fix the code and see the test not failing
-    any more
-  - if you implement new functionality, write tests for it first, then
-    implement it
-  - do an own review of your changes. Use hg diff, hg status - read everything
-    you changed - slowly, looking for stuff that can be improved. Fix
-    everything you find that way before requesting feedback from others.
-  - get feedback from other developers about your changes:
-   
-    + put them on codereview (just run python upload.py in your local repo -
-      if it is not first upload, reuse the same ID to update the already
-      existing codereview)
-    + post the codereview URL to #moin-dev IRC channel asking for review
-    + if you want to get feedback on non-code stuff, either use the issue
-      tracker or a pastebin (only use pastebins for temporary stuff)
-  - repeat until everybody is happy with it
-  - do some final testing - practically and using the unit tests
-  - commit your changes to your local repo, use a meaningful commit comment
+create your development environment
+-----------------------------------
 
-* publish your stuff and request it being merged:
+* if you do not have a bitbucket account, create one at https://bitbucket.org
+* fork the main repository on bitbucket: https://bitbucket.org/thomaswaldmann/moin-2.0
+* clone the main repository to your local development machine
 
-  - push the changeset to your public bitbucket repo
-  - create a pull request to request that your changes get pulled into the
-    main repository
-  - optionally, tell about it on the IRC channel
-  - if you fixed an issue from the issue tracker, make sure the issue gets
-    closed after your fix has been merged.
+  - cd to parent directory of your future repo
+  - "hg clone https://bitbucket.org/thomaswaldmann/moin-2.0 moin-2.0"
+* ensure you are in default branch "hg update default"
+* create the virtualenv and download packages: "python quickinstall.py"
+* create a wiki instance and load sample data: "m sample"
+* start the built-in server: "m run"
+* point your browser at http://127.0.0.1:8080/ to access your development wiki
+* key ctrl+C to stop the built-in server
 
+add more tools, exercise tools
+------------------------------
+
+* if you do not have a google account, create one at http://codereview.appspot.com
+* download upload.py from http://code.google.com/p/rietveld/wiki/CodeReviewHelp
+  to your repo root, then practice using codereview:
+* make a trivial change to any source file, do "python upload.py"
+* inspect your patch set at http://codereview.appspot.com, add a comment
+* click the "Publish and Mail comments" link, check your email inbox
+* make another trivial change to same source file, do "python upload.py -i nnn"
+  where nnn is ID of existing codereview
+* inspect your patch set again, compare patch set 1 to patch set 2
+* click the "Delete" link to delete patchset 2
+* revert the changes on your local repo "hg revert --all"
+* run the unit tests ("m tests"), note any existing test failures
+* install NodeJS with Linux package manager; Windows users may download from http://nodejs.org/download/
+* install stylus
+
+  - "sudo npm install stylus -g" or windows "npm install stylus -g"
+  - "stylus -V"  # show version number to prove it works
+* run Stylus to regenerate CSS files: "m css", verify nothing changed: "hg diff"
+* run "m coding-std" to see if there are any coding errors
+* run "m api" to see any uncommitted API doc changes
+* use "hg revert --all" to revert any changes from above
+* optional: create local docs "m docs"
+* set options on your favorite editor or IDE
+
+  - convert tabs to 4 spaces
+  - delete trailing blanks on file save
+  - use unix line endings (use Windows line endings on .bat and .cmd files)
+  - use mono-spaced font for editing
+* if you are new to mercurial, read a tutorial (http://hginit.com/),
+  consider printing a cheatsheet
+* if you want a Python IDE, try http://www.jetbrains.com/pycharm/ Free Community Edition
+* if you want a graphical interface to Mercurial, install SourceTree (best for mac) or TortoiseHG (best for Windows)
+* join #moin-dev IRC channel; ask questions, learn what other developers are doing
+
+find a task to work on
+----------------------
+
+* look at the issue tracker to find a task you can solve
+* in case you want to work on some (non-trivial) new issue or idea that is
+  not on the issue tracker, create an issue with a detailed description
+* discuss your chosen task with other developers on the #moin-dev IRC
+  channel
+* to avoid duplicate work, add a comment on the issue tracker that you are
+  working on that issue
+* just before you start to code changes, update your local repo: "hg pull -u"
+
+develop a testing strategy
+--------------------------
+
+* if you fix something that had no test, first try to write a correct,
+  but failing test for it, then fix the code and see a successful test
+* if you implement new functionality, write tests for it first, then
+  implement it
+* make a plan for using a browser to test your changes; which wiki pages are
+  effected, how many browsers must be tested
+* run "m tests" to determine if there are any existing test failures before you make changes
+
+develop a working solution
+--------------------------
+
+* work in your local repo on your local development machine
+  (be sure you work in the right branch)
+* concentrate on one issue / one topic, create a clean set of changes
+  (that means not doing more than needed to fix the issue, but also it
+  means fixing the issue completely and everywhere)
+* write good, clean, easy-to-understand code
+* obey PEP-8
+* do not fix or change code unrelated to your task, if you find
+  unrelated bugs, create new issues on the tracker
+* regularly run the unit tests ("m tests"), the amount of failing tests
+  shall not increase due to your changes
+
+review your working solution
+----------------------------
+
+* use hg diff, hg status - read everything you changed - slowly, look for
+  things that can be improved
+
+  - if you have TortoiseHG or SourceTree, use those graphical tools to review changes
+* look for poor variable names, spelling errors in comments, accidental addition
+  or deletion of blank lines, complex code without comments, missing/extra spaces
+* fix everything you find before requesting feedback from others
+* run tests again "m tests"
+
+get feedback from other developers
+----------------------------------
+
+* add changes to codereview: run "python upload.py" in your local repo
+
+  - to update a codereview, "python upload.py -i nnn" where nnn is ID
+* carefully review your changes again on codereview
+
+  - if you find errors, delete the patchset, fix and upload again
+* if you have questions or want to explain something, add comments and click
+  "Publish+Mail Comments"
+* post the codereview URL to #moin-dev IRC channel asking for review
+* repeat until everybody is happy with your changes
+
+publish your change
+-------------------
+
+* do some final testing - practically and using the unit tests
+* commit your changes to your local repo, use a concise commit comment
+  describing the change
+* pull any changes made by others from the main repo on Bitbucket, then
+  merge and commit
+* push the changeset to your public bitbucket repo
+* create a pull request so your changes will get pulled into the
+  main repository
+* optionally, request a pull on the IRC channel
+* if you fixed an issue from the issue tracker, be sure the issue gets
+  closed after your fix has been pulled into main repo.
+* celebrate, loop back to "find a task to work on"
+
+update your virtualenv
+----------------------
+
+Every week or so, do "m quickinstall" to install new releases of
+dependent packages. If any new packages are installed, do a
+quick check for breakages by running tests, starting the
+build-in server, modify an item, etc.
 
 Alternate contribution workflows
 ================================
-If the above workflow looks like overkill (e.g. for simple changes) or you
-can't work with the tools we usually use, you can also do it like this:
-
-* find an existing issue on the issue tracker about the issue you were fixing
-  (or create a new one), make sure to give (or update) all the details, like:
-
-  - precise version number / changeset hashes of the original code your patch
-    is based on
-  - precise description of the issue, how to reproduce it, tracebacks, ...
-  - why your fix is correct / how you tested it
-* create a patch using the diff tool, attach patch.txt to the issue:
-
-    diff -urN originalcodetree/ modifiedcodetree/ > patch.txt
-
-* if you fixed an issue from the issue tracker, make sure the issue gets
-  closed after your fix has been committed to the main repo.
-
-For trivial fixes (like typos), you can also try just grabbing a developer
-on IRC, telling filename, line number and get it fixed by him.
-
-Note: if developers find that the required changes are not that simple or are
-potentially causing other issues, codereview or other parts of the full
-workflow might be needed.
+If the above workflow looks like overkill (e.g. for simple changes)
+or you can't work with the tools we usually use, then just create or
+update an issue on the issue tracker
+https://bitbucket.org/thomaswaldmann/moin-2.0/issues)
+or join us on IRC #moin-dev.
 
 
 MoinMoin architecture
@@ -159,10 +214,6 @@
 * CKeditor, the GUI editor for (x)html
 * TWikiDraw, AnyWikiDraw, svgdraw drawing tools
 
-.. todo::
-
-   add some nice gfx
-
 
 How MoinMoin works
 ==================
@@ -237,7 +288,8 @@
 -------------------------
 How does moin know what the HTML rendering of an item looks like?
 
-Each Item has some contenttype that is stored in the metadata, also called the input contenttype.
+Each Item has some contenttype that is stored in the metadata, also called
+the input contenttype.
 We also know what we want as output, also called the output contenttype.
 
 Moin uses converters to transform the input data into the output data in
@@ -260,7 +312,7 @@
 Finally, the dom-tree will reach the output converter, which will transform it
 into the desired output format, such as `text/html`.
 
-This is just one example of a supported transformation. There are quite a few 
+This is just one example of a supported transformation. There are quite a few
 converters in `MoinMoin.converter` supporting different input formats,
 dom-dom transformations and output formats.
 
@@ -301,7 +353,7 @@
 To run the tests, activate your virtual env and invoke py.test from the
 toplevel directory::
 
-    make test  # easiest way (all tests, pep8, skipped info)
+    m tests  # easiest way (all tests, pep8, skipped info)
     py.test --pep8  # run all tests, including pep8 checks
     py.test -rs  # run all tests and outputs information about skipped tests
     py.test -k somekeyword  # run the tests matching somekeyword only
@@ -335,9 +387,7 @@
 
 Creating docs
 -------------
-Sphinx can create all kinds of documentation formats. The most
-popular ones are::
+Sphinx can create all kinds of documentation formats. The most common are
+the local HTML docs that are linked to under the User tab.
 
-    cd docs
-    make html  # create html docs (to browse online or in the filesystem)
-
+    m docs
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/examples/config/logging/email	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,36 @@
+[DEFAULT]
+# List of admin emails, if left blank will extract the list from wikiconfig ADMIN_EMAILS
+admins=[]
+
+# Email subject, if left blank will display the default subject: [Sitename][Loglevel] Log message
+subject=u""
+
+# Default loglevel, to adjust verbosity: DEBUG, INFO, WARNING, ERROR, CRITICAL
+loglevel=ERROR
+
+# Email loglevel
+emailloglevel=ERROR
+
+[loggers]
+keys=root
+
+[handlers]
+keys=email
+
+[formatters]
+keys=mail
+
+[logger_root]
+level=%(loglevel)s
+handlers=email
+
+[handler_email]
+class=MoinMoin.log.EmailHandler
+level=%(emailloglevel)s
+formatter=default
+args=(%(admins)s, %(subject)s)
+
+[formatter_mail]
+format=%(asctime)s %(levelname)s %(name)s:%(lineno)d %(message)s
+datefmt=
+class=logging.Formatter
\ No newline at end of file
--- a/docs/index.rst	Tue Jan 21 02:25:35 2014 +0530
+++ b/docs/index.rst	Sun Feb 02 17:14:23 2014 +0100
@@ -24,6 +24,8 @@
    user/accounts
    user/markups
    user/search
+   user/namespaces
+   user/subscriptions
 
 Administrating MoinMoin
 =======================
--- a/docs/intro/features.rst	Tue Jan 21 02:25:35 2014 +0530
+++ b/docs/intro/features.rst	Sun Feb 02 17:14:23 2014 +0100
@@ -120,6 +120,7 @@
 * "What did I contribute to?" functionality
 * Sitemap
 * Macro support
+* Multiple names and Namespaces support
 
 Markup support
 --------------
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/user/namespaces.rst	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,36 @@
+==========
+Namespaces
+==========
+
+URL layout
+==========
+``http://server/[NAMESPACE/][[@FIELD/]VALUE][/+VIEW]``
+
+Above defines the URL layout, where uppercase letters are variable parts defined below and [] denotes optional.
+It basically means search for the item field ``FIELD`` value ``VALUE`` in the namespace ``NAMESPACE`` and apply the 
+view ``VIEW`` on it.
+
+NAMESPACE
+ Defines the namespace for looking up the item. NAMESPACE value ``all`` is the "namespace doesn't matter" identifier.
+ It is used to access global views like global history, global tags etc.
+
+FIELD
+ Whoosh schema value where to lookup the VALUE. Default value for field is ``name_exact`` (search by name). FIELD can be a unique identifier like (``itemid, revid, name_exact``) or can be non-unique like (``tags``).
+
+VALUE
+ Value to search in the FIELD. The default value is the default root within that namespace. If the FIELD is non-unique, we
+ show a list items which can have the ``FIELD value:VALUE``.
+
+VIEW
+ used to select the intended view method (default: ``show``).
+
+**Examples**:
+ The following examples show how a url can look like, ``ns1, ns1/ns2`` are namespaces.
+
+ - ``http://localhost:8080/Home``
+ - ``http://localhost:8080/ns1/@tags/sometag``
+ - ``http://localhost:8080/ns1/ns2``
+ - ``http://localhost:8080/ns1/SomePage``
+ - ``http://localhost:8080/+modify/ns1/ns2/SomePage``
+ - ``http://localhost:8080/+delete/ns1/@itemid/37b73d2a6c154bb4ab993d0fb463219c``
+ - ``http://localhost:8080/ns1/@itemid/37b73d2a6c154bb4ab993d0fb463219c``
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/user/subscriptions.rst	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,69 @@
+==================
+User subscriptions
+==================
+
+Users can subscribe to moin items in order to receive notifications about item
+changes. Item changes include:
+
+ * creation of a new item
+ * modification of an existing item
+ * renaming of an item
+ * reverting an item's revision
+ * copying of an item
+ * deletion of an item
+ * destruction of a revision
+ * destruction of all item's revisions
+
+Make sure that Moin is able to send E-Mails, see :ref:`mail-configuration`.
+
+Types of subscriptions
+======================
+
+There are 5 types of subscriptions:
+
+ * by itemid (`itemid:<itemid value>`)
+
+   This is the most common subscription to a single item. The user will be notified
+   even after the item is renamed, because itemid doesn't change. If you click on
+   *Subscribe* on item's page, then you will be subscribed using this type.
+ * by item name (`name:<namespace>:<name value>`),
+
+   The user will be notified, if the name matches any of the item names and also
+   its namespace. Keep in mind that an item can be renamed and notifications for
+   this item would stop working if the new name doesn't match any more.
+ * by tag name (`tags:<namespace>:<tag value>`)
+
+   The user will be notified, if the tag name matches any of the item tags and
+   its namespace.
+ * by a prefix name (`nameprefix:<namespace>:<name prefix>`)
+
+   Used for subscription to a set of items. The user will be notified, if at least
+   one of the item names starts with the given prefix and matches item's namespace.
+   For example if you want to receive notifications about all the items from the
+   default namespace whose name starts with `foo`, you can use `nameprefix::foo`.
+ * by a regular expression (`namere:<namespace>:<name regexp>`)
+
+   Used for subscription to a set of items. The user will be notified, if the
+   regexp matches any of the item names and also its namespace. For example,
+   if you want to receive notifications about all the items on wiki from the default
+   namespace, then you can use `namere::.*`
+
+
+Editing subscriptions
+=====================
+
+The itemid subscription is the most common one and will be used if you click on
+*Subscribe* on item's page. Respectively the *Unsubscribe* will remove the itemid
+subscription.
+
+If you were subscribed to an item by some other way rather than itemid subscription,
+then on *Unsubscribe* you will be told that it is impossible to remove the subscription
+and you need to edit it manually in the User Settings.
+
+All the subscriptions can be added/edited/removed in the User Settings,
+Subscriptions tab. Each subscription is listed on a single line and is
+case-sensitive. Empty lines are ignored.
+
+For itemid subscriptions, we additionally show the current first item name in
+parentheses (this is purely for your information, the name is not stored or used
+in any way).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/make.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,489 @@
+#!/usr/bin/python
+# Copyright: 2013 MoinMoin:RogerHaase
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+make.py provides a menu of commands frequently used by moin2 developers and desktop wiki users.
+
+    - duplicates some common moin commands, do "moin --help" for all alternatives
+    - adds default file names for selected moin commands (backup, restore, ...)
+    - creates log files for functions with large output, extracts success/failure messages
+    - displays error messages when user tries to run commands out of sequence
+
+usage (to display a menu of commands):
+    - unix:     ./m
+    - windows:  m
+
+For make.py to work, it needs to know the name of a python executable and the location of a
+virtual env. These needs are met by running "python quickinstall.py" after cloning the moin2
+repository. quickinstall.py creates these files or symlinks in the repo root:
+    - unix: m, activate, moin
+    - windows: m.bat, activate.bat, deactivate.bat, moin.bat
+
+Executing m.bat or ./m will run make.py. The name of the python executable is within the m.bat or ./m
+script.  The location of the virtual env is within the activate and moin symlinks or activate.bat and
+moin.bat scripts. Depending upon the command to be executed, some mix of the python executable, moin,
+or activate will be used to construct a command string to pass to a subprocess call.
+"""
+
+import os
+import sys
+import subprocess
+import glob
+import shutil
+import fnmatch
+from collections import Counter
+
+import MoinMoin  # validate python version
+
+
+# text files created by commands with high volume output
+QUICKINSTALL = 'm-quickinstall.txt'
+PYTEST = 'm-pytest.txt'
+PEP8 = 'm-pep8.txt'
+CODING_STD = 'm-coding-std.txt'
+DOCS = 'm-docs.txt'
+NEWWIKI = 'm-new-wiki.txt'
+DELWIKI = 'm-delete-wiki.txt'
+BACKUPWIKI = 'm-backup-wiki.txt'
+EXTRAS = 'm-extras.txt'
+DIST = 'm-create-dist.txt'
+# default files used for backup and restore
+BACKUP_FILENAME = 'wiki/backup.moin'
+JUST_IN_CASE_BACKUP = 'wiki/deleted-backup.moin'
+
+
+if os.name == 'nt':
+    M = 'm'  # customize help to local OS
+    WINDOWS_OS = True
+else:
+    M = './m'
+    WINDOWS_OS = False
+
+
+# commands that create log files; "tests" creates 2 log files - pytest + pep8
+CMD_LOGS = {
+    'quickinstall': QUICKINSTALL,
+    'pytest': PYTEST,
+    'pep8': PEP8,
+    # 'coding-std': CODING_STD,  # not logged due to small output
+    'docs': DOCS,
+    'new-wiki': NEWWIKI,
+    'del-wiki': DELWIKI,
+    'backup': BACKUPWIKI,
+    'extras': EXTRAS,
+    'dist': DIST,
+}
+
+
+help = r"""
+
+usage: "%s <target>" where <target> is:
+
+quickinstall    update virtual environment with required packages
+docs            create moin html documentation
+extras          install OpenID, Pillow, pymongo, sqlalchemy, ldap, upload.py
+interwiki       refresh contrib\interwiki\intermap.txt (hg version control)
+log <target>    view detailed log generated by <target>, omit to see list
+
+new-wiki        create empty wiki
+sample          create wiki and load sample data
+restore *       create wiki and restore wiki\backup.moin *option, specify file
+import <dir>    import a moin 1.9 wiki/data instance from <dir>
+
+run             run built-in wiki server with local OS and logging options
+backup *        roll 3 prior backups and create new backup *option, specify file
+
+css             run Stylus to update CSS files
+tests           run tests, output goes to pytest.txt and pytestpep8.txt
+coding-std      correct scripts that taint the repository with trailing spaces..
+api             update moin api docs (files are under hg version control)
+dist            delete wiki data, then create distribution archive in /dist
+
+del-all         same as running the 4 del-* commands below
+del-orig        delete all files matching *.orig
+del-pyc         delete all files matching *.pyc
+del-rej         delete all files matching *.rej
+del-wiki        create a backup, then delete all wiki data
+""" % M
+
+
+def search_for_phrase(filename):
+    """Search a text file for key phrases and print the lines of interest or print a count by phrase."""
+    files = {
+        # filename: (list of phrases)
+        QUICKINSTALL: ('could not find', 'error', 'fail', 'timeout', 'traceback', 'success', 'cache location', 'must be deactivated', ),
+        NEWWIKI: ('error', 'fail', 'timeout', 'traceback', 'success', ),
+        BACKUPWIKI: ('error', 'fail', 'timeout', 'traceback', 'success', ),
+        # use of 'error ' below is to avoid matching .../Modules/errors.o....
+        EXTRAS: ('error ', 'error:', 'error.', 'error,', 'fail', 'timeout', 'traceback', 'success', 'already satisfied', 'active version', 'installed', 'finished', ),
+        PYTEST: ('seconds =', ),
+        PEP8: ('seconds =', ),
+        CODING_STD: ('remove trailing blanks', 'dos line endings', 'unix line endings', 'remove empty lines', ),
+        DIST: ('creating', 'copying', 'adding', 'hard linking', ),
+        DOCS: ('build finished', 'build succeeded', 'traceback', 'failed', 'error', 'usage', 'importerror', 'Exception occurred', )
+    }
+    # for these file names, display a count of occurrances rather than each found line
+    print_counts = (CODING_STD, DIST, )
+
+    with open(filename, "r") as f:
+        lines = f.readlines()
+    name = os.path.split(filename)[1]
+    phrases = files[name]
+    counts = Counter()
+    for idx, line in enumerate(lines):
+        line = line.lower()
+        for phrase in phrases:
+            if phrase in line:
+                if filename in print_counts:
+                    counts[phrase] += 1
+                else:
+                    print idx + 1, line.rstrip()
+                    break
+    for key in counts:
+        print 'The phrase "%s" was found %s times.' % (key, counts[key])
+
+
+def wiki_exists():
+    """Return truthy if a wiki exists."""
+    return glob.glob('wiki/index/_all_revs_*.toc')
+
+
+def make_wiki(command):
+    """Process command to create a new wiki."""
+    if wiki_exists():
+        print 'Error: a wiki exists, delete it and try again.'
+    else:
+        print 'Output messages redirected to %s' % NEWWIKI
+        with open(NEWWIKI, 'w') as messages:
+            result = subprocess.call(command, shell=True, stderr=messages, stdout=messages)
+        if result == 0:
+            print '\nSuccess: a new wiki has been created.'
+        else:
+            print 'Important messages from %s are shown below:' % NEWWIKI
+            search_for_phrase(NEWWIKI)
+            print '\nError: attempt to create wiki failed. Do "%s log new-wiki" to see complete log.' % M
+
+
+def delete_files(pattern):
+    """Recursively delete all files matching pattern."""
+    matches = []
+    for root, dirnames, filenames in os.walk(os.path.abspath(os.path.dirname(__file__))):
+        for filename in fnmatch.filter(filenames, pattern):
+            matches.append(os.path.join(root, filename))
+    for match in matches:
+        os.remove(match)
+    print 'Deleted %s files matching "%s".' % (len(matches), pattern)
+
+
+class Commands(object):
+    """Each cmd_ method processes a choice on the menu."""
+    def __init__(self):
+        pass
+
+    def cmd_quickinstall(self, *args):
+        """create or update a virtual environment with the required packages"""
+        command = '%s quickinstall.py %s' % (sys.executable, ' '.join(args))
+        print 'Running quickinstall.py... output messages redirected to %s' % QUICKINSTALL
+        with open(QUICKINSTALL, 'w') as messages:
+            result = subprocess.call(command, shell=True, stderr=messages, stdout=messages)
+        if result != 0:
+            open(QUICKINSTALL, 'a').write('Error: quickinstall passed non-zero return code: %s' % result)
+        print 'Searching %s, important messages are shown below... Do "%s log quickinstall" to see complete log.\n' % (QUICKINSTALL, M)
+        search_for_phrase(QUICKINSTALL)
+
+    def cmd_docs(self, *args):
+        """create local Sphinx html documentation"""
+        if WINDOWS_OS:
+            command = 'activate.bat & cd docs & make.bat html'  # windows separates commands with "&"
+        else:
+            # in terminal "source activate" works, but shell requires "source ./activate"
+            command = 'source ./activate; cd docs; make html'  # unix separates commands with ";"
+        print 'Creating HTML docs... output messages written to %s.' % DOCS
+        with open(DOCS, 'w') as messages:
+            result = subprocess.call(command, shell=True, stderr=messages, stdout=messages)
+        print 'Searching %s, important messages are shown below...\n' % DOCS
+        search_for_phrase(DOCS)
+        if result == 0:
+            print 'HTML docs successfully created in docs/_build/html.'
+        else:
+            print 'Error: creation of HTML docs failed with return code "%s". Do "%s log docs" to see complete log.' % (result, M)
+
+    def cmd_extras(self, *args):
+        """install optional packages: OpenID, Pillow, pymongo, sqlalchemy, ldap; and upload.py"""
+        upload = '%s MoinMoin/script/win/wget.py https://codereview.appspot.com/static/upload.py upload.py' % sys.executable
+        if WINDOWS_OS:
+            print 'Installing OpenId, Pillow, pymongo, sqlalchemy, upload.py... output messages written to %s.' % EXTRAS
+            # easy_install is used for windows because it installs binary packages, pip does not
+            command = 'activate.bat & easy_install python-openid & easy_install pillow & easy_install pymongo & easy_install sqlalchemy' + ' & ' + upload
+            # TODO: "easy_install python-ldap" fails on windows
+            # try google: installing python-ldap in a virtualenv on windows
+            # or, download from http://www.lfd.uci.edu/~gohlke/pythonlibs/#python-ldap
+            #   activate.bat
+            #   easy_install <path to downloaded .exe file>
+        else:
+            print 'Installing OpenId, Pillow, pymongo, sqlalchemy, ldap, upload.py... output messages written to %s.' % EXTRAS
+            command = 'source ./activate; pip install python-openid; pip install pillow; pip install pymongo; pip install sqlalchemy; pip install python-ldap' + '; ' + upload
+        with open(EXTRAS, 'w') as messages:
+            subprocess.call(command, shell=True, stderr=messages, stdout=messages)
+        print 'Important messages from %s are shown below. Do "%s log extras" to see complete log.' % (EXTRAS, M)
+        search_for_phrase(EXTRAS)
+
+    def cmd_interwiki(self, *args):
+        """refresh contrib\interwiki\intermap.txt"""
+        print 'Refreshing contrib\interwiki\intermap.txt...'
+        command = '%s MoinMoin/script/win/wget.py http://master19.moinmo.in/InterWikiMap?action=raw contrib/interwiki/intermap.txt' % sys.executable
+        subprocess.call(command, shell=True)
+
+    def cmd_log(self, *args):
+        """View a log file with the default text editor"""
+
+        def log_help(logs):
+            """Print list of available logs to view."""
+            print "usage: %s log <target> where <target> is:\n\n" % M
+            choices = '{0: <16}- {1}'
+            for log in sorted(logs):
+                if os.path.isfile(CMD_LOGS[log]):
+                    print choices.format(log, CMD_LOGS[log])
+                else:
+                    print choices.format(log, '* file does not exist')
+
+        logs = set(CMD_LOGS.keys())
+        if args and args[0] in logs and os.path.isfile(CMD_LOGS[args[0]]):
+            if WINDOWS_OS:
+                command = 'start %s' % CMD_LOGS[args[0]]
+            else:
+                command = '${VISUAL:-${FCEDIT:-${EDITOR:-less}}} %s' % CMD_LOGS[args[0]]
+            subprocess.call(command, shell=True)
+        else:
+            log_help(logs)
+
+    def cmd_new_wiki(self, *args):
+        """create empty wiki"""
+        if WINDOWS_OS:
+            command = 'moin.bat index-create -s -i'
+        else:
+            command = './moin index-create -s -i'
+        print 'Creating a new empty wiki...'
+        make_wiki(command)  # share code with loading sample data and restoring backups
+
+    def cmd_sample(self, *args):
+        """create wiki and load sample data"""
+        if WINDOWS_OS:
+            command = 'moin.bat index-create -s -i & moin.bat load --file contrib/serialized/items.moin & moin.bat index-build'
+        else:
+            command = './moin index-create -s -i; ./moin load --file contrib/serialized/items.moin; ./moin index-build'
+        print 'Creating a new wiki populated with sample data...'
+        make_wiki(command)
+
+    def cmd_restore(self, *args):
+        """create wiki and load data from wiki/backup.moin or user specified path"""
+        if WINDOWS_OS:
+            command = 'moin.bat index-create -s -i & moin.bat load --file %s & moin.bat index-build'
+        else:
+            command = './moin index-create -s -i; ./moin load --file %s; ./moin index-build'
+        filename = BACKUP_FILENAME
+        if args:
+            filename = args[0]
+        if os.path.isfile(filename):
+            command = command % filename
+            print 'Creating a new wiki and loading it with data from %s...' % filename
+            make_wiki(command)
+        else:
+            print 'Error: cannot create wiki because %s does not exist.' % filename
+
+    def cmd_import(self, *args):
+        """import a moin 1.9 wiki directory named dir"""
+        if WINDOWS_OS:
+            command = 'moin.bat import19 -s -i --data_dir %s'
+        else:
+            command = './moin import19 -s -i --data_dir %s'
+        if args:
+            dirname = args[0]
+            if os.path.isdir(dirname):
+                command = command % dirname
+                print 'Creating a new wiki populated with data from %s...' % dirname
+                make_wiki(command)
+            else:
+                print 'Error: cannot create wiki because %s does not exist.' % dirname
+        else:
+            print 'Error: a path to the Moin 1.9 wiki/data data directory is required.'
+
+    def cmd_run(self, *args):
+        """run built-in wiki server with local options"""
+        if wiki_exists():
+            if os.path.isfile('logging.conf'):
+                if WINDOWS_OS:
+                    logfile = 'set MOINLOGGINGCONF=logging.conf & '
+                else:
+                    logfile = 'MOINLOGGINGCONF=logging.conf; export MOINLOGGINGCONF; '
+            else:
+                logfile = ''
+            if WINDOWS_OS:
+                command = '%smoin.bat moin %s --threaded' % (logfile, ' '.join(args))
+            else:
+                command = '%s./moin moin %s' % (logfile, ' '.join(args))
+            try:
+                subprocess.call(command, shell=True)
+            except KeyboardInterrupt:
+                pass  # on windows pass eliminates traceback but "Terminate batch job..." message is displayed twice
+        else:
+            print 'Error: a wiki must be created before running the built-in server.'
+
+    def cmd_backup(self, *args):
+        """roll 3 prior backups and create new wiki/backup.moin or backup to user specified file"""
+        if wiki_exists():
+            filename = BACKUP_FILENAME
+            if args:
+                filename = args[0]
+                print 'Creating a wiki backup to %s...' % filename
+            else:
+                print 'Creating a wiki backup to %s after rolling 3 prior backups...' % filename
+                b3 = BACKUP_FILENAME.replace('.', '3.')
+                b2 = BACKUP_FILENAME.replace('.', '2.')
+                b1 = BACKUP_FILENAME.replace('.', '1.')
+                if os.path.exists(b3):
+                    os.remove(b3)
+                for src, dst in ((b2, b3), (b1, b2), (BACKUP_FILENAME, b1)):
+                    if os.path.exists(src):
+                        os.rename(src, dst)
+            if WINDOWS_OS:
+                command = 'moin.bat save --all-backends --file %s' % filename
+            else:
+                command = './moin save --all-backends --file %s' % filename
+            with open(BACKUPWIKI, 'w') as messages:
+                result = subprocess.call(command, shell=True, stderr=messages, stdout=messages)
+            if result == 0:
+                print 'Success: wiki was backed up to %s' % filename
+            else:
+                print 'Important messages from %s are shown below. Do "%s log backup" to see complete log.' % (BACKUPWIKI, M)
+                search_for_phrase(BACKUPWIKI)
+                print '\nError: attempt to backup wiki failed.'
+        else:
+            print 'Error: cannot backup wiki because it has not been created.'
+
+    def cmd_css(self, *args):
+        """run Stylus to update CSS files"""
+        print 'Running Stylus to update CSS files...'
+        if WINDOWS_OS:
+            command = r'cd MoinMoin\themes\modernized\static\css\stylus & stylus --include-css --compress < main.styl > ../common.css'
+        else:
+            command = 'cd MoinMoin/themes/modernized/static/css/stylus; stylus --include-css --compress < main.styl > ../common.css'
+        result = subprocess.call(command, shell=True)
+
+        if WINDOWS_OS:
+            command = r'cd MoinMoin\themes\foobar\static\css\stylus & stylus --include-css --compress < main.styl > ../common.css'
+        else:
+            command = 'cd MoinMoin/themes/foobar/static/css/stylus; stylus --include-css --compress < main.styl > ../common.css'
+        result2 = subprocess.call(command, shell=True)
+
+        if result == 0 and result2 == 0:
+            print 'Success: CSS files updated.'
+        else:
+            print 'Error: stylus failed to update css files, see error messages above.'
+
+    def cmd_tests(self, *args):
+        """run tests, output goes to pytest.txt and pytestpep8.txt"""
+        print 'Running tests... output written to %s and %s.' % (PYTEST, PEP8)
+        if WINDOWS_OS:
+            command = 'activate.bat & py.test.exe > %s 2>&1 & py.test.exe --pep8 -k pep8 --clearcache > %s 2>&1' % (PYTEST, PEP8)
+        else:
+            command = 'source ./activate; py.test > %s 2>&1; py.test --pep8 -k pep8 --clearcache > %s 2>&1' % (PYTEST, PEP8)
+        result = subprocess.call(command, shell=True)
+        print 'Summary message from %s is shown below. Do "%s log pytest" to see complete log.' % (PYTEST, M)
+        search_for_phrase(PYTEST)
+        print 'Summary message from %s is shown below. Do "%s log pep8" to see complete log.' % (PEP8, M)
+        search_for_phrase(PEP8)
+
+    def cmd_coding_std(self, *args):
+        """correct scripts that taint the HG repository and clutter subsequent code reviews"""
+        print 'Checking for trailing blanks, DOS line endings, Unix line endings, empty lines at eof...'
+        command = '%s contrib/pep8/coding_std.py' % sys.executable
+        subprocess.call(command, shell=True)
+
+    def cmd_api(self, *args):
+        """update Sphinx API docs, these docs are under hg version control"""
+        print 'Refreshing api docs...'
+        if WINDOWS_OS:
+            command = 'activate.bat & sphinx-apidoc -f -o docs/devel/api MoinMoin & %s MoinMoin/script/win/dos2unix.py docs/devel/api' % sys.executable
+        else:
+            command = 'source ./activate; sphinx-apidoc -f -o docs/devel/api MoinMoin'
+        result = subprocess.call(command, shell=True)
+
+    def cmd_dist(self, *args):
+        """create distribution archive in dist/"""
+        print 'Deleting wiki data, then creating distribution archive in /dist, output written to %s.' % DIST
+        self.cmd_del_wiki(*args)
+        command = '%s setup.py sdist' % sys.executable
+        with open(DIST, 'w') as messages:
+            result = subprocess.call(command, shell=True, stderr=messages, stdout=messages)
+        print 'Summary message from %s is shown below:' % DIST
+        search_for_phrase(DIST)
+        if result == 0:
+            print 'Success: a distribution archive was created in /dist.'
+        else:
+            print 'Error: create dist failed with return code = %s. Do "%s log dist" to see complete log.' % (result, M)
+
+    def cmd_del_all(self, *args):
+        """same as running the 4 del-* commands below"""
+        self.cmd_del_orig(*args)
+        self.cmd_del_pyc(*args)
+        self.cmd_del_rej(*args)
+        self.cmd_del_wiki(*args)
+
+    def cmd_del_orig(self, *args):
+        """delete all files matching *.orig"""
+        delete_files('*.orig')
+
+    def cmd_del_pyc(self, *args):
+        """delete all files matching *.pyc"""
+        delete_files('*.pyc')
+
+    def cmd_del_rej(self, *args):
+        """delete all files matching *.rej"""
+        delete_files('*.rej')
+
+    def cmd_del_wiki(self, *args):
+        """create a just-in-case backup, then delete all wiki data"""
+        if WINDOWS_OS:
+            command = 'moin.bat save --all-backends --file %s' % JUST_IN_CASE_BACKUP
+        else:
+            command = './moin save --all-backends --file %s' % JUST_IN_CASE_BACKUP
+        if wiki_exists():
+            print 'Creating a backup named %s; then deleting all wiki data and indexes...' % JUST_IN_CASE_BACKUP
+            with open(DELWIKI, 'w') as messages:
+                result = subprocess.call(command, shell=True, stderr=messages, stdout=messages)
+            if result != 0:
+                print 'Error: backup failed with return code = %s. Complete log is in %s.' % (result, DELWIKI)
+        # destroy wiki even if backup fails
+        if os.path.isdir('wiki/data') or os.path.isdir('wiki/index'):
+            shutil.rmtree('wiki/data')
+            shutil.rmtree('wiki/index')
+            print 'Wiki data successfully deleted.'
+        else:
+            print 'Wiki data not deleted because it does not exist.'
+
+
+if __name__ == '__main__':
+    # create a set of valid menu choices
+    commands = Commands()
+    choices = set()
+    names = dir(commands)
+    for name in names:
+        if name.startswith('cmd_'):
+            choices.add(name)
+
+    if len(sys.argv) == 1 or sys.argv[1] == '-h' or sys.argv[1] == '--help':
+        print help
+    else:
+        if sys.argv[1] != 'quickinstall' and not (os.path.isfile('activate') or os.path.isfile('activate.bat')):
+            print 'Error: files created by quickinstall are missing, run "%s quickinstall" and try again.' % M
+        else:
+            choice = 'cmd_%s' % sys.argv[1]
+            choice = choice.replace('-', '_')
+            if choice in choices:
+                choice = getattr(commands, choice)
+                choice(*sys.argv[2:])
+            else:
+                print help
+                print 'Error: unknown menu selection "%s"' % sys.argv[1]
--- a/quickinstall	Tue Jan 21 02:25:35 2014 +0530
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,45 +0,0 @@
-#!/bin/bash
-# create a virtual environment in directory $DIR/
-#
-# set PYTHON environment variable to change the python version
-# set DIR environment variable to change the virtual env directory
-# set VIRTUALENV environment variable to change the virtualenv command
-# for example: PYTHON=/usr/bin/pypy DIR=env-pypy ./quickinstall
-#
-# needs: virtualenv, pip
-
-DLC=dlc
-
-# if DIR is not given, use ./env
-if [ -z "$DIR" ]; then
-    DIR=env
-fi
-
-# find the right python version
-if [ -z "$PYTHON" ]; then
-    for PYTHON in python{2.7,2,}; do
-        hash $PYTHON 2>&- && break
-    done
-fi
-
-# find the right virtualenv version
-if [ -z "$VIRTUALENV" ]; then
-    for VIRTUALENV in virtualenv{2.7,2,}; do
-        hash $VIRTUALENV 2>&- && break
-    done
-fi
-
-$VIRTUALENV --no-site-packages --python $PYTHON $DIR || exit 1
-
-source $DIR/bin/activate || exit 1
-
-# first install babel, moin's setup.py will emit a warning if it is not there
-pip install --download-cache=$DLC babel || exit 1
-
-# "install" moin2 from repo to the env, this will also install required python
-# packages from pypi. we do this LAST, so that breakage is better visible.
-pip install --download-cache=$DLC -e . || exit 1
-
-# compile the translations
-python setup.py compile_catalog --statistics || exit 1
-
--- a/quickinstall.bat	Tue Jan 21 02:25:35 2014 +0530
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-echo off
-echo.
-echo This is the windows version of the "quickinstall" file.
-echo.
-
-echo Creating a virtual environment in directory env/ ...
-virtualenv --no-site-packages env
-
-echo Activating virtual environment ...
-call env\Scripts\activate.bat
-
-echo Installing babel first ...
-pip install --download-cache=dlc babel
-
-echo Installing all required python packages from pypi ...
-pip install --download-cache=dlc -e .
-
-echo Compiling translations (not required if wiki is English only) ...
-python setup.py compile_catalog --statistics
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/quickinstall.py	Sun Feb 02 17:14:23 2014 +0100
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+# Copyright: 2013 MoinMoin:BastianBlank
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+"""
+create a virtual environment and install moin2 (in development mode) and
+its requirements.
+
+needs: virtualenv, pip
+"""
+
+PIP15 = False  # dirty hack to support pip >= 1.5 incompatibilities
+
+import MoinMoin  # validate python version
+import argparse
+import logging
+import os
+import subprocess
+import sys
+try:
+    import virtualenv
+except ImportError:
+    sys.exit("""
+Error: import virtualenv failed, either virtualenv is not installed (see installation docs)
+or the virtual environment must be deactivated before rerunning quickinstall.py
+""")
+
+from make import Commands, WINDOWS_OS, M
+
+
+WIN_INFO = 'm.bat, activate.bat, deactivate.bat, and moin.bat are created by quickinstall.py'
+NIX_INFO = 'the m bash script and the activate and moin symlinks are created by quickinstall.py'
+
+
+def create_m():
+    """Create an 'm.bat or 'm' bash script that will run make.py using this Python"""
+    if WINDOWS_OS:
+        with open('m.bat', 'w') as f:
+            f.write(':: {}\n\n@{} make.py %*\n'.format(WIN_INFO, sys.executable))
+    else:
+        with open('m', 'w') as f:
+            f.write('# {}\n\n{} make.py $*\n'.format(NIX_INFO, sys.executable))
+            os.fchmod(f.fileno(), 0775)
+
+
+class QuickInstall(object):
+    def __init__(self, source, venv=None, download_cache=None):
+        self.dir_source = source
+        if venv is None:
+            base, source_name = os.path.split(source)
+            executable = os.path.basename(sys.executable).split('.exe')[0]
+            venv = os.path.join(base, '{}-venv-{}'.format(source_name, executable))
+        if download_cache is None:
+            # make cache sibling of ~/pip/pip.log or ~/.pip/pip.log
+            if WINDOWS_OS:
+                download_cache = '~/pip/pip-download-cache'
+            else:
+                # XXX: move cache to XDG cache dir
+                download_cache = '~/.pip/pip-download-cache'
+
+        venv = os.path.abspath(venv)
+        venv_home, venv_lib, venv_inc, venv_bin = virtualenv.path_locations(venv)
+        self.dir_venv = venv_home
+        self.dir_venv_bin = venv_bin
+        self.download_cache = os.path.normpath(os.path.expanduser(download_cache))
+
+    def __call__(self):
+        self.do_venv()
+        self.do_install()
+        self.do_catalog()
+        self.do_helpers()
+
+        sys.stdout.write("""
+Pip cache location is at {0}
+
+Successfully created or updated venv at {1}
+""".format(self.download_cache, self.dir_venv))
+
+    def do_venv(self):
+        virtualenv.create_environment(self.dir_venv)
+
+    def do_install(self):
+        args = [
+            os.path.join(self.dir_venv_bin, 'pip'),
+            'install',
+            '--download-cache',
+            self.download_cache,
+            '--editable',
+            self.dir_source,
+        ]
+        if PIP15:
+            args += [
+            '--process-dependency-links',
+            '--allow-external', 'flatland',
+            '--allow-unverified', 'flatland',
+        ]
+        subprocess.check_call(args)
+
+    def do_catalog(self):
+        subprocess.check_call((
+            os.path.join(self.dir_venv_bin, 'python'),
+            os.path.join(self.dir_source, 'setup.py'),
+            'compile_catalog', '--statistics',
+            # needed in case user runs quickinstall.py with a cwd other than the repo root
+            '--directory', os.path.join(os.path.dirname(__file__), 'MoinMoin', 'translations'),
+        ))
+
+    def create_wrapper(self, filename, target):
+        """Create files in the repo root that wrap files in <path-to-virtual-env>\Scripts."""
+        target = os.path.join(self.dir_venv_bin, target)
+        with open(filename, 'w') as f:
+            f.write(':: {}\n\n@call {} %*\n'.format(WIN_INFO, target))
+
+    def do_helpers(self):
+        """Create small helper scripts or symlinks in repo root, avoid keying the long path to virtual env."""
+        create_m()  # recreate m.bat or ./m to insure it is consistent with activate and moin
+        if WINDOWS_OS:
+            # windows commands are: activate | deactivate | moin
+            self.create_wrapper('activate.bat', 'activate.bat')
+            self.create_wrapper('deactivate.bat', 'deactivate.bat')
+            self.create_wrapper('moin.bat', 'moin.exe')
+        else:
+            # linux commands are: source activate | deactivate | ./moin
+            if os.path.exists('activate'):
+                os.unlink('activate')
+            if os.path.exists('moin'):
+                os.unlink('moin')
+            os.symlink(os.path.join(self.dir_venv_bin, 'activate'), 'activate')  # no need to define deactivate on unix
+            os.symlink(os.path.join(self.dir_venv_bin, 'moin'), 'moin')
+
+
+if __name__ == '__main__':
+    if os.path.isfile('m') or os.path.isfile('m.bat'):
+        # create the virtual env
+        logging.basicConfig(level=logging.INFO)
+
+        parser = argparse.ArgumentParser()
+        parser.add_argument('venv', metavar='VENV', nargs='?', help='location of v(irtual)env')
+        parser.add_argument('--download_cache', dest='download_cache', help='location of pip download cache')
+        args = parser.parse_args()
+
+        QuickInstall(os.path.dirname(os.path.realpath(sys.argv[0])), venv=args.venv, download_cache=args.download_cache)()
+    else:
+        # run this same script (quickinstall.py) again to create the virtual env
+        create_m()  # create file so above IF will be true next time around
+        # Use the make.py subprocess so user will see a few success/failure messages instead of ~500 info messages.
+        commands = Commands()
+        choice = getattr(commands, 'cmd_quickinstall')
+        choice(*sys.argv[1:])  # <override-path-to-venv> --download_cache <override-path-to-cache>
+        print '\n> > > Type "%s" for menu < < <' % M
--- a/setup.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/setup.py	Sun Feb 02 17:14:23 2014 +0100
@@ -3,17 +3,12 @@
 # Copyright: 2001-2012 MoinMoin:ThomasWaldmann
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
-import sys
 import os
-
-from MoinMoin import project, version
+import MoinMoin  # validate python version
 
-if sys.hexversion < 0x2070000:
-    # we require 2.7.x, python 3.x does not work yet.
-    sys.stderr.write("%s %s requires Python 2.7.x.\n" % (project, str(version)))
-    sys.exit(1)
 
-with open('README.txt') as f:
+basedir = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(basedir, 'README.txt')) as f:
     long_description = f.read()
 
 from setuptools import setup, find_packages
@@ -21,13 +16,13 @@
 
 setup_args = dict(
     name="moin",
-    version=str(version),
+    version=str(MoinMoin.version),
     description="MoinMoin is an easy to use, full-featured and extensible wiki software package",
     long_description=long_description,
     author="Juergen Hermann et al.",
     author_email="moin-user@lists.sourceforge.net",
     # maintainer(_email) not active because distutils/register can't handle author and maintainer at once
-    download_url='http://static.moinmo.in/files/moin-%s.tar.gz' % (version, ),
+    download_url='http://static.moinmo.in/files/moin-%s.tar.gz' % (MoinMoin.version, ),
     url="http://moinmo.in/",
     license="GNU GPL v2 (or any later version)",
     keywords="wiki web",
@@ -103,7 +98,7 @@
                               # note: pytest-pep8 1.0.3 needs pytest 2.3
         'whoosh>=2.5.0',  # needed for indexed search
         'sphinx>=1.1',  # needed to build the docs
-        'pdfminer',  # pdf -> text/plain conversion
+        'pdfminer==20110515',  # pdf -> text/plain conversion, XXX 20131113 fails see #385
         'passlib>=1.6.0',  # strong password hashing (1.6 needed for consteq)
         'XStatic>=0.0.2',  # support for static file pypi packages
         'XStatic-Bootstrap>=3.0.0.1',
@@ -112,8 +107,6 @@
         'XStatic-autosize',
         'XStatic-jQuery>=1.8.2',
         'XStatic-jQuery-File-Upload>=4.4.2',
-        'XStatic-JSON-js',
-        'XStatic-svgweb>=2011.2.3.2',
         'XStatic-TWikiDraw-moin>=2004.10.23.2',
         'XStatic-AnyWikiDraw>=0.14.2',
         'XStatic-svg-edit-moin>=2012.11.15.1',
--- a/wikiconfig.py	Tue Jan 21 02:25:35 2014 +0530
+++ b/wikiconfig.py	Sun Feb 02 17:14:23 2014 +0100
@@ -44,8 +44,6 @@
                               hierarchic=False, ),
     )
 
-    #item_root = u'Home' # front page
-
     # for display purposes:
     sitename = u'My MoinMoin'
     # it is required that you set this to a unique, stable and non-empty name:
@@ -65,12 +63,10 @@
     # names below must be package names
     mod_names = [
         'jquery', 'jquery_file_upload',
-        'json_js',
         'bootstrap',
         'font_awesome',
         'ckeditor',
         'autosize',
-        'svgweb',
         'svgedit_moin', 'twikidraw_moin', 'anywikidraw',
     ]
     pkg = __import__('xstatic.pkg', fromlist=mod_names)
@@ -79,6 +75,11 @@
         xs = XStatic(mod, root_url='/static', provider='local', protocol='http')
         serve_files.update([(xs.name, xs.base_dir)])
 
+    # list of admin emails
+    admin_emails = []
+    # send tracebacks to admins
+    email_tracebacks = False
+
 
 MOINCFG = Config  # Flask only likes uppercase stuff
 # Flask settings - see the flask documentation about their meaning