Thomas Waldmann  committed 43d7265 Merge

merged changesets from default branch into gae branch

  • Participants
  • Parent commits 52dd2c4, 5b81e54
  • Branches gae

Comments (0)

Files changed (18)

File MoinMoin/apps/frontend/

 from MoinMoin.apps.frontend import frontend
 from MoinMoin.forms import OptionalText, RequiredText, URL, YourOpenID, YourEmail, RequiredPassword, Checkbox, InlineCheckbox, Select, Tags, Natural, Submit, Hidden, MultiSelect
 from MoinMoin.items import BaseChangeForm, Item, NonExistent
+from MoinMoin.items.content import content_registry
 from MoinMoin import config, user, util
-from MoinMoin.config import CONTENTTYPE_GROUPS
 from MoinMoin.constants.keys import *
 from MoinMoin.constants.itemtypes import ITEMTYPES
 from MoinMoin.util import crypto
-contenttype_groups = []
+contenttype_groups = content_registry.group_names[:]
 contenttype_group_descriptions = {}
-for gname, contenttypes in CONTENTTYPE_GROUPS:
-    contenttype_groups.append(gname)
-    contenttype_group_descriptions[gname] = ', '.join([ctlabel for ctname, ctlabel in contenttypes])
+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')
 ContenttypeGroup = MultiSelect.of(Enum.using(valid_values=contenttype_groups).with_properties(descriptions=contenttype_group_descriptions)).using(optional=True)
     initials = list(set(initials))
     initials = sorted(initials)
-    index = item.get_index(startswith, selected_groups)
-    index = sorted(index, key=lambda e: e.relname.lower())
+    dirs, files = item.get_index(startswith, selected_groups)
+    # index = sorted(index, key=lambda e: e.relname.lower())
     item_names = item_name.split(u'/')
     return render_template(item.index_template,
-                           index=index,
+                           files=files,
+                           dirs=dirs,

File MoinMoin/constants/

 CONTENTTYPE_USER = u'application/x.moin.userprofile'
 CONTENTTYPE_DEFAULT = u'application/octet-stream'
-# structure for contenttype groups
-    ('markup text items', [
-        ('text/;charset=utf-8', 'Wiki (MoinMoin)'),
-        ('text/x.moin.creole;charset=utf-8', 'Wiki (Creole)'),
-        ('text/x-mediawiki;charset=utf-8', 'Wiki (MediaWiki)'),
-        ('text/x-markdown;charset=utf-8', 'Markdown'),
-        ('text/x-rst;charset=utf-8', 'ReST'),
-        ('application/docbook+xml;charset=utf-8', 'DocBook'),
-        ('text/html;charset=utf-8', 'HTML'),
-    ]),
-    ('other text items', [
-        ('text/plain;charset=utf-8', 'plain text'),
-        ('text/x-diff;charset=utf-8', 'diff/patch'),
-        ('text/x-python;charset=utf-8', 'python code'),
-        ('text/csv;charset=utf-8', 'csv'),
-        ('text/x-irclog;charset=utf-8', 'IRC log'),
-    ]),
-    ('image items', [
-        ('image/jpeg', 'JPEG'),
-        ('image/png', 'PNG'),
-        ('image/svg+xml', 'SVG'),
-    ]),
-    ('audio items', [
-        ('audio/wave', 'WAV'),
-        ('audio/ogg', 'OGG'),
-        ('audio/mpeg', 'MP3'),
-        ('audio/webm', 'WebM'),
-    ]),
-    ('video items', [
-        ('video/ogg', 'OGG'),
-        ('video/webm', 'WebM'),
-        ('video/mp4', 'MP4'),
-    ]),
-    ('drawing items', [
-        ('application/x-twikidraw', 'TDRAW'),
-        ('application/x-anywikidraw', 'ADRAW'),
-        ('application/x-svgdraw', 'SVGDRAW'),
-    ]),
-    ('other items', [
-        ('application/pdf', 'PDF'),
-        ('application/zip', 'ZIP'),
-        ('application/x-tar', 'TAR'),
-        ('application/x-gtar', 'TGZ'),
-        ('application/octet-stream', 'binary file'),
-    ]),
+GROUP_MARKUP_TEXT = 'markup text items'
+GROUP_OTHER_TEXT = 'other text items'
+GROUP_IMAGE = 'image items'
+GROUP_AUDIO = 'audio items'
+GROUP_VIDEO = 'video items'
+GROUP_DRAWING = 'drawing items'
+GROUP_OTHER = 'other items'

File MoinMoin/items/

 from import NoSuchItemError, NoSuchRevisionError, StorageError
 from MoinMoin.i18n import L_
 from MoinMoin.themes import render_template
+from MoinMoin.util.mime import Type
 from MoinMoin.util.interwiki import url_for_item
 from MoinMoin.util.registry import RegistryBase
+from MoinMoin.util.clock import timed
 from MoinMoin.forms import RequiredText, OptionalText, JSON, Tags, Submit
 from MoinMoin.constants.keys import (
-from MoinMoin.constants.contenttypes import charset, CONTENTTYPE_GROUPS
+from MoinMoin.constants.contenttypes import charset
 from MoinMoin.constants.itemtypes import ITEMTYPES
-from .content import Content, NonExistentContent, Draw
+from .content import content_registry, Content, NonExistentContent, Draw
 COLS = 80
         return form
-IndexEntry = namedtuple('IndexEntry', 'relname meta hassubitems')
+IndexEntry = namedtuple('IndexEntry', 'relname meta')
+MixedIndexEntry = namedtuple('MixedIndexEntry', 'relname meta hassubitems')
 class Item(object):
     """ Highlevel (not storage) Item, wraps around a storage Revision"""
     def subitems_prefix(self):
         return + u'/' if else u''
+    @timed()
     def get_subitem_revs(self):
         Create a list of subitems of this item.
         revs =, sortedby=NAME_EXACT, limit=None)
         return revs
+    @timed()
     def make_flat_index(self, subitems):
-        Create a list of IndexEntry from a list of subitems.
+        Create two IndexEntry lists - ``dirs`` and ``files`` - from a list of
+        subitems.
-        The resulting list contains only IndexEntry for *direct* subitems, e.g.
-        'foo' but not 'foo/bar'. When the latter is encountered, the former has
-        its `hassubitems` flag set in its IndexEntry.
+        Direct subitems are added to the ``files`` list.
-        When disconnected levels are detected, e.g. when there is foo/bar but no
-        foo, a dummy IndexEntry is created for the latter, with 'nonexistent'
-        itemtype and 'application/x.nonexistent' contenttype. Its `hassubitems`
-        flag is also set.
+        For indirect subitems, its ancestor which is a direct subitem is added
+        to the ``dirs`` list. Supposing current index root is 'foo' and when
+        'foo/bar/la' is encountered, 'foo/bar' is added to ``dirs``.
+        The direct subitem need not exist.
+        When both a subitem itself and some of its subitems are in the subitems
+        list, it appears in both ``files`` and ``dirs``.
         prefix = self.subitems_prefix
         prefixlen = len(prefix)
-        index = []
-        # relnames of all encountered subitems
-        relnames = set()
-        # relnames of subitems that need to have `hassubitems` flag set (but didn't)
-        relnames_to_patch = set()
+        # IndexEntry instances of "file" subitems
+        files = []
+        # IndexEntry instances of "directory" subitems
+        dirs = []
+        added_dir_relnames = set()
         for rev in subitems:
             fullname = rev.meta[NAME]
                 # 'foo', and current item (`rev`) is 'foo/bar/lorem/ipsum',
                 # 'foo/bar' will be found.
                 direct_relname = relname.partition('/')[0]
-                direct_fullname = prefix + direct_relname
-                if direct_relname not in relnames:
-                    # Join disconnected level with a dummy IndexEntry.
-                    # NOTE: Patching the index when encountering a disconnected
-                    # subitem might break the ordering. e.g. suppose the global
-                    # index has ['lorem-', 'lorem/ipsum'] (thus 'lorem' is a
-                    # disconnected level; also, note that ord('-') < ord('/'))
-                    # the patched index will have lorem after lorem-, requiring
-                    # one more pass of sorting after generating the index.
-                    e = IndexEntry(direct_relname, DummyRev(DummyItem(direct_fullname)).meta, True)
-                    index.append(e)
-                    relnames.add(direct_relname)
-                else:
-                    relnames_to_patch.add(direct_relname)
+                if direct_relname not in added_dir_relnames:
+                    added_dir_relnames.add(direct_relname)
+                    direct_fullname = prefix + direct_relname
+                    direct_rev = get_storage_revision(direct_fullname)
+                    dirs.append(IndexEntry(direct_relname, direct_rev.meta))
-                e = IndexEntry(relname, rev.meta, False)
-                index.append(e)
-                relnames.add(relname)
+                files.append(IndexEntry(relname, rev.meta))
-        for i in xrange(len(index)):
-            if index[i].relname in relnames_to_patch:
-                index[i] = index[i]._replace(hassubitems=True)
+        return dirs, files
-        return index
+    @timed()
     def filter_index(self, index, startswith=None, selected_groups=None):
         Filter a list of IndexEntry.
                      if e.relname.startswith((startswith, startswith.swapcase()))]
         def build_contenttypes(groups):
-            ctypes = [[ctype for ctype, clabel in contenttypes]
-                      for gname, contenttypes in CONTENTTYPE_GROUPS
-                      if gname in groups]
-            ctypes_chain = itertools.chain(*ctypes)
-            contenttypes = list(ctypes_chain)
-            contenttypes_without_encoding = [contenttype[:contenttype.index(u';')]
-                                             for contenttype in contenttypes
-                                             if u';' in contenttype]
-            contenttypes.extend(contenttypes_without_encoding) # adding more mime-types without the encoding term
+            contenttypes = []
+            for g in groups:
+                entries = content_registry.groups.get(g, []) # .get is a temporary workaround for "unknown items" group
+                contenttypes.extend([e.content_type for e in entries])
             return contenttypes
+        def contenttype_match(tested, cts):
+            for ct in cts:
+                if ct.issupertype(tested):
+                    return True
+            return False
         if selected_groups is not None:
-            all_groups = [gname for gname, contenttypes in CONTENTTYPE_GROUPS]
             selected_contenttypes = build_contenttypes(selected_groups)
-            filtered_index = [e for e in index
-                              if e.meta[CONTENTTYPE] in selected_contenttypes]
+            filtered_index = [e for e in index if contenttype_match(Type(e.meta[CONTENTTYPE]), selected_contenttypes)]
             unknown_item_group = "unknown items"
             if unknown_item_group in selected_groups:
-                all_contenttypes = build_contenttypes(all_groups)
+                all_contenttypes = build_contenttypes(content_registry.group_names)
                 filtered_index.extend([e for e in index
-                                       if e.meta[CONTENTTYPE] not in all_contenttypes])
+                                       if not contenttype_match(Type(e.meta[CONTENTTYPE]), all_contenttypes)])
             index = filtered_index
         return index
     def get_index(self, startswith=None, selected_groups=None):
-        return self.filter_index(self.make_flat_index(self.get_subitem_revs()), startswith, selected_groups)
+        dirs, files = self.make_flat_index(self.get_subitem_revs())
+        return dirs, self.filter_index(files, startswith, selected_groups)
+    def get_mixed_index(self):
+        dirs, files = self.make_flat_index(self.get_subitem_revs())
+        dirs_dict = dict([(e.relname, MixedIndexEntry(*e, hassubitems=True)) for e in dirs])
+        index_dict = dict([(e.relname, MixedIndexEntry(*e, hassubitems=False)) for e in files])
+        index_dict.update(dirs_dict)
+        return sorted(index_dict.values())
     index_template = 'index.html'
                 return render_template('modify_select_contenttype.html',
-                                       contenttype_groups=CONTENTTYPE_GROUPS,
+                                       group_names=content_registry.group_names,
+                                       groups=content_registry.groups,
             item = self
             if isinstance(self.rev, DummyRev):

File MoinMoin/items/_tests/

 from MoinMoin.util import diff_html
 from MoinMoin._tests import become_trusted, update_item
-from MoinMoin.items import Item, NonExistent, IndexEntry
+from MoinMoin.items import Item, NonExistent, IndexEntry, MixedIndexEntry
 from MoinMoin.items.content import Binary, Text, Image, TransformableBitmapImage, MarkupItem
-def build_index(basename, spec):
+def build_index(basename, relnames):
-    Build an index by hand, useful as a test helper.
+    Build a list of IndexEntry by hand, useful as a test helper.
-    return [(IndexEntry(relname, Item.create('/'.join((basename, relname))).meta, hassubitem))
+    return [(IndexEntry(relname, Item.create('/'.join((basename, relname))).meta))
+            for relname in relnames]
+def build_mixed_index(basename, spec):
+    """
+    Build a list of MixedIndexEntry by hand, useful as a test helper.
+    :spec is a list of (relname, hassubitem) tuples.
+    """
+    return [(MixedIndexEntry(relname, Item.create('/'.join((basename, relname))).meta, hassubitem))
             for relname, hassubitem in spec]
 class TestItem(object):
         # test Item.make_flat_index
         # TODO: test Item.get_subitem_revs
-        index = baseitem.make_flat_index(baseitem.get_subitem_revs())
-        assert index == build_index(basename, [
+        dirs, files = baseitem.make_flat_index(baseitem.get_subitem_revs())
+        assert dirs == build_index(basename, [u'cd', u'ij'])
+        assert files == build_index(basename, [u'ab', u'gh', u'ij', u'mn'])
+        # test Item.get_mixed_index
+        mixed_index = baseitem.get_mixed_index()
+        assert mixed_index == build_mixed_index(basename, [
             (u'ab', False),
             (u'cd', True),
             (u'gh', False),
             (u'ij', True),
-            (u'mn', False)
+            (u'mn', False),
         # test Item.filter_index
         # check filtered index when startswith param is passed
-        filtered = baseitem.filter_index(index, startswith=u'a')
-        assert filtered == build_index(basename, [(u'ab', False)])
-        # check that virtual container item is returned with startswith
-        filtered = baseitem.filter_index(index, startswith=u'c')
-        assert filtered == build_index(basename, [(u'cd', True)])
+        filtered_files = baseitem.filter_index(files, startswith=u'a')
+        assert filtered_files == build_index(basename, [u'ab'])
         # check filtered index when contenttype_groups is passed
         ctgroups = ["image items"]
-        filtered = baseitem.filter_index(index, selected_groups=ctgroups)
-        assert filtered == build_index(basename, [(u'mn', False)])
+        filtered_files = baseitem.filter_index(files, selected_groups=ctgroups)
+        assert filtered_files == build_index(basename, [u'mn'])
         # If we ask for text/plain type, should Foo/cd be returned?

File MoinMoin/items/

 from StringIO import StringIO
 from array import array
 from collections import namedtuple
+from operator import attrgetter
 from flask import current_app as app
 from flask import g as flaskg
 from MoinMoin.util.crypto import cache_key
 from MoinMoin.util.clock import timed
 from MoinMoin.forms import File
+from MoinMoin.constants.contenttypes import (
+    )
 from MoinMoin.constants.keys import (
 class RegistryContent(RegistryBase):
-    class Entry(namedtuple('Entry', 'factory content_type priority')):
+    class Entry(namedtuple('Entry', 'factory content_type default_contenttype_params display_name ingroup_order priority')):
         def __call__(self, content_type, *args, **kw):
             if self.content_type.issupertype(Type(content_type)):
                 return self.factory(content_type, *args, **kw)
                 return False
             return NotImplemented
-    def register(self, factory, content_type, priority=RegistryBase.PRIORITY_MIDDLE):
+    def __init__(self, group_names):
+        super(RegistryContent, self).__init__()
+        self.group_names = group_names
+        self.groups = dict([(g, []) for g in group_names])
+    def register(self, factory, contenttype, default_contenttype_params, display_name, group, ingroup_order, priority=RegistryBase.PRIORITY_MIDDLE):
         Register a factory
         :param factory: Factory to register. Callable, must return an object.
-        return self._register(self.Entry(factory, content_type, priority))
+        e = self.Entry(factory, contenttype, default_contenttype_params, display_name, ingroup_order, priority)
+        # If group is specified and contenttype is not a wildcard one
+        if group and contenttype.type and contenttype.subtype:
+            if group not in self.groups:
+                raise ValueError('Unknown group name: {0}'.format(group))
+            self.groups[group].append(e)
+            self.groups[group].sort(key=attrgetter('ingroup_order'))
+        return self._register(e)
-content_registry = RegistryContent()
+content_registry = RegistryContent([
 def register(cls):
-    content_registry.register(cls._factory, Type(cls.contenttype))
+    content_registry.register(cls._factory, Type(cls.contenttype), cls.default_contenttype_params, cls.display_name,, cls.ingroup_order)
     return cls
     Base for content classes defining some helpers, agnostic about content
+    # placeholder values for registry entry properties
+    contenttype = None
+    default_contenttype_params = {}
+    display_name = None
+    group = GROUP_OTHER
+    ingroup_order = 0
     def _factory(cls, *args, **kw):
         return cls(*args, **kw)
 class NonExistentContent(Content):
     """Dummy Content to use with NonExistent."""
     contenttype = 'application/x-nonexistent'
+    group = None
     def do_get(self, force_attachment=False, mimetype=None):
                          add_etags=True, etag=hash, conditional=True)
+class OctetStream(Binary):
+    """
+    Fallback Content for uploaded file of unknown contenttype.
+    """
+    contenttype = 'application/octet-stream'
+    display_name = 'binary file'
 class RenderableBinary(Binary):
     """ Base class for some binary stuff that renders with a object tag. """
     Tar items
     contenttype = 'application/x-tar'
+    display_name = 'TAR'
     Compressed tar items
     contenttype = 'application/x-gtar'
+    display_name = 'TGZ'
 class ZipMixin(object):
     Zip items
     contenttype = 'application/zip'
+    display_name = 'ZIP'
 class PDF(Application):
     """ PDF """
     contenttype = 'application/pdf'
+    display_name = 'PDF'
 class Video(Binary):
     """ Base class for video/* """
     contenttype = 'video/*'
+    group = GROUP_VIDEO
+class OGGVideo(Video):
+    contenttype = 'video/ogg'
+    display_name = 'OGG'
+class WebMVideo(Video):
+    contenttype = 'video/webm'
+    display_name = 'WebM'
+class MP4(Video):
+    contenttype = 'video/mp4'
+    display_name = 'MP4'
 class Audio(Binary):
     """ Base class for audio/* """
     contenttype = 'audio/*'
+    group = GROUP_AUDIO
+class WAV(Audio):
+    contenttype = 'audio/wave'
+    display_name = 'WAV'
+class OGGAudio(Audio):
+    contenttype = 'audio/ogg'
+    display_name = 'OGG'
+class MP3(Audio):
+    contenttype = 'audio/mpeg'
+    display_name = 'MP3'
+class WebMAudio(Audio):
+    contenttype = 'audio/webm'
+    display_name = 'WebM'
 class RenderableImage(RenderableBinary):
     """ Base class for renderable Image mimetypes """
+    group = GROUP_IMAGE
 class SvgImage(RenderableImage):
     """ SVG images use <object> tag mechanism from RenderableBinary base class """
     contenttype = 'image/svg+xml'
+    display_name = 'SVG'
 class RenderableBitmapImage(RenderableImage):
 class PNG(TransformableBitmapImage):
     """ PNG image. """
     contenttype = 'image/png'
+    display_name = 'PNG'
 class JPEG(TransformableBitmapImage):
     """ JPEG image. """
     contenttype = 'image/jpeg'
+    display_name = 'JPEG'
 class GIF(TransformableBitmapImage):
     """ GIF image. """
     contenttype = 'image/gif'
+    display_name = 'GIF'
 class Text(Binary):
     """ Base class for text/* """
     contenttype = 'text/*'
+    default_contenttype_params = dict(charset='utf-8')
+    group = GROUP_OTHER_TEXT
     class ModifyForm(Binary.ModifyForm):
         template = 'modify_text.html'
     some kind of item with markup
     (internal links and transcluded items)
+    group = GROUP_MARKUP_TEXT
 class MoinWiki(MarkupItem):
     """ MoinMoin wiki markup """
     contenttype = 'text/'
+    display_name = 'Wiki (MoinMoin)'
 class CreoleWiki(MarkupItem):
     """ Creole wiki markup """
     contenttype = 'text/x.moin.creole'
+    display_name = 'Wiki (Creole)'
 class MediaWiki(MarkupItem):
     """ MediaWiki markup """
     contenttype = 'text/x-mediawiki'
+    display_name = 'Wiki (MediaWiki)'
 class ReST(MarkupItem):
     """ ReStructured Text markup """
     contenttype = 'text/x-rst'
+    display_name = 'ReST'
 class Markdown(MarkupItem):
     """ Markdown markup """
     contenttype = 'text/x-markdown'
+    display_name = 'Markdown'
-class HTML(Text):
+class HTML(MarkupItem):
     HTML markup
     Note: If raw revision data is accessed, unsafe stuff might be present!
     contenttype = 'text/html'
+    display_name = 'HTML'
     class ModifyForm(Text.ModifyForm):
         template = "modify_text_html.html"
 class DocBook(MarkupItem):
     """ DocBook Document """
     contenttype = 'application/docbook+xml'
+    display_name = 'DocBook'
     def _convert(self, doc):
         from emeraldtree import ElementTree as ET
                          add_etags=False, etag=None, conditional=True)
+class PlainText(Text):
+    contenttype = 'text/plain'
+    display_name = 'plain text'
+class Diff(Text):
+    contenttype = 'text/x-diff'
+    display_name = 'diff/patch'
+class PythonCode(Text):
+    contenttype = 'text/x-python'
+    display_name = 'python code'
+class CSV(Text):
+    contenttype = 'text/csv'
+    display_name = 'csv'
+class IRCLog(Text):
+    contenttype = 'text/x-irclog'
+    display_name = 'IRC log'
 class Draw(TarMixin, Image):
     Base class for *Draw that use special Java/Javascript applets to modify and store data in a tar file.
+    group = GROUP_DRAWING
     class ModifyForm(Binary.ModifyForm):
         # Set the workaround flag respected in modify.html
         is_draw = True
     drawings by TWikiDraw applet. It creates three files which are stored as tar file.
     contenttype = 'application/x-twikidraw'
+    display_name = 'TDRAW'
     class ModifyForm(Draw.ModifyForm):
         template = "modify_twikidraw.html"
     drawings by AnyWikiDraw applet. It creates three files which are stored as tar file.
     contenttype = 'application/x-anywikidraw'
+    display_name = 'ADRAW'
     class ModifyForm(Draw.ModifyForm):
         template = "modify_anywikidraw.html"
 class SvgDraw(Draw):
     """ drawings by svg-edit. It creates two files (svg, png) which are stored as tar file. """
     contenttype = 'application/x-svgdraw'
+    display_name = 'SVGDRAW'
     class ModifyForm(Draw.ModifyForm):
         template = "modify_svg-edit.html"

File MoinMoin/templates/forms.html

 {% macro multi_select(field) %}
   {% set valid_values = field.member_schema.valid_values %}
   {% set labels ='labels', {}) %}
-  {% set descriptions ='helpers', {}) %}
+  {% set descriptions ='descriptions', {}) %}
   {% for value in valid_values %}
       {{ raw_input(field, 'checkbox', value=value) }}

File MoinMoin/templates/index.html

     <div class="moin-index-separator"></div>
-    {% if index %}
+    {% if files or dirs %}
     <div id="moin-initials">
         {% if not startswith %}
             <a class="selected" href="{{ url_for('frontend.index', item_name=item_name) }}">{{ _("All") }}</a>
             {% endif %}
         {% endfor %}
-    <div class="moin-item-index">
-        {% for e in index %}
-            {{ render_entry(e) }}
-        {% endfor %}
-    </div>
-    <div class="moin-clr"></div>
+    {% if dirs %}
+        <div class="moin-item-index">
+            <p>{{ _("These items have subitems that match your filter:") }}</p>
+            {% for e in dirs %}
+                {{ render_entry(e) }}
+            {% endfor %}
+        </div>
+        <div class="moin-clr"></div>
+        <hr />
+    {% endif %}
+    {% if files %}
+        <div class="moin-item-index">
+            {% for e in files %}
+                {{ render_entry(e) }}
+            {% endfor %}
+        </div>
+        <div class="moin-clr"></div>
+    {% endif %}
     {% endif %}
     <div id="popup">
         <div id="popup-for-action" class="popup-container">

File MoinMoin/templates/modify_select_contenttype.html

 {{ _("Please select the contenttype of the new %(itemtype)s item.", itemtype=itemtype) }}
 <table id="moin-create-table" class="zebra">
-    {% for gname, contenttypes in contenttype_groups %}
+    {% for group in group_names %}
-        <th>{{ gname }}</th>
+        <th>{{ group }}</th>
-        <td> |&nbsp
-        {% for ctname, ctlabel in contenttypes %}
-            <a href="{{ url_for('frontend.modify_item', item_name=item_name, itemtype=itemtype, contenttype=ctname) }}">{{ ctlabel }}</a> &nbsp|&nbsp
+        <td> |&nbsp;
+        {% for e in groups[group] %}
+            <a href="{{ url_for('frontend.modify_item', item_name=item_name, itemtype=itemtype, contenttype=Type(e.content_type, parameters=e.default_contenttype_params)|string) }}">{{ e.display_name }}</a> &nbsp;|&nbsp;
         {% endfor %}

File MoinMoin/themes/

 from MoinMoin.util.crypto import cache_key
 from MoinMoin.util.forms import make_generator
 from MoinMoin.util.clock import timed
+from MoinMoin.util.mime import Type
 def get_current_theme():
         from MoinMoin.items import Item
         item = Item.create(item_name)
-        item_index = item.get_index()
-        # Sort items by whether or not they have children, then by name:
-        item_index = sorted(item_index, key=attrgetter('hassubitems', 'relname'))
-        return item_index
+        return item.get_mixed_index()
     def userhome(self):
                             # _, gettext, ngettext
                             'isinstance': isinstance,
                             'list': list,
+                            'Type': Type,
                             # please note that flask-themes installs:
                             # theme, theme_static
                             'theme_supp': ThemeSupport(app.cfg),

File docs/admin/configure.rst

 Kinds of configuration files
 To change how moinmoin behaves and looks, you may customize it by editing
-its configuration files::
+its configuration files:
 * Wiki Engine Configuration
 You can either add some normal css stylesheet or add a choice of alternate
 * `CSS media types <>`_
 * `Alternate Stylesheets <>`_
     user_use_gravatar = True
-Please note that using the gravatar service has some privacy issues::
+Please note that using the gravatar service has some privacy issues:
 * to register your image for your email at, you need to give them
   your email address, which is the same as you use in your wiki user profile.
 javascript library or larger js libraries) and we definitely do not want to merge 
 them into our project.
-For MoinMoin we require the following XStatic Packages in
+For MoinMoin we require the following XStatic Packages in
 * `jquery <>`_
   for jquery lib functions loaded in the template file base.html
     from MoinMoin.auth import GivenAuth
     auth = [GivenAuth(autocreate=True, coding='utf-8')]
-Using this method has some pros and cons::
+Using this method has some pros and cons:
 * you can use lots of authentication extensions available for your web server
 * but the only information moin will get via REMOTE_USER is the authenticated
 and hard security. However, you may need different settings depending on the situation that the wiki
 admin, wiki owner or wiki community will have to deal with.
-So keep in mind::
+So keep in mind:
 * if your wiki is rather open, you might make it easy to contribute, e.g. a
   user who is not a regular user of your wiki could fix some typos he has just
    "before" -> "item acl from metadata (if specified)" -> "after";
    "before" -> "default (otherwise)"                   -> "after";
-How to use before/default/after::
+How to use before/default/after:
 * `before` is usually used to force something, for example if you want to give some
   wiki admin all permissions indiscriminately
 changes happening as a result of changes in the hierarchy, such as when you create,
 rename or delete items.
-Supported capabilities (rights)::
+Supported capabilities (rights):
 * read - read content
 * write - write (edit, modify) content
 ACLs - special groups
 In addition to the groups provided by the group backend(s), there are some
-special group names available within ACLs::
+special group names available within ACLs:
 * All - a virtual group containing every user
 * Known - a virtual group containing every logged-in user
 An ACL is a unicode string with one or more access control entries
 which are space separated.
-An entry is a colon-separated set of two values::
+An entry is a colon-separated set of two values:
 * the left side is a comma-separated list of user and/or group names
 * the right side is a comma-separated list of rights / capabilities for those users/groups.
 destroy is not listed on the right side of the "All" entry. If moin wants to know
 whether he may write, the answer will be "yes".
 * As a consequence of the left-to-right and first-match-counts processing,
   you must order ACL entries so that the more specific ones (like for
 the prefix is '-', the answer will be "no" and it will not even proceed and
 look at the third entry.
 * you usually use these modifiers if most of the rights for a given user shall be specified
   later, but a special user or group should be treated slightly different for
 A TextCHA is a pure text alternative to ''CAPTCHAs''. MoinMoin uses it to
 prevent wiki spamming and it has proven to be very effective.
 * when registering a user or saving an item, it can ask a random question
 * moin matches the given answer against a regular expression
 TextCha Configuration
-Tips for configuration::
+Tips for configuration:
 * have 1 word / 1 number answers
 * ask questions that normal users of your site are likely to be able to answer
-Moin uses secrets to encrypt or cryptographically sign something like::
+Moin uses secrets to encrypt or cryptographically sign something like:
 * textchas
 * tickets
 MoinMoin supports storage backends as different ways of storing wiki items.
-Setup of storage is rather complex and layered, involving::
+Setup of storage is rather complex and layered, involving:
 * a router middleware that dispatches parts of the namespace to the respective
-This is a helper function to make storage setup easier. It helps you to::
+This is a helper function to make storage setup easier. It helps you to:
 * create a simple setup that uses 3 storage backends internally for these
   parts of the namespace:
 the respective backend. `%(kind)s` will be replaced by 'meta' or 'data'
-In this case, the mapping created will look like this::
+In this case, the mapping created will look like this:
 | Namespace part | Filesystem path for storage |
 protecting middleware
 * protects access to lower storage layers by ACLs (Access Control Lists)
 * makes sure there won't be ACL security issues, even if upper layers have bugs
 routing middleware
 * dispatches storage access to different backends depending on the item name
 * in POSIX terms, it is something like fstab/mount
 indexing middleware
 * maintains an index for important metadata values
 * speeds up looking up / selecting items
 fs store
 * stores into the filesystem
 * store metadata and data into separate files/directories
 sqla store
 * stores data into an (SQL) database / table
 * can either use 1 database per store or 1 table per store and you need to
   give different table names then
 * uses slqalchemy (without the ORM) for database abstraction
-* supports multiple types of databases, for example::
+* supports multiple types of databases, for example:
   - sqlite (default, comes built-into Python)
   - postgresql
 sqlite store
 * directly talks to sqlite, without using sqlalchemy
 * stores data into an sqlite database, which is a single file
 kc store
 * uses a Kyoto Cabinet file for storage
 * very fast
 kt store
 * uses a Kyoto Tycoon server for storage
 * fast
 memory store
 * keeps everything in RAM
 * if your system or the moin process crashes, all data is lost, so definitely not for production use
 fileserver backend
 * exposes a part of the filesystem as read-only wiki items
 Sending E-Mail
-Moin can optionally send E-Mail. Possible uses::
+Moin can optionally send E-Mail. Possible uses:
 * send out item change notifications.
 * enable users to reset forgotten passwords

File docs/admin/index.rst

     index_storage = 'FileStorage', ("/path/to/moin-2.0/wiki/index", ), {}
 * The path MUST be absolute, writable and should be on a fast, local filesystem.
 * Moin will use `index.temp` directory as well, if you build an index at
-the `temporary location`.
+  the `temporary location`.
 Process all revisions of the wiki and add the indexable documents to the index.
 * For big wikis, this can take rather long; consider using --tmp.
 * index-build does NOT clear the index at the beginning.
 * index-build does not check the current contents of the index. Therefore you must not run

File docs/admin/install.rst

  # confirm the problems by running:
  pip install -e .
-Now install each package into your virtual env manually::
+Now install each package into your virtual env manually:
 * Find the required packages by looking at "install_requires" within ``.
 * Download each required package from

File docs/devel/development.rst

 We mainly use IRC and the wiki for communication, documentation and planning.
-IRC channels on
+IRC channels on
 * #moin-dev (core development topics)
 * #moin (user support, extensions)
-Issue tracker::
+Issue tracker:
-Code Repositories::
+Code Repositories:
 * - main repository
 * - bitbucket mirror for your
 If you are not using Mercurial, you can still submit patches.
 In that case, open an issue in the issue tracker and attach the patch there.
-Code review::
+Code review:
 Please use for getting feedback on moin-related
 code, especially if you want to contribute or publish that code.
 MoinMoin architecture
-moin2 is a WSGI application and uses::
+moin2 is a WSGI application and uses:
 * flask as framework
 WSGI application creation
-First, the moin Flask application is created; see ``::
+First, the moin Flask application is created; see ``:
 * load the configuration (app.cfg)
 * register some modules that handle different parts of the functionality
 Request processing
-Let's look at how it shows a wiki item::
+Let's look at how it shows a wiki item:
 * the Flask app receives a GET request for /WikiItem
 * Flask's routing rules determine that this request should be served by
-* Flask calls the before request handler of this module, which::
+* Flask calls the before request handler of this module, which:
   - sets up the user as flaskg.user - an anonymous user or logged in user
   - initializes dicts/groups as flaskg.dicts, flaskg.groups
   - initializes jinja2 environment - templating
 * Flask then calls the handler function `MoinMoin.apps.frontend.show_item`,
-  which::
+  which:
   - creates an in-memory Item
 A backend is one layer above. It deals with objects that have metadata and
 data, see ``.
-Above that, there is miscellaneous functionality in `` for::
+Above that, there is miscellaneous functionality in `` for:
 * routing by name to some specific backend, like fstab / mount
 * indexing metadata and data + comfortable and fast index-based access,
 converter. The converter parses this input and creates an in-memory `dom tree`
 representation from it.
-This dom tree is then transformed through multiple dom-to-dom converters for example::
+This dom tree is then transformed through multiple dom-to-dom converters for example:
 * link processing
 * include processing

File docs/devel/support.rst

 Free Support
 You can get free support and information here:
 * on our chat channels, see
 * on our wiki, see - please note that quite a lot of content
   there is about moin 1.x and does not apply to moin2. One page has a lot

File docs/devel/translate.rst

 First steps with a new ``*.po`` file
-A newly created translation needs a few initial preparations::
+A newly created translation needs a few initial preparations:
 * replace "``PROJECT``" with "``MoinMoin 2``"

File docs/intro/features.rst

   - global, using a mapping, so you can apply ACLs on parts of the namespace
   - local, per wiki item
-  - give rights, such as::
+  - give rights, such as:
     + create, destroy
     + read, write, rename

File docs/user/accounts.rst

  If you have an OpenID which you would like to associate with your account, enter it here.
-.. warning::
- **MOINTODO** Leaving the OpenID field blank gives a warning saying "This OpenID is already in use"
  Setting this value will allow you to see edit times as they would appear in your time zone. For
  example, an edit time of 10AM UTC would appear as 8PM AEST if you changed your time zone to 
 Logging out
-.. warning::
- **MOINTODO** Currently logging out just removes the user's session cookie. These cookies remain
- valid after logging out (and even after a password change), and could be used to impersonate a
- user. See BitBucket issue #94.
 Logging out of your account can prevent account hijacking on untrusted or insecure computers, and is
 considered best practice for security. To log out, click the :guilabel:`Logout` button at the top
 of the page. You will be redirected to a page confirming that you have logged out successfully.

File docs/user/search.rst

  * Because it is only using an index, it can only find what was put there
  * If you use wildcards or regexes, it will still use the index, but in a different, slower way
-For example::
+For example:
  * "foobar" is put into the index somehow
  * you search for "ooba" - you will not find it, because only "foobar" was put into the index