Commits

Thomas Waldmann committed 12961b6 Merge

merged default branch into gae branch

Comments (0)

Files changed (266)

MoinMoin/_tests/__init__.py

 """
 
 
-import os, shutil
-import socket, errno
+import socket
 from StringIO import StringIO
 
-from flask import current_app as app
 from flask import g as flaskg
 
-from MoinMoin import config, security, user
-from MoinMoin.config import NAME, CONTENTTYPE
+from MoinMoin.constants.contenttypes import CHARSET
+from MoinMoin.constants.keys import NAME, CONTENTTYPE
 from MoinMoin.items import Item
 from MoinMoin.util.crypto import random_string
-from MoinMoin.storage.error import ItemAlreadyExistsError
 
 # Promoting the test user -------------------------------------------
 # Usually the tests run as anonymous user, but for some stuff, you
 # need more privs...
 
+
 def become_valid(username=u"ValidUser"):
     """ modify flaskg.user to make the user valid.
         Note that a valid user will only be in ACL special group "Known", if
 def update_item(name, meta, data):
     """ creates or updates an item  """
     if isinstance(data, unicode):
-        data = data.encode(config.charset)
+        data = data.encode(CHARSET)
     item = flaskg.storage[name]
 
     meta = meta.copy()
     rev = item.store_revision(meta, StringIO(data), return_rev=True)
     return rev
 
+
 def create_random_string_list(length=14, count=10):
     """ creates a list of random strings """
     chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
     return [u"{0}".format(random_string(length, chars)) for counter in range(count)]
 
+
 def nuke_item(name):
     """ complete destroys an item """
     item = Item.create(name)

MoinMoin/_tests/_test_template.py

     )
 
     from MoinMoin._tests import wikiconfig
+
     class Config(wikiconfig.Config):
         foo = 'bar'  # we want to have this non-default setting
 

MoinMoin/_tests/ldap_testbase.py

 SLAPD_EXECUTABLE = 'slapd'  # filename of LDAP server executable - if it is not
                             # in your PATH, you have to give full path/filename.
 
-import os, shutil, tempfile, time, base64
+import os
+import shutil
+import tempfile
+import time
+import base64
 from StringIO import StringIO
 import signal
 import subprocess
 import hashlib
 
 try:
-    import ldap, ldif, ldap.modlist  # needs python-ldap
+    # needs python-ldap
+    import ldap
+    import ldap.modlist
+    import ldif
 except ImportError:
     ldap = None
 
 
 class Slapd(object):
     """ Manage a slapd process for testing purposes """
-    def __init__(self,
-                 config=None,  # config filename for -f
-                 executable=SLAPD_EXECUTABLE,
-                 debug_flags='', # None,  # for -d stats,acl,args,trace,sync,config
-                 proto='ldap', ip='127.0.0.1', port=3890,  # use -h proto://ip:port
-                 service_name=''  # defaults to -n executable:port, use None to not use -n
-                ):
+    def __init__(
+        self,
+        config=None,  # config filename for -f
+        executable=SLAPD_EXECUTABLE,
+        debug_flags='',  # None,  # for -d stats,acl,args,trace,sync,config
+        proto='ldap', ip='127.0.0.1', port=3890,  # use -h proto://ip:port
+        service_name='',  # defaults to -n executable:port, use None to not use -n
+    ):
         self.executable = executable
         self.config = config
         self.debug_flags = debug_flags
         self.proto = proto
         self.ip = ip
         self.port = port
-        self.url = '{0}://{1}:{2}'.format(proto, ip, port) # can be used for ldap.initialize() call
+        self.url = '{0}://{1}:{2}'.format(proto, ip, port)  # can be used for ldap.initialize() call
         if service_name == '':
             self.service_name = '{0}:{1}'.format(executable, port)
         else:
         started = None
         if timeout:
             lo = ldap.initialize(self.url)
-            ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) # ldap v2 is outdated
+            ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)  # ldap v2 is outdated
             started = False
             wait_until = time.time() + timeout
             while time.time() < wait_until:
 #set_tas_spins 0
 """
 
-    def __init__(self,
-                 basedn,
-                 rootdn, rootpw,
-                 instance=0,  # use different values when running multiple LdapEnvironments
-                 schema_dir='/etc/ldap/schema',  # directory with schemas
-                 coding='utf-8',  # coding used for config files
-                 timeout=10,  # how long to wait for slapd starting [s]
-                ):
+    def __init__(
+        self,
+        basedn,
+        rootdn, rootpw,
+        instance=0,  # use different values when running multiple LdapEnvironments
+        schema_dir='/etc/ldap/schema',  # directory with schemas
+        coding='utf-8',  # coding used for config files
+        timeout=10,  # how long to wait for slapd starting [s]
+    ):
         self.basedn = basedn
         self.rootdn = rootdn
         self.rootpw = rootpw
 
     def start_slapd(self):
         """ start a slapd and optionally wait until it talks with us """
-        self.slapd = Slapd(config=self.slapd_conf, port=3890+self.instance)
+        self.slapd = Slapd(config=self.slapd_conf, port=3890 + self.instance)
         started = self.slapd.start(timeout=self.timeout)
         return started
 
     def load_directory(self, ldif_content):
         """ load the directory with the ldif_content (str) """
         lo = ldap.initialize(self.slapd.url)
-        ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) # ldap v2 is outdated
+        ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)  # ldap v2 is outdated
         lo.simple_bind_s(self.rootdn, self.rootpw)
 
         class LDIFLoader(ldif.LDIFParser):
 try:
     import pytest
 
-    class LDAPTstBase:
+    class LDAPTstBase(object):
         """ Test base class for pytest based tests which need a LDAP server to talk to.
 
             Inherit your test class from this base class to test LDAP stuff.
             started = self.ldap_env.start_slapd()
             if not started:
                 pytest.skip("Failed to start {0} process, please see your syslog / log files"
-                             " (and check if stopping apparmor helps, in case you use it).".format(SLAPD_EXECUTABLE))
+                            " (and check if stopping apparmor helps, in case you use it).".format(SLAPD_EXECUTABLE))
             self.ldap_env.load_directory(ldif_content=self.ldif_content)
 
         def teardown_class(self):

MoinMoin/_tests/test_error.py

 """
 
 
-import pytest
-
 from MoinMoin import error
 
 
         err = error.Error(test)
         assert '%(message)s' % dict(message=err) == test
 
+
 class TestCompositeError(object):
 
     def setup_method(self, method):
         expected = ['This is a fatal Error']
         assert result == expected
 
+
 coverage_modules = ['MoinMoin.error']

MoinMoin/_tests/test_forms.py

 
 from MoinMoin.forms import DateTimeUNIX
 
+
 def test_datetimeunix():
     dt = datetime.datetime(2012, 12, 21, 23, 45, 59)
     timestamp = timegm(dt.timetuple())

MoinMoin/_tests/test_test_environ.py

 
 from StringIO import StringIO
 
-import pytest
-
 from flask import current_app as app
 from flask import g as flaskg
 
-from MoinMoin.conftest import init_test_app, deinit_test_app
-from MoinMoin.config import NAME, CURRENT, CONTENTTYPE, IS_SYSITEM, SYSITEM_VERSION
-from MoinMoin.storage.error import NoSuchItemError
+from MoinMoin.constants.keys import NAME, CONTENTTYPE
 
 from MoinMoin._tests import wikiconfig
 
+
 class TestStorageEnvironWithoutConfig(object):
     def setup_method(self, method):
         self.class_level_value = 123
         itemname = u"this item shouldn't exist yet"
         assert not storage.has_item(itemname)
         item = storage[itemname]
-        new_rev = item.store_revision({NAME: [itemname, ], CONTENTTYPE: u'text/plain'}, StringIO(''))
+        new_rev = item.store_revision({NAME: [itemname, ], CONTENTTYPE: u'text/plain;charset=utf-8'}, StringIO(''))
         assert storage.has_item(itemname)
 
 
 CONTENT_ACL = dict(
-        before="+All:write", # need to write to sys pages
-        default="All:read,write,admin,create,destroy",
-        after="Me:create",
-        hierarchic=False,
+    before="+All:write",  # need to write to sys pages
+    default="All:read,write,admin,create,destroy",
+    after="Me:create",
+    hierarchic=False,
 )
 
+
 class TestStorageEnvironWithConfig(object):
 
     class Config(wikiconfig.Config):

MoinMoin/_tests/test_user.py

 # -*- coding: utf-8 -*-
 # Copyright: 2003-2004 by Juergen Hermann <jh@web.de>
 # Copyright: 2009 by ReimarBauer
-# Copyright: 2013 by ThomasWaldmann
+# Copyright: 2011-2013 by ThomasWaldmann
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
 """
 
 
-import pytest
-
-from flask import current_app as app
 from flask import g as flaskg
 
 from MoinMoin import user
-from MoinMoin.util import crypto
 
 
 class TestSimple(object):
     def testUnicodePassword(self):
         """ user: login with non-ascii password """
         # Create test user
-        name = u'__שם משתמש לא קיים__' # Hebrew
+        name = u'__שם משתמש לא קיים__'  # Hebrew
         password = name
         self.createUser(name, password)
 
         assert theUser.valid
 
         # invalidate the stored password (hash)
-        theUser.set_password("") # emptry str or None means "invalidate"
+        theUser.set_password("")  # emptry str or None means "invalidate"
         theUser.save()
 
         # Try to "login" with previous 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
+        assert theUser.is_subscribed_to([pagename])  # list(!) of pages to check
 
     def testSubscriptionSubPage(self):
         """ user: tests is_subscribed_to on a subpage """
         # 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
+        assert not theUser.is_subscribed_to([testPagename])  # list(!) of pages to check
 
     # Bookmarks -------------------------------------------------------
 
             u' User Name',
             u'User Name ',
             u'User   Name',
-            )
+        )
         for test in cases:
             assert not user.isValidName(test)
 
     def testValid(self):
         """ user: isValidName: accept names in any language, with spaces """
         cases = (
-            u'Jürgen Hermann', # German
-            u'ניר סופר', # Hebrew
-            u'CamelCase', # Good old camel case
-            u'가각간갇갈 갉갊감 갬갯걀갼' # Hangul (gibberish)
-            )
+            u'Jürgen Hermann',  # German
+            u'ניר סופר',  # Hebrew
+            u'CamelCase',  # Good old camel case
+            u'가각간갇갈 갉갊감 갬갯걀갼'  # Hangul (gibberish)
+        )
         for test in cases:
             assert user.isValidName(test)
 

MoinMoin/_tests/test_wikiutil.py

 # Copyright: 2003-2004 by Juergen Hermann <jh@web.de>
-# Copyright: 2007 by MoinMoin:ThomasWaldmann
+# Copyright: 2007-2013 by MoinMoin:ThomasWaldmann
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
 
 from flask import current_app as app
 
-from MoinMoin import config, wikiutil
-from MoinMoin._tests import wikiconfig
+from MoinMoin.constants.chartypes import CHARS_SPACES
+from MoinMoin import wikiutil
 
 from werkzeug import MultiDict
 
 
 class TestCleanInput(object):
     def testCleanInput(self):
-        tests = [(u"", u""), # empty
-                 (u"aaa\r\n\tbbb", u"aaa   bbb"), # ws chars -> blanks
-                 (u"aaa\x00\x01bbb", u"aaabbb"), # strip weird chars
-                 (u"a"*500, u""), # too long
-                ]
+        tests = [
+            (u"", u""),  # empty
+            (u"aaa\r\n\tbbb", u"aaa   bbb"),  # ws chars -> blanks
+            (u"aaa\x00\x01bbb", u"aaabbb"),  # strip weird chars
+            (u"a" * 500, u""),  # too long
+        ]
         for instr, outstr in tests:
             assert wikiutil.clean_input(instr) == outstr
 
         (('MainPage', '/SubPage1'), 'MainPage/SubPage1'),
         (('MainPage', '/SubPage1/SubPage2'), 'MainPage/SubPage1/SubPage2'),
         (('MainPage/SubPage1', '/SubPage2/SubPage3'), 'MainPage/SubPage1/SubPage2/SubPage3'),
-        (('', '/OtherMainPage'), 'OtherMainPage'), # strange
+        (('', '/OtherMainPage'), 'OtherMainPage'),  # strange
         # PARENT_PREFIX
         (('MainPage/SubPage', '../SisterPage'), 'MainPage/SisterPage'),
         (('MainPage/SubPage1/SubPage2', '../SisterPage'), 'MainPage/SubPage1/SisterPage'),
         (('MainPage/SubPage1/SubPage2', '../../SisterPage'), 'MainPage/SisterPage'),
-        (('MainPage', '../SisterPage'), 'SisterPage'), # strange
+        (('MainPage', '../SisterPage'), 'SisterPage'),  # strange
     ]
+
     def test_abs_pagename(self):
         for (current_page, relative_page), absolute_page in self.tests:
             yield self._check_abs_pagename, current_page, relative_page, absolute_page
             (u'a/', u'a'),
             (u'a/////b/////c', u'a/b/c'),
             (u'a b/////c d/////e f', u'a b/c d/e f'),
-            )
+        )
         for test, expected in cases:
             result = wikiutil.normalize_pagename(test, app.cfg)
             assert result == expected
             (u'a     b     c', u'a b c'),
             (u'a   b  /  c    d  /  e   f', u'a b/c d/e f'),
             # All 30 unicode spaces
-            (config.chars_spaces, u''),
-            )
+            (CHARS_SPACES, u''),
+        )
         for test, expected in cases:
             result = wikiutil.normalize_pagename(test, app.cfg)
             assert result == expected
             (u'a  ', u'a'),
             (u'a  b  c', u'a b c'),
             (u'a  b  /  c  d  /  e  f', u'a b/c d/e f'),
-            )
+        )
         for test, expected in cases:
             result = wikiutil.normalize_pagename(test, app.cfg)
             assert result == expected
 
+
 class TestGroupItems(object):
 
     def testNormalizeGroupName(self):
             (u'Name,:Group', u'NameGroup'),
             # remove than normalize spaces
             (u'Name ! @ # $ % ^ & * ( ) + Group', u'Name Group'),
-            )
+        )
         for test, expected in cases:
             # validate we are testing valid group names
             if wikiutil.isGroupItem(test):
     # with no parent
     result = wikiutil.ParentItemName(u'itemname')
     expected = u''
-    assert result == expected, ('Expected "%(expected)s" but got "%(result)s"')
+    assert result == expected, 'Expected "%(expected)s" but got "%(result)s"' % locals()
     # with a parent
     result = wikiutil.ParentItemName(u'some/parent/itemname')
     expected = u'some/parent'
     assert result == expected
 
+
 def testdrawing2fname():
-    # with extension not in config.drawing_extensions
+    # with extension not in DRAWING_EXTENSIONS
     result = wikiutil.drawing2fname('Moin_drawing.txt')
     expected = 'Moin_drawing.txt.tdraw'
     assert result == expected
-    # with extension in config.drawing_extensions
+    # with extension in DRAWING_EXTENSIONS
     result = wikiutil.drawing2fname('Moindir.Moin_drawing.jpg')
     expected = 'Moindir.Moin_drawing.jpg'
     assert result == expected
 
+
 def testgetUnicodeIndexGroup():
     result = wikiutil.getUnicodeIndexGroup(['moin-2', 'MoinMoin'])
     expected = 'MOIN-2'
     with pytest.raises(IndexError):
         result = wikiutil.getUnicodeIndexGroup('')
 
+
 def testis_URL():
     sample_schemes = ['http', 'https', 'ftp', 'ssh']
     for scheme in sample_schemes:
     result = wikiutil.is_URL('invalid_scheme:MoinMoin')
     assert not result
 
+
 def testcontainsConflictMarker():
     # text with conflict marker
     result = wikiutil.containsConflictMarker("/!\\ '''Edit conflict - Conflict marker is present")
     result = wikiutil.containsConflictMarker('No conflict marker')
     assert not result
 
+
 def testsplit_anchor():
     """
     TODO: add the test for for split_anchor when we have better
     expected = ['#MoinMoin', '']
     assert result == expected
 
+
 def testfile_headers():
     test_headers = [
-                #test_file, content_type
-                ('imagefile.gif', 'image/gif'),
-                ('testfile.txt', 'text/plain'),
-                ('pdffile.pdf', 'application/pdf'),
-                ('docfile.doc', 'application/msword'),
-                (None, 'application/octet-stream')
-                ]
+        # test_file, content_type
+        ('imagefile.gif', 'image/gif'),
+        ('testfile.txt', 'text/plain'),
+        ('pdffile.pdf', 'application/pdf'),
+        ('docfile.doc', 'application/msword'),
+        (None, 'application/octet-stream'),
+    ]
 
     for test_file, content_type in test_headers:
         result = wikiutil.file_headers(test_file, None, 10)
     expected = [('Content-Type', 'text/plain')]
     assert result == expected
 
+
 coverage_modules = ['MoinMoin.wikiutil']

MoinMoin/_tests/wikiconfig.py

 # Copyright: 2000-2004 by Juergen Hermann <jh@web.de>
+# Copyright: 2011-2013 by MoinMoin:ThomasWaldmann
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
 
 import os
 from os.path import abspath, dirname, join
+
 from MoinMoin.config.default import DefaultConfig
 
+
 class Config(DefaultConfig):
+    """
+    default configuration for the unit tests
+    """
     _here = abspath(dirname(__file__))
     _root = abspath(join(_here, '..', '..'))
-    data_dir = join(_here, 'wiki', 'data') # needed for plugins package TODO
+    data_dir = join(_here, 'wiki', 'data')  # needed for plugins package TODO
     index_storage = 'FileStorage', (join(_here, 'wiki', 'index'), ), {}
     content_acl = None
     item_root = 'FrontPage'
 from MoinMoin import log
 logging = log.getLogger(__name__)
 
+from MoinMoin.constants.misc import ANON
 from MoinMoin.i18n import i18n_init
 from MoinMoin.i18n import _, L_, N_
 
 
 
 def create_app_ext(flask_config_file=None, flask_config_dict=None,
-                   moin_config_class=None, warn_default=True, **kwargs
-                  ):
+                   moin_config_class=None, warn_default=True, **kwargs):
     """
     Factory for moin wsgi apps
 
 
             Rule('/<itemname:wikipage>')
             Rule('/<itemname:wikipage>/edit')
-
-        :param map: the :class:`Map`.
         """
         regex = '[^/]+?(/[^/]+?)*'
         weight = 200
 
     # if we still have no user obj, create a dummy:
     if not userobj:
-        userobj = user.User(name=u'anonymous', auth_method='invalid')
+        userobj = user.User(name=ANON, auth_method='invalid')
     # if we have a valid user we store it in the session
     if userobj.valid:
         session['user.itemid'] = userobj.itemid

MoinMoin/apps/admin/_tests/test_admin.py

 
 from flask import url_for
 
+
 class TestAdmin(object):
     def _test_view_get(self, url, status='200 OK', data=('<html>', '</html>')):
         with self.app.test_client() as c:

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

 <h1>{{ _("Admin Menu") }}</h1>
 <ul>
     <li><a href="{{ url_for('admin.userbrowser') }}">{{ _("Users") }}</a></li>
-    <li><a href="{{ url_for('admin.sysitems_upgrade') }}">{{ _("Upgrade System Items") }}</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>
 </ul>

MoinMoin/apps/admin/templates/admin/sysitems_upgrade.html

-{% extends theme("layout.html") %}
-{% block content %}
-<h1>{{ _("Upgrade System Items") }}</h1>
-<p>
-{{ _("You can upgrade your system items by uploading an xml file with new items below.") }}
-</p>
-<form action="{{ url_for('admin.sysitems_upgrade') }}" method="POST" enctype="multipart/form-data">
-<fieldset>
-    <label for="xmlfile">System items XML file:</label><input type="file" id="xmlfile" name="xmlfile" />
-    <input type="submit" name="submit" value="{{ _("Upgrade system items") }}" />
-</fieldset>
-</form>
-{% endblock %}

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

             {% endif %}
         </td>
         <td>
-            <form action="{{ url_for('admin.userprofile', user_name=u.name) }}" method="GET">
+            <form action="{{ url_for('admin.userprofile', user_name=u.name[0]) }}" method="POST">
                 <input type="hidden" name="key" value="disabled" />
                 <input type="hidden" name="val" value="{{ u.disabled and "0" or "1" }}" />
                 <input type="submit" name="userprofile" value="{{ u.disabled and _("Enable user") or _("Disable user") }}" />

MoinMoin/apps/admin/templates/admin/wikiconfig.html

 </td>
 </tr>
 {% endfor %}
-</tdbody>
+</tbody>
 </table>
 {% endblock %}

MoinMoin/apps/admin/views.py

 from MoinMoin.themes import render_template
 from MoinMoin.apps.admin import admin
 from MoinMoin import user
-from MoinMoin.storage.error import NoSuchRevisionError
-from MoinMoin.config import NAME, ITEMID, SIZE, EMAIL
-from MoinMoin.config import SUPERUSER
+from MoinMoin.constants.keys import NAME, ITEMID, SIZE, EMAIL, DISABLED
+from MoinMoin.constants.rights import SUPERUSER
 from MoinMoin.security import require_permission
 
+
 @admin.route('/superuser')
 @require_permission(SUPERUSER)
 def index():
     return render_template('admin/index.html', title_name=_(u"Admin"))
 
+
 @admin.route('/user')
 def index_user():
     return render_template('user/index_user.html', title_name=_(u"User"))
     User Account Browser
     """
     groups = flaskg.groups
-    revs = user.search_users() # all users
+    revs = user.search_users()  # all users
     user_accounts = [dict(uid=rev.meta[ITEMID],
                           name=rev.meta[NAME],
                           email=rev.meta[EMAIL],
-                          disabled=False,  # TODO: add to index
+                          disabled=rev.meta[DISABLED],
                           groups=[groupname for groupname in groups if rev.meta[NAME] in groups[groupname]],
-                     )
-                     for rev in revs]
+                     ) for rev in revs]
     return render_template('admin/userbrowser.html', user_accounts=user_accounts, title_name=_(u"Users"))
 
 
         ok = False
         if hasattr(u, key):
             ok = True
-            oldval = getattr(u, key)
+            oldval = u.profile[key]
             if isinstance(oldval, bool):
-                val = bool(val)
+                val = bool(int(val))
             elif isinstance(oldval, int):
                 val = int(val)
             elif isinstance(oldval, unicode):
             else:
                 ok = False
         if ok:
-            setattr(u, key, val)
+            u.profile[key] = val
             u.save()
             flash(u'{0}.{1}: {2} -> {3}'.format(user_name, key, unicode(oldval), unicode(val), ), "info")
         else:
     return redirect(url_for('.userbrowser'))
 
 
-@admin.route('/sysitems_upgrade', methods=['GET', 'POST', ])
-@require_permission(SUPERUSER)
-def sysitems_upgrade():
-    from MoinMoin.storage.backends import upgrade_sysitems
-    from MoinMoin.storage.error import BackendError
-    if request.method == 'GET':
-        action = 'syspages_upgrade'
-        label = 'Upgrade System Pages'
-        return render_template('admin/sysitems_upgrade.html',
-                               title_name=_(u"System items upgrade"))
-    if request.method == 'POST':
-        xmlfile = request.files.get('xmlfile')
-        try:
-            upgrade_sysitems(xmlfile)
-        except BackendError as e:
-            flash(_('System items upgrade failed due to the following error: %(error)s.', error=e), 'error')
-        else:
-            flash(_('System items have been upgraded successfully!'))
-        return redirect(url_for('.index'))
+from MoinMoin.config import default as defaultconfig
 
 
-from MoinMoin.config import default as defaultconfig
-
 @admin.route('/wikiconfig', methods=['GET', ])
 @require_permission(SUPERUSER)
 def wikiconfig():
 def highlighterhelp():
     """display a table with list of available Pygments lexers"""
     import pygments.lexers
-    headings = [_('Lexer description'),
-                _('Lexer names'),
-                _('File patterns'),
-                _('Mimetypes'),
-               ]
+    headings = [
+        _('Lexer description'),
+        _('Lexer names'),
+        _('File patterns'),
+        _('Mimetypes'),
+    ]
     lexers = pygments.lexers.get_all_lexers()
     rows = sorted([[desc, ' '.join(names), ' '.join(patterns), ' '.join(mimetypes), ]
                    for desc, names, patterns, mimetypes in lexers])
 @admin.route('/interwikihelp', methods=['GET', ])
 def interwikihelp():
     """display a table with list of known interwiki names / urls"""
-    headings = [_('InterWiki name'),
-                _('URL'),
-               ]
+    headings = [
+        _('InterWiki name'),
+        _('URL'),
+    ]
     rows = sorted(app.cfg.interwiki_map.items())
     return render_template('user/interwikihelp.html',
                            title_name=_(u"Interwiki Names"),
 @admin.route('/itemsize', methods=['GET', ])
 def itemsize():
     """display a table with item sizes"""
-    headings = [_('Size'),
-                _('Item name'),
-               ]
+    headings = [
+        _('Size'),
+        _('Item name'),
+    ]
     rows = [(rev.meta[SIZE], rev.name)
             for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)]
     rows = sorted(rows, reverse=True)

MoinMoin/apps/feed/_tests/test_feed.py

 
 from flask import url_for
 
-from MoinMoin.items import Item
-from MoinMoin.config import CONTENTTYPE, COMMENT
+from MoinMoin.constants.keys import COMMENT
 from MoinMoin._tests import update_item, wikiconfig
 
+
 class TestFeeds(object):
     class Config(wikiconfig.Config):
         """

MoinMoin/apps/feed/views.py

 from flask import request, Response
 from flask import current_app as app
 from flask import g as flaskg
-from flask import url_for
 
 from werkzeug.contrib.atom import AtomFeed
 from jinja2 import Markup
 
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.apps.feed import feed
-from MoinMoin.config import (NAME, NAME_EXACT, WIKINAME, ACL, ACTION, ADDRESS,
-                            HOSTNAME, USERID, COMMENT, MTIME, REVID, ALL_REVS,
-                            PARENTID, LATEST_REVS)
+from MoinMoin.constants.keys import NAME, NAME_EXACT, WIKINAME, COMMENT, MTIME, REVID, ALL_REVS, PARENTID, LATEST_REVS
 from MoinMoin.themes import get_editor_info, render_template
 from MoinMoin.items import Item
 from MoinMoin.util.crypto import cache_key
 from MoinMoin.util.interwiki import url_for_item
 
+
 @feed.route('/atom/<itemname:item_name>')
 @feed.route('/atom', defaults=dict(item_name=''))
 def atom(item_name):
                     content = hl_item.content._render_data_diff_atom(previous_rev, this_rev)
                 else:
                     # full html rendering for new items
-                    content = render_template('atom.html', get='first_revision', rev=this_rev, content=Markup(hl_item.content._render_data()), revision=this_revid)
+                    content = render_template('atom.html', get='first_revision', rev=this_rev,
+                                              content=Markup(hl_item.content._render_data()), revision=this_revid)
                 content_type = 'html'
             except Exception as e:
                 logging.exception("content rendering crashed")
             if rev_comment:
                 # Trim down extremely long revision comment
                 if len(rev_comment) > 80:
-                    content = render_template('atom.html', get='comment_cont_merge', comment=rev_comment[79:], content=Markup(content))
+                    content = render_template('atom.html', get='comment_cont_merge', comment=rev_comment[79:],
+                                              content=Markup(content))
                     rev_comment = u"{0}...".format(rev_comment[:79])
                 feed_title = u"{0} - {1}".format(author.get(NAME, ''), rev_comment)
             else:
                      author=author,
                      url=url_for_item(name, rev=this_revid, _external=True),
                      updated=datetime.fromtimestamp(rev.meta[MTIME]),
-                    )
+            )
         content = feed.to_string()
         # Hack to add XSLT stylesheet declaration since AtomFeed doesn't allow this
         content = content.split("\n")

MoinMoin/apps/frontend/_tests/test_frontend.py

 
 from StringIO import StringIO
 
-import pytest
-
 from flask import url_for
 from flask import g as flaskg
 from werkzeug import ImmutableMultiDict, FileStorage
 
 from MoinMoin.apps.frontend import views
 from MoinMoin import user
-from MoinMoin.util import crypto
-from MoinMoin._tests import wikiconfig
 
 
 class TestFrontend(object):
         self._test_view_post('frontend.ajaxdelete', status='200 OK', content_types=['application/json', ], data=['{', '}'], form=dict(
             comment='Test',
             itemnames='["DoesntExist"]',
-            ), viewopts=dict(item_name='DoesntExist'))
+        ), viewopts=dict(item_name='DoesntExist'))
 
     def test_ajaxdelete_no_item_name_route(self):
         self._test_view_post('frontend.ajaxdelete', status='200 OK', content_types=['application/json', ], data=['{', '}'], form=dict(
             comment='Test',
             itemnames='["DoesntExist"]',
-            ))
+        ))
 
     def test_ajaxdestroy_item_name_route(self):
         self._test_view_post('frontend.ajaxdestroy', status='200 OK', content_types=['application/json', ], data=['{', '}'], form=dict(
             comment='Test',
             itemnames='["DoesntExist"]',
-            ), viewopts=dict(item_name='DoesntExist'))
+        ), viewopts=dict(item_name='DoesntExist'))
 
     def test_ajaxdestroy_no_item_name_route(self):
         self._test_view_post('frontend.ajaxdestroy', status='200 OK', content_types=['application/json', ], data=['{', '}'], form=dict(
             comment='Test',
             itemnames='["DoesntExist"]',
-            ))
+        ))
 
     def test_ajaxmodify(self):
         self._test_view_post('frontend.ajaxmodify', status='404 NOT FOUND', viewopts=dict(item_name='DoesntExist'))
 
     def test_jfu_server(self):
         self._test_view_post('frontend.jfu_server', status='200 OK', data=['{', '}'], form=dict(
-            data_file=FileStorage(StringIO("Hello, world"), filename='C:\\fakepath\\DoesntExist.txt', content_type='text/plain'),
-            ), viewopts=dict(item_name='WillBeCreated'), content_types=['application/json', ])
+            data_file=FileStorage(StringIO("Hello, world"), filename='C:\\fakepath\\DoesntExist.txt', content_type='text/plain; charset=utf-8'),
+        ), viewopts=dict(item_name='WillBeCreated'), content_types=['application/json', ])
 
     def test_show_item(self):
         self._test_view('frontend.show_item', status='404 NOT FOUND', viewopts=dict(item_name='DoesntExist'))
 
     def test_favicon(self):
         rv = self._test_view('frontend.favicon', content_types=['image/x-icon', 'image/vnd.microsoft.icon', ], data=[])
-        assert rv.data.startswith('\x00\x00') # "reserved word, should always be 0"
+        assert rv.data.startswith('\x00\x00')  # "reserved word, should always be 0"
 
     def test_global_tags(self):
         self._test_view('frontend.global_tags')
         flaskg.user = user.User(name=u'moin', password=u'Xiwejr622')
         form = self.fillPasswordChangeForm(u'Xiwejr622', u'Woodoo645', u'Woodoo645')
         valid = form.validate()
-        assert valid # form data is valid
+        assert valid  # form data is valid
 
     def test_user_unicode_password_change(self):
         name = u'moin'
-        password = u'__שם משתמש לא קיים__' # Hebrew
+        password = u'__שם משתמש לא קיים__'  # Hebrew
 
         self.createUser(name, password)
         flaskg.user = user.User(name=name, password=password)
         form = self.fillPasswordChangeForm(password, u'Woodoo645', u'Woodoo645')
         valid = form.validate()
-        assert valid # form data is valid
+        assert valid  # form data is valid
 
     def test_user_password_change_to_unicode_pw(self):
         name = u'moin'
         password = u'Xiwejr622'
-        new_password = u'__שם משתמש לא קיים__' # Hebrew
+        new_password = u'__שם משתמש לא קיים__'  # Hebrew
 
         self.createUser(name, password)
         flaskg.user = user.User(name=name, password=password)
         form = self.fillPasswordChangeForm(password, new_password, new_password)
         valid = form.validate()
-        assert valid # form data is valid
+        assert valid  # form data is valid
 
     def test_fail_user_password_change_pw_mismatch(self):
         self.createUser(u'moin', u'Xiwejr622')
         """ helper to fill UserSettingsPasswordForm form
         """
         FormClass = views.UserSettingsPasswordForm
-        request_form = ImmutableMultiDict(
-           [
-              ('usersettings_password_password_current', current_password),
-              ('usersettings_password_password1', password1),
-              ('usersettings_password_password2', password2),
-              ('usersettings_password_submit', u'Save')
-           ]
-        )
+        request_form = ImmutableMultiDict([
+            ('usersettings_password_password_current', current_password),
+            ('usersettings_password_password1', password1),
+            ('usersettings_password_password2', password2),
+            ('usersettings_password_submit', u'Save')
+        ])
         form = FormClass.from_flat(request_form)
         return form
 

MoinMoin/apps/frontend/views.py

 import mimetypes
 import json
 from datetime import datetime
-from itertools import chain
 from collections import namedtuple
 from functools import wraps, partial
 
-from flask import request, url_for, flash, Response, make_response, redirect, session, abort, jsonify
+from flask import request, url_for, flash, Response, make_response, redirect, abort, jsonify
 from flask import current_app as app
 from flask import g as flaskg
 from flask.ext.babel import format_date
 from flask.ext.themes import get_themes_list
 
-from flatland import Form, Enum, List
+from flatland import Form, List
 from flatland.validation import Validator
 
 from jinja2 import Markup
 logging = log.getLogger(__name__)
 
 from MoinMoin.i18n import _, L_, N_
-from MoinMoin.themes import render_template, get_editor_info, contenttype_to_class
+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, Submit, Hidden, MultiSelect
+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
 from MoinMoin.items.content import content_registry
-from MoinMoin import config, user, util
+from MoinMoin import user, util
 from MoinMoin.constants.keys import *
+from MoinMoin.constants.itemtypes import ITEMTYPE_DEFAULT
+from MoinMoin.constants.chartypes import CHARS_UPPER, CHARS_LOWER
 from MoinMoin.util import crypto
 from MoinMoin.util.interwiki import url_for_item
-from MoinMoin.search import SearchForm, ValidSearch
-from MoinMoin.security.textcha import TextCha, TextChaizedForm, TextChaValid
-from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError
+from MoinMoin.search import SearchForm
+from MoinMoin.security.textcha import TextCha, TextChaizedForm
 from MoinMoin.signalling import item_displayed, item_modified
 from MoinMoin.storage.middleware.protecting import AccessDenied
 
     item_name = app.cfg.item_root
     return redirect(url_for_item(item_name))
 
+
 @frontend.route('/robots.txt')
 def robots():
     return app.send_static_file('robots.txt')
     refs = OptionalText.using(label='refs')
     tags = Tags.using(optional=True).using(label='tags')
     history = InlineCheckbox.using(label=L_('search also in non-current revisions'))
-    submit = Submit.using(default=L_('Lookup'))
+    submit_label = L_('Lookup')
 
 
 @frontend.route('/+lookup', methods=['GET', 'POST'])
     # TAGS might be there multiple times, thus we need multi:
     lookup_form = LookupForm.from_flat(request.values.items(multi=True))
     valid = lookup_form.validate()
-    lookup_form['submit'].set_default() # XXX from_flat() kills all values
     if valid:
         history = bool(request.values.get('history'))
         idx_name = ALL_REVS if history else LATEST_REVS
                                            title_name=title_name,
                                            lookup_form=lookup_form,
                                            results=results,
-                                          )
+                    )
                     flaskg.clock.stop('lookup render')
                     if not num_results:
                         status = 404
     html = render_template('lookup.html',
                            title_name=title_name,
                            lookup_form=lookup_form,
-                          )
+    )
     return Response(html, status)
 
 
     with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher:
         # The search process should be as fast as possible so use
         # the indexer low-level documents instead of high-level Revisions.
-        doc = searcher.document(name_exact=item_name)
+        doc = searcher.document(**{NAME_EXACT: item_name})
         if not doc:
             return set()
         transcluded_names = set(doc[ITEMTRANSCLUSIONS])
             transcluded_names.update(transclusions)
         return transcluded_names
 
+
 @frontend.route('/+search/<itemname:item_name>', methods=['GET', 'POST'])
 @frontend.route('/+search', defaults=dict(item_name=u''), methods=['GET', 'POST'])
 def search(item_name):
     search_form = SearchForm.from_flat(request.values)
     valid = search_form.validate()
-    search_form['submit'].set_default() # XXX from_flat() kills all values
     query = search_form['q'].value
     if valid:
         history = bool(request.values.get('history'))
         q = qp.parse(query)
 
         _filter = None
-        if item_name: # Only search this item and subitems
+        if item_name:  # Only search this item and subitems
             prefix_name = item_name + u'/'
             terms = [Term(NAME_EXACT, item_name), Prefix(NAME_EXACT, prefix_name), ]
 
             flaskg.clock.start('search')
             results = searcher.search(q, filter=_filter, limit=100)
             flaskg.clock.stop('search')
-            # XXX if found that calling key_terms like you see below is 1000..10000x
-            # slower than the search itself, so we better don't do that right now.
-            key_terms_is_fast = False
-            if key_terms_is_fast:
-                flaskg.clock.start('search suggestions')
-                name_suggestions = u', '.join([word for word, score in results.key_terms(NAME, docs=20, numterms=10)])
-                content_suggestions = u', '.join([word for word, score in results.key_terms(CONTENT, docs=20, numterms=10)])
-                flaskg.clock.stop('search suggestions')
-            else:
-                name_suggestions = u''
-                content_suggestions = u''
+            flaskg.clock.start('search suggestions')
+            name_suggestions = [word for word, score in results.key_terms(NAME, docs=20, numterms=10)]
+            content_suggestions = [word for word, score in results.key_terms(CONTENT, docs=20, numterms=10)]
+            flaskg.clock.stop('search suggestions')
             flaskg.clock.start('search render')
             html = render_template('search.html',
                                    results=results,
-                                   name_suggestions=name_suggestions,
-                                   content_suggestions=content_suggestions,
+                                   name_suggestions=u', '.join(name_suggestions),
+                                   content_suggestions=u', '.join(content_suggestions),
                                    query=query,
                                    medium_search_form=search_form,
                                    item_name=item_name,
-                                  )
+            )
             flaskg.clock.stop('search render')
     else:
         html = render_template('search.html',
                                query=query,
                                medium_search_form=search_form,
                                item_name=item_name,
-                              )
+        )
     return html
 
 
         status = 200
     content = render_template('dom.xml',
                               data_xml=Markup(item.content._render_data_xml()),
-                             )
+    )
     return Response(content, status, mimetype='text/xml')
 
 
     return render_template('highlight.html',
                            item=item, item_name=item.name,
                            data_text=Markup(item.content._render_data_highlight()),
-                          )
+    )
 
 
 @presenter('meta', add_trail=True)
 def show_item_meta(item):
     show_revision = request.view_args['rev'] != CURRENT
-    show_navigation = False # TODO
+    show_navigation = False  # TODO
     first_rev = None
     last_rev = None
     if show_navigation:
                            meta_rendered=Markup(item._render_meta()),
                            show_revision=show_revision,
                            show_navigation=show_navigation,
-                          )
+    )
+
 
 @frontend.route('/+content/+<rev>/<itemname:item_name>')
 @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 """
-    # first check whether we have a valid search query:
-    search_form = SearchForm.from_flat(request.values)
-    if search_form.validate():
-        return _search(search_form, item_name)
-    search_form['submit'].set_default() # XXX from_flat() kills all values
     item_displayed.send(app._get_current_object(),
                         item_name=item_name)
     try:
                            data_rendered=Markup(item.content._render_data()),
                            )
 
+
 @presenter('get')
 def get_item(item):
     return item.content.do_get()
 
+
 @presenter('download')
 def download_item(item):
     mimetype = request.values.get("mimetype")
     return item.content.do_get(force_attachment=True, mimetype=mimetype)
 
+
 @frontend.route('/+convert/<itemname:item_name>')
 def convert_item(item_name):
     """
     item_name_converted = item_name + 'converted'
     try:
         # TODO implement Content.create and use it here
-        converted_item = Item.create(item_name_converted, itemtype=u'default', contenttype=contenttype)
+        converted_item = Item.create(item_name_converted, itemtype=ITEMTYPE_DEFAULT, contenttype=contenttype)
     except AccessDenied:
         abort(403)
     return converted_item.content._convert(item.content.internal_representation())
     After successful POST, redirects to the page.
     """
     # XXX drawing applets don't send itemtype
-    itemtype = request.values.get('itemtype', u'default')
+    itemtype = request.values.get('itemtype', ITEMTYPE_DEFAULT)
     contenttype = request.values.get('contenttype')
     try:
         item = Item.create(item_name, itemtype=itemtype, contenttype=contenttype)
 class TargetChangeForm(BaseChangeForm):
     target = RequiredText.using(label=L_('Target')).with_properties(placeholder=L_("The name of the target item"))
 
+
 class RevertItemForm(BaseChangeForm):
     name = 'revert_item'
 
+
 class DeleteItemForm(BaseChangeForm):
     name = 'delete_item'
 
+
 class DestroyItemForm(BaseChangeForm):
     name = 'destroy_item'
 
+
 class RenameItemForm(TargetChangeForm):
     name = 'rename_item'
 
                            item=item, item_name=item_name,
                            rev_id=rev,
                            form=form,
-                          )
+    )
 
 
 @frontend.route('/+rename/<itemname:item_name>', methods=['GET', 'POST'])
     return render_template(item.rename_template,
                            item=item, item_name=item_name,
                            form=form,
-                          )
+    )
 
 
 @frontend.route('/+delete/<itemname:item_name>', methods=['GET', 'POST'])
     return render_template(item.delete_template,
                            item=item, item_name=item_name,
                            form=form,
-                          )
+    )
+
 
 @frontend.route('/+ajaxdelete/<itemname:item_name>', methods=['POST'])
 @frontend.route('/+ajaxdelete', defaults=dict(item_name=''), methods=['POST'])
 
     return jsonify(response)
 
+
 @frontend.route('/+ajaxdestroy/<itemname:item_name>', methods=['POST'])
 @frontend.route('/+ajaxdestroy', defaults=dict(item_name=''), methods=['POST'])
 def ajaxdestroy(item_name):
 def destroy_item(item_name, rev):
     if rev is None:
         # no revision given
-        _rev = CURRENT # for item creation
+        _rev = CURRENT  # for item creation
         destroy_item = True
     else:
         _rev = rev
                            item=item, item_name=item_name,
                            rev_id=rev,
                            form=form,
-                          )
+    )
 
 
 @frontend.route('/+jfu-server/<itemname:item_name>', methods=['POST'])
     """
     data_file = request.files.get('data_file')
     subitem_name = data_file.filename
-    contenttype = data_file.content_type # guess by browser, based on file name
+    contenttype = data_file.content_type  # guess by browser, based on file name
     data = data_file.stream
     if item_name:
         subitem_prefix = item_name + u'/'
                        size=size,
                        url=url_for('.show_item', item_name=item_name, rev=revid),
                        contenttype=contenttype_to_class(contenttype),
-                      )
+        )
     except AccessDenied:
         abort(403)
 
 
-contenttype_groups = content_registry.group_names[:]
-contenttype_group_descriptions = {}
-for g in contenttype_groups:
-    contenttype_group_descriptions[g] = ', '.join([e.display_name for e in content_registry.groups[g]])
-contenttype_groups.append('unknown items')
+def contenttype_selects_gen():
+    for g in content_registry.group_names:
+        description = u', '.join([e.display_name for e in content_registry.groups[g]])
+        yield g, None, description
+    yield u'unknown items', None, u'Items of contenttype unknown to MoinMoin'
 
-ContenttypeGroup = MultiSelect.of(Enum.using(valid_values=contenttype_groups).with_properties(descriptions=contenttype_group_descriptions)).using(optional=True)
+ContenttypeGroup = MultiSelect.of(Enum.out_of(contenttype_selects_gen())).using(optional=True)
+
 
 class IndexForm(Form):
     contenttype = ContenttypeGroup
-    submit = Submit.using(default=L_('Filter'))
+    submit_label = L_('Filter')
+
 
 @frontend.route('/+index/', defaults=dict(item_name=''), methods=['GET', 'POST'])
 @frontend.route('/+index/<itemname:item_name>', methods=['GET', 'POST'])
 def index(item_name):
     try:
-        item = Item.create(item_name) # when item_name='', it gives toplevel index
+        item = Item.create(item_name)  # when item_name='', it gives toplevel index
     except AccessDenied:
         abort(403)
 
     # values, eg. calling items with multi=True. See Werkzeug documentation for
     # more.
     form = IndexForm.from_flat(request.args.items(multi=True))
-    form['submit'].set_default() # XXX from_flat() kills all values
     if not form['contenttype']:
-        form['contenttype'].set(contenttype_groups)
+        form['contenttype'].set(ContenttypeGroup.member_schema.valid_values)
 
     selected_groups = form['contenttype'].value
     startswith = request.values.get("startswith")
                            initials=initials,
                            startswith=startswith,
                            form=form,
-                          )
+    )
 
 
 @frontend.route('/+mychanges')
                            title_name=_(u'My Changes'),
                            headline=_(u'My Changes'),
                            item_names=my_changes
-                          )
+    )
 
 
 def _mychanges(userid):
                            item_name=item_name,
                            headline=_(u"Items which refer to '%(item_name)s'", item_name=item_name),
                            item_names=refs_here
-                          )
+    )
 
 
 def _backrefs(item_name):
     history = [dict((k, v) for k, v in rev.meta.iteritems() if k != CONTENT) for rev in revs]
     history_page = util.getPageContent(history, offset, results_per_page)
     return render_template('history.html',
-                           item_name=item_name, # XXX no item here
+                           item_name=item_name,  # XXX no item here
                            history_page=history_page,
                            bookmark_time=bookmark_time,
-                          )
+    )
 
 
 @frontend.route('/+history')
                            history=history,
                            current_timestamp=current_timestamp,
                            bookmark_time=bookmark_time,
-                          )
+    )
+
 
 def _compute_item_sets():
     """
         # 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"
+                _("Edit the subscription regular expressions in your settings."), "error"
     else:
         # Try to subscribe
         if not u.subscribe(item_name):
             return False
         if element['password1'].value != element['password2'].value:
             return self.note_error(element, state, 'passwords_mismatch_msg')
+        return True
 
-        return True
 
 class RegistrationForm(TextChaizedForm):
     """a simple user registration form"""
     password2 = RequiredPassword.with_properties(placeholder=L_("Repeat the same password"))
     email = YourEmail
     openid = YourOpenID.using(optional=True)
-    submit = Submit.using(default=L_('Register'))
+    submit_label = L_('Register')
 
     validators = [ValidRegistration()]
 
 
     openid = YourOpenID
 
+
 def _using_moin_auth():
     """Check if MoinAuth is being used for authentication.
 
                     if is_ok:
                         flash(_('Account verification required, please see the email we sent to your address.'), "info")
                     else:
-                        flash(_('An error occurred while sending the verification email: "%(message)s" Please contact an administrator to activate your account.',
-                            message=msg), "error")
+                        flash(_('An error occurred while sending the verification email: "%(message)s" '
+                                'Please contact an administrator to activate your account.',
+                                message=msg), "error")
                 else:
                     flash(_('Account created, please log in now.'), "info")
                 return redirect(url_for('.show_root'))
     return render_template(template,
                            title_name=title_name,
                            form=form,
-                          )
+    )
 
 
 @frontend.route('/+verifyemail', methods=['GET'])
 def verifyemail():
-    u = None
+    u = token = None
     if 'username' in request.values and 'token' in request.values:
         u = user.User(auth_username=request.values['username'])
         token = request.values['token']
-    if u and u.disabled and u.validate_recovery_token(token):
+    if u and u.disabled and token and u.validate_recovery_token(token):
         u.profile[DISABLED] = False
         u.save()
         flash(_("Your account has been activated, you can log in now."), "info")
     else:
-        flash(_('Your token is invalid!'), "error")
+        flash(_('Your username and/or token is invalid!'), "error")
     return redirect(url_for('.show_root'))
 
 
 
     username = OptionalText.using(label=L_('Name')).with_properties(placeholder=L_("Your login name"))
     email = YourEmail.using(optional=True)
-    submit = Submit.using(default=L_('Recover password'))
+    submit_label = L_('Recover password')
 
     validators = [ValidLostPassword()]
 
     return render_template('lostpass.html',
                            title_name=title_name,
                            form=form,
-                          )
+    )
+
 
 class ValidPasswordRecovery(Validator):
     """Validator for a valid password recovery form
 
         return True
 
+
 class PasswordRecoveryForm(Form):
     """a simple password recovery form"""
     name = 'recoverpass'
 
     username = RequiredText.using(label=L_('Name')).with_properties(placeholder=L_("Your login name"))
-    token = RequiredText.using(label=L_('Recovery token')).with_properties(placeholder=L_("The recovery token that has been sent to you"))
-    password1 = RequiredPassword.using(label=L_('New password')).with_properties(placeholder=L_("The login password you want to use"))
-    password2 = RequiredPassword.using(label=L_('New password (repeat)')).with_properties(placeholder=L_("Repeat the same password"))
-    submit = Submit.using(default=L_('Change password'))
+    token = RequiredText.using(label=L_('Recovery token')).with_properties(
+        placeholder=L_("The recovery token that has been sent to you"))
+    password1 = RequiredPassword.using(label=L_('New password')).with_properties(
+        placeholder=L_("The login password you want to use"))
+    password2 = RequiredPassword.using(label=L_('New password (repeat)')).with_properties(
+        placeholder=L_("Repeat the same password"))
+    submit_label = L_('Change password')
 
     validators = [ValidPasswordRecovery()]
 
     return render_template('recoverpass.html',
                            title_name=title_name,
                            form=form,
-                          )
+    )
 
 
 class ValidLogin(Validator):
     username = RequiredText.using(label=L_('Name'), optional=False).with_properties(autofocus=True)
     password = RequiredPassword
     openid = YourOpenID.using(optional=True)
-    submit = Submit.using(default=L_('Log in'))
+    # This field results in a login_submit field in the POST form, which is in
+    # turn looked for by setup_user() in app.py as marker for login requests.
+    submit = Hidden.using(default='1')
+    submit_label = L_('Log in')
 
     validators = [ValidLogin()]
 
                            title_name=title_name,
                            login_inputs=app.cfg.auth_login_inputs,
                            form=form,
-                          )
+    )
 
 
 @frontend.route('/+logout')
     name = 'usersettings_password'
     validators = [ValidChangePass()]
 
-    password_current = RequiredPassword.using(label=L_('Current Password')).with_properties(placeholder=L_("Your current login password"))
-    password1 = RequiredPassword.using(label=L_('New password')).with_properties(placeholder=L_("The login password you want to use"))
-    password2 = RequiredPassword.using(label=L_('New password (repeat)')).with_properties(placeholder=L_("Repeat the same password"))
-    submit = Submit.using(default=L_('Change password'))
+    password_current = RequiredPassword.using(label=L_('Current Password')).with_properties(
+        placeholder=L_("Your current login password"))
+    password1 = RequiredPassword.using(label=L_('New password')).with_properties(
+        placeholder=L_("The login password you want to use"))
+    password2 = RequiredPassword.using(label=L_('New password (repeat)')).with_properties(
+        placeholder=L_("Repeat the same password"))
+    submit_label = L_('Change password')
+
 
 class UserSettingsNotificationForm(Form):
     name = 'usersettings_notification'
     email = YourEmail
-    submit = Submit.using(default=L_('Save'))
+    submit_label = L_('Save')
 
 
 class UserSettingsNavigationForm(Form):
     name = 'usersettings_navigation'
+    # XXX Flatland insists a form having at least one element
+    dummy = Hidden
     # TODO: find a good way to handle quicklinks here
-    submit = Submit.using(default=L_('Save'))
+    submit_label = L_('Save')
 
 
 class UserSettingsOptionsForm(Form):
     scroll_page_after_edit = Checkbox.using(label=L_('Scroll page after edit'))
     show_comments = Checkbox.using(label=L_('Show comment sections'))
     disabled = Checkbox.using(label=L_('Disable this account forever'))
-    submit = Submit.using(default=L_('Save'))
+    submit_label = L_('Save')
 
 
 @frontend.route('/+usersettings', methods=['GET', 'POST'])
 
     # these forms can't be global because we need app object, which is only available within a request:
     class UserSettingsPersonalForm(Form):
-        name = 'usersettings_personal' # "name" is duplicate
+        name = 'usersettings_personal'  # "name" is duplicate
         name = Names.using(label=L_('Names')).with_properties(placeholder=L_("The login names you want to use"))
-        display_name = OptionalText.using(label=L_('Display-Name')).with_properties(placeholder=L_("Your display name (informational)"))
+        display_name = OptionalText.using(label=L_('Display-Name')).with_properties(
+            placeholder=L_("Your display name (informational)"))
         openid = YourOpenID.using(optional=True)
-        #timezones_keys = sorted(Locale('en').time_zones.keys())
-        timezones_keys = [unicode(tz) for tz in pytz.common_timezones]
-        timezone = Select.using(label=L_('Timezone')).valued(*timezones_keys)
-        supported_locales = [Locale('en')] + app.babel_instance.list_translations()
-        locales_available = sorted([(unicode(l), l.display_name) for l in supported_locales],
-                                   key=lambda x: x[1])
-        locales_keys = [l[0] for l in locales_available]
-        locale = Select.using(label=L_('Locale')).with_properties(labels=dict(locales_available)).valued(*locales_keys)
-        submit = Submit.using(default=L_('Save'))
+        #_timezones_keys = sorted(Locale('en').time_zones.keys())
+        _timezones_keys = [unicode(tz) for tz in pytz.common_timezones]
+        timezone = Select.using(label=L_('Timezone')).out_of((e, e) for e in _timezones_keys)
+        _supported_locales = [Locale('en')] + app.babel_instance.list_translations()
+        locale = Select.using(label=L_('Locale')).out_of(
+            ((unicode(l), l.display_name) for l in _supported_locales), sort_by=1)
+        submit_label = L_('Save')
 
     class UserSettingsUIForm(Form):
         name = 'usersettings_ui'
-        themes_available = sorted([(unicode(t.identifier), t.name) for t in get_themes_list()],
-                                  key=lambda x: x[1])
-        themes_keys = [t[0] for t in themes_available]
-        theme_name = Select.using(label=L_('Theme name')).with_properties(labels=dict(themes_available)).valued(*themes_keys)
-        css_url = URL.using(label=L_('User CSS URL'), optional=True).with_properties(placeholder=L_("Give the URL of your custom CSS (optional)"))
-        edit_rows = Natural.using(label=L_('Editor size')).with_properties(placeholder=L_("Editor textarea height (0=auto)"))
-        results_per_page = Natural.using(label=L_('History results per page')).with_properties(placeholder=L_("Number of results per page (0=no paging)"))
-        submit = Submit.using(default=L_('Save'))
+        theme_name = Select.using(label=L_('Theme name')).out_of(
+            ((unicode(t.identifier), t.name) for t in get_themes_list()), sort_by=1)
+        css_url = URL.using(label=L_('User CSS URL'), optional=True).with_properties(
+            placeholder=L_("Give the URL of your custom CSS (optional)"))
+        edit_rows = Natural.using(label=L_('Editor size')).with_properties(
+            placeholder=L_("Editor textarea height (0=auto)"))
+        results_per_page = Natural.using(label=L_('History results per page')).with_properties(
+            placeholder=L_("Number of results per page (0=no paging)"))
+        submit_label = L_('Save')
 
     form_classes = dict(
         personal=UserSettingsPersonalForm,
                     response['flash'].append((_("Your password has been changed."), "info"))
                 else:
                     if part == 'personal':
-                        if form['openid'].value and form['openid'].value != flaskg.user.openid and user.search_users(openid=form['openid'].value):
+                        if (form['openid'].value and form['openid'].value != flaskg.user.openid and
+                            user.search_users(openid=form['openid'].value)):
                             # duplicate openid
                             response['flash'].append((_("This openid is already in use."), "error"))
                             success = False
                         if set(form['name'].value) != set(flaskg.user.name):
                             new_names = set(form['name'].value) - set(flaskg.user.name)
                             for name in new_names:
-                                if user.search_users(name_exact=name):
+                                if user.search_users(**{NAME_EXACT: name}):
                                     # duplicate name
-                                    response['flash'].append((_("The username %(name)r is already in use.", name=name), "error"))
+                                    response['flash'].append((_("The username %(name)r is already in use.", name=name),
+                                                              "error"))
                                     success = False
                     if part == 'notification':
                         if (form['email'].value != flaskg.user.email and
-                            user.search_