Commits

Andreas Kloeckner  committed ef151b2 Merge

Merge upstream changes into theme-tweaks-for-bootstrap

  • Participants
  • Parent commits b1eed94, 2aaa55f

Comments (0)

Files changed (65)

 ^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/

File MoinMoin/__init__.py

 project = "MoinMoin"
 
 import sys
-if sys.hexversion < 0x2060000:
-    sys.exit("%s requires Python 2.6 or greater.\n" % project)
+if sys.hexversion < 0x2070000:
+    sys.exit("%s requires Python 2.7.x.\n" % project)
 
 
 from MoinMoin.util.version import Version

File MoinMoin/_template.py

-# Copyright: 2012 MoinMoin:YourNameHere
+# Copyright: 2013 MoinMoin:YourNameHere
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """

File MoinMoin/_tests/test_wikiutil.py

     def test_anchor_name_encoding(self):
         tests = [
             # text, expected output
-            (u'\xf6\xf6ll\xdf\xdf', 'A.2BAPYA9g-ll.2BAN8A3w-'),
+            (u'\xf6\xf6ll\xdf\xdf', 'A_APYA9g-ll_AN8A3w-'),
             (u'level 2', 'level_2'),
             (u'level_2', 'level_2'),
             (u'', 'A'),

File MoinMoin/apps/admin/views.py

     """
     u = user.User(auth_username=user_name)
     if request.method == 'GET':
-        return _(u"User profile of %(username)s: %(email)r", username=user_name,
-                 email=(u.email, u.disabled))
+        return _(u"User profile of %(username)s: %(email)s %(disabled)s", username=user_name,
+                 email=u.email, disabled=u.disabled)
 
     if request.method == 'POST':
         key = request.form.get('key', '')

File MoinMoin/apps/frontend/views.py

 from MoinMoin.util import crypto
 from MoinMoin.util.interwiki import url_for_item
 from MoinMoin.search import SearchForm
+from MoinMoin.search.analyzers import item_name_analyzer
 from MoinMoin.security.textcha import TextCha, TextChaizedForm
 from MoinMoin.signalling import item_displayed, item_modified
 from MoinMoin.storage.middleware.protecting import AccessDenied
     submit_label = L_('Lookup')
 
 
+def analyze(analyzer, text):
+    return [token.text for token in analyzer(text, mode='index')]
+
+
 @frontend.route('/+lookup', methods=['GET', 'POST'])
 def lookup():
     """
                     term = Term(key, value)
                 terms.append(term)
         if terms:
+            LookupEntry = namedtuple('LookupEntry', 'name revid wikiname')
+            name = lookup_form[NAME].value
+            name_exact = lookup_form[NAME_EXACT].value or u''
             terms.append(Term(WIKINAME, app.cfg.interwikiname))
             q = And(terms)
             with flaskg.storage.indexer.ix[idx_name].searcher() as searcher:
                 flaskg.clock.start('lookup')
                 results = searcher.search(q, limit=100)
                 flaskg.clock.stop('lookup')
-                num_results = results.scored_length()
-                if num_results == 1:
-                    result = results[0]
-                    rev = result[REVID] if history else CURRENT
-                    url = url_for('.show_item', item_name=result[NAME], rev=rev)
+                lookup_results = []
+                for result in results:
+                    analyzer = item_name_analyzer()
+                    lookup_results += [LookupEntry(n, result[REVID], result[WIKINAME])
+                                       for n in result[NAME]
+                                       if not name or name.lower() in analyze(analyzer, n)
+                                       if n.startswith(name_exact)]
+
+                if len(lookup_results) == 1:
+                    result = lookup_results[0]
+                    rev = result.revid if history else CURRENT
+                    url = url_for('.show_item', item_name=result.name, rev=rev)
                     return redirect(url)
                 else:
                     flaskg.clock.start('lookup render')
                     html = render_template('lookup.html',
                                            title_name=title_name,
                                            lookup_form=lookup_form,
-                                           results=results,
+                                           results=lookup_results,
                     )
                     flaskg.clock.stop('lookup render')
-                    if not num_results:
+                    if not lookup_results:
                         status = 404
                     return Response(html, status)
     html = render_template('lookup.html',
                         item_name=item_name)
     try:
         item = Item.create(item_name, rev_id=rev)
+        result = item.do_show(rev)
     except AccessDenied:
         abort(403)
-    return item.do_show(rev)
+    return result
 
 
 @frontend.route('/<itemname:item_name>/')  # note: unwanted trailing slash
                             for name in new_names:
                                 if user.search_users(**{NAME_EXACT: name}):
                                     # duplicate name
-                                    response['flash'].append((_("The username %(name)r is already in use.", name=name),
+                                    response['flash'].append((_("The username '%(name)s' is already in use.", name=name),
                                                               "error"))
                                     success = False
                     if part == 'notification':

File MoinMoin/auth/_tests/test_ldap_login.py

 from MoinMoin._tests import wikiconfig
 from MoinMoin.auth import handle_login
 
-# first check if we have python 2.4, python-ldap and slapd:
 msg = check_environ()
 if msg:
     pytest.skip(msg)

File MoinMoin/config/default.py

     # the options dictionary.
 
 
-def _default_password_checker(cfg, username, password):
+def _default_password_checker(cfg, username, password,
+                              min_length=8, min_different=5):
     """ Check if a password is secure enough.
         We use a built-in check to get rid of the worst passwords.
 
                  some unicode object with an error msg, if the password is problematic.
     """
     # in any case, do a very simple built-in check to avoid the worst passwords
-    if len(password) < 6:
-        return _("Password is too short.")
-    if len(set(password)) < 4:
-        return _("Password has not enough different characters.")
+    if len(password) < min_length:
+        return _("For a password a minimum length of %(min_length)d characters is required.",
+                 min_length=min_length)
+    if len(set(password)) < min_different:
+        return _("For a password a minimum of %(min_different)d different characters is required.",
+                 min_different=min_different)
 
     username_lower = username.lower()
     password_lower = password.lower()
          [],
          "Exclude unwanted endpoints (list of strings)"),
         ('password_checker', DefaultExpression('_default_password_checker'),
-         'checks whether a password is acceptable (default check is length >= 6, at least 4 different chars, no keyboard sequence, not username used somehow (you can switch this off by using `None`)'),
+         'does simple checks whether a password is acceptable (you can switch this off by using `None` or enhance it by using a custom checker)'),
 
         ('passlib_crypt_context', dict(
             # schemes we want to support (or deprecated schemes for which we still have
             SHOW_COMMENTS: False,
             WANT_TRIVIAL: False,
             ENC_PASSWORD: u'',  # empty value == invalid hash
+            RECOVERPASS_KEY: u'',  # empty value == invalid key
             SESSION_KEY: u'',  # empty value == invalid key
             DISABLED: False,
             BOOKMARKS: {},

File MoinMoin/constants/keys.py

 MoinMoin - meta data key / index field name related constants
 """
 
-# IMPORTANT: until we require a python >= 2.6.5, we need to keep the keys as
-#            str (not unicode), because of "Issue #4978: Passing keyword
-#            arguments as unicode strings is now allowed." (from 2.6.5 chglog)
-
 # metadata keys
-NAME = "name"
-NAME_OLD = "name_old"
-NAMESPACE = "namespace"
+NAME = u"name"
+NAME_OLD = u"name_old"
+NAMESPACE = u"namespace"
 
 # if an item is reverted, we store the revision number we used for reverting there:
-REVERTED_TO = "reverted_to"
+REVERTED_TO = u"reverted_to"
 
 # some metadata key constants:
-ACL = "acl"
+ACL = u"acl"
 
 # keys for storing group and dict information
 # group of user names, e.g. for ACLs:
-USERGROUP = "usergroup"
+USERGROUP = u"usergroup"
 # needs more precise name / use case:
-SOMEDICT = "somedict"
+SOMEDICT = u"somedict"
 
-CONTENTTYPE = "contenttype"
-ITEMTYPE = "itemtype"
-SIZE = "size"
-LANGUAGE = "language"
-EXTERNALLINKS = "externallinks"
-ITEMLINKS = "itemlinks"
-ITEMTRANSCLUSIONS = "itemtransclusions"
-TAGS = "tags"
+CONTENTTYPE = u"contenttype"
+ITEMTYPE = u"itemtype"
+SIZE = u"size"
+LANGUAGE = u"language"
+EXTERNALLINKS = u"externallinks"
+ITEMLINKS = u"itemlinks"
+ITEMTRANSCLUSIONS = u"itemtransclusions"
+TAGS = u"tags"
 
-ACTION = "action"
-ADDRESS = "address"
-HOSTNAME = "hostname"
-USERID = "userid"
-MTIME = "mtime"
-EXTRA = "extra"
-COMMENT = "comment"
-SUMMARY = "summary"
+ACTION = u"action"
+ADDRESS = u"address"
+HOSTNAME = u"hostname"
+USERID = u"userid"
+MTIME = u"mtime"
+EXTRA = u"extra"
+COMMENT = u"comment"
+SUMMARY = u"summary"
 
 # we need a specific hash algorithm to store hashes of revision data into meta
 # data. meta[HASH_ALGORITHM] = hash(rev_data, HASH_ALGORITHM)
 # some backends may use this also for other purposes.
-HASH_ALGORITHM = 'sha1'
+HASH_ALGORITHM = u"sha1"
 HASH_LEN = 40  # length of hex str representation of hash value
 
 # some field names for whoosh index schema / documents in index:
-NAME_EXACT = "name_exact"
-ITEMID = "itemid"
-REVID = "revid"
-PARENTID = "parentid"
-DATAID = "dataid"
-WIKINAME = "wikiname"
-CONTENT = "content"
+NAME_EXACT = u"name_exact"
+ITEMID = u"itemid"
+REVID = u"revid"
+PARENTID = u"parentid"
+DATAID = u"dataid"
+WIKINAME = u"wikiname"
+CONTENT = u"content"
 
 # magic REVID for current revision:
-CURRENT = "current"
+CURRENT = u"current"
 
 # stuff from user profiles / for whoosh index
-EMAIL = "email"
-OPENID = "openid"
-DISPLAY_NAME = "display_name"
-THEME_NAME = "theme_name"
-LOCALE = "locale"
-TIMEZONE = "timezone"
-ENC_PASSWORD = "enc_password"
-SUBSCRIBED_ITEMS = "subscribed_items"
-BOOKMARKS = "bookmarks"
-QUICKLINKS = "quicklinks"
-SESSION_KEY = "session_key"
-SESSION_TOKEN = "session_token"
-RECOVERPASS_KEY = "recoverpass_key"
-EDIT_ON_DOUBLECLICK = "edit_on_doubleclick"
-SCROLL_PAGE_AFTER_EDIT = "scroll_page_after_edit"
-SHOW_COMMENTS = "show_comments"
-MAILTO_AUTHOR = "mailto_author"
-CSS_URL = "css_url"
-EDIT_ROWS = "edit_rows"
-RESULTS_PER_PAGE = "results_per_page"
-WANT_TRIVIAL = "want_trivial"
-EMAIL_SUBSCRIBED_EVENTS = "email_subscribed_events"
-DISABLED = "disabled"
+EMAIL = u"email"
+OPENID = u"openid"
+DISPLAY_NAME = u"display_name"
+THEME_NAME = u"theme_name"
+LOCALE = u"locale"
+TIMEZONE = u"timezone"
+ENC_PASSWORD = u"enc_password"
+SUBSCRIBED_ITEMS = u"subscribed_items"
+BOOKMARKS = u"bookmarks"
+QUICKLINKS = u"quicklinks"
+SESSION_KEY = u"session_key"
+SESSION_TOKEN = u"session_token"
+RECOVERPASS_KEY = u"recoverpass_key"
+EDIT_ON_DOUBLECLICK = u"edit_on_doubleclick"
+SCROLL_PAGE_AFTER_EDIT = u"scroll_page_after_edit"
+SHOW_COMMENTS = u"show_comments"
+MAILTO_AUTHOR = u"mailto_author"
+CSS_URL = u"css_url"
+EDIT_ROWS = u"edit_rows"
+RESULTS_PER_PAGE = u"results_per_page"
+WANT_TRIVIAL = u"want_trivial"
+EMAIL_SUBSCRIBED_EVENTS = u"email_subscribed_events"
+DISABLED = u"disabled"
 
 # in which backend is some revision stored?
-BACKENDNAME = "backendname"
+BACKENDNAME = u"backendname"
 
 USEROBJ_ATTRS = [
     # User objects proxy these attributes of the UserProfile objects:
 ]
 
 # keys for blog homepages
-LOGO = "logo"
-SUPERTAGS = "supertags"
+LOGO = u"logo"
+SUPERTAGS = u"supertags"
 # keys for blog entries
-PTIME = "ptime"
+PTIME = u"ptime"
+
+# keys for tickets
+EFFORT = u"effort"
+DIFFICULTY = u"difficulty"
+SEVERITY = u"severity"
+PRIORITY = u"priority"
+ASSIGNED_TO = u"assigned_to"
+SUPERSEDED_BY = u"superseded_by"
+DEPENDS_ON = u"depends_on"
+CLOSED = u"closed"
 
 # index names
 LATEST_REVS = 'latest_revs'

File MoinMoin/converter/_tests/test_image.py

+# Copyright: 2013 MoinMoin:RishabhRaj
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+MoinMoin - Tests for MoinMoin.converter for different imagetypes
+"""
+
+import pytest
+
+from emeraldtree import tree as ET
+
+from MoinMoin.converter.html_out import ConverterPage
+
+
+class TestImg(object):
+    def setup_class(self):
+        self.converter = ConverterPage()
+
+    def testImage(self):
+        tree_xml = ('<ns0:page ns0:page-href="wiki:///Home" xmlns:ns0="http://moinmo.in/namespaces/page" '
+                    'xmlns:ns1="http://www.w3.org/2001/XInclude" xmlns:ns2="http://www.w3.org/1999/xhtml" '
+                    'xmlns:ns3="http://www.w3.org/1999/xlink"><ns0:body><ns0:p ns2:data-lineno="1">'
+                    '<ns0:page ns2:class="moin-transclusion" ns0:page-href="wiki:///imagetest" ns2:data-href="/imagetest">'
+                    '<ns0:body><ns0:object ns3:href="/+get/+2882c905b2ab409fbf79cd05637a112d/imagetest" ns0:type="{0}" />'
+                    '</ns0:body></ns0:page></ns0:p></ns0:body></ns0:page>')
+        tests = [
+            ('image/jpeg', 'img'),
+            ('image/svg+xml', 'img'),
+            ('image/png', 'img'),
+            ('image/gif', 'img'),
+        ]
+
+        for imagetype, tag_expected in tests:
+            self.runTest(tree_xml.format(imagetype), tag_expected)
+
+    def runTest(self, tree_xml, tag_expected):
+        tree = ET.XML(tree_xml)
+        tree = self.converter(tree)
+        assert len(tree) and len(tree[0]) and len(tree[0][0]) == 1
+        assert tree[0][0][0].tag.name == tag_expected

File MoinMoin/converter/html_out.py

         Return value is an str, one of the following:
             image, video, audio, object
         """
-        if Type('image/').issupertype(mimetype) and not Type('image/svg+xml').issupertype(mimetype):
-            # Firefox fails completely to show svg in img tags (displays: nothing).
-            # Firefox displays them with on object tag (but sometimes displays scrollbars without need).
+        if Type('image/').issupertype(mimetype):
             return "img"
         elif Type('video/').issupertype(mimetype):
             return "video"

File MoinMoin/converter/link.py

             path = self.absolute_path(path, page.path)
             item_name = unicode(path)
             if not flaskg.storage.has_item(item_name):
+                # XXX these index accesses slow down the link converter quite a bit
                 elem.set(moin_page.class_, 'moin-nonexistent')
         else:
             item_name = unicode(page.path[1:])

File MoinMoin/converter/text_csv_in.py

     def __call__(self, data, contenttype=None, arguments=None):
         text = decode_data(data, contenttype)
         content = normalize_split_text(text)
-        # as of py 2.6.5 (and in the year 2010), the csv module seems to still
+        # as of py 2.7.x (and in the year 2013), the csv module seems to still
         # have troubles with unicode, thus we encode to utf-8 ...
         content = [line.encode('utf-8') for line in content]
         dialect = csv.Sniffer().sniff(content[0])

File MoinMoin/forms.py

 from flatland.validation import Validator, Present, IsEmail, ValueBetween, URLValidator, Converted, ValueAtLeast
 from flatland.exc import AdaptationError
 
+from whoosh.query import Term, Or, Not, And
+
 from flask import g as flaskg
 
 from MoinMoin.constants.forms import *
-from MoinMoin.constants.keys import ITEMID, NAME
+from MoinMoin.constants.keys import ITEMID, NAME, LATEST_REVS
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.util.forms import FileStorage
 
     """Validator for JSON
     """
     invalid_json_msg = L_('Invalid JSON.')
+    invalid_name_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.")
+            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 False
+        return True
 
     def validate(self, element, state):
         try:
-            json.loads(element.value)
+            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')
         return True
 
 JSON = OptionalMultilineText.with_properties(lang='en', dir='ltr').validated_by(ValidJSON())
         return cls
 
     @classmethod
-    def _get_choices(cls):
+    def _get_choice_specs(cls):
         revs = flaskg.storage.search(cls._query, **cls._query_args)
-        choices = [(rev.meta[ITEMID], rev.meta[NAME]) for rev in revs]
+        choices = [(rev.meta[ITEMID], rev.meta[NAME][0]) for rev in revs]
         if cls.optional:
             choices.append((u'', cls.properties['empty_label']))
         return choices
         # NOTE There is a slight chance of two instances of the same Reference
         # subclass having different set of choices when the storage changes
         # between their initialization.
-        choices = self._get_choices()
-        self.properties['labels'] = dict(choices)
-        self.valid_values = [id_ for id_, name in choices]
+        choice_specs = self._get_choice_specs()
+        self.properties['choice_specs'] = choice_specs
+        self.valid_values = [id_ for id_, name in choice_specs]
 
 
 class BackReference(ReadonlyItemLinkList):

File MoinMoin/items/__init__.py

         """
         raise NotImplementedError
 
-    def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=u'',
+    def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=None,
               overwrite=False, delete=False):
         backend = flaskg.storage
         storage_item = backend[self.name]
         else:
             meta[NAME] = [name]
 
-        if comment:
+        if comment is not None:
             meta[COMMENT] = unicode(comment)
 
         if not overwrite and REVID in meta:
     def subitems_prefix(self):
         return self.name + u'/' if self.name else u''
 
-    @timed()
     def get_subitem_revs(self):
         """
         Create a list of subitems of this item.
         revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
         return revs
 
-    @timed()
     def make_flat_index(self, subitems):
         """
         Create two IndexEntry lists - ``dirs`` and ``files`` - from a list of
                     # break them
                     return "OK"
             form = self.ModifyForm.from_request(request)
-            if form.validate():
+            state = dict(name=self.name, itemid=self.meta.get(ITEMID))
+            if form.validate(state):
                 meta, data, contenttype_guessed, comment = form._dump(self)
                 contenttype_qs = request.values.get('contenttype')
                 try:

File MoinMoin/items/_tests/test_Blog.py

 
 import re
 
+from datetime import datetime
 from flask import url_for
 
 from MoinMoin._tests import become_trusted, update_item
 from MoinMoin.constants.misc import ANON
 from MoinMoin.items.blog import ITEMTYPE_BLOG, ITEMTYPE_BLOG_ENTRY
 from MoinMoin.items.blog import Blog, BlogEntry
+from MoinMoin.themes import utctimestamp
 
 
 class TestView(object):
     def test_do_show_entries(self):
         item = Item.create(self.name, itemtype=ITEMTYPE_BLOG)
         item._save(self.meta, self.data, comment=self.comment)
-        # store some unpublished entries
+        # store entries without PTIME
         for entry in self.entries:
             item = Item.create(entry['name'], itemtype=ITEMTYPE_BLOG_ENTRY)
             item._save(self.entry_meta, entry['data'], comment=self.comment)
-        # still empty blog
-        data_tokens = [self.data, self.NO_ENTRIES_MSG, ]
-        self._test_view(self.name, data_tokens=data_tokens)
-        # publish the first three entries, ptime value is a UNIX timestamp
-        self._publish_entry(self.entries[0], ptime=1000)
-        self._publish_entry(self.entries[1], ptime=3000)
-        self._publish_entry(self.entries[2], ptime=2000)
-        # the blog is not empty and the 4th entry is not published
-        exclude_data_tokens = [self.NO_ENTRIES_MSG, self.entries[3]['data'], ]
-        # blog entries are published in reverse order relative to their PTIMEs
+        # the blog is not empty
+        exclude_data_tokens = [self.NO_ENTRIES_MSG, ]
+        # all stored blog entries are listed on the blog index page
+        data_tokens = [self.data, ] + [entry['data'] for entry in self.entries]
+        self._test_view(self.name, data_tokens=data_tokens, exclude_data_tokens=exclude_data_tokens)
+
+    def test_do_show_sorted_entries(self):
+        item = Item.create(self.name, itemtype=ITEMTYPE_BLOG)
+        item._save(self.meta, self.data, comment=self.comment)
+        # store entries
+        for entry in self.entries:
+            item = Item.create(entry['name'], itemtype=ITEMTYPE_BLOG_ENTRY)
+            item._save(self.entry_meta, entry['data'], comment=self.comment)
+        # Add PTIME to some of the entries, ptime value is a UNIX timestamp. If PTIME
+        # is not defined, we use MTIME as publication time (which is usually in the past).
+        self._publish_entry(self.entries[0], ptime=2000)
+        self._publish_entry(self.entries[1], ptime=1000)
+        time_in_future = utctimestamp(datetime(2029, 1, 1))
+        self._publish_entry(self.entries[2], ptime=time_in_future)
+        # the blog is not empty
+        exclude_data_tokens = [self.NO_ENTRIES_MSG, ]
+        # blog entries are listed in reverse order relative to their PTIME/MTIMEs,
+        # entries published in the future are also listed here
         ordered_data = [self.data,
-                        self.entries[1]['data'],
                         self.entries[2]['data'],
-                        self.entries[0]['data'], ]
-        regex = re.compile(r'{0}.*{1}.*{2}.*{3}'.format(*ordered_data), re.DOTALL)
+                        self.entries[3]['data'],
+                        self.entries[0]['data'],
+                        self.entries[1]['data'], ]
+        regex = re.compile(r'{0}.*{1}.*{2}.*{3}.*{4}'.format(*ordered_data), re.DOTALL)
         self._test_view(self.name, exclude_data_tokens=exclude_data_tokens, regex=regex)
 
     def test_filter_by_tag(self):

File MoinMoin/items/content.py

     return cls
 
 
-@timed()
 def conv_serialize(doc, namespaces, method='polyglot'):
     out = array('u')
     doc.write(out.fromunicode, namespaces=namespaces, method=method)

File MoinMoin/items/ticket.py

 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
+from MoinMoin.constants.keys import ITEMTYPE, CONTENTTYPE, ITEMID, CURRENT, SUPERSEDED_BY, DEPENDS_ON, SUBSCRIBED_ITEMS
 from MoinMoin.constants.contenttypes import CONTENTTYPE_USER
 from MoinMoin.items import Item, Contentful, register, BaseModifyForm
 from MoinMoin.items.content import NonExistentContent
 
     def _load(self, item):
         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['supersedes'].set(Term(SUPERSEDED_BY, id_))
+        self['required_by'].set(Term(DEPENDS_ON, id_))
+        self['subscribers'].set(Term(SUBSCRIBED_ITEMS, id_))
 
 
 class TicketForm(BaseModifyForm):
             CONTENTTYPE: 'text/x.moin.wiki;charset=utf-8',
             'closed': False,
         }
+        meta.update(self['meta'].value)
         return meta, message_markup(self['message'].value)
 
 
 
 %(message)s
 }}}}}}
-''' % dict(author=flaskg.user.name, timestamp=time.time(), message=message)
+''' % dict(author=flaskg.user.name[0], timestamp=time.time(), message=message)
 
 
 @register
     itemtype = ITEMTYPE_TICKET
     display_name = L_('Ticket')
     description = L_('Ticket item')
-    modify_template = 'ticket.html'
+    submit_template = 'ticket/submit.html'
+    modify_template = 'ticket/modify.html'
 
     def do_show(self, revid):
         if revid != CURRENT:
         # XXX When creating new item, suppress the "foo doesn't exist. Create it?" dummy content
         data_rendered = None if is_new else Markup(self.content._render_data())
 
-        return render_template(self.modify_template,
+        return render_template(self.submit_template if is_new else self.modify_template,
                                is_new=is_new,
                                closed=closed,
                                item_name=self.name,

File MoinMoin/mail/_tests/test_sendmail.py

 """
 
 
-from email.Charset import Charset, QP
-from email.Header import Header
+from email.charset import Charset, QP
+from email.header import Header
 
 from MoinMoin.mail import sendmail
 from MoinMoin.constants.contenttypes import CHARSET

File MoinMoin/mail/sendmail.py

 
 import os
 import re
-from email.Header import Header
+from email.header import Header
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
     """
     import smtplib
     import socket
-    from email.Message import Message
-    from email.Charset import Charset, QP
-    from email.Utils import formatdate, make_msgid
+    from email.message import Message
+    from email.charset import Charset, QP
+    from email.utils import formatdate, make_msgid
 
     cfg = app.cfg
     if not cfg.mail_enabled:

File MoinMoin/script/account/resetpw.py

 """
 
 
+import sys
+
+from flask import current_app as app
 from flask.ext.script import Command, Option
 
 from MoinMoin.constants.keys import ITEMID, NAME, NAME_EXACT, EMAIL
     """raised if no such user exists"""
 
 
+class UserHasNoEMail(Fault):
+    """raised if user has no e-mail address in his profile"""
+
+
 class MailFailed(Fault):
     """raised if e-mail sending failed"""
 
 
-def set_password(uid, password, notify=False):
+def set_password(uid, password, notify=False, skip_invalid=False, subject=None, text=None):
     u = user.User(uid)
     if u and u.exists():
+        if skip_invalid and u.has_invalidated_password():
+            return
         u.set_password(password)
         u.save()
-        if notify and not u.disabled and u.email:
-            mailok, msg = u.mail_password_recovery()
+        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:
+            mailok, msg = u.mail_password_recovery(subject=subject, text=text)
             if not mailok:
                 raise MailFailed(msg)
     else:
                help='Notify user(s), send them an E-Mail with a password reset link.'),
         Option('--verbose', '-v', required=False, dest='verbose', action='store_true', default=False,
                help='Verbose operation'),
+        Option('--subject', required=False, dest='subject', type=unicode,
+               help='Subject text for the password reset notification E-Mail.'),
+        Option('--text', required=False, dest='text', type=unicode,
+               help='Template text for the password reset notification E-Mail. Default: use the builtin standard template'),
+        Option('--text-from-file', required=False, dest='text_file', type=unicode,
+               help='Read full template for the password reset notification E-Mail from the given file, overrides --text. Default: None'),
+        Option('--skip-invalid', required=False, dest='skip_invalid', action='store_true',
+               help='If a user\'s password hash is already invalid (pw is already reset), skip this user.'),
     )
 
-    def run(self, name, uid, password, all_users, notify, verbose):
+    def run(self, name, uid, password, all_users, notify, verbose, subject, text, text_file, skip_invalid):
         flags_given = name or uid or all_users
         if not flags_given:
             print 'incorrect number of arguments'
-            import sys
-            sys.exit()
+            sys.exit(1)
+
+        if notify and not app.cfg.mail_enabled:
+            print "This wiki is not enabled for mail processing. The --notify option requires this. Aborting..."
+            sys.exit(1)
+
+        if text_file:
+            with open(text_file) as f:
+                text = f.read().decode('utf-8')
 
         before_wiki()
         if uid:
             name = meta[NAME]
             email = meta[EMAIL]
             try:
-                set_password(uid, password, notify=notify)
-            except Fault, err:
+                set_password(uid, password, notify=notify, skip_invalid=skip_invalid,
+                             subject=subject, text=text)
+            except Fault as err:
                 status = "FAILURE: [%s]" % str(err)
             else:
                 status = "SUCCESS"

File MoinMoin/security/__init__.py

 
             +SomeUser:read -OtherUser:write
 
-        The acl line above will grant SomeUser read right, and OtherUser
+        The acl line above will grant SomeUser read right, and deny OtherUser
         write right, but will NOT block automatically all other rights
         for these users. For example, if SomeUser asks to write, the
         above acl line does not define if he can or can not write. He

File MoinMoin/storage/backends/_tests/__init__.py

         assert m == meta
         assert d.read() == data
         d.close()
-        self.be.remove(metaid)
+        self.be.remove(metaid, destroy_data=True)
         with pytest.raises(KeyError):
             self.be.retrieve(metaid)
 

File MoinMoin/storage/backends/stores.py

     def _del_data(self, dataid):
         del self.data_store[dataid]
 
-    def remove(self, metaid):
+    def remove(self, metaid, destroy_data):
         meta = self._get_meta(metaid)
         dataid = meta[DATAID]
         self._del_meta(metaid)
-        self._del_data(dataid)
+        if destroy_data:
+            self._del_data(dataid)

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

     assert other_name.split(':')[1] == other_meta[NAME][0]
 
     # delete revs:
-    router.remove(default_backend_name, default_revid)
-    router.remove(other_backend_name, other_revid)
+    router.remove(default_backend_name, default_revid, destroy_data=True)
+    router.remove(other_backend_name, other_revid, destroy_data=True)
 
 
 def test_store_readonly_fails(router):

File MoinMoin/storage/middleware/indexing.py

 from MoinMoin import log
 logging = log.getLogger(__name__)
 
-from MoinMoin.constants.keys import (WIKINAME, NAMESPACE, NAME, NAME_EXACT, MTIME, CONTENTTYPE, TAGS, LANGUAGE,
-                                     USERID, ADDRESS, HOSTNAME, SIZE, ACTION, COMMENT, SUMMARY, CONTENT,
-                                     EXTERNALLINKS, ITEMLINKS, ITEMTRANSCLUSIONS, ACL, DISABLED, EMAIL, OPENID,
-                                     ITEMID, REVID, CURRENT, PARENTID, PTIME, LATEST_REVS, ALL_REVS, BACKENDNAME)
+from MoinMoin.constants.keys import *
 from MoinMoin.constants.contenttypes import CONTENTTYPE_USER
 from MoinMoin.constants.namespaces import NAMESPACE_DEFAULT
-from MoinMoin.constants import keys
-from MoinMoin.constants.keys import ITEMTYPE
 
 from MoinMoin import user
 from MoinMoin.search.analyzers import item_name_analyzer, MimeTokenizer, AclTokenizer
     :param wikiname: interwikiname of this wiki
     :returns: document to put into whoosh index
     """
-    doc = dict([(str(key), value)
+    doc = dict([(key, value)
                 for key, value in meta.items()
                 if key in schema])
     for key in [MTIME, PTIME]:
             COMMENT: TEXT(stored=True),
             # SUMMARY from metadata
             SUMMARY: TEXT(stored=True),
+            # DATAID from metadata
+            DATAID: ID(stored=True),
             # data (content), converted to text/plain and tokenized
             CONTENT: TEXT(stored=True),
         }
 
         # XXX This is a highly adhoc way to support indexing of ticket items.
         ticket_fields = {
-            'effort': NUMERIC(stored=True),
-            'difficulty': NUMERIC(stored=True),
-            'severity': NUMERIC(stored=True),
-            'priority': NUMERIC(stored=True),
-            'assigned_to': ID(stored=True),
-            'superseded_by': ID(stored=True),
-            'depends_on': ID(stored=True),
-            'closed': BOOLEAN(stored=True),
+            EFFORT: NUMERIC(stored=True),
+            DIFFICULTY: NUMERIC(stored=True),
+            SEVERITY: NUMERIC(stored=True),
+            PRIORITY: NUMERIC(stored=True),
+            ASSIGNED_TO: ID(stored=True),
+            SUPERSEDED_BY: ID(stored=True),
+            DEPENDS_ON: ID(stored=True),
+            CLOSED: BOOLEAN(stored=True),
         }
         latest_revs_fields.update(**ticket_fields)
 
             return userid_pseudo_field
         qp.add_plugin(PseudoFieldPlugin(dict(
             # username:JoeDoe searches for revisions modified by JoeDoe
-            username=userid_pseudo_field_factory(keys.USERID),
+            username=userid_pseudo_field_factory(USERID),
             # assigned:JoeDoe searches for tickets assigned to JoeDoe
-            assigned=userid_pseudo_field_factory('assigned_to'),  # XXX should be keys.ASSIGNED_TO
+            assigned=userid_pseudo_field_factory(ASSIGNED_TO),
         )))
         return qp
 
         if wikiname is None:
             wikiname = app.cfg.interwikiname
         state = {'trusted': trusted,
-                 keys.NAME: [name],
-                 keys.ACTION: action,
-                 keys.ADDRESS: remote_addr,
-                 keys.USERID: userid,
-                 keys.WIKINAME: wikiname,
-                 keys.NAMESPACE: None,
-                 keys.ITEMID: self.itemid,  # real itemid or None
+                 NAME: [name],
+                 ACTION: action,
+                 ADDRESS: remote_addr,
+                 USERID: userid,
+                 WIKINAME: wikiname,
+                 NAMESPACE: None,
+                 ITEMID: self.itemid,  # real itemid or None
                  'contenttype_current': contenttype_current,
                  'contenttype_guessed': contenttype_guessed,
                  'acl_parent': acl_parent,
                 }
-        ct = meta.get(keys.CONTENTTYPE)
+        ct = meta.get(CONTENTTYPE)
         if ct == CONTENTTYPE_USER:
             Schema = UserMetaSchema
         else:
         Destroy revision <revid>.
         """
         rev = Revision(self, revid)
-        self.backend.remove(rev.backend_name, revid)
+        query = {DATAID: rev.meta[DATAID]}
+        with flaskg.storage.indexer.ix[ALL_REVS].searcher() as searcher:
+            refcount = len(list(searcher.document_numbers(**query)))
+        self.backend.remove(rev.backend_name, revid, destroy_data=refcount == 1)
         self.indexer.remove_revision(revid)
 
     def destroy_all_revisions(self):

File MoinMoin/storage/middleware/routing.py

         meta[BACKENDNAME] = backend_name
         return backend_name, revid
 
-    def remove(self, backend_name, revid):
+    def remove(self, backend_name, revid, destroy_data):
         backend = self.backends[backend_name]
         if not isinstance(backend, MutableBackendBase):
             raise TypeError('backend {0} is readonly'.format(backend_name))
-        backend.remove(revid)
+        backend.remove(revid, destroy_data)

File MoinMoin/templates/base.html

     {% endblock %}
 
     {% block head_links %}
+
+    {% block bootstrap_stylesheet %}
+    <link rel="stylesheet" href="{{ url_for('serve.files', name='bootstrap', filename='bootstrap/css/bootstrap.css') }}" />
+    {% endblock %}
+
     <link rel="shortcut icon" href="{{ url_for('static', filename='logos/favicon.ico') }}" />
 
     {% block theme_stylesheets %}

File MoinMoin/templates/common.js

     if ($('#moin-usersettings').length === 0) { return; }
 
     // create a UL that will be displayed as row of tabs or column of buttons
-    // "nav nav-tabs" for Bootstrap compatibility.
     $(function () {
         var tabs = $('#moin-usersettings'),
             titles = $('<ul class="moin-tab-titles nav nav-tabs">');
+            hashTag = window.location.hash;
         // for each form on page, create a corresponding LI
         $('.moin-tab-body').each(function () {
             var li = $(document.createElement('li')),
                 // parent() active for Bootstrap compatibility
                 $('.moin-current-tab').parent().removeClass('active');
                 $(ev.target).parent().addClass('active');
-
+                window.location.hash = tab;
                 $('.moin-current-tab').removeClass('moin-current-tab');
                 $(ev.target).addClass('moin-current-tab');
                 tabs.children('.moin-tab-body').hide().removeClass('moin-current-form');

File MoinMoin/templates/destroy.html

     {{ gen.form.close() }}
     </div>
 {% else %}
-    <h1>{{ _("DESTROY REVISION '%(item_name)s' (rev %(rev_id)s)", item_name=item.name, rev_id=rev_id) }}</h1>
+    <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)) }}
       {{ forms.render_errors(form) }}

File MoinMoin/templates/lookup.html

             {% for result in results %}
                 {% if result['wikiname'] == cfg.interwikiname %}
                     <tr>
-                        <td class="moin-wordbreak">{{ result.pos + 1 }}
-                        <a href="{{ url_for_item(item_name=result['name'], wiki_name='Self', rev=result['revid']) }}"><b>{{ result['name'] }}</b></a>
+                        <td class="moin-wordbreak">{{ loop.index }}
+                        <a href="{{ url_for_item(item_name=result.name, wiki_name='Self', rev=result.revid) }}"><b>{{ result.name }}</b></a>
                         </td>
                     </tr>
                 {% else %}
                     <tr>
-                        <td class="moin-wordbreak">{{ result.pos + 1 }}
-                        <a class="moin-interwiki" href="{{ url_for_item(item_name=result['name'], wiki_name=result['wikiname'], rev=result['revid']) }}"><b>{{ "%s:%s" % (result['wikiname'], result['name']) }}</b></a>
+                        <td class="moin-wordbreak">{{ loop.index }}
+                        <a class="moin-interwiki" href="{{ url_for_item(item_name=result.name, wiki_name=result.wikiname, rev=result.revid) }}"><b>{{ "%s:%s" % (result.wikiname, result.name) }}</b></a>
                         </td>
                     </tr>
                 {% endif %}

File MoinMoin/templates/mail/account_verification.txt

+{{ _("""\
+Somebody has created an account with this email address.
+
+Please use the link below to verify your email address:
+
+%(link)s
+
+If you didn't create this account, please ignore this email.
+
+""", link=link) }}

File MoinMoin/templates/mail/password_recovery.txt

+{{ _("""\
+Somebody has requested to email you a password recovery link.
+
+Please use the link below to change your password to a known value:
+
+%(link)s
+
+If you didn't forget your password, please ignore this email.
+
+""", link=link) }}

File MoinMoin/templates/meta.html

 {% block content_data %}
 <h1>
     {{ title }}
-    {% if show_revision %}({{ _("Revision") }} {{ rev.revid }}){% endif %}
+    {% if show_revision %}({{ _("Revision") }} {{ rev.revid | shorten_id }}){% endif %}
 </h1>
 {% if meta_rendered %}
 <div id="moin-content-meta">

File MoinMoin/templates/modify.html

     {% call(fullname, shortname, contenttype, has_children) utils.render_subitem_navigation(item_name, True) %}
         {% set shortname = shortname|json_dumps %}
         {% set fullname = fullname|json_dumps %}
-        <button class="link-action" onclick='linkSubitem({{ shortname }}, {{ fullname }})'
-            title="{{ _('Link to Subitem') }}">{{ _('Link') }}</button>
-        <button class="transclude-action"
-            onclick='transcludeSubitem({{ shortname }}, {{ fullname }})'
-            title="{{ _('Transclude Subitem') }}">{{ _('Transclude') }}</button>
+        <button class="moin-insertname-action" onclick='InsertName({{ fullname }})'
+            title="{{ _('Insert Name') }}">{{ _('Insert Name') }}</button>
     {% endcall %}
 {% endblock %}
 
        POSTs originate from their respective applets.
     #}
     {% if not form['content_form'].is_draw %}
-        {{ forms.render_submit(form) }}
+        {{ forms.render_submit(form, id='moin-save-text-button') }}
         <dl>
             {{ forms.render_textcha(gen, form) }}
             {{ forms.render(form['comment']) }}

File MoinMoin/templates/recoverpass.html

 {{ gen.form.open(form, method="post", action=url_for('frontend.recoverpass')) }}
   {{ forms.render_errors(form) }}
   <dl>
+    {{ forms.render(form['token']) }}
     {{ forms.render(form['username']) }}
-    {{ forms.render(form['token']) }}
     {{ forms.render(form['password1']) }}
     {{ forms.render(form['password2']) }}
   </dl>

File MoinMoin/templates/revert.html

 {% 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) }}</h1>
+<h1>{{ _("Revert '%(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.revert_item', item_name=item.name, rev=rev_id)) }}
   {{ forms.render_errors(form) }}

File MoinMoin/templates/search.html

                             {% endif %}
                             <tr>
                                 <td>
-                                    <p class="info searchhitinfobar">{{ _("Revision: %(revid)s Last Change: %(mtime)s", revid=result['revid'], mtime=result['mtime']|datetimeformat) }}</p>
+                                    <p class="info searchhitinfobar">{{ _("Revision: %(revid)s Last Change: %(mtime)s", revid=result['revid']|shorten_id, mtime=result['mtime']|datetimeformat) }}</p>
                                 </td>
                             </tr>
                             <tr>

File MoinMoin/templates/ticket.html

-{% import "forms.html" as forms %}
-{% extends theme("layout.html") %}
-
-{% if is_new %}
-    {% set title = _("Creating new ticket: '%(item_name)s'", item_name=item_name) %}
-{% else %}
-    {% set title = _("Ticket: '%(item_name)s'", item_name=item_name) %}
-{% endif %}
-
-{% block head %}
-    {{ super() }}
-    <link media="all" rel="stylesheet" href="{{ url_for('static', filename='css/ticket.css') }}" />
-{% endblock %}
-
-{% block content %}
-<h1>
-    {{ title }}
-    {% if closed %}
-        {# TODO style .moin-ticket-closed #}
-        <span class="moin-ticket-closed">{{ _("(Closed)") }}</span>
-    {% endif %}
-</h1>
-<div class="moin-form" id="moin-ticket-form">
-    {{ gen.form.open(form, method='post', enctype='multipart/form-data') }}
-
-    {% if data_rendered %}
-        <div id="moin-content-data">
-            {{ data_rendered }}
-        </div>
-    {% endif %}
-
-    {% if is_new %}
-        <h2>Describe the ticket</h2>
-    {% else %}
-        <h2>Add comment</h2>
-    {% endif %}
-    <dl>
-    {{ forms.render(form['message']) }}
-    </dl>
-
-    <h2>Edit metadata</h2>
-    <dl>
-    {{ forms.render_errors(form) }}
-    {% for e in [
-        'summary',
-        'effort',
-        'difficulty',
-        'severity',
-        'priority',
-        'tags',
-        'assigned_to',
-        'superseded_by',
-        'depends_on',
-        ] %}
-        {{ forms.render(form['meta'][e]) }}
-    {% endfor %}
-    </dl>
-
-    {# see comments concerning the submit button within TicketForm in items/ticket.py #}
-    {% if is_new %}
-        {{ forms.render_submit(form) }}
-    {% else %}
-        {{ forms.render(form['submit']) }}
-    {% endif %}
-
-    <h2>Back references</h2>
-    <dl>
-    {% for e in [
-        'supersedes',
-        'required_by',
-        'subscribers',
-        ] %}
-        {{ forms.render(form['backrefs'][e]) }}
-    {% endfor %}
-
-    {{ gen.form.close() }}
-</div>
-{% endblock %}

File MoinMoin/templates/ticket/base.html

+{% import "forms.html" as forms %}
+{% extends theme("layout.html") %}
+
+{% macro render_meta() %}
+    {% for e in [
+        'summary',
+        'effort',
+        'difficulty',
+        'severity',
+        'priority',
+        'tags',
+        'assigned_to',
+        'superseded_by',
+        'depends_on',
+        ] %}
+        {{ forms.render(form['meta'][e]) }}
+    {% endfor %}
+{% endmacro %}
+
+{% macro render_backref() %}
+    {% for e in [
+        'supersedes',
+        'required_by',
+        'subscribers',
+        ] %}
+        {{ forms.render(form['backrefs'][e]) }}
+    {% endfor %}
+{% endmacro %}
+
+{% block head %}
+    {{ super() }}
+    <link media="all" rel="stylesheet" href="{{ url_for('static', filename='css/ticket.css') }}" />
+{% endblock %}
+
+{% block content %}
+<h1>
+    {% block title_text %}
+    {% endblock %}
+    {% if closed %}
+        {# TODO style .moin-ticket-closed #}
+        <span class="moin-ticket-closed">{{ _("(Closed)") }}</span>
+    {% endif %}
+</h1>
+<div class="moin-form" id="moin-ticket-form">
+    {{ gen.form.open(form, method='post', enctype='multipart/form-data') }}
+
+    {% if data_rendered %}
+        <div id="moin-content-data">
+            {{ data_rendered }}
+        </div>
+    {% endif %}
+
+    {% block form_controls %}
+    {% endblock %}
+
+    {{ gen.form.close() }}
+</div>
+{% endblock %}

File MoinMoin/templates/ticket/modify.html

+{% extends "ticket/base.html" %}
+
+{% block title_text %}
+    {{ _("Ticket: '%(item_name)s'", item_name=item_name) }}
+{% endblock %}
+
+{% block form_controls %}
+    <h2>{{ _("Add comment") }}</h2>
+    <dl>
+    {{ forms.render(form['message']) }}
+    </dl>
+
+    <h2>{{ _("Edit metadata") }}</h2>
+    <dl>
+    {{ forms.render_errors(form) }}
+    {{ render_meta() }}
+    </dl>
+
+    {{ forms.render(form['submit']) }}
+
+    <h2>{{ _("Back references") }}</h2>
+    <dl>
+    {{ render_backref() }}
+    </dl>
+{% endblock %}

File MoinMoin/templates/ticket/submit.html

+{% extends "ticket/base.html" %}
+
+{% block title_text %}
+    {{ _("Creating new ticket: '%(item_name)s'", item_name=item_name) }}
+{% endblock %}
+
+{% block form_controls %}
+    <h2>{{ _("Describe the ticket") }}</h2>
+
+    <dl>
+    {{ forms.render(form['message']) }}
+    </dl>
+
+    <h2>{{ _("Provide metadata") }}</h2>
+    <dl>
+    {{ forms.render_errors(form) }}
+    {{ render_meta() }}
+    </dl>
+
+    {{ forms.render_submit(form) }}
+{% endblock %}

File MoinMoin/themes/__init__.py

         return get_theme(theme_name)
 
 
-@timed()
 def render_template(template, **context):
     return render_theme_template(get_current_theme(), template, **context)
 

File MoinMoin/themes/foobar/static/css/common.css

 .moin-diff-removed span{background-color:#f1eeb9}
 body{color:#000;background-color:#d6d5d0;font-family:Helvetica,Arial,sans-serif;background-image:url("../img/base.png")}
 p{margin:.2em .4em;font-size:1em;line-height:1.3em}
-#moin-global-tray{float:left;width:16.666666666666668%;padding:.5%;overflow:hidden}
-#moin-main-container{float:left;width:81.13333333333333%;padding:.5%}
+#moin-global-tray{float:left;width:16.55%;padding:.5%;overflow:hidden}
+#moin-main-container{float:left;width:81.095%;padding:.5%}
 #moin-main-box{background-color:#f4f4f4;border:1px solid #ccc;border-radius:6px;box-shadow:2px 2px 4px #9d9d9b;overflow:hidden}
 #moin-header{background-color:#2d2d2d;color:#fff;border-bottom:1px solid #ccc;padding:0;overflow:auto;}
 #moin-header a{color:#fff}
-#moin-content-no-panel{float:left;width:98.56746096959736%;padding:.6162695152013148%;border-right:1px solid #ccc}
-#moin-content{float:left;width:76.79260476581759%;padding:.6162695152013148%;border-right:1px solid #ccc}
-#moin-local-panel{float:left;width:20.54231717337716%;padding:.6162695152013148%;margin-left:-1px;border-left:1px solid #ccc}
+#moin-content-no-panel{float:left;width:98.307%;padding:.5%;border-right:1px solid #ccc}
+#moin-content{float:left;width:81.095%;padding:.5%;border-right:1px solid #ccc}
+#moin-local-panel{float:left;width:16.55%;padding:.5%;margin-left:-1px;border-left:1px solid #ccc}
 #moin-footer{clear:both;font-size:.8em;color:#616161;padding:.5% .5%;overflow:auto}
 #moin-breadcrumbs-location{float:left;padding:.5em;font-weight:bold}
 #moin-interwiki{vertical-align:middle;}

File MoinMoin/themes/foobar/static/css/stylus/main.styl

 
 scale = 1  // we have the whole screen width as base
 global_tray_width = 100% / 6 * scale
+local_panel_width = 100% / 6 * scale
 global_padding_width = 0.5% * scale
-main_container_width = 99.8% - global_tray_width - (4 * global_padding_width)
+local_padding_width = 0.5% * scale
+
+main_container_width = 99.3% - global_tray_width - (4 * global_padding_width)
 
 scale = 100% / main_container_width  // we have the main container width as base
-local_panel_width = 100% / 6 * scale
-local_padding_width = 0.5% * scale
-content_width_with_panel = 99.8% - local_panel_width - (4 * local_padding_width)
-content_width_no_panel = 99.8% - (2 * local_padding_width)
+content_width_with_panel = 99.3% - local_panel_width - (4 * local_padding_width)
+content_width_no_panel = 99.3% - (2 * local_padding_width)
 
 body
     color font_color
     text-indent -9000%
     box-shadow none
 
-.link-action,
-.transclude-action
+.moin-insertname-action
     border: hidden
     text-indent: -9000%
     padding: 0
     overflow: hidden
     cursor: pointer
     box-shadow none
-
-.link-action
     background: url(../img/moin-link.png) no-repeat center center
 
-.transclude-action
-    background: url(../img/moin-transclusion.png) no-repeat center center
 
 // transclusion
 .moin-transclusion

File MoinMoin/themes/modernized/static/css/stylus/main.styl

     border hidden
     text-indent -9000%
 
-.link-action,
-.transclude-action
+.moin-insertname-action
     border hidden
     text-indent -9000%
     padding 0
     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-footer
     clear both
     margin 0 0

File MoinMoin/user.py

 
 from flask import current_app as app
 from flask import g as flaskg
-from flask import session, request, url_for
+from flask import session, request, url_for, render_template
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
         # Invalidate all other browser sessions except this one.
         session['user.session_token'] = self.generate_session_token(False)
 
+    def has_invalidated_password(self):
+        """
+        Check if the password hash of this user is invalid.
+        """
+        return self.profile[ENC_PASSWORD] == ''
+
     def save(self, force=False):
         """
         Save user account data to user account file on disk.
         self.save()
         return True
 
-    def mail_password_recovery(self, cleartext_passwd=None):
+    def mail_password_recovery(self, cleartext_passwd=None, subject=None, text=None):
         """ Mail a user who forgot his password a message enabling
             him to login again.
         """
+        if not self.email:
+            return False, "user has no E-Mail address in his profile."
+
         token = self.generate_recovery_token()
 
-        text = _("""\
-Somebody has requested to email you a password recovery link.
+        if subject is None:
+            subject = _('[%(sitename)s] Your wiki password recovery link', sitename='%(sitename)s')
+        subject = subject % dict(sitename=self._cfg.sitename or "Wiki")
+        if text is None:
+            link = url_for('frontend.recoverpass', username=self.name0, token=token, _external=True)
+            text = render_template('mail/password_recovery.txt', link=link)
 
-Please use the link below to change your password to a known value:
-
-%(link)s
-
-If you didn't forget your password, please ignore this email.
-
-""", link=url_for('frontend.recoverpass', username=self.name0, token=token, _external=True))
-
-        subject = _('[%(sitename)s] Your wiki password recovery link',
-                    sitename=self._cfg.sitename or "Wiki")
         mailok, msg = sendmail.sendmail(subject, text, to=[self.email], mail_from=self._cfg.mail_from)
         return mailok, msg
 
         """ Mail a user a link to verify his email address. """
         token = self.generate_recovery_token()
 
-        text = _("""\
-Somebody has created an account with this email address.
-
-Please use the link below to verify your email address:
-
-%(link)s
-
-If you didn't create this account, please ignore this email.
-
-""", link=url_for('frontend.verifyemail', username=self.name0, token=token, _external=True))
+        link = url_for('frontend.verifyemail', username=self.name0, token=token, _external=True)
+        text = render_template('mail/account_verification.txt', link=link)
 
         subject = _('[%(sitename)s] Please verify your email address',
                     sitename=self._cfg.sitename or "Wiki")

File MoinMoin/util/_tests/test_interwiki.py

                  # 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), '/%2Bmodify/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/%2Bmodify/SomePage
-                 # (('SomePage', '', '', CURRENT, 'frontend.modify_item', True), 'http://localhost:8080/%2Bmodify/SomePage'),
-                 (('SomePage', '', '', revid, 'frontend.show_item', False), '/%2Bshow/%2B{0}/SomePage'.format(revid)),
-                 (('SomePage', '', '', revid, 'frontend.show_item_meta', False), '/%2Bmeta/%2B{0}/SomePage'.format(revid)),
+                 # 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)),
                  # 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), '/%2Bmodify/:ns1:SomePage'),
-                 (('SomePage', '', 'ns1:ns2', CURRENT, 'frontend.show_item_meta', True), 'http://localhost:8080/%2Bmeta/:ns1:ns2:SomePage'),
-                 (('SomePage', '', 'ns1', revid, 'frontend.show_item', False), '/%2Bshow/%2B{0}/:ns1:SomePage'.format(revid)),
-                 (('SomePage', '', 'ns1:ns2', revid, 'frontend.show_item_meta', False), '/%2Bmeta/%2B{0}/:ns1:ns2:SomePage'.format(revid)),
+                 (('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', '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/%2Bshow/%2B{0}/SomePage'.format(revid)),
+                 (('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'),
                 ]

File MoinMoin/util/crypto.py

 - generate password recovery tokens
 - verify password recovery tokens
 - generate random strings of given length (for salting)
-
-Code is tested on Python 2.6/2.7.
 """
 
 from __future__ import absolute_import, division

File MoinMoin/util/interwiki.py

                 # 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
                 # base URL.
-                i = local_url.index('/%2B')
+                i = local_url.index('/+')
                 path = local_url[i + 1:]
                 url = wiki_base_url + path
     return url

File MoinMoin/util/tree.py

         return Name(key, self)
 
     def __repr__(self):
-        return '<{0}({1!r})>'.format(self.__class__.__name__, self)
+        return '<{0}({1!r})>'.format(self.__class__.__name__, str(self))
 
     @property
     def namespace(self):

File docs/admin/configure.rst

 password checker that is enabled by default and does some sanity checks, 
 so users don't choose easily crackable passwords.
 
-If your site has rather low security requirements, you can disable the checker by::
+It **does** check:
 
-    password_checker = None # no password checking
+* length of password (default minimum: 8)
+* amount of different characters in password (default minimum: 5)
+* password does not contain user name
+* user name does not contain password
+* password is not a keyboard sequence (like "ASDFghjkl" or "987654321"),
+  currently we have only US and DE keyboard data built-in.
 
-Note that the builtin password checker only does a few very fundamental
-checks, it e.g. won't forbid using a dictionary word as password.
+It **does not** check:
+
+* whether the password is in a well-known dictionary or password list
+* whether a password cracker can break it
+
+If you are not satisfied with the default values, you can easily customize the
+checker::
+
+    from MoinMoin.config.default import DefaultConfig, _default_password_checker
+    password_checker = lambda cfg, name, pw: _default_password_checker(
+                           cfg, name, pw, min_length=10, min_different=6)
+
+You could also completely replace it with your own implementation.
+
+If your site has rather low security requirements, you can disable the checker
+by::
+
+    password_checker = None  # no password checking
+
 
 Password storage
 ----------------

File docs/admin/password-reset.rst

+===============================
+Password Resetting/Invalidation
+===============================
+There might be circumstances when the wiki admin wants or needs to reset one
+user's or all users' password (hash).
+
+For example:
+
+* you had a security breach on your wiki server (or somewhere else) and the
+  old password hashes (or passwords) were exposed
+* you want to make sure some user or all users set a new password, e.g. if:
+
+  - your password policy has changed (requiring longer passwords for example)
+  - you changed your passlib configuration and want to immediately have all
+    hashes upgraded
+
+Note: if we say "reset a password" (to use a commonly used term), we mean to
+"invalidate the password hash" (so that no password exists that validates
+against that hash). MoinMoin does not keep user passwords in cleartext.
+
+The files we refer to below are located in docs/examples/password-reset/...
+
+
+Resetting one or few password(s)
+================================
+If you somehow interact with the users corresponding to the user accounts in
+question (by phone or directly), you don't need the extensive procedure as
+described below, just use::
+
+    moin account-password --name JoeDoe
+
+That will reset JoeDoe's password. Tell him to visit the login URL and use
+the "forgot my password" functionality to define a new password.
+
+If that doesn't work (e.g. if e-mail is not enabled for your wiki or he has
+a non-working e-mail address in his profile), you can also set a password for
+him::
+
+    moin account-password --name JoeDoe --password uIkV9.-a3
+
+Choose a rather complicated password to make sure they change it a minute
+afterwards (to another, hopefully safe password).
+
+
+Resetting many or all password(s)
+=================================
+If you have a lot of passwords to reset, you need a better procedure that
+avoids having to deal with too many users individually.
+
+
+Preparing your users
+--------------------
+Tell your users beforehands that you will be doing a password reset, otherwise
+they might find the automatically generated E-Mail they'll get suspicious and
+you'll have to explain it to them individually that the E-Mail is legitimate.
+
+Also, remind your users that having a valid E-Mail address in their user
+settings is essential for getting a password recovery E-Mail.
+
+If an active user does somehow not get such a mail, you likely will have to
+manually define a valid E-Mail address (or even password) for that user.
+
+
+Make sure E-Mail functionality works
+------------------------------------
+If you know you have working E-Mail functionality, skip this section.
+
+Password recovery and password reset notification work via E-Mail, so you
+should have it configured::
+
+    # the E-Mail address used for From: (consider using an address that
+    # can be directly replied to, at least while doing the pw reset):
+    mail_from = 'wiki@example.org'
+    # your smtp mail server hostname:port (default is 25)
+    mail_smarthost = 'mail.example.org:587'
+    # the login there, if authentication is needed
+    mail_username = 'wiki@example.org'
+    mail_password = 'SuperSecretSMTPPassword'
+
+You can try whether it works by using the "forgot my password" functionality
+on the login page.
+
+
+Editing mailtemplate.txt
+------------------------
+If you edit mailtemplate.txt, please be very careful and follow these rules
+(otherwise you might just see the script command crashing):
+
+The contents must be utf-8 (or ascii, which is a subset of utf-8).
+In case of doubt, just use plain English.
+
+Some places you likely should edit are marked with XXX.
+
+Do not use any % character in your text (except for the placeholders).
+If you need a verbatim % character, you need to write %%.
+
+It is a very good idea to give some URL (e.g. of a web or wiki page) in
+the text where users can read more information.
+
+Of course the information at that URL should be readable without requiring
+a wiki login (you just have invalidated his/her password!), so the user can
+get informed before clicking links he got from someone via E-Mail.
+
+We have added a wikitemplate.txt you can use to create such a wiki page.
+
+Instead of creating a web or wiki page with the information, you could
+also write all the stuff into the mail template directly, but please consider
+that E-Mail delivery to some users might fail for misc. reasons, so having
+some information on the web/wiki is usually better.
+
+
+Editing wikitemplate.txt
+------------------------
+Just copy & paste it to some public page in your wiki, e.g. "PasswordReset".
+
+Some places you likely should edit are marked with XXX.
+
+
+Doing the password reset
+------------------------
+Maybe first try it with a single user account::
+
+    moin account-password --name JoeDoe --notify --subject 'Wiki password reset' --text-from-file mailtemplate.txt
+
+Use some valid name, maybe a testing account of yourself. You should now have
+mail. If that worked ok, you can now do a global password reset for your wiki::
+
+    moin account-password --verbose --all-users --notify --subject 'Wiki password reset' --text-from-file mailtemplate.txt
+
+The subject may contain a placeholder for the sitename, which is useful for
+wiki farms (showing the builtin default here)::
+
+    '[%(sitename)s] Your wiki account data'
+

File docs/admin/requirements.rst

 Requirements
 ============
 
-MoinMoin requires Python >= 2.6 and < 3.0.
+MoinMoin requires Python 2.7.x.
 We usually test using CPython and this is what we recommend.
 
-You can also try PyPy: PyPy >= 1.6 seems to work with moin.
+You can also try PyPy, it seems to work.
 
 Servers
 =======

File docs/examples/password-reset/mailtemplate-de.txt

+Der Wiki-Administrator hat Ihr Passwort invalidiert und angefordert,
+Ihnen diese E-Mail zu senden, damit Sie ein neues Passwort setzen koennen.
+
+Bitte lesen Sie wichtige Informationen hierzu (sowie Hinweise zur
+Problembeseitigung und Wiki-Administrator-Kontaktinformationen) dort:
+
+(XXX hier die URL angeben XXX)
+
+
+Bitte besuchen Sie nun die unten angezeigte Passwort-Ruecksetz-URL und
+setzen Sie ein neues Passwort.
+
+%(link)s
+

File docs/examples/password-reset/mailtemplate.txt

+The wiki administrator has invalidated your wiki password and requested
+to send this E-Mail to you, so you can set a new one.
+