Merge branch 't34_valid_html_css' into 42cc
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index e90d5da..3162dad 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -288,12 +288,14 @@
         subscriptions = []
         mailboxes = M.Mailbox.query.find(dict(user_id=c.user._id, is_flash=False))
         mailboxes = list(mailboxes.ming_cursor)
+        project_collection = M.Project.query.mapper.collection
+        app_collection = M.AppConfig.query.mapper.collection
         projects = dict(
-            (p._id, p) for p in M.Project.query.find(dict(
-                    _id={'$in': [mb.project_id for mb in mailboxes ]})).ming_cursor)
+            (p._id, p) for p in project_collection.m.find(dict(
+                    _id={'$in': [mb.project_id for mb in mailboxes ]})))
         app_index = dict(
-            (ac._id, ac) for ac in M.AppConfig.query.find(dict(
-                    _id={'$in': [ mb.app_config_id for mb in mailboxes ] })).ming_cursor)
+            (ac._id, ac) for ac in app_collection.m.find(dict(
+                    _id={'$in': [mb.project_id for mb in mailboxes]})))
 
         for mb in mailboxes:
             project = projects.get(mb.project_id, None)
diff --git a/Allura/allura/controllers/project.py b/Allura/allura/controllers/project.py
index 416c863..21cf785 100644
--- a/Allura/allura/controllers/project.py
+++ b/Allura/allura/controllers/project.py
@@ -174,7 +174,7 @@
         if tools and not neighborhood.project_template:
             for i, tool in enumerate(tools):
                 c.project.install_app(tool, ordinal=i+offset)
-        flash('Welcome to the SourceForge Beta System! '
+        flash('Welcome to the SourceForge Project System! '
               'To get started, fill out some information about your project.')
         redirect(c.project.script_name + 'admin/overview')
 
diff --git a/Allura/allura/ext/admin/templates/project_admin.html b/Allura/allura/ext/admin/templates/project_admin.html
index ee0f629..a193588 100644
--- a/Allura/allura/ext/admin/templates/project_admin.html
+++ b/Allura/allura/ext/admin/templates/project_admin.html
@@ -10,7 +10,7 @@
 {% block header %}Project Admin{% endblock %}
 
 {% block content %}
-  <p>SourceForge beta projects come with a number of Tools, which can be configured and adjusted to your needs.</p>
+  <p>SourceForge projects come with a number of Tools, which can be configured and adjusted to your needs.</p>
 
   <div class="grid-2">
     <img src="{{g.forge_static('images/project_default.png')}}" alt="">
diff --git a/Allura/allura/lib/custom_middleware.py b/Allura/allura/lib/custom_middleware.py
index 31fba5d..a323298 100644
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -169,15 +169,15 @@
 
     def instrument_pymongo(self):
         import pymongo.collection
-        import ming.orm
+        import ming.odm
         timing('mongo').decorate(pymongo.collection.Collection,
                                  'count find find_one')
         timing('mongo').decorate(pymongo.cursor.Cursor,
                                  'count distinct explain hint limit next rewind'
                                  ' skip sort where')
-        timing('ming').decorate(ming.orm.ormsession.ORMSession,
+        timing('ming').decorate(ming.odm.odmsession.ODMSession,
                                 'flush find get')
-        timing('ming').decorate(ming.orm.ormsession.ORMCursor,
+        timing('ming').decorate(ming.odm.odmsession.ODMCursor,
                                 'next')
 
     def instrument_template(self):
diff --git a/Allura/allura/lib/gravatar.py b/Allura/allura/lib/gravatar.py
index fdc2e71..ba313d0 100644
--- a/Allura/allura/lib/gravatar.py
+++ b/Allura/allura/lib/gravatar.py
@@ -14,7 +14,7 @@
     match = _wrapped_email.match(email)
     if match:
         email = match.group(1)
-    return hashlib.md5(email.strip().lower()).hexdigest()
+    return hashlib.md5(email.strip().lower().encode('utf8')).hexdigest()
 
 def url(email=None, gravatar_id=None, **kw):
     """Build a complete gravatar URL with our favorite defaults.
diff --git a/Allura/allura/lib/htmltruncate.py b/Allura/allura/lib/htmltruncate.py
deleted file mode 100644
index e0257f0..0000000
--- a/Allura/allura/lib/htmltruncate.py
+++ /dev/null
@@ -1,134 +0,0 @@
-#!/usr/bin/env python
-
-# Code pulled from https://github.com/eentzel/htmltruncate.py
-
-import sys
-
-
-END = -1
-
-class UnbalancedError(Exception):
-    pass
-
-class OpenTag:
-    def __init__(self, tag, rest=''):
-        self.tag = tag
-        self.rest = rest
-
-    def as_string(self):
-        return '<' + self.tag + self.rest + '>'
-        
-class CloseTag(OpenTag):
-    def as_string(self):
-        return '</' + self.tag + '>'
-
-class SelfClosingTag(OpenTag):
-    pass
-    
-class Tokenizer:
-    def __init__(self, input):
-        self.input = input
-        self.counter = 0  # points at the next unconsumed character of the input
-
-    def __next_char(self):
-        self.counter += 1
-        return self.input[self.counter]
-        
-    def next_token(self):
-        try:
-            char = self.input[self.counter]
-            self.counter += 1
-            if char == '&':
-                return self.__entity()
-            elif char != '<':
-                return char
-            elif self.input[self.counter] == '/':
-                self.counter += 1
-                return self.__close_tag()
-            else:
-                return self.__open_tag()
-        except IndexError:
-            return END
-
-    def __entity(self):
-        """Return a token representing an HTML character entity.
-        Precondition: self.counter points at the charcter after the &
-        Postcondition: self.counter points at the character after the ;
-        """
-        char = self.input[self.counter]
-        entity = ['&']
-        while char != ';':
-            entity.append(char)
-            char = self.__next_char()
-        entity.append(';')
-        self.counter += 1
-        return ''.join(entity)
-        
-    def __open_tag(self):
-        """Return an open/close tag token.
-        Precondition: self.counter points at the first character of the tag name
-        Postcondition: self.counter points at the character after the <tag>
-        """
-        char = self.input[self.counter]
-        tag = []
-        rest = []
-        while char != '>' and char != ' ':
-            tag.append(char)
-            char = self.__next_char()
-        while char != '>':
-            rest.append(char)
-            char = self.__next_char()
-        if self.input[self.counter - 1] == '/':
-            self.counter += 1
-            return SelfClosingTag( ''.join(tag), ''.join(rest) )
-        else:
-            self.counter += 1
-            return OpenTag( ''.join(tag), ''.join(rest) )
-
-    def __close_tag(self):
-        """Return an open/close tag token.
-        Precondition: self.counter points at the first character of the tag name
-        Postcondition: self.counter points at the character after the <tag>
-        """
-        char = self.input[self.counter]
-        tag = []
-        while char != '>':
-            tag.append(char)
-            char = self.__next_char()
-        self.counter += 1
-        return CloseTag( ''.join(tag) )
-
-def truncate(str, target_len, ellipsis = ''):
-    """Returns a copy of str truncated to target_len characters,
-    preserving HTML markup (which does not count towards the length).
-    Any tags that would be left open by truncation will be closed at
-    the end of the returned string.  Optionally append ellipsis if
-    the string was truncated."""
-    stack = []   # open tags are pushed on here, then popped when the matching close tag is found
-    retval = []  # string to be returned
-    length = 0   # number of characters (not counting markup) placed in retval so far
-    tokens = Tokenizer(str)
-    tok = tokens.next_token()
-    while tok != END and length < target_len:
-        if tok.__class__.__name__ == 'OpenTag':
-            stack.append(tok)
-            retval.append( tok.as_string() )
-        elif tok.__class__.__name__ == 'CloseTag':
-            if stack[-1].tag == tok.tag:
-                stack.pop()
-                retval.append( tok.as_string() )
-            else:
-                raise UnbalancedError( tok.as_string() )
-        elif tok.__class__.__name__ == 'SelfClosingTag':
-            retval.append( tok.as_string() )
-        else:
-            retval.append(tok)
-            length += 1
-        tok = tokens.next_token()
-    while len(stack) > 0:
-        tok = CloseTag( stack.pop().tag )
-        retval.append( tok.as_string() )
-    if length == target_len:
-        return ''.join(retval) + ellipsis
-    else:
-        return ''.join(retval)
\ No newline at end of file
diff --git a/Allura/allura/lib/macro.py b/Allura/allura/lib/macro.py
index d90a539..04999c2 100644
--- a/Allura/allura/lib/macro.py
+++ b/Allura/allura/lib/macro.py
@@ -134,7 +134,8 @@
 
 @macro('neighborhood-wiki')
 def projects(category=None, display_mode='grid', sort='last_updated',
-        show_total=False, limit=100, labels='', award='', private=False):
+        show_total=False, limit=100, labels='', award='', private=False,
+        columns=2, show_proj_icon='on', show_download_button='on'):
     from allura.lib.widgets.project_list import ProjectList
     from allura.lib import utils
     from allura import model as M
@@ -164,6 +165,10 @@
         sort_key, sort_dir = 'name', pymongo.ASCENDING
     elif sort == 'random':
         sort_key, sort_dir = None, None
+    elif sort == 'last_registred':
+        sort_key, sort_dir = '_id', pymongo.DESCENDING
+    elif sort == '_id':
+        sort_key, sort_dir = '_id', pymongo.DESCENDING
 
     projects = []
     if private:
@@ -202,7 +207,9 @@
 
     pl = ProjectList()
     g.resource_manager.register(pl)
-    response = pl.display(projects=projects, display_mode=display_mode)
+    response = pl.display(projects=projects, display_mode=display_mode,
+                          columns=columns, show_proj_icon=show_proj_icon,
+                          show_download_button=show_download_button)
     if show_total:
         if total is None:
             total = 0
diff --git a/Allura/allura/lib/ordereddict.py b/Allura/allura/lib/ordereddict.py
deleted file mode 100644
index b024772..0000000
--- a/Allura/allura/lib/ordereddict.py
+++ /dev/null
@@ -1,259 +0,0 @@
-# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
-# Passes Python2.7's test suite and incorporates all the latest updates.
-# From an ActiveState recipe: <http://code.activestate.com/recipes/576693/>
-
-try:
-    from thread import get_ident as _get_ident
-except ImportError:
-    from dummy_thread import get_ident as _get_ident
-
-try:
-    from _abcoll import KeysView, ValuesView, ItemsView
-except ImportError:
-    pass
-
-
-class OrderedDict(dict):
-    'Dictionary that remembers insertion order'
-    # An inherited dict maps keys to values.
-    # The inherited dict provides __getitem__, __len__, __contains__, and get.
-    # The remaining methods are order-aware.
-    # Big-O running times for all methods are the same as for regular dictionaries.
-
-    # The internal self.__map dictionary maps keys to links in a doubly linked list.
-    # The circular doubly linked list starts and ends with a sentinel element.
-    # The sentinel element never gets deleted (this simplifies the algorithm).
-    # Each link is stored as a list of length three:  [PREV, NEXT, KEY].
-
-    def __init__(self, *args, **kwds):
-        '''Initialize an ordered dictionary.  Signature is the same as for
-        regular dictionaries, but keyword arguments are not recommended
-        because their insertion order is arbitrary.
-
-        '''
-        if len(args) > 1:
-            raise TypeError('expected at most 1 arguments, got %d' % len(args))
-        try:
-            self.__root
-        except AttributeError:
-            self.__root = root = []                     # sentinel node
-            root[:] = [root, root, None]
-            self.__map = {}
-        self.__update(*args, **kwds)
-
-    def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
-        'od.__setitem__(i, y) <==> od[i]=y'
-        # Setting a new item creates a new link which goes at the end of the linked
-        # list, and the inherited dictionary is updated with the new key/value pair.
-        if key not in self:
-            root = self.__root
-            last = root[0]
-            last[1] = root[0] = self.__map[key] = [last, root, key]
-        dict_setitem(self, key, value)
-
-    def __delitem__(self, key, dict_delitem=dict.__delitem__):
-        'od.__delitem__(y) <==> del od[y]'
-        # Deleting an existing item uses self.__map to find the link which is
-        # then removed by updating the links in the predecessor and successor nodes.
-        dict_delitem(self, key)
-        link_prev, link_next, key = self.__map.pop(key)
-        link_prev[1] = link_next
-        link_next[0] = link_prev
-
-    def __iter__(self):
-        'od.__iter__() <==> iter(od)'
-        root = self.__root
-        curr = root[1]
-        while curr is not root:
-            yield curr[2]
-            curr = curr[1]
-
-    def __reversed__(self):
-        'od.__reversed__() <==> reversed(od)'
-        root = self.__root
-        curr = root[0]
-        while curr is not root:
-            yield curr[2]
-            curr = curr[0]
-
-    def clear(self):
-        'od.clear() -> None.  Remove all items from od.'
-        try:
-            for node in self.__map.itervalues():
-                del node[:]
-            root = self.__root
-            root[:] = [root, root, None]
-            self.__map.clear()
-        except AttributeError:
-            pass
-        dict.clear(self)
-
-    def popitem(self, last=True):
-        '''od.popitem() -> (k, v), return and remove a (key, value) pair.
-        Pairs are returned in LIFO order if last is true or FIFO order if false.
-
-        '''
-        if not self:
-            raise KeyError('dictionary is empty')
-        root = self.__root
-        if last:
-            link = root[0]
-            link_prev = link[0]
-            link_prev[1] = root
-            root[0] = link_prev
-        else:
-            link = root[1]
-            link_next = link[1]
-            root[1] = link_next
-            link_next[0] = root
-        key = link[2]
-        del self.__map[key]
-        value = dict.pop(self, key)
-        return key, value
-
-    # -- the following methods do not depend on the internal structure --
-
-    def keys(self):
-        'od.keys() -> list of keys in od'
-        return list(self)
-
-    def values(self):
-        'od.values() -> list of values in od'
-        return [self[key] for key in self]
-
-    def items(self):
-        'od.items() -> list of (key, value) pairs in od'
-        return [(key, self[key]) for key in self]
-
-    def iterkeys(self):
-        'od.iterkeys() -> an iterator over the keys in od'
-        return iter(self)
-
-    def itervalues(self):
-        'od.itervalues -> an iterator over the values in od'
-        for k in self:
-            yield self[k]
-
-    def iteritems(self):
-        'od.iteritems -> an iterator over the (key, value) items in od'
-        for k in self:
-            yield (k, self[k])
-
-    def update(*args, **kwds):
-        '''od.update(E, **F) -> None.  Update od from dict/iterable E and F.
-
-        If E is a dict instance, does:           for k in E: od[k] = E[k]
-        If E has a .keys() method, does:         for k in E.keys(): od[k] = E[k]
-        Or if E is an iterable of items, does:   for k, v in E: od[k] = v
-        In either case, this is followed by:     for k, v in F.items(): od[k] = v
-
-        '''
-        if len(args) > 2:
-            raise TypeError('update() takes at most 2 positional '
-                            'arguments (%d given)' % (len(args),))
-        elif not args:
-            raise TypeError('update() takes at least 1 argument (0 given)')
-        self = args[0]
-        # Make progressively weaker assumptions about "other"
-        other = ()
-        if len(args) == 2:
-            other = args[1]
-        if isinstance(other, dict):
-            for key in other:
-                self[key] = other[key]
-        elif hasattr(other, 'keys'):
-            for key in other.keys():
-                self[key] = other[key]
-        else:
-            for key, value in other:
-                self[key] = value
-        for key, value in kwds.items():
-            self[key] = value
-
-    __update = update  # let subclasses override update without breaking __init__
-
-    __marker = object()
-
-    def pop(self, key, default=__marker):
-        '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
-        If key is not found, d is returned if given, otherwise KeyError is raised.
-
-        '''
-        if key in self:
-            result = self[key]
-            del self[key]
-            return result
-        if default is self.__marker:
-            raise KeyError(key)
-        return default
-
-    def setdefault(self, key, default=None):
-        'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
-        if key in self:
-            return self[key]
-        self[key] = default
-        return default
-
-    def __repr__(self, _repr_running={}):
-        'od.__repr__() <==> repr(od)'
-        call_key = id(self), _get_ident()
-        if call_key in _repr_running:
-            return '...'
-        _repr_running[call_key] = 1
-        try:
-            if not self:
-                return '%s()' % (self.__class__.__name__,)
-            return '%s(%r)' % (self.__class__.__name__, self.items())
-        finally:
-            del _repr_running[call_key]
-
-    def __reduce__(self):
-        'Return state information for pickling'
-        items = [[k, self[k]] for k in self]
-        inst_dict = vars(self).copy()
-        for k in vars(OrderedDict()):
-            inst_dict.pop(k, None)
-        if inst_dict:
-            return (self.__class__, (items,), inst_dict)
-        return self.__class__, (items,)
-
-    def copy(self):
-        'od.copy() -> a shallow copy of od'
-        return self.__class__(self)
-
-    @classmethod
-    def fromkeys(cls, iterable, value=None):
-        '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
-        and values equal to v (which defaults to None).
-
-        '''
-        d = cls()
-        for key in iterable:
-            d[key] = value
-        return d
-
-    def __eq__(self, other):
-        '''od.__eq__(y) <==> od==y.  Comparison to another OD is order-sensitive
-        while comparison to a regular mapping is order-insensitive.
-
-        '''
-        if isinstance(other, OrderedDict):
-            return len(self)==len(other) and self.items() == other.items()
-        return dict.__eq__(self, other)
-
-    def __ne__(self, other):
-        return not self == other
-
-    # -- the following methods are only used in Python 2.7 --
-
-    def viewkeys(self):
-        "od.viewkeys() -> a set-like object providing a view on od's keys"
-        return KeysView(self)
-
-    def viewvalues(self):
-        "od.viewvalues() -> an object providing a view on od's values"
-        return ValuesView(self)
-
-    def viewitems(self):
-        "od.viewitems() -> a set-like object providing a view on od's items"
-        return ItemsView(self)
diff --git a/Allura/allura/lib/security.py b/Allura/allura/lib/security.py
index fa8e5db..2f16b32 100644
--- a/Allura/allura/lib/security.py
+++ b/Allura/allura/lib/security.py
@@ -193,9 +193,10 @@
                 if rid in visited: continue
                 yield role
                 pr_index = self.cred.project_roles(role.project_id).index
-                for i in pr_index[rid].roles:
-                    if i in pr_index:
-                        to_visit.append((i, pr_index[i]))
+                if rid in pr_index:
+                    for i in pr_index[rid].roles:
+                        if i in pr_index:
+                            to_visit.append((i, pr_index[i]))
         return RoleCache(self.cred, _iter())
 
     @LazyProperty
diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py
index 50bca63..2cb279d 100644
--- a/Allura/allura/lib/widgets/forms.py
+++ b/Allura/allura/lib/widgets/forms.py
@@ -153,20 +153,22 @@
             display = '<table class="table_class">'
             ctx = self.context_for(field)
             for inp in self.color_inputs:
+                additional_inputs = inp.get('additional', '')
                 empty_val = False
                 if inp['value'] is None or inp['value'] == '':
                     empty_val = True
-                display += '<tr><td class="left"><label>%(label)s<label/></td>'\
-                           '<td><input type="checkbox" name="%(ctx_name)s-%(inp_name)s-def" %(def_checked)s/>default</td>'\
-                           '<td class="right"><span class="%(ctx_name)s-%(inp_name)s-inp"><input type="text" class="%(inp_type)s" name="%(ctx_name)s-%(inp_name)s" '\
-                           'rendered_name="%(ctx_name)s-%(inp_name)s" '\
-                           'value="%(inp_value)s"/></span></td></tr>\n' % {'ctx_id': ctx['id'],
+                display += '<tr><td class="left"><label>%(label)s</label></td>'\
+                           '<td><input type="checkbox" name="%(ctx_name)s-%(inp_name)s-def" %(def_checked)s>default</td>'\
+                           '<td class="right"><div class="%(ctx_name)s-%(inp_name)s-inp"><table class="input_inner">'\
+                           '<tr><td><input type="text" class="%(inp_type)s" name="%(ctx_name)s-%(inp_name)s" '\
+                           'value="%(inp_value)s"></td><td>%(inp_additional)s</td></tr></table></div></td></tr>\n' % {'ctx_id': ctx['id'],
                                                             'ctx_name': ctx['name'],
                                                             'inp_name': inp['name'],
                                                             'inp_value': inp['value'],
                                                             'label': inp['label'],
                                                             'inp_type': inp['type'],
-                                                            'def_checked': 'checked="checked"' if empty_val else ''}
+                                                            'def_checked': 'checked="checked"' if empty_val else '',
+                                                            'inp_additional': additional_inputs}
             display += '</table>'
 
             if ctx['errors'] and field.show_errors and not ignore_errors:
@@ -194,7 +196,7 @@
         yield ew.CSSLink('css/colorPicker.css')
         yield ew.CSSLink('css/jqfontselector.css')
         yield ew.CSSScript('''
-table.table_class{
+table.table_class, table.input_inner{
   margin: 0;
   padding: 0;
   width: 99%;
@@ -203,6 +205,7 @@
 table.table_class .left{ text-align: left; }
 table.table_class .right{ text-align: right; width: 50%;}
 table.table_class tbody tr td { border: none; }
+table.table_class select.add_opt {width: 5em; margin:0; padding: 0;}
         ''')
         yield ew.JSLink('js/jquery.colorPicker.js')
         yield ew.JSLink('js/jqfontselector.js')
@@ -211,7 +214,7 @@
               $('.table_class').find('input[type="checkbox"]').each(function(index, element) {
                 var cb_name = $(this).attr('name');
                 var inp_name = cb_name.substr(0, cb_name.length-4);
-                var inp_el = $('span[class="'+inp_name+'-inp"]');
+                var inp_el = $('div[class="'+inp_name+'-inp"]');
 
                 if ($(this).attr('checked')) {
                   inp_el.hide();
diff --git a/Allura/allura/lib/widgets/project_list.py b/Allura/allura/lib/widgets/project_list.py
index dd3ca53..34efa6a 100644
--- a/Allura/allura/lib/widgets/project_list.py
+++ b/Allura/allura/lib/widgets/project_list.py
@@ -14,7 +14,10 @@
         icon=None,
         value=None,
         icon_url=None,
-        accolades=None)
+        accolades=None,
+        columns=3,
+        show_proj_icon='on',
+        show_download_button='on')
 
     def prepare_context(self, context):
         response = super(ProjectSummary, self).prepare_context(context)
diff --git a/Allura/allura/model/neighborhood.py b/Allura/allura/model/neighborhood.py
index f0888f6..b20095c 100644
--- a/Allura/allura/model/neighborhood.py
+++ b/Allura/allura/model/neighborhood.py
@@ -36,6 +36,7 @@
 re_bgcolor_barontop = re.compile('background\-color:([^;}]+);')
 re_bgcolor_titlebar = re.compile('background\-color:([^;}]+);')
 re_color_titlebar = re.compile('color:([^;}]+);')
+re_icon_theme = re.compile('neo-icon-set-(ffffff|454545)-256x350.png')
 
 class Neighborhood(MappedClass):
     '''Provide a grouping of related projects.
@@ -135,7 +136,14 @@
         projecttitlecolor = {'label': 'Project title, color', 'name': 'projecttitlecolor', 'value':'', 'type': 'color'}
         barontop = {'label': 'Bar on top', 'name': 'barontop', 'value': '', 'type': 'color'}
         titlebarbackground = {'label': 'Title bar, background', 'name': 'titlebarbackground', 'value': '', 'type': 'color'}
-        titlebarcolor = {'label': 'Title bar, foreground', 'name': 'titlebarcolor', 'value': '', 'type': 'color'}
+        titlebarcolor = {'label': 'Title bar, foreground', 'name': 'titlebarcolor', 'value': '', 'type': 'color', 
+                         'additional': """<label>Icons theme:</label> <select name="css-addopt-icon-theme" class="add_opt">
+                        <option value="default">default</option>
+                        <option value="dark"%(titlebarcolor_dark)s>dark</option>
+                        <option value="white"%(titlebarcolor_white)s>white</option>
+                      </select>"""}
+        titlebarcolor_dark = ''
+        titlebarcolor_white = ''
 
         if self.css is not None:
             for css_line in self.css.split('\n'):
@@ -168,7 +176,16 @@
                     m = re_color_titlebar.search(css_line)
                     if m:
                         titlebarcolor['value'] = m.group(1)
+                        m = re_icon_theme.search(css_line)
+                        if m:
+                            icon_theme = m.group(1)
+                            if icon_theme == "ffffff":
+                                titlebarcolor_dark = ' selected="selected"'
+                            elif icon_theme == "454545":
+                                titlebarcolor_white = ' selected="selected"'
 
+        titlebarcolor['additional'] = titlebarcolor['additional'] % {'titlebarcolor_dark': titlebarcolor_dark,
+                                                                     'titlebarcolor_white': titlebarcolor_white}
 
         styles_list = []
         styles_list.append(projecttitlefont)
@@ -201,7 +218,18 @@
                        {'bgcolor': css_form_dict['titlebarbackground']}
 
         if 'titlebarcolor' in css_form_dict and css_form_dict['titlebarcolor'] != '':
-           css_text += "/*titlebarcolor*/.pad h2.title{color:%s;}\n" % (css_form_dict['titlebarcolor'])
+           icon_theme = ''
+           if 'addopt-icon-theme' in css_form_dict:
+               if css_form_dict['addopt-icon-theme'] == "dark":
+                  icon_theme = ".pad h2.dark small b.ico {background-image: url('%s%s');}" % (
+                               pylons.g.theme_href(''),
+                               'images/neo-icon-set-ffffff-256x350.png')
+               elif css_form_dict['addopt-icon-theme'] == "white":
+                  icon_theme = ".pad h2.dark small b.ico {background-image: url('%s%s');}" % (
+                               pylons.g.theme_href(''),
+                               'images/neo-icon-set-454545-256x350.png')
+
+           css_text += "/*titlebarcolor*/.pad h2.title{color:%s;} %s\n" % (css_form_dict['titlebarcolor'], icon_theme)
 
         return css_text
 
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index 0ba72e9..12d8be0 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -91,7 +91,7 @@
         return result
 
 class ProjectMapperExtension(MapperExtension):
-    def after_insert(self, obj, st):
+    def after_insert(self, obj, st, sess):
         g.zarkov_event('project_create', project=obj)
 
 class Project(MappedClass):
@@ -406,7 +406,6 @@
         entries = sorted(entries, key=lambda e: e['ordinal'])
         for e in entries:
             sitemap.children.append(e['entry'])
-            log.info("ENTRY: %s %s" % (e['entry'].label, e['ordinal']))
         return sitemap.children
 
     def parent_iter(self):
diff --git a/Allura/allura/nf/allura/css/site_style.css b/Allura/allura/nf/allura/css/site_style.css
index dee2646..3e08a4f 100644
--- a/Allura/allura/nf/allura/css/site_style.css
+++ b/Allura/allura/nf/allura/css/site_style.css
@@ -1900,7 +1900,7 @@
   border: 1px solid #333333;
   color: #fff;
   font-size: 14px;
-  text-shadow: #333333 0 -1px 0;
+  text-shadow: rgba(0,0,0,0.1) 0 -1px 0;
   padding: 5px 10px;
   z-index: 20;
   border: none;
@@ -2266,13 +2266,13 @@
   box-shadow: rgba(0, 0, 0, 0.4) 0 1px 5px 0;
   background-color: #f5f5f5;
   border: 1px solid #cccccc;
-  width: 220px;
   display: inline;
   float: left;
   overflow: hidden;
   *zoom: 1;
   margin: 0 10px;
   height: 250px;
+  width: 220px;
   overflow: hidden;
   -moz-box-shadow: #666666 0 2px 4px 0;
   -webkit-box-shadow: #666666 0 2px 4px 0;
@@ -2283,6 +2283,7 @@
   background-image: -moz-linear-gradient(100% 100% 90deg, #e5e5e5, white, white 25%);
   margin: 0 0 10px 10px;
 }
+
 .card .box {
   border: none;
   -moz-border-radius: 8px;
diff --git a/Allura/allura/templates/jinja_master/lib.html b/Allura/allura/templates/jinja_master/lib.html
index f13a8da..b277f2d 100644
--- a/Allura/allura/templates/jinja_master/lib.html
+++ b/Allura/allura/templates/jinja_master/lib.html
@@ -113,7 +113,7 @@
 <h1>Markdown Syntax Guide</h1>
 
 <div class="markdown_syntax_section md_ex_toc{{id}}">
-<p>The Allura code uses markdown syntax everywhere to allow you to create rich<br>text markup, and extends markdown in several ways to allow for quick linking<br>to other artifacts in your project. </p>
+<p>SourceForge uses markdown syntax everywhere to allow you to create rich<br>text markup, and extends markdown in several ways to allow for quick linking<br>to other artifacts in your project. </p>
 <p>Markdown was created to be easy to read, easy to write, and still readable in plain text format.</p>
 
 <ul class="markdown_syntax_toc">
diff --git a/Allura/allura/templates/jinja_master/master.html b/Allura/allura/templates/jinja_master/master.html
index 944011d..f5016af 100644
--- a/Allura/allura/templates/jinja_master/master.html
+++ b/Allura/allura/templates/jinja_master/master.html
@@ -36,7 +36,7 @@
 
     {% if c.project and c.project.neighborhood.css %}
       <style type="text/css">
-        {{c.project.neighborhood.get_custom_css()}} 
+        {{c.project.neighborhood.get_custom_css()|safe}}
       </style>
     {% elif neighborhood and neighborhood.css %}
       <style type="text/css">
diff --git a/Allura/allura/templates/jinja_master/top_nav.html b/Allura/allura/templates/jinja_master/top_nav.html
index 65d9e83..dfa288a 100644
--- a/Allura/allura/templates/jinja_master/top_nav.html
+++ b/Allura/allura/templates/jinja_master/top_nav.html
@@ -8,7 +8,7 @@
           <span class="diamond"></span>
         {% endif %}
       {% else %}
-        {% if s.url in request.url or c.project.neighborhood.url()+'_admin' in request.url %}
+        {% if s.url in request.upath_info or c.project.neighborhood.url()+'_admin' in request.upath_info %}
           <span class="diamond"></span>
         {% endif %}
       {% endif %}
diff --git a/Allura/allura/templates/widgets/project_list_widget.html b/Allura/allura/templates/widgets/project_list_widget.html
index 047c959..65be12b 100644
--- a/Allura/allura/templates/widgets/project_list_widget.html
+++ b/Allura/allura/templates/widgets/project_list_widget.html
@@ -12,7 +12,10 @@
             icon_url=icon_urls[project._id], 
             accolades=accolades_index[project._id],
             sitemap=sitemaps[project._id],
-            display_mode=display_mode)}}
+            display_mode=display_mode,
+            columns=columns,
+            show_proj_icon=show_proj_icon,
+            show_download_button=show_download_button)}}
       {% endif %}
     {% endfor %}
     {% do g.set_project(old_project) %}
diff --git a/Allura/allura/templates/widgets/project_summary.html b/Allura/allura/templates/widgets/project_summary.html
index 8a8592c..ea834d1 100644
--- a/Allura/allura/templates/widgets/project_summary.html
+++ b/Allura/allura/templates/widgets/project_summary.html
@@ -1,30 +1,34 @@
 {% if display_mode == 'list' %}
-<div class="list card">
-  {% if accolades %}
+<div class="list card"{% if columns == '2' %} style="width: 330px"{% endif %}>
+  {% if show_proj_icon == 'on' %}
+   {% if accolades %}
     <div class="box notch sponsor">
       <div class="feature">{{accolades[0].award.short}}</div>
       <img src="{{icon_url}}" alt="{{value.name}} Logo"/>
     </div>
-  {% else %}
+   {% else %}
     <div class="box">
       <img src="{{icon_url}}" alt="{{value.name}} Logo"/>
     </div>
+   {% endif %}
   {% endif %}
   <h2><a href="{{value.url()}}">{{value.name}}</a></h2>
   <p class="desc">{% if value.summary %}{{value.summary}}{% else %}{{h.text.truncate(value.short_description, 50)}}{% endif %}</p>
-  {{g.markdown_wiki.convert('[[download_button]]')}}
+  {% if show_download_button == 'on' %}{{g.markdown_wiki.convert('[[download_button]]')}}{% endif %}
 </div>
 {% else %}
   <div class="border card">
-    {% if accolades %}
+    {% if show_proj_icon == 'on' %}
+     {% if accolades %}
       <div class="box notch sponsor">
         <div class="feature">{{accolades[0].award.short}}</div>
         <img src="{{icon_url}}" alt="{{value.name}} Logo"/>
       </div>
-    {% else %}
+     {% else %}
       <div class="box">
         <img src="{{icon_url}}" alt="{{value.name}} Logo"/>
       </div>
+     {% endif %}
     {% endif %}
     <h2><a href="{{value.url()}}">{{value.name}}</a></h2>
     <p class="desc">{% if value.summary %}{{value.summary}}{% else %}{{h.text.truncate(value.short_description, 50)}}{% endif %}</p>
@@ -34,4 +38,4 @@
       {% endfor %}
     </div>
   </div>
-{% endif %}
\ No newline at end of file
+{% endif %}
diff --git a/Allura/allura/templates/widgets/repo/revision.html b/Allura/allura/templates/widgets/repo/revision.html
index 08f5b15..471b855 100644
--- a/Allura/allura/templates/widgets/repo/revision.html
+++ b/Allura/allura/templates/widgets/repo/revision.html
@@ -4,42 +4,53 @@
         <div class="first-line">{{g.markdown.convert(h.really_unicode(value.message.split('\n')[0]))}}</div>
         {{g.markdown.convert(h.really_unicode('\n'.join(value.message.split('\n')[1:])))}}
     </div>
-    <h2 class="commit-details">
-        <ul class="commit-links">
-            <li><a class="commit-tree-link" href="{{value.url()}}tree/">Tree</a></li>
-            {% if prev %}
-            <li class="commit-parents">
-              Parent(s):
-              {% for ci in prev %}<a href="{{ci.url()}}">{{ci.shorthand_id()}}</a>{% endfor %}
-            </li>
-            {% endif %}
-            {% if next %}
-            <li class="commit-children">
-              Child(ren):
-              {% for ci in next %}<a href="{{ci.url()}}">{{ci.shorthand_id()}}</a>{% endfor %}
-            </li>
-            {% endif %}
-        </ul>
+    <div class="commit-details">
 
-        Authored by
-        {% if value.author_url %}
-            <a href="{{value.author_url}}">{{email_gravatar(value.authored.email, title=h.really_unicode(value.authored.name), size=16)}}</a>
-            <a href="{{value.author_url}}">{{h.really_unicode(value.authored.name)}}</a>
-        {% else %}
-            {{email_gravatar(value.authored.email, title=h.really_unicode(value.authored.name), size=16)}} {{h.really_unicode(value.authored.name)}}
-        {% endif %}
-
-        {% if value.authored.date %}{{abbr_date(value.authored.date)}}{% endif %}
-
-        {% if value.committed.email != value.authored.email %}
-            Committed by
-            {% if value.committer_url %}
-                <a href="{{value.committer_url}}">{{email_gravatar(value.committed.email, title=h.really_unicode(value.committed.name), size=16)}}</a>
-                <a href="{{value.committer_url}}">{{h.really_unicode(value.committed.name)}}</a>
+        <div class="commit-authorship">
+            <p>
+            <label>Authored by:</label>
+            {% if value.author_url %}
+                <a href="{{value.author_url}}">{{email_gravatar(value.authored.email, title=h.really_unicode(value.authored.name), size=16)}}</a>
+                <a href="{{value.author_url}}">{{h.really_unicode(value.authored.name)}}</a>
             {% else %}
-                {{email_gravatar(value.committed.email, title=h.really_unicode(value.committed.name), size=16)}} {{h.really_unicode(value.committed.name)}}
+                {{email_gravatar(value.authored.email, title=h.really_unicode(value.authored.name), size=16)}} {{h.really_unicode(value.authored.name)}}
             {% endif %}
-            {% if value.committed.date %}{{abbr_date(value.committed.date)}}{% endif %}
-        {% endif %}
-    </h2>
+
+            {% if value.authored.date %}{{abbr_date(value.authored.date)}}{% endif %}
+            </p>
+
+            {% if value.committed.email != value.authored.email %}
+                <p>
+                <label>Committed by:</label>
+                {% if value.committer_url %}
+                    <a href="{{value.committer_url}}">{{email_gravatar(value.committed.email, title=h.really_unicode(value.committed.name), size=16)}}</a>
+                    <a href="{{value.committer_url}}">{{h.really_unicode(value.committed.name)}}</a>
+                {% else %}
+                    {{email_gravatar(value.committed.email, title=h.really_unicode(value.committed.name), size=16)}} {{h.really_unicode(value.committed.name)}}
+                {% endif %}
+                {% if value.committed.date %}{{abbr_date(value.committed.date)}}{% endif %}
+                </p>
+            {% endif %}
+        </div>
+
+        <div class="commit-links">
+            <a class="commit-tree-link" href="{{value.url()}}tree/">Tree</a>
+            <div class="commit-ancestry">
+                {% if prev %}
+                <p class="commit-parents">
+                  Parent(s):
+                  {% for ci in prev %}<a href="{{ci.url()}}">{{ci.shorthand_id()}}</a>{% endfor %}
+                </p>
+                {% endif %}
+                {% if next %}
+                <p class="commit-children">
+                  Child(ren):
+                  {% for ci in next %}<a href="{{ci.url()}}">{{ci.shorthand_id()}}</a>{% endfor %}
+                </p>
+                {% endif %}
+            </div>
+        </div>
+
+        <div class="clearfix"></div>
+    </div>
 </div>
diff --git a/Allura/allura/tests/functional/test_gravatar.py b/Allura/allura/tests/functional/test_gravatar.py
index a6e724c..b991f70 100644
--- a/Allura/allura/tests/functional/test_gravatar.py
+++ b/Allura/allura/tests/functional/test_gravatar.py
@@ -12,6 +12,12 @@
         actual_id = gravatar.id(email)
         assert expected_id == actual_id
 
+    def test_unicode_id(self):
+        email = u'Vin\u00EDcius@example.com'
+        expected_id = 'e00968255d68523b034a6a39c522efdb'
+        actual_id = gravatar.id(email)
+        assert expected_id == actual_id, 'Expected gravitar ID %s, got %s' % (repr(expected_id), repr(actual_id))
+
     def test_url(self):
         email = 'Wolf@example.com'
         expected_id = 'd3514940ac1b2051c8aa42970d17e3fe'
diff --git a/Allura/allura/tests/functional/test_neighborhood.py b/Allura/allura/tests/functional/test_neighborhood.py
index aca0fc5..7fc062d 100644
--- a/Allura/allura/tests/functional/test_neighborhood.py
+++ b/Allura/allura/tests/functional/test_neighborhood.py
@@ -125,14 +125,17 @@
                                   'css-projecttitlecolor': 'green',
                                   'css-barontop': '#555555',
                                   'css-titlebarbackground': '#333',
-                                  'css-titlebarcolor': '#444'},
+                                  'css-titlebarcolor': '#444',
+                                  'css-addopt-icon-theme': 'dark'},
                           extra_environ=dict(username='root'), upload_files=[])
         neighborhood = M.Neighborhood.query.get(name='Adobe')
         assert '/*projecttitlefont*/.project_title{font-family:arial,sans-serif;}' in neighborhood.css
         assert '/*projecttitlecolor*/.project_title{color:green;}' in neighborhood.css
         assert '/*barontop*/.pad h2.colored {background-color:#555555; background-image: none;}' in neighborhood.css
         assert '/*titlebarbackground*/.pad h2.title{background-color:#333; background-image: none;}' in neighborhood.css
-        assert '/*titlebarcolor*/.pad h2.title{color:#444;}' in neighborhood.css
+        assert "/*titlebarcolor*/.pad h2.title{color:#444;} "\
+               ".pad h2.dark small b.ico {background-image: "\
+               "url('/nf/_ew_/theme/allura/images/neo-icon-set-ffffff-256x350.png');}" in neighborhood.css
 
     def test_max_projects(self):
         # Set max value to unlimit
diff --git a/Allura/allura/tests/functional/test_wiki_macro.py b/Allura/allura/tests/functional/test_wiki_macro.py
new file mode 100644
index 0000000..ab77320
--- /dev/null
+++ b/Allura/allura/tests/functional/test_wiki_macro.py
@@ -0,0 +1,113 @@
+from allura import model as M
+from allura.tests import TestController
+from allura.tests import decorators as td
+
+class TestNeighborhood(TestController):
+
+    @staticmethod
+    def get_project_names(r):
+        """
+        Extracts a list of project names from a wiki page HTML.
+        """
+        # projects short names are in h2 elements without any attributes
+        # there is one more h2 element, but it has `class` attribute
+        return [e.text for e in r.html.findAll('h2') if not e.attrs]
+
+    @staticmethod
+    def get_projects_property_in_the_same_order(names, prop):
+        """
+        Returns a list of projects properties `prop` in the same order as
+        project `names`.
+        It is required because results of the query are not in the same order as names.
+        """
+        projects = M.Project.query.find(dict(name={'$in': names})).all()
+        projects_dict = dict([(p['name'],p[prop]) for p in projects])
+        return [projects_dict[name] for name in names]
+
+    @td.with_wiki
+    def test_sort_alpha(self):
+        r = self.app.post('/p/wiki/Home/update',
+                          params={
+                                  'title': 'Home',
+                                  'text': '[[projects sort=alpha]]'
+                                  },
+                          extra_environ=dict(username='root'), upload_files=[]).follow()
+        project_list = self.get_project_names(r)
+        assert project_list == sorted(project_list)
+    
+    @td.with_wiki
+    def test_sort_registered(self):
+        r = self.app.post('/p/wiki/Home/update',
+                          params={
+                                  'title': 'Home',
+                                  'text': '[[projects sort=last_registred]]'
+                                  },
+                          extra_environ=dict(username='root'), upload_files=[]).follow()
+        project_names = self.get_project_names(r)
+        ids = self.get_projects_property_in_the_same_order(project_names, '_id')
+        assert ids == sorted(ids, reverse=True)
+
+    @td.with_wiki
+    def test_sort_updated(self):
+        r = self.app.post('/p/wiki/Home/update',
+                          params={
+                                  'title': 'Home',
+                                  'text': '[[projects sort=last_updated]]'
+                                  },
+                          extra_environ=dict(username='root'), upload_files=[]).follow()
+        project_names = self.get_project_names(r)
+        updated_at = self.get_projects_property_in_the_same_order(project_names, 'last_updated') 
+        assert updated_at == sorted(updated_at, reverse=True)
+
+    @td.with_wiki
+    def test_projects_makro(self):
+        # test columns
+        two_column_style = 'width: 330px;'
+        r = self.app.post('/p/wiki/Home/update',
+                          params={
+                                  'title': 'Home',
+                                  'text': '[[projects display_mode=list columns=2]]'
+                                  },
+                          extra_environ=dict(username='root'), upload_files=[]).follow()
+        assert two_column_style in r
+
+        r = self.app.post('/p/wiki/Home/update',
+                          params={
+                                  'title': 'Home',
+                                  'text': '[[projects display_mode=list columns=3]]'
+                                  },
+                          extra_environ=dict(username='root'), upload_files=[]).follow()
+        assert two_column_style not in r
+
+        # test project icon
+        r = self.app.post('/p/wiki/Home/update',
+                          params={
+                                  'title': 'Home',
+                                  'text': '[[projects display_mode=list show_proj_icon=on]]'
+                                  },
+                          extra_environ=dict(username='root'), upload_files=[]).follow()
+        assert 'test Logo' in r
+        r = self.app.post('/p/wiki/Home/update',
+                          params={
+                                  'title': 'Home',
+                                  'text': '[[projects display_mode=list show_proj_icon=off]]'
+                                  },
+                          extra_environ=dict(username='root'), upload_files=[]).follow()
+        assert 'test Logo' not in r
+
+        # test project download button
+        r = self.app.post('/p/wiki/Home/update',
+                          params={
+                                  'title': 'Home',
+                                  'text': '[[projects display_mode=list show_download_button=on]]'
+                                  },
+                          extra_environ=dict(username='root'), upload_files=[]).follow()
+        assert 'download-button' in r
+
+        r = self.app.post('/p/wiki/Home/update',
+                          params={
+                                  'title': 'Home',
+                                  'text': '[[projects display_mode=list show_download_button=off]]'
+                                  },
+                          extra_environ=dict(username='root'), upload_files=[]).follow()
+        assert 'download-button' not in r
diff --git a/Allura/allura/tests/model/test_neighborhood.py b/Allura/allura/tests/model/test_neighborhood.py
index 953df13..ecbc5eb 100644
--- a/Allura/allura/tests/model/test_neighborhood.py
+++ b/Allura/allura/tests/model/test_neighborhood.py
@@ -53,17 +53,21 @@
                      'titlebarbackground': u'#555',
                      'projecttitlefont': u'arial,sans-serif',
                      'projecttitlecolor': u'#333',
-                     'titlebarcolor': u'#666'}
+                     'titlebarcolor': u'#666',
+                     'addopt-icon-theme': 'dark'}
     css_text = neighborhood.compile_css_for_gold_level(test_css_dict)
     assert '#333' in css_text
     assert '#444' in css_text
     assert '#555' in css_text
     assert '#666' in css_text
     assert 'arial,sans-serif' in css_text
+    assert 'images/neo-icon-set-ffffff-256x350.png' in css_text
     neighborhood.css = css_text
     styles_list = neighborhood.get_css_for_gold_level()
     for style in styles_list:
         assert test_css_dict[style['name']] == style['value']
+        if style['name'] == 'titlebarcolor':
+            assert '<option value="dark" selected="selected">' in style['additional']
 
     # Check neighborhood custom css showing
     neighborhood.level = 'silver'
diff --git a/Allura/allura/tests/test_tasks.py b/Allura/allura/tests/test_tasks.py
index b8278b7..4ed0ad4 100644
--- a/Allura/allura/tests/test_tasks.py
+++ b/Allura/allura/tests/test_tasks.py
@@ -99,7 +99,7 @@
 
     def test_send_email(self):
         c.user = M.User.by_username('test-admin')
-        with mock.patch_object(mail_tasks.smtp_client, 'sendmail') as f:
+        with mock.patch.object(mail_tasks.smtp_client, 'sendmail') as f:
             mail_tasks.sendmail(
                 str(c.user._id),
                 [ str(c.user._id) ],
@@ -121,7 +121,7 @@
     def test_receive_email_ok(self):
         c.user = M.User.by_username('test-admin')
         import forgewiki
-        with mock.patch_object(forgewiki.wiki_main.ForgeWikiApp, 'handle_message') as f:
+        with mock.patch.object(forgewiki.wiki_main.ForgeWikiApp, 'handle_message') as f:
             mail_tasks.route_email(
                 '0.0.0.0', c.user.email_addresses[0],
                 ['Page@wiki.test.p.in.sf.net'],
@@ -137,8 +137,8 @@
         setup_global_objects()
 
     def test_delivers_messages(self):
-        with mock.patch_object(M.Mailbox, 'deliver') as deliver:
-            with mock.patch_object(M.Mailbox, 'fire_ready') as fire_ready:
+        with mock.patch.object(M.Mailbox, 'deliver') as deliver:
+            with mock.patch.object(M.Mailbox, 'fire_ready') as fire_ready:
                 notification_tasks.notify('42', '52', 'none')
                 assert deliver.called_with('42', '52', 'none')
                 assert fire_ready.called_with()
@@ -155,7 +155,7 @@
 
     def test_init(self):
         ns = M.Notification.query.find().count()
-        with mock.patch_object(c.app.repo, 'init') as f:
+        with mock.patch.object(c.app.repo, 'init') as f:
             repo_tasks.init()
             M.main_orm_session.flush()
             assert f.called_with()
@@ -163,19 +163,19 @@
 
     def test_clone(self):
         ns = M.Notification.query.find().count()
-        with mock.patch_object(c.app.repo, 'init_as_clone') as f:
+        with mock.patch.object(c.app.repo, 'init_as_clone') as f:
             repo_tasks.clone('foo', 'bar', 'baz')
             M.main_orm_session.flush()
             f.assert_called_with('foo', 'bar', 'baz')
             assert ns + 1 == M.Notification.query.find().count()
 
     def test_refresh(self):
-        with mock.patch_object(c.app.repo, 'refresh') as f:
+        with mock.patch.object(c.app.repo, 'refresh') as f:
             repo_tasks.refresh()
             f.assert_called_with()
 
     def test_uninstall(self):
-        with mock.patch_object(shutil, 'rmtree') as f:
+        with mock.patch.object(shutil, 'rmtree') as f:
             repo_tasks.uninstall()
             f.assert_called_with('/tmp/svn/p/test/src', ignore_errors=True)
 
diff --git a/Allura/allura/tests/unit/patches.py b/Allura/allura/tests/unit/patches.py
index 57c75b2..ee60c34 100644
--- a/Allura/allura/tests/unit/patches.py
+++ b/Allura/allura/tests/unit/patches.py
@@ -1,4 +1,4 @@
-from mock import Mock, patch, patch_object
+from mock import Mock, patch
 from pylons import c
 
 from allura.tests.unit.factories import create_project, create_app_config
@@ -11,7 +11,7 @@
     app.__version__ = '0'
     app.config = app_config
     app.url = '/-app-/'
-    return patch_object(c, 'app', app, create=True)
+    return patch.object(c, 'app', app, create=True)
 
 
 def project_app_loading_patch(test_case):
diff --git a/Allura/allura/websetup/bootstrap.py b/Allura/allura/websetup/bootstrap.py
index fc08fe9..fa3d5e8 100644
--- a/Allura/allura/websetup/bootstrap.py
+++ b/Allura/allura/websetup/bootstrap.py
@@ -64,9 +64,10 @@
     log.info('Initializing search')
 
     log.info('Registering root user & default neighborhoods')
-    anonymous = M.User(_id=None,
-                       username='*anonymous',
-                       display_name='Anonymous')
+    anonymous = M.User(
+        _id=None,
+        username='*anonymous',
+        display_name='Anonymous')
     root = create_user('Root')
 
     n_projects = M.Neighborhood(name='Projects', url_prefix='/p/')
diff --git a/Allura/development.ini b/Allura/development.ini
index 2e6dc80..46682ab 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -19,7 +19,7 @@
 
 [server:main]
 use = egg:Paste#http
-host = 0.0.0.0 
+host = 0.0.0.0
 port = 8080
 
 [filter-app:profile]
diff --git a/Allura/docs/index.rst b/Allura/docs/index.rst
index 109473a..5153dd4 100644
--- a/Allura/docs/index.rst
+++ b/Allura/docs/index.rst
@@ -17,6 +17,7 @@
 .. toctree::
    :maxdepth: 2
 
+   installation
    platform_tour
    scm_host
    migration
diff --git a/Allura/docs/installation.rst b/Allura/docs/installation.rst
new file mode 100644
index 0000000..48abdd7
--- /dev/null
+++ b/Allura/docs/installation.rst
@@ -0,0 +1,41 @@
+Installation
+=================
+
+Easy Setup
+---------------
+
+Our easy setup instructions are in our README.rst file.  You can read it online at https://sourceforge.net/p/allura/git/#readme
+
+You should be able to get Allura up and running in well under an hour by following those instructions.
+
+Enabling inbound email
+----------------------
+
+Allura can listen for email messages and update tools and artifacts.  For example, every ticket has an email address, and
+emails sent to that address will be added as comments on the ticket.  To set up the SMTP listener, run::
+
+(anvil)~/src/forge/Allura$ nohup paster smtp_server development.ini > ~/logs/smtp.log &
+
+By default this uses port 8825.  Depending on your mail routing, you may need to change that port number.
+And if the port is in use, this command will fail.  You can check the log file for any errors.
+To change the port number, edit `development.ini` and change `forgemail.port` to the appropriate port number for your environment.
+
+
+Enabling RabbitMQ
+-----------------
+
+For faster notification of background jobs, you can use RabbitMQ.  Assuming a base setup from the README, run these commands
+to install rabbitmq and set it up::
+
+(anvil)~$ sudo aptitude install rabbitmq-server
+(anvil)~$ sudo rabbitmqctl add_user testuser testpw
+(anvil)~$ sudo rabbitmqctl add_vhost testvhost
+(anvil)~$ sudo rabbitmqctl set_permissions -p testvhost testuser ""  ".*" ".*"
+(anvil)~$ pip install amqplib==0.6.1 kombu==1.0.4
+
+Then edit Allura/development.ini and change `amqp.enabled = false` to `amqp.enabled = true` and uncomment the other `amqp` settings.
+
+If your `paster taskd` process is still running, restart it::
+
+(anvil)~/src/forge/Allura$ pkill -f taskd
+(anvil)~/src/forge/Allura$ nohup paster taskd development.ini > ~/logs/taskd.log &
diff --git a/Allura/setup.py b/Allura/setup.py
index a96b3cd..fa635d2 100644
--- a/Allura/setup.py
+++ b/Allura/setup.py
@@ -39,7 +39,6 @@
         ],
     install_requires=[
         "TurboGears2",
-        "tg.devtools",
         "pypeline",
         "datadiff",
         "BeautifulSoup",
@@ -47,15 +46,9 @@
         "Babel >= 0.9.4",
         "jinja2",
         "pysolr",
-        "repoze.what-quickstart",
-        "sqlalchemy-migrate",
         "Markdown >= 2.0.3",
         "Pygments >= 1.1.1",
-        "PyYAML >= 3.09",
         "python-openid >= 2.2.4",
-        "python-dateutil >= 1.4.1",
-        "WebOb >= 0.9.8",
-        "WebTest >= 1.2",
         "EasyWidgets >= 0.1.1",
         "PIL >= 1.1.7",
         "iso8601",
@@ -65,7 +58,7 @@
         "Ming >= 0.2.2dev-20110930",
         ],
     setup_requires=["PasteScript >= 1.7"],
-    paster_plugins=['PasteScript', 'Pylons', 'TurboGears2', 'tg.devtools', 'Ming'],
+    paster_plugins=['PasteScript', 'Pylons', 'TurboGears2', 'Ming'],
     packages=find_packages(exclude=['ez_setup']),
     include_package_data=True,
     test_suite='nose.collector',
@@ -121,7 +114,7 @@
     create-trove-categories = allura.command:CreateTroveCategoriesCommand
     set-neighborhood-level = allura.command:SetNeighborhoodLevelCommand
     set-neighborhood-private = allura.command:SetNeighborhoodPrivateCommand
-   
+
     [easy_widgets.resources]
     ew_resources=allura.config.resources:register_ew_resources
 
@@ -130,4 +123,3 @@
 
     """,
 )
-
diff --git a/ForgeBlog/forgeblog/model/blog.py b/ForgeBlog/forgeblog/model/blog.py
index 7f05816..938b408 100644
--- a/ForgeBlog/forgeblog/model/blog.py
+++ b/ForgeBlog/forgeblog/model/blog.py
@@ -11,7 +11,7 @@
 from ming.orm import FieldProperty, ForeignIdProperty, Mapper, session, state
 from allura import model as M
 from allura.lib import helpers as h
-from allura.lib import utils, patience, htmltruncate
+from allura.lib import utils, patience
 
 config = utils.ConfigProxy(
     common_suffix='forgemail.domain')
diff --git a/ForgeDiscussion/forgediscussion/import_support.py b/ForgeDiscussion/forgediscussion/import_support.py
index 6ceca12..8e8e34b 100644
--- a/ForgeDiscussion/forgediscussion/import_support.py
+++ b/ForgeDiscussion/forgediscussion/import_support.py
@@ -96,7 +96,7 @@
         self.warnings = warnings
         super(AlluraUser, self).__init__(**kw)
 
-    def _validate(self, value):
+    def _validate(self, value, **kw):
         value = S.String().validate(value)
         sf_username = self.mapping.get(value, value)
         result = M.User.by_username(sf_username)
@@ -116,7 +116,7 @@
 
 class TimeStamp(S.FancySchemaItem):
 
-    def _validate(self, value):
+    def _validate(self, value, **kwargs):
         try:
             value = int(value)
         except TypeError:
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index f8abdd5..b96c2ef 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -85,7 +85,7 @@
     @property
     def not_closed_mongo_query(self):
         return dict(
-            status={'$in': list(self.set_of_open_status_names)})
+            status={'$nin': list(self.set_of_closed_status_names)})
 
     @property
     def closed_query(self):
@@ -181,7 +181,7 @@
 
     type_s = 'Bin'
     _id = FieldProperty(schema.ObjectId)
-    summary = FieldProperty(str, required=True)
+    summary = FieldProperty(str, required=True, allow_none=False)
     terms = FieldProperty(str, if_missing='')
     sort = FieldProperty(str, if_missing='')
 
@@ -222,7 +222,7 @@
 
     super_id = FieldProperty(schema.ObjectId, if_missing=None)
     sub_ids = FieldProperty([schema.ObjectId])
-    ticket_num = FieldProperty(int, required=True)
+    ticket_num = FieldProperty(int, required=True, allow_none=False)
     summary = FieldProperty(str)
     description = FieldProperty(str, if_missing='')
     reported_by_id = ForeignIdProperty(User, if_missing=lambda:c.user._id)
diff --git a/ForgeTracker/forgetracker/templates/tracker/index.html b/ForgeTracker/forgetracker/templates/tracker/index.html
index c6c5846..50c839c 100644
--- a/ForgeTracker/forgetracker/templates/tracker/index.html
+++ b/ForgeTracker/forgetracker/templates/tracker/index.html
@@ -16,7 +16,7 @@
     {{c.subscribe_form.display(value=subscribed, action='subscribe', style='icon')}}
   {% endif %}
   {% if allow_edit %}
-    <a href="{{tg.url(c.app.url+'edit/', dict(q=url_q, limit=limit, sort=sort, page=page))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}}"></b></a>
+    <a href="{{tg.url(c.app.url+'edit/', dict(q=url_q, limit=limit, sort=url_sort, page=page))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}}"></b></a>
   {% endif %}
 {% endblock %}
 
diff --git a/ForgeTracker/forgetracker/templates/tracker/milestone.html b/ForgeTracker/forgetracker/templates/tracker/milestone.html
index 4615cbf..d40132f 100644
--- a/ForgeTracker/forgetracker/templates/tracker/milestone.html
+++ b/ForgeTracker/forgetracker/templates/tracker/milestone.html
@@ -8,7 +8,7 @@
 
 {% block actions %}
 {% if allow_edit %}
-  <a href="{{tg.url(c.app.url+'edit/', dict(q=q, limit=limit, sort=sort, page=page))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}}"></b></a>
+  <a href="{{tg.url(c.app.url+'edit/', dict(q=q, limit=limit, sort=url_sort, page=page))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}}"></b></a>
 {% endif %}
 {% endblock %}
 
diff --git a/ForgeTracker/forgetracker/templates/tracker_widgets/mass_edit_form.html b/ForgeTracker/forgetracker/templates/tracker_widgets/mass_edit_form.html
index f92a013..2e415a5 100644
--- a/ForgeTracker/forgetracker/templates/tracker_widgets/mass_edit_form.html
+++ b/ForgeTracker/forgetracker/templates/tracker_widgets/mass_edit_form.html
@@ -3,7 +3,7 @@
       {% if field.name == '_milestone' %}
       <div class="grid-6">{{milestones}}
         <label for="{{field.name}}" class="cr">{{field.label}}:</label>
-        <select name="{{field.name}}" class="wide">
+        <select name="{{field.name}}" id="{{field.name}}" class="wide">
           <option value="" selected="selected">no change</option>
           {% for m in field.milestones %}
             {% if not m.complete %}
@@ -16,7 +16,7 @@
     {% endfor %}
     <div class="grid-6">
       <label for="status" class="cr">Status:</label>
-      <select name="status" class="wide">
+      <select name="status" id="status" class="wide">
         <option value="" selected="selected">no change</option>
         {% for option in globals.all_status_names.split() %}
           <option value="{{option}}">{{option}}</option>
@@ -25,7 +25,7 @@
     </div>
     <div class="grid-6">
       <label for="assigned_to" class="cr">Owner:</label>
-      {{c.user_select.display(name='assigned_to', value='', className='wide')}}
+      {{c.user_select.display(name='assigned_to', id='assigned_to', value='', className='wide')}}
     </div>
     {% set cf_count = 0 %}
     {% for field in globals.custom_fields %}
@@ -36,7 +36,7 @@
         <div class="grid-6">
           <label for="{{field.id}}" class="cr">{{field.label}}:</label>
           {% if field.type == 'boolean' %}
-            <input name="{{field.name}}" type="checkbox" value="True"/>
+            <input name="{{field.name}}" id="{{field.name}}" type="checkbox" value="True"/>
           {% elif field.type == 'select' %}
             <select name="{{field.name}}" class="wide">
               <option value="" selected="selected">no change</option>
@@ -65,7 +65,7 @@
       {% endif %}
     {% endfor %}
     <div class="grid-18">
-      <input type="button" onclick="update_tickets()" value="Save"/>
+      <input type="button" class="update_tickets" value="Save"/>
       <a href="{{cancel_href}}" class="btn link">Cancel</a>
       <!-- tg.url(c.app.url+'search/', dict(q=query, limit=limit, sort=sort))}}" class="btn link">Cancel</a>-->
     </div>
diff --git a/ForgeTracker/forgetracker/tests/functional/test_root.py b/ForgeTracker/forgetracker/tests/functional/test_root.py
index dc848c7..a6c647e 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_root.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_root.py
@@ -4,7 +4,7 @@
 import allura
 
 from mock import patch
-from nose.tools import assert_true, assert_false, assert_equal
+from nose.tools import assert_true, assert_false, assert_equal, assert_in
 from formencode.variabledecode import variable_encode
 
 from alluratest.controller import TestController
@@ -692,6 +692,67 @@
         r = self.app.get('/bugs/1/', dict(page=1, limit=2))
         assert_true('Page 2 of 2' in r)
 
+    def test_bulk_edit_index(self):
+        self.new_ticket(summary='test first ticket', status='open')
+        self.new_ticket(summary='test second ticket', status='accepted')
+        self.new_ticket(summary='test third ticket', status='closed')
+        ThreadLocalORMSession.flush_all()
+        M.MonQTask.run_ready()
+        ThreadLocalORMSession.flush_all()
+        response = self.app.get('/p/test/bugs/?sort=summary+asc')
+        ticket_rows = response.html.find('table', {'class':'ticket-list'}).find('tbody')
+        assert_in('test first ticket', str(ticket_rows))
+        assert_in('test second ticket', str(ticket_rows))
+        edit_link = response.html.find('a',{'title':'Bulk Edit'})
+        expected_link = "/p/test/bugs/edit/?q=%21status%3Awont-fix+%26%26+%21status%3Aclosed&sort=snippet_s+asc&limit=25&page=0"
+        assert_equal(expected_link, edit_link['href'])
+        response = self.app.get(edit_link['href'])
+        ticket_rows = response.html.find('tbody', {'class':'ticket-list'})
+        assert_in('test first ticket', str(ticket_rows))
+        assert_in('test second ticket', str(ticket_rows))
+
+    def test_bulk_edit_milestone(self):
+        self.new_ticket(summary='test first ticket', status='open', _milestone='1.0')
+        self.new_ticket(summary='test second ticket', status='accepted', _milestone='1.0')
+        self.new_ticket(summary='test third ticket', status='closed', _milestone='1.0')
+        ThreadLocalORMSession.flush_all()
+        M.MonQTask.run_ready()
+        ThreadLocalORMSession.flush_all()
+        response = self.app.get('/p/test/bugs/milestone/1.0/?sort=ticket_num+asc')
+        ticket_rows = response.html.find('table', {'class':'ticket-list'}).find('tbody')
+        assert_in('test first ticket', str(ticket_rows))
+        assert_in('test second ticket', str(ticket_rows))
+        assert_in('test third ticket', str(ticket_rows))
+        edit_link = response.html.find('a',{'title':'Bulk Edit'})
+        expected_link = "/p/test/bugs/edit/?q=_milestone%3A1.0&sort=ticket_num_i+asc&limit=25&page=0"
+        assert_equal(expected_link, edit_link['href'])
+        response = self.app.get(edit_link['href'])
+        ticket_rows = response.html.find('tbody', {'class':'ticket-list'})
+        assert_in('test first ticket', str(ticket_rows))
+        assert_in('test second ticket', str(ticket_rows))
+        assert_in('test third ticket', str(ticket_rows))
+
+    def test_bulk_edit_search(self):
+        self.new_ticket(summary='test first ticket', status='open')
+        self.new_ticket(summary='test second ticket', status='open')
+        self.new_ticket(summary='test third ticket', status='closed', _milestone='1.0')
+        ThreadLocalORMSession.flush_all()
+        M.MonQTask.run_ready()
+        ThreadLocalORMSession.flush_all()
+        response = self.app.get('/p/test/bugs/search/?q=status%3Aopen')
+        ticket_rows = response.html.find('table', {'class':'ticket-list'}).find('tbody')
+        assert_in('test first ticket', str(ticket_rows))
+        assert_in('test second ticket', str(ticket_rows))
+        assert_false('test third ticket' in str(ticket_rows))
+        edit_link = response.html.find('a',{'title':'Bulk Edit'})
+        expected_link = "/p/test/bugs/edit/?q=status%3Aopen&limit=25&page=0"
+        assert_equal(expected_link, edit_link['href'])
+        response = self.app.get(edit_link['href'])
+        ticket_rows = response.html.find('tbody', {'class':'ticket-list'})
+        assert_in('test first ticket', str(ticket_rows))
+        assert_in('test second ticket', str(ticket_rows))
+        assert_false('test third ticket' in str(ticket_rows))
+
 class TestMilestoneAdmin(TrackerTestController):
     def _post(self, params, **kw):
         params['open_status_names'] = 'aa bb'
diff --git a/ForgeTracker/forgetracker/tests/unit/__init__.py b/ForgeTracker/forgetracker/tests/unit/__init__.py
index e5d352c..5fe9365 100644
--- a/ForgeTracker/forgetracker/tests/unit/__init__.py
+++ b/ForgeTracker/forgetracker/tests/unit/__init__.py
@@ -1,3 +1,6 @@
+import pylons
+pylons.c = pylons.tmpl_context
+pylons.g = pylons.app_globals
 from pylons import c
 from ming.orm.ormsession import ThreadLocalORMSession
 
diff --git a/ForgeTracker/forgetracker/tracker_main.py b/ForgeTracker/forgetracker/tracker_main.py
index 3816d38..39232b5 100644
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -57,6 +57,22 @@
     page=validators.Int(if_empty=0),
     sort=validators.UnicodeString(if_empty=None))
 
+def _mongo_col_to_solr_col(name):
+    if name == 'ticket_num':
+        return 'ticket_num_i'
+    elif name == 'summary':
+        return 'snippet_s'
+    elif name == '_milestone':
+        return 'milestone_s'
+    elif name == 'status':
+        return 'status_s'
+    elif name == 'assigned_to':
+        return 'assigned_to_s'
+    else:
+        for field in c.app.globals.sortable_custom_fields_shown_in_search():
+            if name == field['name']:
+                return field['sortable_name']
+
 class W:
     thread=w.Thread(
         page=None, limit=None, page_size=None, count=None,
@@ -382,7 +398,7 @@
     @with_trailing_slash
     @h.vardec
     @expose('jinja:forgetracker:templates/tracker/index.html')
-    def index(self, limit=25, columns=None, page=0, sort='ticket_num_i asc', **kw):
+    def index(self, limit=25, columns=None, page=0, sort='ticket_num desc', **kw):
         kw.pop('q', None) # it's just our original query mangled and sent back to us
         result = TM.Ticket.paged_query(c.app.globals.not_closed_mongo_query,
                                         sort=sort, limit=int(limit),
@@ -392,6 +408,11 @@
         result['allow_edit'] = has_access(c.app, 'write')()
         result['help_msg'] = c.app.config.options.get('TicketHelpSearch')
         result['url_q'] = c.app.globals.not_closed_query
+        result['url_sort'] = ''
+        if sort:
+            sort_split = sort.split(' ')
+            solr_col = _mongo_col_to_solr_col(sort_split[0])
+            result['url_sort'] = '%s %s' % (solr_col, sort_split[1])
         c.ticket_search_results = W.ticket_search_results
         return result
 
@@ -1413,7 +1434,13 @@
             field=self.field,
             milestone=self.milestone,
             total=progress['hits'],
-            closed=progress['closed'])
+            closed=progress['closed'],
+            q=self.progress_key)
+        result['url_sort'] = ''
+        if sort:
+            sort_split = sort.split(' ')
+            solr_col = _mongo_col_to_solr_col(sort_split[0])
+            result['url_sort'] = '%s %s' % (solr_col, sort_split[1])
         c.ticket_search_results = W.ticket_search_results
         c.auto_resize_textarea = W.auto_resize_textarea
         return result
diff --git a/ForgeTracker/forgetracker/widgets/resources/js/mass-edit.js b/ForgeTracker/forgetracker/widgets/resources/js/mass-edit.js
index 443979e..a8d91d0 100644
--- a/ForgeTracker/forgetracker/widgets/resources/js/mass-edit.js
+++ b/ForgeTracker/forgetracker/widgets/resources/js/mass-edit.js
@@ -1,23 +1,3 @@
-function update_tickets(){
-    var $checked=$('tbody.ticket-list input:checked'), count=$checked.length;
-
-    if ( !count ) {
-        $('#result').text('No tickets selected for update.');
-        return;
-    }
-
-    var data={};
-    data.selected = $checked.map(function(){ return this.name; }).get().join(',');
-    $('#update-values').find('input, select').each(function(){
-        this.value && (data[this.name]=this.value);
-    });
-
-    $.post('../update_tickets', data, function(){
-        flash('Updated '+count+' ticket'+(count!=1 ? 's' : ''))
-        location.reload();
-    });
-}
-
 $(function(){
     $('#assigned_to').val('');
     $('#select_all').click(function(){
@@ -28,4 +8,23 @@
             $('tbody.ticket-list input[type=checkbox]').removeAttr('checked');
         }
     });
+    $('input.update_tickets').click(function(){
+        var $checked=$('tbody.ticket-list input:checked'), count=$checked.length;
+
+        if ( !count ) {
+            $('#result').text('No tickets selected for update.');
+            return;
+        }
+
+        var data={};
+        data.selected = $checked.map(function(){ return this.name; }).get().join(',');
+        $('#update-values').find('input, select').each(function(){
+            this.value && (data[this.name]=this.value);
+        });
+
+        $.post('../update_tickets', data, function(){
+            flash('Updated '+count+' ticket'+(count!=1 ? 's' : ''))
+            location.reload();
+        });
+    });
 });
diff --git a/ForgeTracker/setup.py b/ForgeTracker/setup.py
index d70c9eb..b44f1a4 100644
--- a/ForgeTracker/setup.py
+++ b/ForgeTracker/setup.py
@@ -20,7 +20,6 @@
       install_requires=[
           # -*- Extra requirements: -*-
           'Allura',
-          'tw.forms',
       ],
       entry_points="""
       # -*- Entry points: -*-
diff --git a/README.markdown b/README.markdown
index 518c1d5..978aa7f 100644
--- a/README.markdown
+++ b/README.markdown
@@ -26,22 +26,24 @@
     ~$ sudo aptitude install default-jdk python-dev libssl-dev libldap2-dev libsasl2-dev libjpeg8-dev zlib1g-dev
     ~$ sudo ln -s /usr/lib/x86_64-linux-gnu/libz.so /usr/lib
 
-And finally our document-oriented database, MongoDB, and our messaging server, RabbitMQ.  Note that RabbitMQ is optional, but will make messages flow faster through our asynchronous processors.  By default, rabbitmq is disabled in development.ini.
+And finally our document-oriented database, MongoDB
 
-    ~$ sudo aptitude install mongodb-server rabbitmq-server
+    ~$ sudo aptitude install mongodb-server
+
+If you are using a different base system, make sure you have Mongo 1.8 or better.  If you need to upgrade, you can download the latest from <http://www.mongodb.org/downloads>
 
 ## Setting up a virtual python environment
 
 The first step to installing the Forge platform is installing a virtual environment via `virtualenv`.  This helps keep our distribution python installation clean.
 
-    ~$ sudo aptitude install python-setuptools
-    ~$ sudo easy_install -U virtualenv
+    ~$ sudo aptitude install python-pip
+    ~$ sudo pip install virtualenv
 
 Once you have virtualenv installed, you need to create a virtual environment.  We'll call our Forge environment 'anvil'.
 
     ~$ virtualenv --system-site-packages anvil
 
-This gives us a nice, clean environment into which we can install all the forge dependencies.  In order to use the virtual environment, you'll need to activate it.  You'll need to do this whenever you're working on the Forge codebase so you may want to consider adding it to your `~/.bashrc` file.
+This gives us a nice, clean environment into which we can install all the forge dependencies.  (The site-packages flag is to include the python-svn package).  In order to use the virtual environment, you'll need to activate it.  You'll need to do this whenever you're working on the Forge codebase so you may want to consider adding it to your `~/.bashrc` file.
 
     ~$ . anvil/bin/activate
 
@@ -56,10 +58,9 @@
 Although the application setup.py files define a number of dependencies, the `requirements.txt` files are currently the authoritative source, so we'll use those with `pip` to make sure the correct versions are installed.
 
     (anvil)~/src$ cd forge
-    (anvil)~/src/forge$ easy_install pip
-    (anvil)~/src/forge$ pip install -r requirements-dev.txt
+    (anvil)~/src/forge$ pip install -r requirements.txt
 
-If you want to use RabbitMQ for faster message processing (optional), also pip install 'amqplib' and 'kombu'.
+This will take a while.  If you get an error from pip, it is typically a temporary download error.  Just run the command again and it will quickly pass through the packages it already downloaded and then continue.
 
 And now to setup each of the Forge applications for development.  Because there are quite a few (at last count 15), we'll use a simple shell loop to set them up.
 
@@ -84,18 +85,6 @@
 
 The forge consists of several components, all of which need to be running to have full functionality.
 
-
-### MongoDB database server
-
-Generally set up with its own directory, we'll use ~/var/mongodata to keep our installation localized.  We also need to disable the default distribution server.
-
-    (anvil)~$ sudo service mongodb stop
-    (anvil)~$ sudo update-rc.d mongodb remove
-
-    (anvil)~$ mkdir -p ~/var/mongodata ~/logs
-    (anvil)~$ nohup mongod --dbpath ~/var/mongodata > ~/logs/mongodb.log &
-
-
 ### SOLR search and indexing server
 
 We have a custom config ready for use.
@@ -109,15 +98,6 @@
     (anvil)~/src/apache-solr-1.4.1/example/$ nohup java -Dsolr.solr.home=$(cd;pwd)/src/forge/solr_config -jar start.jar > ~/logs/solr.log &
 
 
-### RabbitMQ message queue (optional)
-
-We'll need to setup some development users and privileges.
-
-    (anvil)~$ sudo rabbitmqctl add_user testuser testpw
-    (anvil)~$ sudo rabbitmqctl add_vhost testvhost
-    (anvil)~$ sudo rabbitmqctl set_permissions -p testvhost testuser ""  ".*" ".*"
-
-
 ### Forge task processing
 
 Responds to asynchronous task requests.
@@ -125,14 +105,6 @@
     (anvil)~$ cd ~/src/forge/Allura
     (anvil)~/src/forge/Allura$ nohup paster taskd development.ini > ~/logs/taskd.log &
 
-
-### Forge SMTP for inbound mail
-
-Routes messages from email addresses to tools in the forge.
-
-    (anvil)~/src/forge/Allura$ nohup paster smtp_server development.ini > ~/logs/smtp.log &
-
-
 ### TurboGears application server
 
 In order to initialize the Forge database, you'll need to run the following:
@@ -143,26 +115,19 @@
 
     (anvil)~/src/forge/Allura$ nohup paster serve --reload development.ini > ~/logs/tg.log &
 
-And now you should be able to visit the server running on your [local machine](http://localhost:8080/).
+## Next Steps
+
+Go to the server running on your [local machine](http://localhost:8080/) port 8080.
 You can log in with username admin1, test-user or root.  They all have password "foo".  (For more details
 on the default data, see bootstrap.py)
 
+There are a few default projects (like "test") and neighborhoods.  Feel free to experiment with them.  If you want to
+register a new project in your own forge, visit /p/add_project
 
-## Next Steps
+## Extra
 
-
-### Generate the documentation
-
-Forge documentation currently lives in the `Allura/docs` directory and can be converted to HTML using `Sphinx`:
-
-    (anvil)~$ cd ~/src/forge/Allura/docs
-    (anvil)~/src/forge/Allura/docs$ easy_install sphinx
-    (anvil)~/src/forge/Allura/docs$ make html
-
-You will also want to give the test suite a run, to verify there were no problems with the installation.
-
-    (anvil)~$ cd ~/src/forge
-    (anvil)~/src/forge$ export ALLURA_VALIDATION=none
-    (anvil)~/src/forge$ ./run_tests
-
-Happy hacking!
+* Read more documentation: http://allura.sourceforge.net/
+    * Including how to enable extra features: http://allura.sourceforge.net/installation.html
+* Run the test suite (slow): `$ ALLURA_VALIDATION=none ./run_tests`
+* File bug reports at <https://sourceforge.net/p/allura/tickets/new/> (login required)
+* Contribute code according to this guide: <http://sourceforge.net/p/allura/wiki/Contributing%20Code/>
diff --git a/requirements-common.txt b/requirements-common.txt
index 1f7d758..16dc4b4 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -1,77 +1,69 @@
 # requirements for all deployment environments
 
-async==0.6.1
-Babel==0.9.6
-Beaker==1.5.4
 BeautifulSoup==3.2.0
 chardet==1.0.1
 colander==0.9.3
+# dep of pypeline
 Creoleparser==0.7.3
-datadiff==1.1.3
 decorator==3.3.2
+# dep of pypeline
 docutils==0.8.1
 EasyWidgets==0.2dev-20110726
 feedparser==5.0.1
 FormEncode==1.2.4
+# dep of Creoleparser
 Genshi==0.6
-gitdb==0.5.4
-GitPython==0.3.0-beta2
+# dep of oauth2
 httplib2==0.7.4
 iso8601==0.1.4
 Jinja2==2.6
-# Mako is not used directly
-Mako==0.3.2
 Markdown==2.0.3
-MarkupSafe==0.15
 mercurial==1.4.3
-Ming==0.2.2dev-20120305
-mock==0.7.2
-nose==1.1.2
+Ming==0.3.1dev-20120323
 oauth2==1.5.170
-Paste==1.7.5.1
-PasteDeploy==1.5.0
 PasteScript==1.7.4.2
 PIL==1.1.7
 poster==0.8.1
-pyflakes==0.4.0
-Pylons==1.0
+Pygments==1.5
 pymongo==2.1.1
 Pypeline==0.1dev
-pyprof2calltree==1.1.0
 pysolr==2.1.0-beta
 python-dateutil==1.5
 python-openid==2.2.5
 pytidylib==0.2.1
-PyYAML==3.10
-# needed for profiling
-repoze.profile==1.3
-repoze.tm2==1.0a5
-repoze.what==1.0.9
-repoze.what.plugins.sql==1.0.1
-repoze.what-quickstart==1.0.8
-repoze.who==1.0.19
-repoze.who-friendlyform==1.0.8
-repoze.who.plugins.sa==1.0
-repoze.who-testutil==1.0.1
-Routes==1.12.3
-simplejson==2.2.1
-# used by gitdb
-smmap==0.8.1
-SQLAlchemy==0.7.2
-sqlalchemy-migrate==0.7.1
-# suds needed for teamforge import script
-suds==0.4
-Tempita==0.5.1
+# dep of pypeline
 textile==2.1.5
-tg.devtools==2.1.3
-ToscaWidgets==0.9.12
+# dep of colander
 translationstring==0.4
 TurboGears2==2.1.3
-tw.forms==0.9.9
+# part of the stdlib, but with a version number.  see http://guide.python-distribute.org/pip.html#listing-installed-packages
+wsgiref==0.1.2
+
+# tg2 deps (not used directly)
+Babel==0.9.6
+Beaker==1.5.4
+Mako==0.3.2
+MarkupSafe==0.15
+Paste==1.7.5.1
+PasteDeploy==1.5.0
+Pylons==1.0
+simplejson==2.2.1
+Tempita==0.5.1
+Routes==1.12.3
 WebError==0.10.3
 WebFlash==0.1a9
 WebHelpers==1.3
 WebOb==1.1.1
+
+# git deps
+async==0.6.1
+gitdb==0.5.4
+GitPython==0.3.0-beta2
+smmap==0.8.1
+
+# testing & development
+datadiff==1.1.3
+mock==0.8.0
+nose==1.1.2
+pyflakes==0.5.0
 WebTest==1.3.1
-wsgiref==0.1.2
-zope.interface==3.8.0
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index bce9648..0000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-# requirements for local development, or other deployment instances
-
-python-ldap>=2.3.9
-Pygments==1.4
-
-# Include our common requirements
--r requirements-common.txt
diff --git a/requirements-sf.txt b/requirements-sf.txt
new file mode 100644
index 0000000..bcf161d
--- /dev/null
+++ b/requirements-sf.txt
@@ -0,0 +1,29 @@
+# requirements for the SF instance
+
+amqplib==0.6.1
+kombu==1.0.4
+coverage==3.5a1-20110413
+mechanize==0.2.4
+MySQL-python==1.2.3c1
+phpserialize==1.2
+psycopg2==2.2.2
+sf.phpsession==0.1
+SQLAlchemy==0.7.2
+sqlalchemy-migrate==0.7.1
+pyzmq==2.1.7
+
+# for the migration scripts only
+html2text==3.101
+postmarkup==1.1.4
+# suds needed for teamforge import script
+suds==0.4
+
+# development
+blessings==1.3
+ipython==0.11
+nose-progressive==1.3
+pyprof2calltree==1.1.0
+repoze.profile==1.3
+
+# Include our common requirements
+-r requirements-common.txt
diff --git a/requirements.txt b/requirements.txt
index 71ced86..e361bb0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,20 +1,4 @@
-# requirements for the SF instance
-
-amqplib==0.6.1
-kombu==1.0.4
-coverage==3.5a1-20110413
-mechanize==0.2.4
-MySQL-python==1.2.3c1
-phpserialize==1.2
-psycopg2==2.2.2
-sf.phpsession==0.1
-pyzmq==2.1.7
-Pygments==1.4sf1-20110601
-
-# for the migration scripts only
-html2text==3.101
-postmarkup==1.1.4
-
+# requirements for local development, or other deployment instances
 
 # Include our common requirements
 -r requirements-common.txt
diff --git a/scripts/migrations/010-fix-home-permissions.py b/scripts/migrations/010-fix-home-permissions.py
index aac7aaf..05212da 100644
--- a/scripts/migrations/010-fix-home-permissions.py
+++ b/scripts/migrations/010-fix-home-permissions.py
@@ -1,12 +1,12 @@
 import sys
 import logging
+from collections import OrderedDict
 
 from pylons import c
 from ming.orm import session
 from bson import ObjectId
 
 from allura import model as M
-from allura.lib.ordereddict import OrderedDict
 from forgewiki.wiki_main import ForgeWikiApp
 
 log = logging.getLogger('fix-home-permissions')
diff --git a/scripts/project-import.py b/scripts/project-import.py
index 6725dce..3b2685a 100644
--- a/scripts/project-import.py
+++ b/scripts/project-import.py
@@ -157,7 +157,7 @@
 
 def trove_ids(orig, new_):
     if new_ is None: return orig
-    return set(t._id for t in list(new_))
+    return list(set(t._id for t in list(new_)))
 
 def create_project(p, nbhd, user, options):
     worker_name = multiprocessing.current_process().name